crawlo 1.1.5__py3-none-any.whl → 1.1.8__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.

@@ -267,7 +267,21 @@ class QueueManager:
267
267
  if ':' in self.config.queue_name:
268
268
  parts = self.config.queue_name.split(':')
269
269
  if len(parts) >= 2:
270
- project_name = parts[1] # 取第二个部分作为项目名称
270
+ # 处理可能的双重 crawlo 前缀
271
+ if parts[0] == "crawlo" and parts[1] == "crawlo":
272
+ # 双重 crawlo 前缀,取第三个部分作为项目名称
273
+ if len(parts) >= 3:
274
+ project_name = parts[2]
275
+ else:
276
+ project_name = "default"
277
+ elif parts[0] == "crawlo":
278
+ # 正常的 crawlo 前缀,取第二个部分作为项目名称
279
+ project_name = parts[1]
280
+ else:
281
+ # 没有 crawlo 前缀,使用第一个部分作为项目名称
282
+ project_name = parts[0]
283
+ else:
284
+ project_name = self.config.queue_name or "default"
271
285
  else:
272
286
  project_name = self.config.queue_name or "default"
273
287
 
@@ -1,17 +1,16 @@
1
+ import asyncio
1
2
  import pickle
2
3
  import time
3
- import asyncio
4
+ import traceback
4
5
  from typing import Optional
6
+
5
7
  import redis.asyncio as aioredis
6
- import traceback
7
- import os
8
8
 
9
9
  from crawlo import Request
10
- from crawlo.utils.log import get_logger
11
- from crawlo.utils.request_serializer import RequestSerializer
12
10
  from crawlo.utils.error_handler import ErrorHandler
11
+ from crawlo.utils.log import get_logger
13
12
  from crawlo.utils.redis_connection_pool import get_redis_pool, OptimizedRedisConnectionPool
14
-
13
+ from crawlo.utils.request_serializer import RequestSerializer
15
14
 
16
15
  logger = get_logger(__name__)
17
16
  error_handler = ErrorHandler(__name__)
@@ -45,11 +44,8 @@ class RedisPriorityQueue:
45
44
  if queue_name is None:
46
45
  self.queue_name = f"crawlo:{module_name}:queue:requests"
47
46
  else:
48
- # 如果提供了 queue_name,但不符合规范格式,则转换为规范格式
49
- if not queue_name.startswith("crawlo:"):
50
- self.queue_name = f"crawlo:{module_name}:queue:requests"
51
- else:
52
- self.queue_name = queue_name
47
+ # 保持用户提供的队列名称不变,不做修改
48
+ self.queue_name = queue_name
53
49
 
54
50
  # 如果未提供 processing_queue,则根据 queue_name 自动生成
55
51
  if processing_queue is None:
@@ -85,14 +81,16 @@ class RedisPriorityQueue:
85
81
 
86
82
  for attempt in range(max_retries):
87
83
  try:
88
- # 使用优化的连接池
84
+ # 使用优化的连接池,确保 decode_responses=False 以避免编码问题
89
85
  self._redis_pool = get_redis_pool(
90
86
  self.redis_url,
91
87
  max_connections=self.max_connections,
92
88
  socket_connect_timeout=5,
93
89
  socket_timeout=30,
94
90
  health_check_interval=30,
95
- retry_on_timeout=True
91
+ retry_on_timeout=True,
92
+ decode_responses=False, # 确保不自动解码响应
93
+ encoding='utf-8'
96
94
  )
97
95
 
98
96
  self._redis = await self._redis_pool.get_connection()
@@ -131,7 +129,15 @@ class RedisPriorityQueue:
131
129
  # 🔥 使用专用的序列化工具清理 Request
132
130
  clean_request = self.request_serializer.prepare_for_serialization(request)
133
131
 
134
- serialized = pickle.dumps(clean_request)
132
+ # 确保序列化后的数据可以被正确反序列化
133
+ try:
134
+ serialized = pickle.dumps(clean_request)
135
+ # 验证序列化数据可以被反序列化
136
+ pickle.loads(serialized)
137
+ except Exception as serialize_error:
138
+ logger.error(f"❌ 请求序列化验证失败 (Module: {self.module_name}): {serialize_error}")
139
+ return False
140
+
135
141
  pipe = self._redis.pipeline()
136
142
  pipe.zadd(self.queue_name, {key: score})
137
143
  pipe.hset(f"{self.queue_name}:data", key, serialized)
@@ -174,7 +180,23 @@ class RedisPriorityQueue:
174
180
  pipe.hdel(f"{self.queue_name}:data", key)
175
181
  await pipe.execute()
176
182
 
177
- return pickle.loads(serialized)
183
+ # 更安全的反序列化方式
184
+ try:
185
+ # 首先尝试标准的 pickle 反序列化
186
+ request = pickle.loads(serialized)
187
+ return request
188
+ except UnicodeDecodeError:
189
+ # 如果出现编码错误,尝试使用 latin1 解码
190
+ request = pickle.loads(serialized, encoding='latin1')
191
+ return request
192
+ except Exception as pickle_error:
193
+ # 如果pickle反序列化失败,记录错误并跳过这个任务
194
+ logger.error(f"❌ 无法反序列化请求数据 (Module: {self.module_name}): {pickle_error}")
195
+ # 从processing队列中移除这个无效的任务
196
+ await self._redis.zrem(self.processing_queue, processing_key)
197
+ await self._redis.hdel(f"{self.processing_queue}:data", processing_key)
198
+ # 继续尝试下一个任务
199
+ continue
178
200
 
179
201
  # 检查是否超时
180
202
  if asyncio.get_event_loop().time() - start_time > timeout:
@@ -25,6 +25,9 @@ CONCURRENCY = get_runtime_config()['CONCURRENCY']
25
25
 
26
26
  # ============================== 爬虫核心配置 ==============================
27
27
 
28
+ # 默认下载器
29
+ DOWNLOADER = 'crawlo.downloader.aiohttp_downloader.AioHttpDownloader'
30
+
28
31
  # 请求延迟(秒)
29
32
  DOWNLOAD_DELAY = 1
30
33
 
@@ -97,4 +97,27 @@ class SettingManager(MutableMapping):
97
97
  self.set(key, value)
98
98
 
99
99
  def copy(self):
100
- return deepcopy(self)
100
+ return deepcopy(self)
101
+
102
+ def __deepcopy__(self, memo):
103
+ """
104
+ 自定义深度复制方法,避免复制logger等不可pickle的对象
105
+ """
106
+ # 创建一个新的实例
107
+ cls = self.__class__
108
+ new_instance = cls.__new__(cls)
109
+
110
+ # 复制attributes字典,但排除不可pickle的对象
111
+ new_attributes = {}
112
+ for key, value in self.attributes.items():
113
+ try:
114
+ # 尝试深度复制值
115
+ new_attributes[key] = deepcopy(value, memo)
116
+ except Exception:
117
+ # 如果复制失败,保留原始引用(对于logger等对象)
118
+ new_attributes[key] = value
119
+
120
+ # 设置新实例的attributes
121
+ new_instance.attributes = new_attributes
122
+
123
+ return new_instance
@@ -10,7 +10,6 @@
10
10
  import random
11
11
  from crawlo import Request, Response
12
12
  from crawlo.utils.log import get_logger
13
- from crawlo.exceptions import IgnoreRequest
14
13
 
15
14
 
16
15
  class ExampleMiddleware:
@@ -8,7 +8,6 @@
8
8
  这是一个简单的示例管道,您可以根据需要添加更多管道。
9
9
  """
10
10
 
11
- import json
12
11
  from datetime import datetime
13
12
  from crawlo.exceptions import DropItem
14
13
  from crawlo.utils.log import get_logger
@@ -3,250 +3,44 @@
3
3
  """
4
4
  {{project_name}} 项目运行脚本
5
5
  ============================
6
- 基于 Crawlo 框架的智能爬虫启动器。
7
- 支持单机/分布式模式,灵活配置,开箱即用。
8
-
9
- 🎯 快速使用:
10
- python run.py spider_name # 单机模式运行
11
- python run.py spider_name --distributed # 分布式模式运行
12
- python run.py spider_name --env production # 使用预设配置
13
- python run.py all # 运行所有爬虫
14
-
15
- 🔧 高级选项:
16
- python run.py spider_name --dry-run # 干运行(不执行实际爬取)
17
- python run.py spider_name --concurrency 16 # 自定义并发数
18
- python run.py spider_name --mode gentle # 温和模式(低负载)
19
- python run.py spider1 spider2 --distributed # 多爬虫分布式运行
20
-
21
- 📦 配置模式:
22
- --standalone 单机模式(默认)- 内存队列,无需外部依赖
23
- --distributed 分布式模式 - Redis队列,支持多节点
24
- --auto 自动模式 - 智能检测Redis可用性
25
-
26
- 🎛️ 预设配置:
27
- --env development 开发环境(调试友好)
28
- --env production 生产环境(高性能)
29
- --env large-scale 大规模爬取(优化内存)
30
- --env gentle 温和模式(低负载)
6
+ 基于 Crawlo 框架的简化爬虫启动器。
31
7
  """
32
8
 
33
- import os
34
9
  import sys
10
+ import os
35
11
  import asyncio
36
- import argparse
37
- from pathlib import Path
38
- from crawlo.crawler import CrawlerProcess
39
- from crawlo.config import CrawloConfig
40
- from crawlo.mode_manager import standalone_mode, distributed_mode, auto_mode
41
-
42
-
43
- def create_parser():
44
- """创建命令行参数解析器"""
45
- parser = argparse.ArgumentParser(
46
- description='{{project_name}} 爬虫启动器 - 基于 Crawlo 框架',
47
- formatter_class=argparse.RawDescriptionHelpFormatter,
48
- epilog="""
49
- 示例用法:
50
- python run.py my_spider # 默认单机模式
51
- python run.py my_spider --distributed # 分布式模式
52
- python run.py my_spider --env production # 生产环境配置
53
- python run.py spider1 spider2 # 运行多个爬虫
54
- python run.py all # 运行所有爬虫
55
- python run.py my_spider --dry-run # 测试模式
56
- """
57
- )
58
-
59
- # 爬虫名称(位置参数)
60
- parser.add_argument(
61
- 'spiders',
62
- nargs='*',
63
- help='要运行的爬虫名称(可指定多个,"all"表示运行所有爬虫)'
64
- )
65
-
66
- # 运行模式选择
67
- mode_group = parser.add_mutually_exclusive_group()
68
- mode_group.add_argument(
69
- '--standalone',
70
- action='store_true',
71
- help='单机模式(默认)- 使用内存队列,无需外部依赖'
72
- )
73
- mode_group.add_argument(
74
- '--distributed',
75
- action='store_true',
76
- help='分布式模式 - 使用 Redis 队列,支持多节点爬取'
77
- )
78
- mode_group.add_argument(
79
- '--auto',
80
- action='store_true',
81
- help='自动模式 - 智能检测 Redis 可用性选择队列类型'
82
- )
83
-
84
- # 预设环境配置
85
- parser.add_argument(
86
- '--env',
87
- choices=['development', 'production', 'large-scale', 'gentle'],
88
- help='预设环境配置(优先级高于模式选择)'
89
- )
90
-
91
- # 性能调优选项
92
- parser.add_argument(
93
- '--concurrency',
94
- type=int,
95
- help='并发请求数(覆盖默认设置)'
96
- )
97
-
98
- parser.add_argument(
99
- '--delay',
100
- type=float,
101
- help='请求延迟时间(秒)'
102
- )
103
-
104
- # 功能选项
105
- parser.add_argument(
106
- '--dry-run',
107
- action='store_true',
108
- help='干运行模式 - 解析页面但不执行实际爬取操作'
109
- )
110
-
111
- parser.add_argument(
112
- '--debug',
113
- action='store_true',
114
- help='启用调试模式 - 详细日志输出'
115
- )
116
-
117
- parser.add_argument(
118
- '--config-file',
119
- type=str,
120
- help='自定义配置文件路径'
121
- )
122
-
123
- # 环境变量支持
124
- parser.add_argument(
125
- '--from-env',
126
- action='store_true',
127
- help='从环境变量加载配置(CRAWLO_*)'
128
- )
129
-
130
- return parser
131
12
 
13
+ # 添加项目根目录到 Python 路径
14
+ project_root = os.path.dirname(os.path.abspath(__file__))
15
+ sys.path.insert(0, project_root)
132
16
 
133
- def build_config(args):
134
- """根据命令行参数构建配置"""
135
- config = None
136
-
137
- # 1. 优先使用环境变量配置
138
- if args.from_env:
139
- config = CrawloConfig.from_env()
140
- print("📋 使用环境变量配置")
141
-
142
- # 2. 使用预设环境配置
143
- elif args.env:
144
- presets = {
145
- 'development': CrawloConfig.presets().development(),
146
- 'production': CrawloConfig.presets().production(),
147
- 'large-scale': CrawloConfig.presets().large_scale(),
148
- 'gentle': CrawloConfig.presets().gentle()
149
- }
150
- config = presets[args.env]
151
- print(f"🎛️ 使用预设配置: {args.env}")
152
-
153
- # 3. 使用模式配置
154
- elif args.distributed:
155
- config = CrawloConfig.distributed()
156
- print("🌐 启用分布式模式")
157
- elif args.auto:
158
- config = CrawloConfig.auto()
159
- print("🤖 启用自动检测模式")
160
- else:
161
- # 默认单机模式
162
- config = CrawloConfig.standalone()
163
- print("💻 使用单机模式(默认)")
164
-
165
- # 4. 应用命令行参数覆盖
166
- if args.concurrency:
167
- config.set('CONCURRENCY', args.concurrency)
168
- print(f"⚡ 设置并发数: {args.concurrency}")
169
-
170
- if args.delay:
171
- config.set('DOWNLOAD_DELAY', args.delay)
172
- print(f"⏱️ 设置请求延迟: {args.delay}秒")
173
-
174
- if args.debug:
175
- config.set('LOG_LEVEL', 'DEBUG')
176
- print("🐛 启用调试模式")
177
-
178
- if args.dry_run:
179
- # 干运行模式的配置(可根据需要调整)
180
- config.set('DOWNLOAD_DELAY', 0.1) # 加快速度
181
- config.set('CONCURRENCY', 1) # 降低并发
182
- print("🧪 启用干运行模式")
183
-
184
- return config
17
+ # 切换到项目根目录
18
+ os.chdir(project_root)
185
19
 
20
+ from crawlo.crawler import CrawlerProcess
186
21
 
187
- async def main():
188
- """主函数:解析参数,构建配置,启动爬虫"""
189
-
190
- # 解析命令行参数
191
- parser = create_parser()
192
- args = parser.parse_args()
193
-
194
- # 检查是否指定了爬虫
195
- if not args.spiders:
196
- print("❌ 请指定要运行的爬虫名称")
197
- print("\n可用的爬虫:")
198
- print(" # TODO: 在这里列出你的爬虫")
199
- print(" # from {{project_name}}.spiders import MySpider")
200
- print("\n使用方法: python run.py <spider_name>")
201
- parser.print_help()
202
- return
203
-
204
- # 构建配置
205
- config = build_config(args)
206
-
207
- # 创建爬虫进程
208
- print(f"\n🚀 正在启动爬虫: {', '.join(args.spiders)}")
209
-
210
- if args.dry_run:
211
- print(" 🧪 [干运行模式] 将解析页面但不执行实际爬取")
22
+ def main():
23
+ """主函数:运行固定的爬虫"""
24
+ print("🚀 启动 {{project_name}} 爬虫")
212
25
 
26
+ # 创建爬虫进程(自动加载默认配置)
213
27
  try:
214
- # 应用配置并启动
215
- process = CrawlerProcess(settings=config.to_dict())
28
+ # 确保 spider 模块被正确导入
29
+ spider_modules = ['{{project_name}}.spiders']
30
+ process = CrawlerProcess(spider_modules=spider_modules)
31
+ print("✅ 爬虫进程初始化成功")
216
32
 
217
- # 检查是否要运行所有爬虫
218
- if 'all' in [s.lower() for s in args.spiders]:
219
- # 获取所有已注册的爬虫名称
220
- spider_names = process.get_spider_names()
221
- if not spider_names:
222
- print("❌ 未找到任何爬虫")
223
- print("💡 请确保:")
224
- print(" • 爬虫定义在 'spiders/' 目录中")
225
- print(" • 爬虫类有 'name' 属性")
226
- return 1
227
-
228
- print(f"📋 找到 {len(spider_names)} 个爬虫: {', '.join(spider_names)}")
229
- # 运行所有爬虫
230
- await process.crawl(spider_names)
231
- else:
232
- # 运行指定爬虫
233
- await process.crawl(args.spiders)
33
+ # 运行固定的爬虫
34
+ # TODO: 请将 'your_spider_name' 替换为实际的爬虫名称
35
+ asyncio.run(process.crawl('your_spider_name'))
234
36
 
235
- print("\n所有爬虫执行完成")
37
+ print("✅ 爬虫运行完成")
236
38
 
237
- except ImportError as e:
238
- print(f"❌ 无法导入爬虫: {e}")
239
- print(" 请检查爬虫文件是否存在,并更新 run.py 中的导入语句")
240
39
  except Exception as e:
241
- print(f"❌ 运行错误: {e}")
242
- raise
243
-
40
+ print(f"❌ 运行失败: {e}")
41
+ import traceback
42
+ traceback.print_exc()
43
+ sys.exit(1)
244
44
 
245
45
  if __name__ == '__main__':
246
- try:
247
- asyncio.run(main())
248
- except KeyboardInterrupt:
249
- print("\n⏹️ 用户中断爬虫执行")
250
- except Exception as e:
251
- print(f"❌ 运行错误: {e}")
252
- sys.exit(1)
46
+ main()
@@ -149,10 +149,10 @@ REQUEST_DIR = '.'
149
149
  # 分布式模式默认使用Redis去重管道
150
150
  if RUN_MODE == 'distributed':
151
151
  # 分布式模式下默认使用Redis去重管道
152
- DEFAULT_DEDUP_PIPELINE = 'crawlo.pipelines.RedisDedupPipeline'
152
+ DEFAULT_DEDUP_PIPELINE = 'crawlo.pipelines.redis_dedup_pipeline.RedisDedupPipeline'
153
153
  else:
154
154
  # 单机模式下默认使用内存去重管道
155
- DEFAULT_DEDUP_PIPELINE = 'crawlo.pipelines.MemoryDedupPipeline'
155
+ DEFAULT_DEDUP_PIPELINE = 'crawlo.pipelines.memory_dedup_pipeline.MemoryDedupPipeline'
156
156
 
157
157
  # 去重过滤器(推荐分布式项目使用 Redis 过滤器)
158
158
  FILTER_CLASS = 'crawlo.filters.memory_filter.MemoryFilter'