crawlo 1.1.4__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crawlo might be problematic. Click here for more details.
- crawlo/__init__.py +61 -34
- crawlo/__version__.py +1 -1
- crawlo/cleaners/__init__.py +61 -0
- crawlo/cleaners/data_formatter.py +226 -0
- crawlo/cleaners/encoding_converter.py +126 -0
- crawlo/cleaners/text_cleaner.py +233 -0
- crawlo/cli.py +40 -40
- crawlo/commands/__init__.py +13 -13
- crawlo/commands/check.py +594 -594
- crawlo/commands/genspider.py +151 -151
- crawlo/commands/list.py +155 -155
- crawlo/commands/run.py +285 -285
- crawlo/commands/startproject.py +300 -196
- crawlo/commands/stats.py +188 -188
- crawlo/commands/utils.py +186 -186
- crawlo/config.py +309 -279
- crawlo/config_validator.py +253 -0
- crawlo/core/__init__.py +2 -2
- crawlo/core/engine.py +346 -172
- crawlo/core/processor.py +40 -40
- crawlo/core/scheduler.py +137 -166
- crawlo/crawler.py +1027 -1027
- crawlo/downloader/__init__.py +266 -242
- crawlo/downloader/aiohttp_downloader.py +220 -212
- crawlo/downloader/cffi_downloader.py +256 -251
- crawlo/downloader/httpx_downloader.py +259 -259
- crawlo/downloader/hybrid_downloader.py +214 -0
- crawlo/downloader/playwright_downloader.py +403 -0
- crawlo/downloader/selenium_downloader.py +473 -0
- crawlo/event.py +11 -11
- crawlo/exceptions.py +81 -81
- crawlo/extension/__init__.py +37 -37
- crawlo/extension/health_check.py +141 -141
- crawlo/extension/log_interval.py +57 -57
- crawlo/extension/log_stats.py +81 -81
- crawlo/extension/logging_extension.py +43 -43
- crawlo/extension/memory_monitor.py +104 -88
- crawlo/extension/performance_profiler.py +133 -117
- crawlo/extension/request_recorder.py +107 -107
- crawlo/filters/__init__.py +154 -154
- crawlo/filters/aioredis_filter.py +280 -242
- crawlo/filters/memory_filter.py +269 -269
- crawlo/items/__init__.py +23 -23
- crawlo/items/base.py +21 -21
- crawlo/items/fields.py +53 -53
- crawlo/items/items.py +104 -104
- crawlo/middleware/__init__.py +21 -21
- crawlo/middleware/default_header.py +32 -32
- crawlo/middleware/download_delay.py +28 -28
- crawlo/middleware/middleware_manager.py +135 -135
- crawlo/middleware/proxy.py +272 -248
- crawlo/middleware/request_ignore.py +30 -30
- crawlo/middleware/response_code.py +18 -18
- crawlo/middleware/response_filter.py +26 -26
- crawlo/middleware/retry.py +124 -124
- crawlo/mode_manager.py +206 -201
- crawlo/network/__init__.py +21 -21
- crawlo/network/request.py +338 -311
- crawlo/network/response.py +360 -271
- crawlo/pipelines/__init__.py +21 -21
- crawlo/pipelines/bloom_dedup_pipeline.py +156 -156
- crawlo/pipelines/console_pipeline.py +39 -39
- crawlo/pipelines/csv_pipeline.py +316 -316
- crawlo/pipelines/database_dedup_pipeline.py +224 -224
- crawlo/pipelines/json_pipeline.py +218 -218
- crawlo/pipelines/memory_dedup_pipeline.py +115 -115
- crawlo/pipelines/mongo_pipeline.py +131 -131
- crawlo/pipelines/mysql_pipeline.py +316 -316
- crawlo/pipelines/pipeline_manager.py +56 -56
- crawlo/pipelines/redis_dedup_pipeline.py +166 -162
- crawlo/project.py +153 -153
- crawlo/queue/pqueue.py +37 -37
- crawlo/queue/queue_manager.py +320 -307
- crawlo/queue/redis_priority_queue.py +277 -209
- crawlo/settings/__init__.py +7 -7
- crawlo/settings/default_settings.py +216 -278
- crawlo/settings/setting_manager.py +99 -99
- crawlo/spider/__init__.py +639 -639
- crawlo/stats_collector.py +59 -59
- crawlo/subscriber.py +130 -130
- crawlo/task_manager.py +30 -30
- crawlo/templates/crawlo.cfg.tmpl +10 -10
- crawlo/templates/project/__init__.py.tmpl +3 -3
- crawlo/templates/project/items.py.tmpl +17 -17
- crawlo/templates/project/middlewares.py.tmpl +110 -110
- crawlo/templates/project/pipelines.py.tmpl +97 -97
- crawlo/templates/project/run.py.tmpl +251 -251
- crawlo/templates/project/settings.py.tmpl +326 -279
- crawlo/templates/project/settings_distributed.py.tmpl +120 -0
- crawlo/templates/project/settings_gentle.py.tmpl +95 -0
- crawlo/templates/project/settings_high_performance.py.tmpl +152 -0
- crawlo/templates/project/settings_simple.py.tmpl +69 -0
- crawlo/templates/project/spiders/__init__.py.tmpl +5 -5
- crawlo/templates/spider/spider.py.tmpl +141 -141
- crawlo/tools/__init__.py +183 -0
- crawlo/tools/anti_crawler.py +269 -0
- crawlo/tools/authenticated_proxy.py +241 -0
- crawlo/tools/data_validator.py +181 -0
- crawlo/tools/date_tools.py +36 -0
- crawlo/tools/distributed_coordinator.py +387 -0
- crawlo/tools/retry_mechanism.py +221 -0
- crawlo/tools/scenario_adapter.py +263 -0
- crawlo/utils/__init__.py +35 -7
- crawlo/utils/batch_processor.py +261 -0
- crawlo/utils/controlled_spider_mixin.py +439 -439
- crawlo/utils/date_tools.py +290 -233
- crawlo/utils/db_helper.py +343 -343
- crawlo/utils/enhanced_error_handler.py +360 -0
- crawlo/utils/env_config.py +106 -0
- crawlo/utils/error_handler.py +126 -0
- crawlo/utils/func_tools.py +82 -82
- crawlo/utils/large_scale_config.py +286 -286
- crawlo/utils/large_scale_helper.py +343 -343
- crawlo/utils/log.py +128 -128
- crawlo/utils/performance_monitor.py +285 -0
- crawlo/utils/queue_helper.py +175 -175
- crawlo/utils/redis_connection_pool.py +335 -0
- crawlo/utils/redis_key_validator.py +200 -0
- crawlo/utils/request.py +267 -267
- crawlo/utils/request_serializer.py +219 -219
- crawlo/utils/spider_loader.py +62 -62
- crawlo/utils/system.py +11 -11
- crawlo/utils/tools.py +4 -4
- crawlo/utils/url.py +39 -39
- {crawlo-1.1.4.dist-info → crawlo-1.1.5.dist-info}/METADATA +401 -403
- crawlo-1.1.5.dist-info/RECORD +185 -0
- examples/__init__.py +7 -7
- tests/__init__.py +7 -7
- tests/advanced_tools_example.py +276 -0
- tests/authenticated_proxy_example.py +237 -0
- tests/cleaners_example.py +161 -0
- tests/config_validation_demo.py +103 -0
- {examples → tests}/controlled_spider_example.py +205 -205
- tests/date_tools_example.py +181 -0
- tests/dynamic_loading_example.py +524 -0
- tests/dynamic_loading_test.py +105 -0
- tests/env_config_example.py +134 -0
- tests/error_handling_example.py +172 -0
- tests/redis_key_validation_demo.py +131 -0
- tests/response_improvements_example.py +145 -0
- tests/test_advanced_tools.py +149 -0
- tests/test_all_redis_key_configs.py +146 -0
- tests/test_authenticated_proxy.py +142 -0
- tests/test_cleaners.py +55 -0
- tests/test_comprehensive.py +147 -0
- tests/test_config_validator.py +194 -0
- tests/test_date_tools.py +124 -0
- tests/test_dynamic_downloaders_proxy.py +125 -0
- tests/test_dynamic_proxy.py +93 -0
- tests/test_dynamic_proxy_config.py +147 -0
- tests/test_dynamic_proxy_real.py +110 -0
- tests/test_edge_cases.py +304 -0
- tests/test_enhanced_error_handler.py +271 -0
- tests/test_env_config.py +122 -0
- tests/test_error_handler_compatibility.py +113 -0
- tests/test_final_validation.py +153 -153
- tests/test_framework_env_usage.py +104 -0
- tests/test_integration.py +357 -0
- tests/test_item_dedup_redis_key.py +123 -0
- tests/test_parsel.py +30 -0
- tests/test_performance.py +328 -0
- tests/test_proxy_health_check.py +32 -32
- tests/test_proxy_middleware_integration.py +136 -136
- tests/test_proxy_providers.py +56 -56
- tests/test_proxy_stats.py +19 -19
- tests/test_proxy_strategies.py +59 -59
- tests/test_queue_manager_redis_key.py +177 -0
- tests/test_redis_config.py +28 -28
- tests/test_redis_connection_pool.py +295 -0
- tests/test_redis_key_naming.py +182 -0
- tests/test_redis_key_validator.py +124 -0
- tests/test_redis_queue.py +224 -224
- tests/test_request_serialization.py +70 -70
- tests/test_response_improvements.py +153 -0
- tests/test_scheduler.py +241 -241
- tests/test_simple_response.py +62 -0
- tests/test_telecom_spider_redis_key.py +206 -0
- tests/test_template_content.py +88 -0
- tests/test_template_redis_key.py +135 -0
- tests/test_tools.py +154 -0
- tests/tools_example.py +258 -0
- crawlo/core/enhanced_engine.py +0 -190
- crawlo-1.1.4.dist-info/RECORD +0 -117
- {crawlo-1.1.4.dist-info → crawlo-1.1.5.dist-info}/WHEEL +0 -0
- {crawlo-1.1.4.dist-info → crawlo-1.1.5.dist-info}/entry_points.txt +0 -0
- {crawlo-1.1.4.dist-info → crawlo-1.1.5.dist-info}/top_level.txt +0 -0
crawlo/extension/__init__.py
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
#!/usr/bin/python
|
|
2
|
-
# -*- coding:UTF-8 -*-
|
|
3
|
-
from typing import List, Any
|
|
4
|
-
from pprint import pformat
|
|
5
|
-
|
|
6
|
-
from crawlo.utils.log import get_logger
|
|
7
|
-
from crawlo.project import load_class
|
|
8
|
-
from crawlo.exceptions import ExtensionInitError
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ExtensionManager(object):
|
|
12
|
-
|
|
13
|
-
def __init__(self, crawler: Any):
|
|
14
|
-
self.crawler = crawler
|
|
15
|
-
self.extensions: List = []
|
|
16
|
-
extensions = self.crawler.settings.get_list('EXTENSIONS')
|
|
17
|
-
self.logger = get_logger(self.__class__.__name__, crawler.settings.get('LOG_LEVEL'))
|
|
18
|
-
self._add_extensions(extensions)
|
|
19
|
-
|
|
20
|
-
@classmethod
|
|
21
|
-
def create_instance(cls, *args: Any, **kwargs: Any) -> 'ExtensionManager':
|
|
22
|
-
return cls(*args, **kwargs)
|
|
23
|
-
|
|
24
|
-
def _add_extensions(self, extensions: List[str]) -> None:
|
|
25
|
-
for extension_path in extensions:
|
|
26
|
-
try:
|
|
27
|
-
extension_cls = load_class(extension_path)
|
|
28
|
-
if not hasattr(extension_cls, 'create_instance'):
|
|
29
|
-
raise ExtensionInitError(
|
|
30
|
-
f"Extension '{extension_path}' init failed: Must have method 'create_instance()'"
|
|
31
|
-
)
|
|
32
|
-
self.extensions.append(extension_cls.create_instance(self.crawler))
|
|
33
|
-
except Exception as e:
|
|
34
|
-
self.logger.error(f"Failed to load extension '{extension_path}': {e}")
|
|
35
|
-
raise ExtensionInitError(f"Failed to load extension '{extension_path}': {e}")
|
|
36
|
-
|
|
37
|
-
if extensions:
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# -*- coding:UTF-8 -*-
|
|
3
|
+
from typing import List, Any
|
|
4
|
+
from pprint import pformat
|
|
5
|
+
|
|
6
|
+
from crawlo.utils.log import get_logger
|
|
7
|
+
from crawlo.project import load_class
|
|
8
|
+
from crawlo.exceptions import ExtensionInitError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExtensionManager(object):
|
|
12
|
+
|
|
13
|
+
def __init__(self, crawler: Any):
|
|
14
|
+
self.crawler = crawler
|
|
15
|
+
self.extensions: List = []
|
|
16
|
+
extensions = self.crawler.settings.get_list('EXTENSIONS')
|
|
17
|
+
self.logger = get_logger(self.__class__.__name__, crawler.settings.get('LOG_LEVEL'))
|
|
18
|
+
self._add_extensions(extensions)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def create_instance(cls, *args: Any, **kwargs: Any) -> 'ExtensionManager':
|
|
22
|
+
return cls(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def _add_extensions(self, extensions: List[str]) -> None:
|
|
25
|
+
for extension_path in extensions:
|
|
26
|
+
try:
|
|
27
|
+
extension_cls = load_class(extension_path)
|
|
28
|
+
if not hasattr(extension_cls, 'create_instance'):
|
|
29
|
+
raise ExtensionInitError(
|
|
30
|
+
f"Extension '{extension_path}' init failed: Must have method 'create_instance()'"
|
|
31
|
+
)
|
|
32
|
+
self.extensions.append(extension_cls.create_instance(self.crawler))
|
|
33
|
+
except Exception as e:
|
|
34
|
+
self.logger.error(f"Failed to load extension '{extension_path}': {e}")
|
|
35
|
+
raise ExtensionInitError(f"Failed to load extension '{extension_path}': {e}")
|
|
36
|
+
|
|
37
|
+
if extensions:
|
|
38
38
|
self.logger.info(f"Enabled extensions: \n{pformat(extensions)}")
|
crawlo/extension/health_check.py
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
#!/usr/bin/python
|
|
2
|
-
# -*- coding:UTF-8 -*-
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import Any, Optional, Dict
|
|
5
|
-
from datetime import datetime, timedelta
|
|
6
|
-
|
|
7
|
-
from crawlo.utils.log import get_logger
|
|
8
|
-
from crawlo.event import spider_opened, spider_closed, response_received, request_scheduled
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class HealthCheckExtension:
|
|
12
|
-
"""
|
|
13
|
-
健康检查扩展
|
|
14
|
-
监控爬虫的健康状态,包括响应时间、错误率等指标
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
def __init__(self, crawler: Any):
|
|
18
|
-
self.settings = crawler.settings
|
|
19
|
-
self.logger = get_logger(self.__class__.__name__, crawler.settings.get('LOG_LEVEL'))
|
|
20
|
-
|
|
21
|
-
# 获取配置参数
|
|
22
|
-
self.enabled = self.settings.get_bool('HEALTH_CHECK_ENABLED', True)
|
|
23
|
-
self.check_interval = self.settings.get_int('HEALTH_CHECK_INTERVAL', 60) # 默认60秒
|
|
24
|
-
|
|
25
|
-
# 健康状态统计
|
|
26
|
-
self.stats: Dict[str, Any] = {
|
|
27
|
-
'start_time': None,
|
|
28
|
-
'total_requests': 0,
|
|
29
|
-
'total_responses': 0,
|
|
30
|
-
'error_responses': 0,
|
|
31
|
-
'last_check_time': None,
|
|
32
|
-
'response_times': [], # 存储最近的响应时间
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
self.task: Optional[asyncio.Task] = None
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def create_instance(cls, crawler: Any) -> 'HealthCheckExtension':
|
|
39
|
-
# 只有当配置启用时才创建实例
|
|
40
|
-
if not crawler.settings.get_bool('HEALTH_CHECK_ENABLED', True):
|
|
41
|
-
from crawlo.exceptions import NotConfigured
|
|
42
|
-
raise NotConfigured("HealthCheckExtension: HEALTH_CHECK_ENABLED is False")
|
|
43
|
-
|
|
44
|
-
o = cls(crawler)
|
|
45
|
-
if o.enabled:
|
|
46
|
-
crawler.subscriber.subscribe(o.spider_opened, event=spider_opened)
|
|
47
|
-
crawler.subscriber.subscribe(o.spider_closed, event=spider_closed)
|
|
48
|
-
crawler.subscriber.subscribe(o.response_received, event=response_received)
|
|
49
|
-
crawler.subscriber.subscribe(o.request_scheduled, event=request_scheduled)
|
|
50
|
-
return o
|
|
51
|
-
|
|
52
|
-
async def spider_opened(self) -> None:
|
|
53
|
-
"""爬虫启动时初始化健康检查"""
|
|
54
|
-
if not self.enabled:
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
self.stats['start_time'] = datetime.now()
|
|
58
|
-
self.task = asyncio.create_task(self._health_check_loop())
|
|
59
|
-
self.logger.info("Health check extension started.")
|
|
60
|
-
|
|
61
|
-
async def spider_closed(self) -> None:
|
|
62
|
-
"""爬虫关闭时停止健康检查"""
|
|
63
|
-
if not self.enabled:
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
if self.task:
|
|
67
|
-
self.task.cancel()
|
|
68
|
-
try:
|
|
69
|
-
await self.task
|
|
70
|
-
except asyncio.CancelledError:
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
# 输出最终健康状态
|
|
74
|
-
await self._check_health()
|
|
75
|
-
self.logger.info("Health check extension stopped.")
|
|
76
|
-
|
|
77
|
-
async def request_scheduled(self, request: Any, spider: Any) -> None:
|
|
78
|
-
"""记录调度的请求"""
|
|
79
|
-
if not self.enabled:
|
|
80
|
-
return
|
|
81
|
-
self.stats['total_requests'] += 1
|
|
82
|
-
|
|
83
|
-
async def response_received(self, response: Any, spider: Any) -> None:
|
|
84
|
-
"""记录接收到的响应"""
|
|
85
|
-
if not self.enabled:
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
self.stats['total_responses'] += 1
|
|
89
|
-
|
|
90
|
-
# 记录错误响应
|
|
91
|
-
if hasattr(response, 'status_code') and response.status_code >= 400:
|
|
92
|
-
self.stats['error_responses'] += 1
|
|
93
|
-
|
|
94
|
-
async def _health_check_loop(self) -> None:
|
|
95
|
-
"""健康检查循环"""
|
|
96
|
-
while True:
|
|
97
|
-
try:
|
|
98
|
-
await asyncio.sleep(self.check_interval)
|
|
99
|
-
await self._check_health()
|
|
100
|
-
except asyncio.CancelledError:
|
|
101
|
-
break
|
|
102
|
-
except Exception as e:
|
|
103
|
-
self.logger.error(f"Error in health check loop: {e}")
|
|
104
|
-
|
|
105
|
-
async def _check_health(self) -> None:
|
|
106
|
-
"""执行健康检查并输出报告"""
|
|
107
|
-
try:
|
|
108
|
-
now_time = datetime.now()
|
|
109
|
-
self.stats['last_check_time'] = now_time
|
|
110
|
-
|
|
111
|
-
# 计算基本统计信息
|
|
112
|
-
runtime = (now_time - self.stats['start_time']).total_seconds() if self.stats['start_time'] else 0
|
|
113
|
-
requests_per_second = self.stats['total_requests'] / runtime if runtime > 0 else 0
|
|
114
|
-
responses_per_second = self.stats['total_responses'] / runtime if runtime > 0 else 0
|
|
115
|
-
|
|
116
|
-
# 计算错误率
|
|
117
|
-
error_rate = (
|
|
118
|
-
self.stats['error_responses'] / self.stats['total_responses']
|
|
119
|
-
if self.stats['total_responses'] > 0 else 0
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
# 输出健康报告
|
|
123
|
-
health_report = {
|
|
124
|
-
'runtime_seconds': round(runtime, 2),
|
|
125
|
-
'total_requests': self.stats['total_requests'],
|
|
126
|
-
'total_responses': self.stats['total_responses'],
|
|
127
|
-
'requests_per_second': round(requests_per_second, 2),
|
|
128
|
-
'responses_per_second': round(responses_per_second, 2),
|
|
129
|
-
'error_responses': self.stats['error_responses'],
|
|
130
|
-
'error_rate': f"{error_rate:.2%}",
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
# 根据错误率判断健康状态
|
|
134
|
-
if error_rate > 0.1: # 错误率超过10%
|
|
135
|
-
self.logger.warning(f"Health check report: {health_report}")
|
|
136
|
-
elif error_rate > 0.05: # 错误率超过5%
|
|
137
|
-
self.logger.info(f"Health check report: {health_report}")
|
|
138
|
-
else:
|
|
139
|
-
self.logger.debug(f"Health check report: {health_report}")
|
|
140
|
-
|
|
141
|
-
except Exception as e:
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# -*- coding:UTF-8 -*-
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Optional, Dict
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
from crawlo.utils.log import get_logger
|
|
8
|
+
from crawlo.event import spider_opened, spider_closed, response_received, request_scheduled
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HealthCheckExtension:
|
|
12
|
+
"""
|
|
13
|
+
健康检查扩展
|
|
14
|
+
监控爬虫的健康状态,包括响应时间、错误率等指标
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, crawler: Any):
|
|
18
|
+
self.settings = crawler.settings
|
|
19
|
+
self.logger = get_logger(self.__class__.__name__, crawler.settings.get('LOG_LEVEL'))
|
|
20
|
+
|
|
21
|
+
# 获取配置参数
|
|
22
|
+
self.enabled = self.settings.get_bool('HEALTH_CHECK_ENABLED', True)
|
|
23
|
+
self.check_interval = self.settings.get_int('HEALTH_CHECK_INTERVAL', 60) # 默认60秒
|
|
24
|
+
|
|
25
|
+
# 健康状态统计
|
|
26
|
+
self.stats: Dict[str, Any] = {
|
|
27
|
+
'start_time': None,
|
|
28
|
+
'total_requests': 0,
|
|
29
|
+
'total_responses': 0,
|
|
30
|
+
'error_responses': 0,
|
|
31
|
+
'last_check_time': None,
|
|
32
|
+
'response_times': [], # 存储最近的响应时间
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
self.task: Optional[asyncio.Task] = None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def create_instance(cls, crawler: Any) -> 'HealthCheckExtension':
|
|
39
|
+
# 只有当配置启用时才创建实例
|
|
40
|
+
if not crawler.settings.get_bool('HEALTH_CHECK_ENABLED', True):
|
|
41
|
+
from crawlo.exceptions import NotConfigured
|
|
42
|
+
raise NotConfigured("HealthCheckExtension: HEALTH_CHECK_ENABLED is False")
|
|
43
|
+
|
|
44
|
+
o = cls(crawler)
|
|
45
|
+
if o.enabled:
|
|
46
|
+
crawler.subscriber.subscribe(o.spider_opened, event=spider_opened)
|
|
47
|
+
crawler.subscriber.subscribe(o.spider_closed, event=spider_closed)
|
|
48
|
+
crawler.subscriber.subscribe(o.response_received, event=response_received)
|
|
49
|
+
crawler.subscriber.subscribe(o.request_scheduled, event=request_scheduled)
|
|
50
|
+
return o
|
|
51
|
+
|
|
52
|
+
async def spider_opened(self) -> None:
|
|
53
|
+
"""爬虫启动时初始化健康检查"""
|
|
54
|
+
if not self.enabled:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
self.stats['start_time'] = datetime.now()
|
|
58
|
+
self.task = asyncio.create_task(self._health_check_loop())
|
|
59
|
+
self.logger.info("Health check extension started.")
|
|
60
|
+
|
|
61
|
+
async def spider_closed(self) -> None:
|
|
62
|
+
"""爬虫关闭时停止健康检查"""
|
|
63
|
+
if not self.enabled:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if self.task:
|
|
67
|
+
self.task.cancel()
|
|
68
|
+
try:
|
|
69
|
+
await self.task
|
|
70
|
+
except asyncio.CancelledError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# 输出最终健康状态
|
|
74
|
+
await self._check_health()
|
|
75
|
+
self.logger.info("Health check extension stopped.")
|
|
76
|
+
|
|
77
|
+
async def request_scheduled(self, request: Any, spider: Any) -> None:
|
|
78
|
+
"""记录调度的请求"""
|
|
79
|
+
if not self.enabled:
|
|
80
|
+
return
|
|
81
|
+
self.stats['total_requests'] += 1
|
|
82
|
+
|
|
83
|
+
async def response_received(self, response: Any, spider: Any) -> None:
|
|
84
|
+
"""记录接收到的响应"""
|
|
85
|
+
if not self.enabled:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
self.stats['total_responses'] += 1
|
|
89
|
+
|
|
90
|
+
# 记录错误响应
|
|
91
|
+
if hasattr(response, 'status_code') and response.status_code >= 400:
|
|
92
|
+
self.stats['error_responses'] += 1
|
|
93
|
+
|
|
94
|
+
async def _health_check_loop(self) -> None:
|
|
95
|
+
"""健康检查循环"""
|
|
96
|
+
while True:
|
|
97
|
+
try:
|
|
98
|
+
await asyncio.sleep(self.check_interval)
|
|
99
|
+
await self._check_health()
|
|
100
|
+
except asyncio.CancelledError:
|
|
101
|
+
break
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self.logger.error(f"Error in health check loop: {e}")
|
|
104
|
+
|
|
105
|
+
async def _check_health(self) -> None:
|
|
106
|
+
"""执行健康检查并输出报告"""
|
|
107
|
+
try:
|
|
108
|
+
now_time = datetime.now()
|
|
109
|
+
self.stats['last_check_time'] = now_time
|
|
110
|
+
|
|
111
|
+
# 计算基本统计信息
|
|
112
|
+
runtime = (now_time - self.stats['start_time']).total_seconds() if self.stats['start_time'] else 0
|
|
113
|
+
requests_per_second = self.stats['total_requests'] / runtime if runtime > 0 else 0
|
|
114
|
+
responses_per_second = self.stats['total_responses'] / runtime if runtime > 0 else 0
|
|
115
|
+
|
|
116
|
+
# 计算错误率
|
|
117
|
+
error_rate = (
|
|
118
|
+
self.stats['error_responses'] / self.stats['total_responses']
|
|
119
|
+
if self.stats['total_responses'] > 0 else 0
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# 输出健康报告
|
|
123
|
+
health_report = {
|
|
124
|
+
'runtime_seconds': round(runtime, 2),
|
|
125
|
+
'total_requests': self.stats['total_requests'],
|
|
126
|
+
'total_responses': self.stats['total_responses'],
|
|
127
|
+
'requests_per_second': round(requests_per_second, 2),
|
|
128
|
+
'responses_per_second': round(responses_per_second, 2),
|
|
129
|
+
'error_responses': self.stats['error_responses'],
|
|
130
|
+
'error_rate': f"{error_rate:.2%}",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# 根据错误率判断健康状态
|
|
134
|
+
if error_rate > 0.1: # 错误率超过10%
|
|
135
|
+
self.logger.warning(f"Health check report: {health_report}")
|
|
136
|
+
elif error_rate > 0.05: # 错误率超过5%
|
|
137
|
+
self.logger.info(f"Health check report: {health_report}")
|
|
138
|
+
else:
|
|
139
|
+
self.logger.debug(f"Health check report: {health_report}")
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
142
|
self.logger.error(f"Error in health check: {e}")
|
crawlo/extension/log_interval.py
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
#!/usr/bin/python
|
|
2
|
-
# -*- coding:UTF-8 -*-
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import Any, Optional
|
|
5
|
-
|
|
6
|
-
from crawlo.utils.log import get_logger
|
|
7
|
-
from crawlo.event import spider_opened, spider_closed
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class LogIntervalExtension(object):
|
|
11
|
-
|
|
12
|
-
def __init__(self, crawler: Any):
|
|
13
|
-
self.task: Optional[asyncio.Task] = None
|
|
14
|
-
self.stats = crawler.stats
|
|
15
|
-
self.item_count = 0
|
|
16
|
-
self.response_count = 0
|
|
17
|
-
self.seconds = crawler.settings.get('INTERVAL', 60) # 默认60秒
|
|
18
|
-
self.interval = int(self.seconds / 60) if self.seconds % 60 == 0 else self.seconds
|
|
19
|
-
self.interval = "" if self.interval == 1 else self.interval
|
|
20
|
-
self.unit = 'min' if self.seconds % 60 == 0 else 's'
|
|
21
|
-
|
|
22
|
-
self.logger = get_logger(self.__class__.__name__, crawler.settings.get('LOG_LEVEL'))
|
|
23
|
-
|
|
24
|
-
@classmethod
|
|
25
|
-
def create_instance(cls, crawler: Any) -> 'LogIntervalExtension':
|
|
26
|
-
o = cls(crawler)
|
|
27
|
-
crawler.subscriber.subscribe(o.spider_opened, event=spider_opened)
|
|
28
|
-
crawler.subscriber.subscribe(o.spider_closed, event=spider_closed)
|
|
29
|
-
return o
|
|
30
|
-
|
|
31
|
-
async def spider_opened(self) -> None:
|
|
32
|
-
self.task = asyncio.create_task(self.interval_log())
|
|
33
|
-
|
|
34
|
-
async def spider_closed(self) -> None:
|
|
35
|
-
if self.task:
|
|
36
|
-
self.task.cancel()
|
|
37
|
-
try:
|
|
38
|
-
await self.task
|
|
39
|
-
except asyncio.CancelledError:
|
|
40
|
-
pass
|
|
41
|
-
self.task = None
|
|
42
|
-
|
|
43
|
-
async def interval_log(self) -> None:
|
|
44
|
-
while True:
|
|
45
|
-
try:
|
|
46
|
-
last_item_count = self.stats.get_value('item_successful_count', default=0)
|
|
47
|
-
last_response_count = self.stats.get_value('response_received_count', default=0)
|
|
48
|
-
item_rate = last_item_count - self.item_count
|
|
49
|
-
response_rate = last_response_count - self.response_count
|
|
50
|
-
self.item_count, self.response_count = last_item_count, last_response_count
|
|
51
|
-
self.logger.info(
|
|
52
|
-
f'Crawled {last_response_count} pages (at {response_rate} pages/{self.interval}{self.unit}),'
|
|
53
|
-
f' Got {last_item_count} items (at {item_rate} items/{self.interval}{self.unit}).'
|
|
54
|
-
)
|
|
55
|
-
await asyncio.sleep(self.seconds)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
self.logger.error(f"Error in interval logging: {e}")
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# -*- coding:UTF-8 -*-
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from crawlo.utils.log import get_logger
|
|
7
|
+
from crawlo.event import spider_opened, spider_closed
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogIntervalExtension(object):
|
|
11
|
+
|
|
12
|
+
def __init__(self, crawler: Any):
|
|
13
|
+
self.task: Optional[asyncio.Task] = None
|
|
14
|
+
self.stats = crawler.stats
|
|
15
|
+
self.item_count = 0
|
|
16
|
+
self.response_count = 0
|
|
17
|
+
self.seconds = crawler.settings.get('INTERVAL', 60) # 默认60秒
|
|
18
|
+
self.interval = int(self.seconds / 60) if self.seconds % 60 == 0 else self.seconds
|
|
19
|
+
self.interval = "" if self.interval == 1 else self.interval
|
|
20
|
+
self.unit = 'min' if self.seconds % 60 == 0 else 's'
|
|
21
|
+
|
|
22
|
+
self.logger = get_logger(self.__class__.__name__, crawler.settings.get('LOG_LEVEL'))
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def create_instance(cls, crawler: Any) -> 'LogIntervalExtension':
|
|
26
|
+
o = cls(crawler)
|
|
27
|
+
crawler.subscriber.subscribe(o.spider_opened, event=spider_opened)
|
|
28
|
+
crawler.subscriber.subscribe(o.spider_closed, event=spider_closed)
|
|
29
|
+
return o
|
|
30
|
+
|
|
31
|
+
async def spider_opened(self) -> None:
|
|
32
|
+
self.task = asyncio.create_task(self.interval_log())
|
|
33
|
+
|
|
34
|
+
async def spider_closed(self) -> None:
|
|
35
|
+
if self.task:
|
|
36
|
+
self.task.cancel()
|
|
37
|
+
try:
|
|
38
|
+
await self.task
|
|
39
|
+
except asyncio.CancelledError:
|
|
40
|
+
pass
|
|
41
|
+
self.task = None
|
|
42
|
+
|
|
43
|
+
async def interval_log(self) -> None:
|
|
44
|
+
while True:
|
|
45
|
+
try:
|
|
46
|
+
last_item_count = self.stats.get_value('item_successful_count', default=0)
|
|
47
|
+
last_response_count = self.stats.get_value('response_received_count', default=0)
|
|
48
|
+
item_rate = last_item_count - self.item_count
|
|
49
|
+
response_rate = last_response_count - self.response_count
|
|
50
|
+
self.item_count, self.response_count = last_item_count, last_response_count
|
|
51
|
+
self.logger.info(
|
|
52
|
+
f'Crawled {last_response_count} pages (at {response_rate} pages/{self.interval}{self.unit}),'
|
|
53
|
+
f' Got {last_item_count} items (at {item_rate} items/{self.interval}{self.unit}).'
|
|
54
|
+
)
|
|
55
|
+
await asyncio.sleep(self.seconds)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
self.logger.error(f"Error in interval logging: {e}")
|
|
58
58
|
await asyncio.sleep(self.seconds) # 即使出错也继续执行
|
crawlo/extension/log_stats.py
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
#!/usr/bin/python
|
|
2
|
-
# -*- coding:UTF-8 -*-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from crawlo import event
|
|
6
|
-
from crawlo.utils.date_tools import now, time_diff
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class LogStats(object):
|
|
10
|
-
|
|
11
|
-
def __init__(self, stats: Any):
|
|
12
|
-
self._stats = stats
|
|
13
|
-
|
|
14
|
-
@classmethod
|
|
15
|
-
def create_instance(cls, crawler: Any) -> 'LogStats':
|
|
16
|
-
o = cls(crawler.stats)
|
|
17
|
-
# 订阅所有需要的事件
|
|
18
|
-
event_subscriptions = [
|
|
19
|
-
(o.spider_opened, event.spider_opened),
|
|
20
|
-
(o.spider_closed, event.spider_closed),
|
|
21
|
-
(o.item_successful, event.item_successful),
|
|
22
|
-
(o.item_discard, event.item_discard),
|
|
23
|
-
(o.response_received, event.response_received),
|
|
24
|
-
(o.request_scheduled, event.request_scheduled),
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
for handler, evt in event_subscriptions:
|
|
28
|
-
try:
|
|
29
|
-
crawler.subscriber.subscribe(handler, event=evt)
|
|
30
|
-
except Exception as e:
|
|
31
|
-
# 获取日志记录器并记录错误
|
|
32
|
-
from crawlo.utils.log import get_logger
|
|
33
|
-
logger = get_logger(cls.__name__)
|
|
34
|
-
logger.error(f"Failed to subscribe to event {evt}: {e}")
|
|
35
|
-
|
|
36
|
-
return o
|
|
37
|
-
|
|
38
|
-
async def spider_opened(self) -> None:
|
|
39
|
-
try:
|
|
40
|
-
self._stats['start_time'] = now(fmt='%Y-%m-%d %H:%M:%S')
|
|
41
|
-
except Exception as e:
|
|
42
|
-
# 静默处理,避免影响爬虫运行
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
async def spider_closed(self) -> None:
|
|
46
|
-
try:
|
|
47
|
-
self._stats['end_time'] = now(fmt='%Y-%m-%d %H:%M:%S')
|
|
48
|
-
self._stats['cost_time(s)'] = time_diff(start=self._stats['start_time'], end=self._stats['end_time'])
|
|
49
|
-
except Exception as e:
|
|
50
|
-
# 静默处理,避免影响爬虫运行
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
async def item_successful(self, _item: Any, _spider: Any) -> None:
|
|
54
|
-
try:
|
|
55
|
-
self._stats.inc_value('item_successful_count')
|
|
56
|
-
except Exception as e:
|
|
57
|
-
# 静默处理,避免影响爬虫运行
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
async def item_discard(self, _item: Any, exc: Any, _spider: Any) -> None:
|
|
61
|
-
try:
|
|
62
|
-
self._stats.inc_value('item_discard_count')
|
|
63
|
-
reason = getattr(exc, 'msg', None) # 更安全地获取属性
|
|
64
|
-
if reason:
|
|
65
|
-
self._stats.inc_value(f"item_discard/{reason}")
|
|
66
|
-
except Exception as e:
|
|
67
|
-
# 静默处理,避免影响爬虫运行
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
async def response_received(self, _response: Any, _spider: Any) -> None:
|
|
71
|
-
try:
|
|
72
|
-
self._stats.inc_value('response_received_count')
|
|
73
|
-
except Exception as e:
|
|
74
|
-
# 静默处理,避免影响爬虫运行
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
async def request_scheduled(self, _request: Any, _spider: Any) -> None:
|
|
78
|
-
try:
|
|
79
|
-
self._stats.inc_value('request_scheduler_count')
|
|
80
|
-
except Exception as e:
|
|
81
|
-
# 静默处理,避免影响爬虫运行
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# -*- coding:UTF-8 -*-
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from crawlo import event
|
|
6
|
+
from crawlo.utils.date_tools import now, time_diff
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LogStats(object):
|
|
10
|
+
|
|
11
|
+
def __init__(self, stats: Any):
|
|
12
|
+
self._stats = stats
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def create_instance(cls, crawler: Any) -> 'LogStats':
|
|
16
|
+
o = cls(crawler.stats)
|
|
17
|
+
# 订阅所有需要的事件
|
|
18
|
+
event_subscriptions = [
|
|
19
|
+
(o.spider_opened, event.spider_opened),
|
|
20
|
+
(o.spider_closed, event.spider_closed),
|
|
21
|
+
(o.item_successful, event.item_successful),
|
|
22
|
+
(o.item_discard, event.item_discard),
|
|
23
|
+
(o.response_received, event.response_received),
|
|
24
|
+
(o.request_scheduled, event.request_scheduled),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
for handler, evt in event_subscriptions:
|
|
28
|
+
try:
|
|
29
|
+
crawler.subscriber.subscribe(handler, event=evt)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
# 获取日志记录器并记录错误
|
|
32
|
+
from crawlo.utils.log import get_logger
|
|
33
|
+
logger = get_logger(cls.__name__)
|
|
34
|
+
logger.error(f"Failed to subscribe to event {evt}: {e}")
|
|
35
|
+
|
|
36
|
+
return o
|
|
37
|
+
|
|
38
|
+
async def spider_opened(self) -> None:
|
|
39
|
+
try:
|
|
40
|
+
self._stats['start_time'] = now(fmt='%Y-%m-%d %H:%M:%S')
|
|
41
|
+
except Exception as e:
|
|
42
|
+
# 静默处理,避免影响爬虫运行
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
async def spider_closed(self) -> None:
|
|
46
|
+
try:
|
|
47
|
+
self._stats['end_time'] = now(fmt='%Y-%m-%d %H:%M:%S')
|
|
48
|
+
self._stats['cost_time(s)'] = time_diff(start=self._stats['start_time'], end=self._stats['end_time'])
|
|
49
|
+
except Exception as e:
|
|
50
|
+
# 静默处理,避免影响爬虫运行
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
async def item_successful(self, _item: Any, _spider: Any) -> None:
|
|
54
|
+
try:
|
|
55
|
+
self._stats.inc_value('item_successful_count')
|
|
56
|
+
except Exception as e:
|
|
57
|
+
# 静默处理,避免影响爬虫运行
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
async def item_discard(self, _item: Any, exc: Any, _spider: Any) -> None:
|
|
61
|
+
try:
|
|
62
|
+
self._stats.inc_value('item_discard_count')
|
|
63
|
+
reason = getattr(exc, 'msg', None) # 更安全地获取属性
|
|
64
|
+
if reason:
|
|
65
|
+
self._stats.inc_value(f"item_discard/{reason}")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
# 静默处理,避免影响爬虫运行
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
async def response_received(self, _response: Any, _spider: Any) -> None:
|
|
71
|
+
try:
|
|
72
|
+
self._stats.inc_value('response_received_count')
|
|
73
|
+
except Exception as e:
|
|
74
|
+
# 静默处理,避免影响爬虫运行
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
async def request_scheduled(self, _request: Any, _spider: Any) -> None:
|
|
78
|
+
try:
|
|
79
|
+
self._stats.inc_value('request_scheduler_count')
|
|
80
|
+
except Exception as e:
|
|
81
|
+
# 静默处理,避免影响爬虫运行
|
|
82
82
|
pass
|