crawlo 1.4.5__py3-none-any.whl → 1.4.6__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/__version__.py +1 -1
- crawlo/downloader/cffi_downloader.py +3 -1
- crawlo/middleware/proxy.py +171 -348
- crawlo/pipelines/mysql_pipeline.py +339 -188
- crawlo/settings/default_settings.py +38 -30
- crawlo/stats_collector.py +10 -1
- crawlo/templates/project/settings.py.tmpl +10 -55
- crawlo/templates/project/settings_distributed.py.tmpl +20 -22
- crawlo/templates/project/settings_gentle.py.tmpl +5 -0
- crawlo/templates/project/settings_high_performance.py.tmpl +5 -0
- crawlo/templates/project/settings_minimal.py.tmpl +25 -1
- crawlo/templates/project/settings_simple.py.tmpl +5 -0
- crawlo/templates/run.py.tmpl +1 -8
- crawlo/templates/spider/spider.py.tmpl +5 -108
- crawlo/utils/db_helper.py +11 -5
- {crawlo-1.4.5.dist-info → crawlo-1.4.6.dist-info}/METADATA +1 -1
- {crawlo-1.4.5.dist-info → crawlo-1.4.6.dist-info}/RECORD +43 -29
- tests/authenticated_proxy_example.py +10 -6
- tests/explain_mysql_update_behavior.py +77 -0
- tests/simulate_mysql_update_test.py +140 -0
- tests/test_asyncmy_usage.py +57 -0
- tests/test_crawlo_proxy_integration.py +8 -2
- tests/test_downloader_proxy_compatibility.py +24 -20
- tests/test_mysql_pipeline_config.py +165 -0
- tests/test_mysql_pipeline_error.py +99 -0
- tests/test_mysql_pipeline_init_log.py +83 -0
- tests/test_mysql_pipeline_integration.py +133 -0
- tests/test_mysql_pipeline_refactor.py +144 -0
- tests/test_mysql_pipeline_refactor_simple.py +86 -0
- tests/test_mysql_pipeline_robustness.py +196 -0
- tests/test_mysql_pipeline_types.py +89 -0
- tests/test_mysql_update_columns.py +94 -0
- tests/test_proxy_middleware.py +104 -8
- tests/test_proxy_middleware_enhanced.py +1 -5
- tests/test_proxy_middleware_integration.py +7 -2
- tests/test_proxy_middleware_refactored.py +25 -2
- tests/test_proxy_only.py +84 -0
- tests/test_proxy_with_downloader.py +153 -0
- tests/test_real_scenario_proxy.py +17 -17
- tests/verify_mysql_warnings.py +110 -0
- crawlo/middleware/simple_proxy.py +0 -65
- {crawlo-1.4.5.dist-info → crawlo-1.4.6.dist-info}/WHEEL +0 -0
- {crawlo-1.4.5.dist-info → crawlo-1.4.6.dist-info}/entry_points.txt +0 -0
- {crawlo-1.4.5.dist-info → crawlo-1.4.6.dist-info}/top_level.txt +0 -0
|
@@ -64,6 +64,10 @@ MYSQL_DB = 'crawl_pro'
|
|
|
64
64
|
MYSQL_TABLE = 'crawlo'
|
|
65
65
|
MYSQL_BATCH_SIZE = 100
|
|
66
66
|
MYSQL_USE_BATCH = False # 是否启用批量插入
|
|
67
|
+
# MySQL SQL生成行为控制配置
|
|
68
|
+
MYSQL_AUTO_UPDATE = False # 是否使用 REPLACE INTO(完全覆盖已存在记录)
|
|
69
|
+
MYSQL_INSERT_IGNORE = False # 是否使用 INSERT IGNORE(忽略重复数据)
|
|
70
|
+
MYSQL_UPDATE_COLUMNS = () # 冲突时需更新的列名;指定后 MYSQL_AUTO_UPDATE 失效
|
|
67
71
|
|
|
68
72
|
# Redis配置
|
|
69
73
|
redis_config = get_redis_config()
|
|
@@ -176,15 +180,39 @@ RANDOM_USER_AGENT_ENABLED = False # 是否启用随机用户代理
|
|
|
176
180
|
# 站外过滤配置
|
|
177
181
|
ALLOWED_DOMAINS = [] # 允许的域名列表
|
|
178
182
|
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
# 代理配置(通用版,支持静态代理列表和动态代理API两种模式)
|
|
184
|
+
PROXY_LIST = [] # 静态代理列表配置
|
|
185
|
+
PROXY_API_URL = "" # 动态代理API配置
|
|
186
|
+
# 代理提取配置,用于指定如何从API返回的数据中提取代理地址
|
|
187
|
+
# 可选值:
|
|
188
|
+
# - 字符串:直接作为字段名使用,如 "proxy"(默认值)
|
|
189
|
+
# - 字典:包含type和value字段,支持多种提取方式
|
|
190
|
+
# - {"type": "field", "value": "data"}:从指定字段提取
|
|
191
|
+
# - {"type": "jsonpath", "value": "$.data[0].proxy"}:使用JSONPath表达式提取
|
|
192
|
+
# - {"type": "custom", "function": your_function}:使用自定义函数提取
|
|
193
|
+
PROXY_EXTRACTOR = "proxy" # 代理提取配置
|
|
194
|
+
# 代理失败处理配置
|
|
195
|
+
PROXY_MAX_FAILED_ATTEMPTS = 3 # 代理最大失败尝试次数,超过此次数将标记为失效
|
|
196
|
+
|
|
197
|
+
# 代理使用示例:
|
|
198
|
+
# 1. 静态代理列表:
|
|
199
|
+
# PROXY_LIST = ["http://proxy1:8080", "http://proxy2:8080"]
|
|
200
|
+
# PROXY_API_URL = "" # 不使用动态代理
|
|
201
|
+
#
|
|
202
|
+
# 2. 动态代理API(默认字段提取):
|
|
203
|
+
# PROXY_LIST = [] # 不使用静态代理
|
|
204
|
+
# PROXY_API_URL = "http://api.example.com/get_proxy"
|
|
205
|
+
# PROXY_EXTRACTOR = "proxy" # 从"proxy"字段提取
|
|
206
|
+
#
|
|
207
|
+
# 3. 动态代理API(自定义字段提取):
|
|
208
|
+
# PROXY_LIST = [] # 不使用静态代理
|
|
209
|
+
# PROXY_API_URL = "http://api.example.com/get_proxy"
|
|
210
|
+
# PROXY_EXTRACTOR = "data" # 从"data"字段提取
|
|
211
|
+
#
|
|
212
|
+
# 4. 动态代理API(嵌套字段提取):
|
|
213
|
+
# PROXY_LIST = [] # 不使用静态代理
|
|
214
|
+
# PROXY_API_URL = "http://api.example.com/get_proxy"
|
|
215
|
+
# PROXY_EXTRACTOR = {"type": "field", "value": "result"} # 从"result"字段提取
|
|
188
216
|
|
|
189
217
|
# 下载器通用配置
|
|
190
218
|
DOWNLOAD_TIMEOUT = 30 # 下载超时时间(秒)
|
|
@@ -247,24 +275,4 @@ PLAYWRIGHT_MAX_PAGES_PER_BROWSER = 10 # 单浏览器最大页面数量
|
|
|
247
275
|
|
|
248
276
|
# 通用优化配置
|
|
249
277
|
CONNECTION_TTL_DNS_CACHE = 300 # DNS缓存TTL(秒)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
# --------------------------------- 9. 数据存储配置 ------------------------------------
|
|
253
|
-
|
|
254
|
-
# CSV管道配置
|
|
255
|
-
CSV_DELIMITER = ',' # CSV分隔符
|
|
256
|
-
CSV_QUOTECHAR = '"' # CSV引号字符
|
|
257
|
-
CSV_INCLUDE_HEADERS = True # 是否包含表头
|
|
258
|
-
CSV_EXTRASACTION = 'ignore' # 额外字段处理方式:ignore, raise
|
|
259
|
-
CSV_FIELDNAMES = None # 字段名列表
|
|
260
|
-
CSV_FILE = None # CSV文件路径
|
|
261
|
-
CSV_DICT_FILE = None # CSV字典文件路径
|
|
262
|
-
CSV_BATCH_SIZE = 100 # CSV批处理大小
|
|
263
|
-
CSV_BATCH_FILE = None # CSV批处理文件路径
|
|
264
|
-
|
|
265
|
-
# 数据库去重管道配置
|
|
266
|
-
DB_HOST = 'localhost' # 数据库主机
|
|
267
|
-
DB_PORT = 3306 # 数据库端口
|
|
268
|
-
DB_USER = 'root' # 数据库用户
|
|
269
|
-
DB_PASSWORD = '' # 数据库密码
|
|
270
|
-
DB_NAME = 'crawlo' # 数据库名称
|
|
278
|
+
CONNECTION_KEEPALIVE = True # 是否启用HTTP连接保持
|
crawlo/stats_collector.py
CHANGED
|
@@ -69,5 +69,14 @@ class StatsCollector(object):
|
|
|
69
69
|
# 同时更新_stats中的spider_name
|
|
70
70
|
self._stats['spider_name'] = spider_name
|
|
71
71
|
|
|
72
|
+
# 对统计信息中的浮点数进行四舍五入处理
|
|
73
|
+
formatted_stats = {}
|
|
74
|
+
for key, value in self._stats.items():
|
|
75
|
+
if isinstance(value, float):
|
|
76
|
+
# 对浮点数进行四舍五入,保留2位小数
|
|
77
|
+
formatted_stats[key] = round(value, 2)
|
|
78
|
+
else:
|
|
79
|
+
formatted_stats[key] = value
|
|
80
|
+
|
|
72
81
|
# 输出统计信息(这是唯一输出统计信息的地方)
|
|
73
|
-
self.logger.info(f'{spider_name} stats: \n{pformat(
|
|
82
|
+
self.logger.info(f'{spider_name} stats: \n{pformat(formatted_stats)}')
|
|
@@ -87,6 +87,11 @@ MYSQL_TABLE = '{{project_name}}_data'
|
|
|
87
87
|
MYSQL_BATCH_SIZE = 100
|
|
88
88
|
MYSQL_USE_BATCH = False # 是否启用批量插入
|
|
89
89
|
|
|
90
|
+
# MySQL SQL生成行为控制配置
|
|
91
|
+
MYSQL_AUTO_UPDATE = False # 是否使用 REPLACE INTO(完全覆盖已存在记录)
|
|
92
|
+
MYSQL_INSERT_IGNORE = False # 是否使用 INSERT IGNORE(忽略重复数据)
|
|
93
|
+
MYSQL_UPDATE_COLUMNS = () # 冲突时需更新的列名;指定后 MYSQL_AUTO_UPDATE 失效
|
|
94
|
+
|
|
90
95
|
# MongoDB配置
|
|
91
96
|
MONGO_URI = 'mongodb://localhost:27017'
|
|
92
97
|
MONGO_DATABASE = '{{project_name}}_db'
|
|
@@ -96,62 +101,12 @@ MONGO_MIN_POOL_SIZE = 20
|
|
|
96
101
|
MONGO_BATCH_SIZE = 100 # 批量插入条数
|
|
97
102
|
MONGO_USE_BATCH = False # 是否启用批量插入
|
|
98
103
|
|
|
99
|
-
# ===================================
|
|
100
|
-
|
|
101
|
-
# 代理配置
|
|
102
|
-
# 代理功能默认不启用,如需使用请在项目配置文件中启用并配置相关参数
|
|
103
|
-
PROXY_ENABLED = False # 是否启用代理
|
|
104
|
+
# =================================== 代理配置 ===================================
|
|
104
105
|
|
|
105
106
|
# 简化版代理配置(适用于SimpleProxyMiddleware)
|
|
106
|
-
|
|
107
|
+
# 只要配置了代理列表,中间件就会自动启用
|
|
108
|
+
# PROXY_LIST = ["http://proxy1:8080", "http://proxy2:8080"]
|
|
107
109
|
|
|
108
110
|
# 高级代理配置(适用于ProxyMiddleware)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# 代理提取方式(支持字段路径或函数)
|
|
112
|
-
# 示例: "proxy" 适用于 {"proxy": "http://1.1.1.1:8080"}
|
|
113
|
-
# 示例: "data.proxy" 适用于 {"data": {"proxy": "http://1.1.1.1:8080"}}
|
|
114
|
-
PROXY_EXTRACTOR = "proxy"
|
|
115
|
-
|
|
116
|
-
# 代理刷新控制
|
|
117
|
-
PROXY_REFRESH_INTERVAL = 60 # 代理刷新间隔(秒)
|
|
118
|
-
PROXY_API_TIMEOUT = 10 # 请求代理 API 超时时间
|
|
119
|
-
|
|
120
|
-
# 浏览器指纹模拟(仅 CurlCffi 下载器有效)
|
|
121
|
-
CURL_BROWSER_TYPE = "chrome" # 可选: chrome, edge, safari, firefox 或版本如 chrome136
|
|
122
|
-
|
|
123
|
-
# 自定义浏览器版本映射(可覆盖默认行为)
|
|
124
|
-
CURL_BROWSER_VERSION_MAP = {
|
|
125
|
-
"chrome": "chrome136",
|
|
126
|
-
"edge": "edge101",
|
|
127
|
-
"safari": "safari184",
|
|
128
|
-
"firefox": "firefox135",
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
# 下载器优化配置
|
|
132
|
-
# 下载器健康检查
|
|
133
|
-
DOWNLOADER_HEALTH_CHECK = True # 是否启用下载器健康检查
|
|
134
|
-
HEALTH_CHECK_INTERVAL = 60 # 健康检查间隔(秒)
|
|
135
|
-
|
|
136
|
-
# 请求统计配置
|
|
137
|
-
REQUEST_STATS_ENABLED = True # 是否启用请求统计
|
|
138
|
-
STATS_RESET_ON_START = False # 启动时是否重置统计
|
|
139
|
-
|
|
140
|
-
# HttpX 下载器专用配置
|
|
141
|
-
HTTPX_HTTP2 = True # 是否启用HTTP/2支持
|
|
142
|
-
HTTPX_FOLLOW_REDIRECTS = True # 是否自动跟随重定向
|
|
143
|
-
|
|
144
|
-
# AioHttp 下载器专用配置
|
|
145
|
-
AIOHTTP_AUTO_DECOMPRESS = True # 是否自动解压响应
|
|
146
|
-
AIOHTTP_FORCE_CLOSE = False # 是否强制关闭连接
|
|
147
|
-
|
|
148
|
-
# 通用优化配置
|
|
149
|
-
CONNECTION_TTL_DNS_CACHE = 300 # DNS缓存TTL(秒)
|
|
150
|
-
CONNECTION_KEEPALIVE_TIMEOUT = 15 # Keep-Alive超时(秒)
|
|
151
|
-
|
|
152
|
-
# 内存监控配置
|
|
153
|
-
# 内存监控扩展默认不启用,如需使用请在项目配置文件中启用
|
|
154
|
-
MEMORY_MONITOR_ENABLED = False # 是否启用内存监控
|
|
155
|
-
MEMORY_MONITOR_INTERVAL = 60 # 内存监控检查间隔(秒)
|
|
156
|
-
MEMORY_WARNING_THRESHOLD = 80.0 # 内存使用率警告阈值(百分比)
|
|
157
|
-
MEMORY_CRITICAL_THRESHOLD = 90.0 # 内存使用率严重阈值(百分比)
|
|
111
|
+
# 只要配置了代理API URL,中间件就会自动启用
|
|
112
|
+
# PROXY_API_URL = "http://your-proxy-api.com/get-proxy"
|
|
@@ -92,6 +92,11 @@ MYSQL_TABLE = '{{project_name}}_data'
|
|
|
92
92
|
MYSQL_BATCH_SIZE = 100
|
|
93
93
|
MYSQL_USE_BATCH = True # 是否启用批量插入
|
|
94
94
|
|
|
95
|
+
# MySQL SQL生成行为控制配置
|
|
96
|
+
MYSQL_AUTO_UPDATE = False # 是否使用 REPLACE INTO(完全覆盖已存在记录)
|
|
97
|
+
MYSQL_INSERT_IGNORE = False # 是否使用 INSERT IGNORE(忽略重复数据)
|
|
98
|
+
MYSQL_UPDATE_COLUMNS = () # 冲突时需更新的列名;指定后 MYSQL_AUTO_UPDATE 失效
|
|
99
|
+
|
|
95
100
|
# MongoDB配置
|
|
96
101
|
MONGO_URI = 'mongodb://localhost:27017'
|
|
97
102
|
MONGO_DATABASE = '{{project_name}}_db'
|
|
@@ -101,26 +106,7 @@ MONGO_MIN_POOL_SIZE = 20
|
|
|
101
106
|
MONGO_BATCH_SIZE = 100 # 批量插入条数
|
|
102
107
|
MONGO_USE_BATCH = True # 是否启用批量插入
|
|
103
108
|
|
|
104
|
-
# ===================================
|
|
105
|
-
|
|
106
|
-
# 代理配置
|
|
107
|
-
# 代理功能默认不启用,如需使用请在项目配置文件中启用并配置相关参数
|
|
108
|
-
PROXY_ENABLED = False # 是否启用代理
|
|
109
|
-
|
|
110
|
-
# 简化版代理配置(适用于SimpleProxyMiddleware)
|
|
111
|
-
PROXY_LIST = [] # 代理列表,例如: ["http://proxy1:8080", "http://proxy2:8080"]
|
|
112
|
-
|
|
113
|
-
# 高级代理配置(适用于ProxyMiddleware)
|
|
114
|
-
PROXY_API_URL = "" # 代理获取接口(请替换为真实地址)
|
|
115
|
-
|
|
116
|
-
# 代理提取方式(支持字段路径或函数)
|
|
117
|
-
# 示例: "proxy" 适用于 {"proxy": "http://1.1.1.1:8080"}
|
|
118
|
-
# 示例: "data.proxy" 适用于 {"data": {"proxy": "http://1.1.1.1:8080"}}
|
|
119
|
-
PROXY_EXTRACTOR = "proxy"
|
|
120
|
-
|
|
121
|
-
# 代理刷新控制
|
|
122
|
-
PROXY_REFRESH_INTERVAL = 60 # 代理刷新间隔(秒)
|
|
123
|
-
PROXY_API_TIMEOUT = 10 # 请求代理 API 超时时间
|
|
109
|
+
# =================================== 浏览器指纹模拟 ===================================
|
|
124
110
|
|
|
125
111
|
# 浏览器指纹模拟(仅 CurlCffi 下载器有效)
|
|
126
112
|
CURL_BROWSER_TYPE = "chrome" # 可选: chrome, edge, safari, firefox 或版本如 chrome136
|
|
@@ -133,7 +119,8 @@ CURL_BROWSER_VERSION_MAP = {
|
|
|
133
119
|
"firefox": "firefox135",
|
|
134
120
|
}
|
|
135
121
|
|
|
136
|
-
# 下载器优化配置
|
|
122
|
+
# =================================== 下载器优化配置 ===================================
|
|
123
|
+
|
|
137
124
|
# 下载器健康检查
|
|
138
125
|
DOWNLOADER_HEALTH_CHECK = True # 是否启用下载器健康检查
|
|
139
126
|
HEALTH_CHECK_INTERVAL = 60 # 健康检查间隔(秒)
|
|
@@ -154,7 +141,18 @@ AIOHTTP_FORCE_CLOSE = False # 是否强制关闭连接
|
|
|
154
141
|
CONNECTION_TTL_DNS_CACHE = 300 # DNS缓存TTL(秒)
|
|
155
142
|
CONNECTION_KEEPALIVE_TIMEOUT = 15 # Keep-Alive超时(秒)
|
|
156
143
|
|
|
157
|
-
#
|
|
144
|
+
# =================================== 代理配置 ===================================
|
|
145
|
+
|
|
146
|
+
# 简化版代理配置(适用于SimpleProxyMiddleware)
|
|
147
|
+
# 只要配置了代理列表,中间件就会自动启用
|
|
148
|
+
# PROXY_LIST = ["http://proxy1:8080", "http://proxy2:8080"]
|
|
149
|
+
|
|
150
|
+
# 高级代理配置(适用于ProxyMiddleware)
|
|
151
|
+
# 只要配置了代理API URL,中间件就会自动启用
|
|
152
|
+
# PROXY_API_URL = "http://your-proxy-api.com/get-proxy"
|
|
153
|
+
|
|
154
|
+
# =================================== 内存监控配置 ===================================
|
|
155
|
+
|
|
158
156
|
# 内存监控扩展默认不启用,如需使用请在项目配置文件中启用
|
|
159
157
|
MEMORY_MONITOR_ENABLED = False # 是否启用内存监控
|
|
160
158
|
MEMORY_MONITOR_INTERVAL = 60 # 内存监控检查间隔(秒)
|
|
@@ -102,6 +102,11 @@ MYSQL_TABLE = '{{project_name}}_data'
|
|
|
102
102
|
MYSQL_BATCH_SIZE = 100
|
|
103
103
|
MYSQL_USE_BATCH = False # 是否启用批量插入
|
|
104
104
|
|
|
105
|
+
# MySQL SQL生成行为控制配置
|
|
106
|
+
MYSQL_AUTO_UPDATE = False # 是否使用 REPLACE INTO(完全覆盖已存在记录)
|
|
107
|
+
MYSQL_INSERT_IGNORE = False # 是否使用 INSERT IGNORE(忽略重复数据)
|
|
108
|
+
MYSQL_UPDATE_COLUMNS = () # 冲突时需更新的列名;指定后 MYSQL_AUTO_UPDATE 失效
|
|
109
|
+
|
|
105
110
|
# MongoDB配置
|
|
106
111
|
MONGO_URI = 'mongodb://localhost:27017'
|
|
107
112
|
MONGO_DATABASE = '{{project_name}}_db'
|
|
@@ -103,6 +103,11 @@ MYSQL_TABLE = '{{project_name}}_data'
|
|
|
103
103
|
MYSQL_BATCH_SIZE = 100
|
|
104
104
|
MYSQL_USE_BATCH = True # 是否启用批量插入
|
|
105
105
|
|
|
106
|
+
# MySQL SQL生成行为控制配置
|
|
107
|
+
MYSQL_AUTO_UPDATE = False # 是否使用 REPLACE INTO(完全覆盖已存在记录)
|
|
108
|
+
MYSQL_INSERT_IGNORE = False # 是否使用 INSERT IGNORE(忽略重复数据)
|
|
109
|
+
MYSQL_UPDATE_COLUMNS = () # 冲突时需更新的列名;指定后 MYSQL_AUTO_UPDATE 失效
|
|
110
|
+
|
|
106
111
|
# MongoDB配置
|
|
107
112
|
MONGO_URI = 'mongodb://localhost:27017'
|
|
108
113
|
MONGO_DATABASE = '{{project_name}}_db'
|
|
@@ -74,4 +74,28 @@ LOG_ENCODING = 'utf-8' # 明确指定日志文件编码
|
|
|
74
74
|
STATS_DUMP = True
|
|
75
75
|
|
|
76
76
|
# 输出配置
|
|
77
|
-
OUTPUT_DIR = 'output'
|
|
77
|
+
OUTPUT_DIR = 'output'
|
|
78
|
+
|
|
79
|
+
# =================================== 数据库配置 ===================================
|
|
80
|
+
|
|
81
|
+
# MySQL配置
|
|
82
|
+
MYSQL_HOST = '127.0.0.1'
|
|
83
|
+
MYSQL_PORT = 3306
|
|
84
|
+
MYSQL_USER = 'root'
|
|
85
|
+
MYSQL_PASSWORD = '123456'
|
|
86
|
+
MYSQL_DB = '{{project_name}}'
|
|
87
|
+
MYSQL_TABLE = '{{project_name}}_data'
|
|
88
|
+
MYSQL_BATCH_SIZE = 100
|
|
89
|
+
MYSQL_USE_BATCH = False # 是否启用批量插入
|
|
90
|
+
|
|
91
|
+
# MySQL SQL生成行为控制配置
|
|
92
|
+
MYSQL_AUTO_UPDATE = False # 是否使用 REPLACE INTO(完全覆盖已存在记录)
|
|
93
|
+
MYSQL_INSERT_IGNORE = False # 是否使用 INSERT IGNORE(忽略重复数据)
|
|
94
|
+
MYSQL_UPDATE_COLUMNS = () # 冲突时需更新的列名;指定后 MYSQL_AUTO_UPDATE 失效
|
|
95
|
+
|
|
96
|
+
# MongoDB配置
|
|
97
|
+
MONGO_URI = 'mongodb://localhost:27017'
|
|
98
|
+
MONGO_DATABASE = '{{project_name}}_db'
|
|
99
|
+
MONGO_COLLECTION = '{{project_name}}_items'
|
|
100
|
+
MONGO_BATCH_SIZE = 100 # 批量插入条数
|
|
101
|
+
MONGO_USE_BATCH = False # 是否启用批量插入
|
|
@@ -100,6 +100,11 @@ MYSQL_TABLE = '{{project_name}}_data'
|
|
|
100
100
|
MYSQL_BATCH_SIZE = 100
|
|
101
101
|
MYSQL_USE_BATCH = False # 是否启用批量插入
|
|
102
102
|
|
|
103
|
+
# MySQL SQL生成行为控制配置
|
|
104
|
+
MYSQL_AUTO_UPDATE = False # 是否使用 REPLACE INTO(完全覆盖已存在记录)
|
|
105
|
+
MYSQL_INSERT_IGNORE = False # 是否使用 INSERT IGNORE(忽略重复数据)
|
|
106
|
+
MYSQL_UPDATE_COLUMNS = () # 冲突时需更新的列名;指定后 MYSQL_AUTO_UPDATE 失效
|
|
107
|
+
|
|
103
108
|
# MongoDB配置
|
|
104
109
|
MONGO_URI = 'mongodb://localhost:27017'
|
|
105
110
|
MONGO_DATABASE = '{{project_name}}_db'
|
crawlo/templates/run.py.tmpl
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
#!/usr/bin/
|
|
1
|
+
#!/usr/bin/python
|
|
2
2
|
# -*- coding: UTF-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
{{project_name}} 项目运行脚本
|
|
5
|
-
============================
|
|
6
|
-
基于 Crawlo 框架的简化爬虫启动器。
|
|
7
3
|
|
|
8
|
-
框架会自动处理爬虫模块的导入和注册,用户无需手动导入。
|
|
9
|
-
框架会自动从settings.py中读取SPIDER_MODULES配置。
|
|
10
|
-
"""
|
|
11
4
|
import sys
|
|
12
5
|
import asyncio
|
|
13
6
|
|
|
@@ -3,142 +3,39 @@
|
|
|
3
3
|
{{project_name}}.spiders.{{spider_name}}
|
|
4
4
|
=======================================
|
|
5
5
|
由 `crawlo genspider` 命令生成的爬虫。
|
|
6
|
-
基于 Crawlo 框架,支持异步并发、分布式爬取等功能。
|
|
7
|
-
|
|
8
|
-
使用示例:
|
|
9
|
-
crawlo crawl {{spider_name}}
|
|
10
6
|
"""
|
|
11
7
|
|
|
12
8
|
from crawlo.spider import Spider
|
|
13
9
|
from crawlo import Request
|
|
14
|
-
from ..items import
|
|
10
|
+
from ..items import {{item_class}}
|
|
15
11
|
|
|
16
12
|
|
|
17
13
|
class {{class_name}}(Spider):
|
|
18
14
|
"""
|
|
19
15
|
爬虫:{{spider_name}}
|
|
20
|
-
|
|
21
|
-
功能说明:
|
|
22
|
-
- 支持并发爬取
|
|
23
|
-
- 自动去重过滤
|
|
24
|
-
- 错误重试机制
|
|
25
|
-
- 数据管道处理
|
|
26
16
|
"""
|
|
27
17
|
name = '{{spider_name}}'
|
|
28
18
|
allowed_domains = ['{{domain}}']
|
|
29
19
|
start_urls = ['https://{{domain}}/']
|
|
30
20
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
# 'DOWNLOAD_DELAY': 2.0,
|
|
34
|
-
# 'CONCURRENCY': 4,
|
|
35
|
-
# 'RETRY_HTTP_CODES': [500, 502, 503, 504, 408, 429],
|
|
36
|
-
# 'ALLOWED_RESPONSE_CODES': [200, 301, 302], # 只允许特定状态码
|
|
37
|
-
# 'DENIED_RESPONSE_CODES': [403, 404], # 拒绝特定状态码
|
|
38
|
-
# }
|
|
21
|
+
# 自定义设置(可选)
|
|
22
|
+
custom_settings = {}
|
|
39
23
|
|
|
40
24
|
def start_requests(self):
|
|
41
25
|
"""
|
|
42
26
|
生成初始请求。
|
|
43
|
-
|
|
44
|
-
支持自定义请求头、代理、优先级等。
|
|
45
27
|
"""
|
|
46
|
-
headers = {
|
|
47
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
48
|
-
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
|
|
49
|
-
}
|
|
50
|
-
|
|
51
28
|
for url in self.start_urls:
|
|
52
|
-
yield Request(
|
|
53
|
-
url=url,
|
|
54
|
-
callback=self.parse,
|
|
55
|
-
headers=headers,
|
|
56
|
-
# meta={'proxy': 'http://proxy.example.com:8080'}, # 自定义代理
|
|
57
|
-
# priority=10, # 请求优先级(数字越大优先级越高)
|
|
58
|
-
)
|
|
29
|
+
yield Request(url=url, callback=self.parse)
|
|
59
30
|
|
|
60
31
|
def parse(self, response):
|
|
61
32
|
"""
|
|
62
33
|
解析响应的主方法。
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
response: 响应对象,包含页面内容和元数据
|
|
66
|
-
|
|
67
|
-
Yields:
|
|
68
|
-
Request: 新的请求对象(用于深度爬取)
|
|
69
|
-
Item: 数据项对象(用于数据存储)
|
|
70
34
|
"""
|
|
71
35
|
self.logger.info(f'正在解析页面: {response.url}')
|
|
72
36
|
|
|
73
|
-
# ================== 数据提取示例 ==================
|
|
74
|
-
|
|
75
|
-
# 提取数据并创建 Item
|
|
76
|
-
# item = {{item_class}}()
|
|
77
|
-
# item['title'] = response.xpath('//title/text()').get(default='')
|
|
78
|
-
# item['url'] = response.url
|
|
79
|
-
# item['content'] = response.xpath('//div[@class="content"]//text()').getall()
|
|
80
|
-
# yield item
|
|
81
|
-
|
|
82
|
-
# 直接返回字典(简单数据)
|
|
83
37
|
yield {
|
|
84
38
|
'title': response.xpath('//title/text()').get(default=''),
|
|
85
39
|
'url': response.url,
|
|
86
40
|
'status_code': response.status_code,
|
|
87
|
-
|
|
88
|
-
# 'keywords': response.xpath('//meta[@name="keywords"]/@content').get(),
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# ================== 链接提取示例 ==================
|
|
92
|
-
|
|
93
|
-
# 提取并跟进链接
|
|
94
|
-
# links = response.xpath('//a/@href').getall()
|
|
95
|
-
# for link in links:
|
|
96
|
-
# # 过滤有效链接
|
|
97
|
-
# if link and not link.startswith(('javascript:', 'mailto:', '#')):
|
|
98
|
-
# yield response.follow(
|
|
99
|
-
# link,
|
|
100
|
-
# callback=self.parse_detail, # 或者 self.parse 继续递归
|
|
101
|
-
# meta={'parent_url': response.url} # 传递父页面信息
|
|
102
|
-
# )
|
|
103
|
-
|
|
104
|
-
# 用 CSS 选择器提取链接
|
|
105
|
-
# for link in response.css('a.item-link::attr(href)').getall():
|
|
106
|
-
# yield response.follow(link, callback=self.parse_detail)
|
|
107
|
-
|
|
108
|
-
# ================== 分页处理示例 ==================
|
|
109
|
-
|
|
110
|
-
# 处理分页
|
|
111
|
-
# next_page = response.xpath('//a[@class="next"]/@href').get()
|
|
112
|
-
# if next_page:
|
|
113
|
-
# yield response.follow(next_page, callback=self.parse)
|
|
114
|
-
|
|
115
|
-
# 数字分页
|
|
116
|
-
# current_page = int(response.meta.get('page', 1))
|
|
117
|
-
# max_pages = 100 # 设置最大页数
|
|
118
|
-
# if current_page < max_pages:
|
|
119
|
-
# next_url = f'https://{{domain}}/page/{current_page + 1}'
|
|
120
|
-
# yield Request(
|
|
121
|
-
# url=next_url,
|
|
122
|
-
# callback=self.parse,
|
|
123
|
-
# meta={'page': current_page + 1}
|
|
124
|
-
# )
|
|
125
|
-
|
|
126
|
-
def parse_detail(self, response):
|
|
127
|
-
"""
|
|
128
|
-
解析详情页面的方法(可选)。
|
|
129
|
-
|
|
130
|
-
用于处理从列表页跳转而来的详情页。
|
|
131
|
-
"""
|
|
132
|
-
self.logger.info(f'正在解析详情页: {response.url}')
|
|
133
|
-
|
|
134
|
-
# parent_url = response.meta.get('parent_url', '')
|
|
135
|
-
#
|
|
136
|
-
# yield {
|
|
137
|
-
# 'title': response.xpath('//h1/text()').get(default=''),
|
|
138
|
-
# 'content': '\n'.join(response.xpath('//div[@class="content"]//text()').getall()),
|
|
139
|
-
# 'url': response.url,
|
|
140
|
-
# 'parent_url': parent_url,
|
|
141
|
-
# 'publish_time': response.xpath('//time/@datetime').get(),
|
|
142
|
-
# }
|
|
143
|
-
|
|
144
|
-
pass
|
|
41
|
+
}
|
crawlo/utils/db_helper.py
CHANGED
|
@@ -93,7 +93,7 @@ class SQLBuilder:
|
|
|
93
93
|
@staticmethod
|
|
94
94
|
def _build_update_clause(update_columns: Union[Tuple, List]) -> str:
|
|
95
95
|
"""
|
|
96
|
-
|
|
96
|
+
构建更新子句,使用新的 MySQL 语法避免 VALUES() 函数弃用警告
|
|
97
97
|
|
|
98
98
|
Args:
|
|
99
99
|
update_columns (tuple or list): 更新列名
|
|
@@ -103,7 +103,9 @@ class SQLBuilder:
|
|
|
103
103
|
"""
|
|
104
104
|
if not isinstance(update_columns, (tuple, list)):
|
|
105
105
|
update_columns = (update_columns,)
|
|
106
|
-
|
|
106
|
+
# 使用新的语法:INSERT ... VALUES (...) AS alias ... UPDATE ... alias.col
|
|
107
|
+
# 确保使用 excluded 别名而不是 VALUES() 函数
|
|
108
|
+
return ", ".join(f"`{key}`=`excluded`.`{key}`" for key in update_columns)
|
|
107
109
|
|
|
108
110
|
@staticmethod
|
|
109
111
|
def make_insert(
|
|
@@ -133,7 +135,8 @@ class SQLBuilder:
|
|
|
133
135
|
if update_columns:
|
|
134
136
|
update_clause = SQLBuilder._build_update_clause(update_columns)
|
|
135
137
|
ignore_flag = " IGNORE" if insert_ignore else ""
|
|
136
|
-
|
|
138
|
+
# 使用新的语法避免 VALUES() 函数弃用警告
|
|
139
|
+
sql = f"INSERT{ignore_flag} INTO `{table}` {keys_str} VALUES {values_str} AS `excluded` ON DUPLICATE KEY UPDATE {update_clause}"
|
|
137
140
|
|
|
138
141
|
elif auto_update:
|
|
139
142
|
sql = f"REPLACE INTO `{table}` {keys_str} VALUES {values_str}"
|
|
@@ -225,16 +228,19 @@ class SQLBuilder:
|
|
|
225
228
|
update_columns = (update_columns,)
|
|
226
229
|
|
|
227
230
|
if update_columns_value:
|
|
231
|
+
# 当提供了固定值时,使用这些值进行更新
|
|
228
232
|
update_pairs = [
|
|
229
233
|
f"`{key}`={value}"
|
|
230
234
|
for key, value in zip(update_columns, update_columns_value)
|
|
231
235
|
]
|
|
232
236
|
else:
|
|
237
|
+
# 使用新的语法避免 VALUES() 函数弃用警告
|
|
238
|
+
# INSERT ... VALUES (...) AS excluded ... ON DUPLICATE KEY UPDATE col=excluded.col
|
|
233
239
|
update_pairs = [
|
|
234
|
-
f"`{key}
|
|
240
|
+
f"`{key}`=`excluded`.`{key}`" for key in update_columns
|
|
235
241
|
]
|
|
236
242
|
update_clause = ", ".join(update_pairs)
|
|
237
|
-
sql = f"INSERT INTO `{table}` ({keys_str}) VALUES ({placeholders_str}) ON DUPLICATE KEY UPDATE {update_clause}"
|
|
243
|
+
sql = f"INSERT INTO `{table}` ({keys_str}) VALUES ({placeholders_str}) AS `excluded` ON DUPLICATE KEY UPDATE {update_clause}"
|
|
238
244
|
|
|
239
245
|
elif auto_update:
|
|
240
246
|
sql = f"REPLACE INTO `{table}` ({keys_str}) VALUES ({placeholders_str})"
|