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

Files changed (128) hide show
  1. crawlo/__init__.py +34 -33
  2. crawlo/__version__.py +1 -1
  3. crawlo/cli.py +40 -40
  4. crawlo/commands/__init__.py +13 -13
  5. crawlo/commands/check.py +594 -594
  6. crawlo/commands/genspider.py +152 -126
  7. crawlo/commands/list.py +156 -147
  8. crawlo/commands/run.py +285 -285
  9. crawlo/commands/startproject.py +196 -111
  10. crawlo/commands/stats.py +188 -188
  11. crawlo/commands/utils.py +187 -0
  12. crawlo/config.py +280 -0
  13. crawlo/core/__init__.py +2 -2
  14. crawlo/core/engine.py +171 -158
  15. crawlo/core/enhanced_engine.py +190 -0
  16. crawlo/core/processor.py +40 -40
  17. crawlo/core/scheduler.py +166 -57
  18. crawlo/crawler.py +1028 -495
  19. crawlo/downloader/__init__.py +242 -78
  20. crawlo/downloader/aiohttp_downloader.py +212 -199
  21. crawlo/downloader/cffi_downloader.py +251 -241
  22. crawlo/downloader/httpx_downloader.py +259 -246
  23. crawlo/event.py +11 -11
  24. crawlo/exceptions.py +82 -78
  25. crawlo/extension/__init__.py +31 -31
  26. crawlo/extension/log_interval.py +49 -49
  27. crawlo/extension/log_stats.py +44 -44
  28. crawlo/extension/logging_extension.py +34 -34
  29. crawlo/filters/__init__.py +154 -37
  30. crawlo/filters/aioredis_filter.py +242 -150
  31. crawlo/filters/memory_filter.py +269 -202
  32. crawlo/items/__init__.py +23 -23
  33. crawlo/items/base.py +21 -21
  34. crawlo/items/fields.py +53 -53
  35. crawlo/items/items.py +104 -104
  36. crawlo/middleware/__init__.py +21 -21
  37. crawlo/middleware/default_header.py +32 -32
  38. crawlo/middleware/download_delay.py +28 -28
  39. crawlo/middleware/middleware_manager.py +135 -135
  40. crawlo/middleware/proxy.py +248 -245
  41. crawlo/middleware/request_ignore.py +30 -30
  42. crawlo/middleware/response_code.py +18 -18
  43. crawlo/middleware/response_filter.py +26 -26
  44. crawlo/middleware/retry.py +125 -90
  45. crawlo/mode_manager.py +201 -0
  46. crawlo/network/__init__.py +21 -7
  47. crawlo/network/request.py +311 -203
  48. crawlo/network/response.py +271 -166
  49. crawlo/pipelines/__init__.py +22 -13
  50. crawlo/pipelines/bloom_dedup_pipeline.py +157 -0
  51. crawlo/pipelines/console_pipeline.py +39 -39
  52. crawlo/pipelines/csv_pipeline.py +317 -0
  53. crawlo/pipelines/database_dedup_pipeline.py +225 -0
  54. crawlo/pipelines/json_pipeline.py +219 -0
  55. crawlo/pipelines/memory_dedup_pipeline.py +116 -0
  56. crawlo/pipelines/mongo_pipeline.py +116 -116
  57. crawlo/pipelines/mysql_pipeline.py +195 -195
  58. crawlo/pipelines/pipeline_manager.py +56 -56
  59. crawlo/pipelines/redis_dedup_pipeline.py +163 -0
  60. crawlo/project.py +153 -153
  61. crawlo/queue/__init__.py +0 -0
  62. crawlo/queue/pqueue.py +37 -0
  63. crawlo/queue/queue_manager.py +308 -0
  64. crawlo/queue/redis_priority_queue.py +209 -0
  65. crawlo/settings/__init__.py +7 -7
  66. crawlo/settings/default_settings.py +245 -167
  67. crawlo/settings/setting_manager.py +99 -99
  68. crawlo/spider/__init__.py +639 -129
  69. crawlo/stats_collector.py +59 -59
  70. crawlo/subscriber.py +106 -106
  71. crawlo/task_manager.py +30 -27
  72. crawlo/templates/crawlo.cfg.tmpl +10 -10
  73. crawlo/templates/project/__init__.py.tmpl +3 -3
  74. crawlo/templates/project/items.py.tmpl +17 -17
  75. crawlo/templates/project/middlewares.py.tmpl +87 -76
  76. crawlo/templates/project/pipelines.py.tmpl +342 -64
  77. crawlo/templates/project/run.py.tmpl +252 -0
  78. crawlo/templates/project/settings.py.tmpl +251 -54
  79. crawlo/templates/project/spiders/__init__.py.tmpl +5 -5
  80. crawlo/templates/spider/spider.py.tmpl +178 -32
  81. crawlo/utils/__init__.py +7 -7
  82. crawlo/utils/controlled_spider_mixin.py +440 -0
  83. crawlo/utils/date_tools.py +233 -233
  84. crawlo/utils/db_helper.py +343 -343
  85. crawlo/utils/func_tools.py +82 -82
  86. crawlo/utils/large_scale_config.py +287 -0
  87. crawlo/utils/large_scale_helper.py +344 -0
  88. crawlo/utils/log.py +128 -128
  89. crawlo/utils/queue_helper.py +176 -0
  90. crawlo/utils/request.py +267 -267
  91. crawlo/utils/request_serializer.py +220 -0
  92. crawlo/utils/spider_loader.py +62 -62
  93. crawlo/utils/system.py +11 -11
  94. crawlo/utils/tools.py +4 -4
  95. crawlo/utils/url.py +39 -39
  96. crawlo-1.1.3.dist-info/METADATA +635 -0
  97. crawlo-1.1.3.dist-info/RECORD +113 -0
  98. examples/__init__.py +7 -7
  99. examples/controlled_spider_example.py +205 -0
  100. tests/__init__.py +7 -7
  101. tests/test_final_validation.py +154 -0
  102. tests/test_proxy_health_check.py +32 -32
  103. tests/test_proxy_middleware_integration.py +136 -136
  104. tests/test_proxy_providers.py +56 -56
  105. tests/test_proxy_stats.py +19 -19
  106. tests/test_proxy_strategies.py +59 -59
  107. tests/test_redis_config.py +29 -0
  108. tests/test_redis_queue.py +225 -0
  109. tests/test_request_serialization.py +71 -0
  110. tests/test_scheduler.py +242 -0
  111. crawlo/pipelines/mysql_batch_pipline.py +0 -273
  112. crawlo/utils/pqueue.py +0 -174
  113. crawlo-1.1.1.dist-info/METADATA +0 -220
  114. crawlo-1.1.1.dist-info/RECORD +0 -100
  115. examples/baidu_spider/__init__.py +0 -7
  116. examples/baidu_spider/demo.py +0 -94
  117. examples/baidu_spider/items.py +0 -46
  118. examples/baidu_spider/middleware.py +0 -49
  119. examples/baidu_spider/pipeline.py +0 -55
  120. examples/baidu_spider/run.py +0 -27
  121. examples/baidu_spider/settings.py +0 -121
  122. examples/baidu_spider/spiders/__init__.py +0 -7
  123. examples/baidu_spider/spiders/bai_du.py +0 -61
  124. examples/baidu_spider/spiders/miit.py +0 -159
  125. examples/baidu_spider/spiders/sina.py +0 -79
  126. {crawlo-1.1.1.dist-info → crawlo-1.1.3.dist-info}/WHEEL +0 -0
  127. {crawlo-1.1.1.dist-info → crawlo-1.1.3.dist-info}/entry_points.txt +0 -0
  128. {crawlo-1.1.1.dist-info → crawlo-1.1.3.dist-info}/top_level.txt +0 -0
crawlo/commands/stats.py CHANGED
@@ -1,188 +1,188 @@
1
- #!/usr/bin/python
2
- # -*- coding: UTF-8 -*-
3
- """
4
- # @Time : 2025-08-31 22:36
5
- # @Author : crawl-coder
6
- # @Desc : 命令行入口:crawlo stats,查看最近运行的爬虫统计信息。
7
- """
8
- import json
9
- from pathlib import Path
10
- from datetime import datetime
11
- from typing import Dict, Any
12
-
13
- from rich.console import Console
14
- from rich.table import Table
15
- from rich.panel import Panel
16
- from rich.text import Text
17
- from rich import box
18
-
19
- from crawlo.utils.log import get_logger
20
-
21
-
22
- logger = get_logger(__name__)
23
- console = Console()
24
-
25
- # 默认存储目录(相对于项目根目录)
26
- STATS_DIR = "logs/stats"
27
-
28
-
29
- def get_stats_dir() -> Path:
30
- """
31
- 获取统计文件存储目录,优先使用项目根下的 logs/stats/
32
- 如果不在项目中,回退到当前目录
33
- """
34
- current = Path.cwd()
35
- for _ in range(10):
36
- if (current / "crawlo.cfg").exists():
37
- return current / STATS_DIR
38
- if current == current.parent:
39
- break
40
- current = current.parent
41
- return Path.cwd() / STATS_DIR
42
-
43
-
44
- def record_stats(crawler):
45
- """
46
- 【供爬虫运行时调用】记录爬虫结束后的统计信息到 JSON 文件
47
- 需在 Crawler 的 closed 回调中调用
48
- """
49
- spider_name = getattr(crawler.spider, "name", "unknown")
50
- stats = crawler.stats.get_stats() if crawler.stats else {}
51
-
52
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
53
- stats_dir = Path(get_stats_dir())
54
- stats_dir.mkdir(parents=True, exist_ok=True)
55
-
56
- filename = stats_dir / f"{spider_name}_{timestamp}.json"
57
- try:
58
- with open(filename, "w", encoding="utf-8") as f:
59
- json.dump({
60
- "spider": spider_name,
61
- "timestamp": datetime.now().isoformat(),
62
- "stats": stats
63
- }, f, ensure_ascii=False, indent=2, default=str)
64
- logger.info(f"📊 Stats saved for spider '{spider_name}' → {filename}")
65
- except Exception as e:
66
- logger.error(f"Failed to save stats for '{spider_name}': {e}")
67
-
68
-
69
- def load_all_stats() -> Dict[str, list]:
70
- """
71
- 加载所有已保存的统计文件,按 spider name 分组
72
- 返回: {spider_name: [stats_record, ...]}
73
- """
74
- stats_dir = get_stats_dir()
75
- if not stats_dir.exists():
76
- return {}
77
-
78
- result = {}
79
- json_files = sorted(stats_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)
80
-
81
- for file in json_files:
82
- try:
83
- with open(file, "r", encoding="utf-8") as f:
84
- data = json.load(f)
85
- spider_name = data.get("spider", "unknown")
86
- result.setdefault(spider_name, []).append(data)
87
- except Exception as e:
88
- logger.warning(f"Failed to load stats file {file}: {e}")
89
- return result
90
-
91
-
92
- def format_value(v: Any) -> str:
93
- """格式化值,防止太长或不可打印"""
94
- if isinstance(v, float):
95
- return f"{v:.4f}"
96
- s = str(v)
97
- if len(s) > 80:
98
- return s[:77] + "..."
99
- return s
100
-
101
-
102
- def display_stats_table(stats_data: dict, title: str = "Statistics"):
103
- """通用函数:用 rich.table 展示统计数据"""
104
- table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold magenta")
105
- table.add_column("Key", style="cyan", no_wrap=True)
106
- table.add_column("Value", style="green")
107
-
108
- for k in sorted(stats_data.keys()):
109
- table.add_row(k, format_value(stats_data[k]))
110
-
111
- console.print(table)
112
-
113
-
114
- def main(args):
115
- """
116
- 主函数:查看统计信息
117
- 用法:
118
- crawlo stats → 显示所有爬虫最近一次运行
119
- crawlo stats myspider → 显示指定爬虫所有历史记录
120
- crawlo stats myspider --all → 显示所有历史(同上)
121
- """
122
- if len(args) > 2:
123
- console.print("[bold red]Error:[/bold red] Usage: [blue]crawlo stats[/blue] [spider_name] [--all]")
124
- return 1
125
-
126
- spider_name = None
127
- show_all = False
128
-
129
- if args:
130
- spider_name = args[0]
131
- show_all = "--all" in args or "-a" in args
132
-
133
- all_stats = load_all_stats()
134
-
135
- if not all_stats:
136
- console.print(Panel(
137
- Text.from_markup(
138
- ":chart_with_upwards_trend: [bold]No stats found.[/bold]\n"
139
- "💡 Run a spider first to generate statistics.\n"
140
- f"📁 Stats directory: [cyan]{get_stats_dir()}[/cyan]"
141
- ),
142
- title="📊 Statistics",
143
- border_style="yellow",
144
- padding=(1, 2)
145
- ))
146
- return 0
147
-
148
- # 显示所有爬虫最近一次运行
149
- if not spider_name:
150
- console.print(Panel(
151
- "[bold]Recent Spider Statistics (last run)[/bold]",
152
- title="📊 Spider Stats Overview",
153
- border_style="green",
154
- padding=(0, 1)
155
- ))
156
-
157
- for name, runs in all_stats.items():
158
- latest = runs[0]
159
- ts = latest['timestamp'][:19]
160
- console.print(f"🕷️ [bold cyan]{name}[/bold cyan] ([green]{ts}[/green])")
161
- display_stats_table(latest["stats"], title=f"Stats for {name}")
162
- console.print() # 空行分隔
163
-
164
- return 0
165
-
166
- # 显示指定爬虫的历史
167
- if spider_name not in all_stats:
168
- console.print(f"[bold red]:cross_mark: No stats found for spider '[cyan]{spider_name}[/cyan]'[/bold red]")
169
- available = ', '.join(all_stats.keys())
170
- if available:
171
- console.print(f":bulb: Available spiders: [green]{available}[/green]")
172
- return 1
173
-
174
- runs = all_stats[spider_name]
175
- if show_all:
176
- console.print(f":bar_chart: [bold]All runs for '[cyan]{spider_name}[/cyan]' ({len(runs)} runs):[/bold]")
177
- else:
178
- runs = runs[:1]
179
- console.print(f":bar_chart: [bold]Last run for '[cyan]{spider_name}[/cyan]':[/bold]")
180
-
181
- for i, run in enumerate(runs, 1):
182
- ts = run['timestamp']
183
- subtitle = f"Run #{i} · {ts}" if show_all else f"Last Run · {ts}"
184
- display_stats_table(run["stats"], title=f"Stats for {spider_name} — {subtitle}")
185
- if i < len(runs):
186
- console.print("─" * 60)
187
-
188
- return 0
1
+ #!/usr/bin/python
2
+ # -*- coding: UTF-8 -*-
3
+ """
4
+ # @Time : 2025-08-31 22:36
5
+ # @Author : crawl-coder
6
+ # @Desc : 命令行入口:crawlo stats,查看最近运行的爬虫统计信息。
7
+ """
8
+ import json
9
+ from pathlib import Path
10
+ from datetime import datetime
11
+ from typing import Dict, Any
12
+
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+ from rich.panel import Panel
16
+ from rich.text import Text
17
+ from rich import box
18
+
19
+ from crawlo.utils.log import get_logger
20
+
21
+
22
+ logger = get_logger(__name__)
23
+ console = Console()
24
+
25
+ # 默认存储目录(相对于项目根目录)
26
+ STATS_DIR = "logs/stats"
27
+
28
+
29
+ def get_stats_dir() -> Path:
30
+ """
31
+ 获取统计文件存储目录,优先使用项目根下的 logs/stats/
32
+ 如果不在项目中,回退到当前目录
33
+ """
34
+ current = Path.cwd()
35
+ for _ in range(10):
36
+ if (current / "crawlo.cfg").exists():
37
+ return current / STATS_DIR
38
+ if current == current.parent:
39
+ break
40
+ current = current.parent
41
+ return Path.cwd() / STATS_DIR
42
+
43
+
44
+ def record_stats(crawler):
45
+ """
46
+ 【供爬虫运行时调用】记录爬虫结束后的统计信息到 JSON 文件
47
+ 需在 Crawler 的 closed 回调中调用
48
+ """
49
+ spider_name = getattr(crawler.spider, "name", "unknown")
50
+ stats = crawler.stats.get_stats() if crawler.stats else {}
51
+
52
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
53
+ stats_dir = Path(get_stats_dir())
54
+ stats_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ filename = stats_dir / f"{spider_name}_{timestamp}.json"
57
+ try:
58
+ with open(filename, "w", encoding="utf-8") as f:
59
+ json.dump({
60
+ "spider": spider_name,
61
+ "timestamp": datetime.now().isoformat(),
62
+ "stats": stats
63
+ }, f, ensure_ascii=False, indent=2, default=str)
64
+ logger.info(f"📊 Stats saved for spider '{spider_name}' → {filename}")
65
+ except Exception as e:
66
+ logger.error(f"Failed to save stats for '{spider_name}': {e}")
67
+
68
+
69
+ def load_all_stats() -> Dict[str, list]:
70
+ """
71
+ 加载所有已保存的统计文件,按 spider name 分组
72
+ 返回: {spider_name: [stats_record, ...]}
73
+ """
74
+ stats_dir = get_stats_dir()
75
+ if not stats_dir.exists():
76
+ return {}
77
+
78
+ result = {}
79
+ json_files = sorted(stats_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)
80
+
81
+ for file in json_files:
82
+ try:
83
+ with open(file, "r", encoding="utf-8") as f:
84
+ data = json.load(f)
85
+ spider_name = data.get("spider", "unknown")
86
+ result.setdefault(spider_name, []).append(data)
87
+ except Exception as e:
88
+ logger.warning(f"Failed to load stats file {file}: {e}")
89
+ return result
90
+
91
+
92
+ def format_value(v: Any) -> str:
93
+ """格式化值,防止太长或不可打印"""
94
+ if isinstance(v, float):
95
+ return f"{v:.4f}"
96
+ s = str(v)
97
+ if len(s) > 80:
98
+ return s[:77] + "..."
99
+ return s
100
+
101
+
102
+ def display_stats_table(stats_data: dict, title: str = "Statistics"):
103
+ """通用函数:用 rich.table 展示统计数据"""
104
+ table = Table(title=title, box=box.ROUNDED, show_header=True, header_style="bold magenta")
105
+ table.add_column("Key", style="cyan", no_wrap=True)
106
+ table.add_column("Value", style="green")
107
+
108
+ for k in sorted(stats_data.keys()):
109
+ table.add_row(k, format_value(stats_data[k]))
110
+
111
+ console.print(table)
112
+
113
+
114
+ def main(args):
115
+ """
116
+ 主函数:查看统计信息
117
+ 用法:
118
+ crawlo stats → 显示所有爬虫最近一次运行
119
+ crawlo stats myspider → 显示指定爬虫所有历史记录
120
+ crawlo stats myspider --all → 显示所有历史(同上)
121
+ """
122
+ if len(args) > 2:
123
+ console.print("[bold red]Error:[/bold red] Usage: [blue]crawlo stats[/blue] [spider_name] [--all]")
124
+ return 1
125
+
126
+ spider_name = None
127
+ show_all = False
128
+
129
+ if args:
130
+ spider_name = args[0]
131
+ show_all = "--all" in args or "-a" in args
132
+
133
+ all_stats = load_all_stats()
134
+
135
+ if not all_stats:
136
+ console.print(Panel(
137
+ Text.from_markup(
138
+ ":chart_with_upwards_trend: [bold]No stats found.[/bold]\n"
139
+ "💡 Run a spider first to generate statistics.\n"
140
+ f"📁 Stats directory: [cyan]{get_stats_dir()}[/cyan]"
141
+ ),
142
+ title="📊 Statistics",
143
+ border_style="yellow",
144
+ padding=(1, 2)
145
+ ))
146
+ return 0
147
+
148
+ # 显示所有爬虫最近一次运行
149
+ if not spider_name:
150
+ console.print(Panel(
151
+ "[bold]Recent Spider Statistics (last run)[/bold]",
152
+ title="📊 Spider Stats Overview",
153
+ border_style="green",
154
+ padding=(0, 1)
155
+ ))
156
+
157
+ for name, runs in all_stats.items():
158
+ latest = runs[0]
159
+ ts = latest['timestamp'][:19]
160
+ console.print(f"🕷️ [bold cyan]{name}[/bold cyan] ([green]{ts}[/green])")
161
+ display_stats_table(latest["stats"], title=f"Stats for {name}")
162
+ console.print() # 空行分隔
163
+
164
+ return 0
165
+
166
+ # 显示指定爬虫的历史
167
+ if spider_name not in all_stats:
168
+ console.print(f"[bold red]:cross_mark: No stats found for spider '[cyan]{spider_name}[/cyan]'[/bold red]")
169
+ available = ', '.join(all_stats.keys())
170
+ if available:
171
+ console.print(f":bulb: Available spiders: [green]{available}[/green]")
172
+ return 1
173
+
174
+ runs = all_stats[spider_name]
175
+ if show_all:
176
+ console.print(f":bar_chart: [bold]All runs for '[cyan]{spider_name}[/cyan]' ({len(runs)} runs):[/bold]")
177
+ else:
178
+ runs = runs[:1]
179
+ console.print(f":bar_chart: [bold]Last run for '[cyan]{spider_name}[/cyan]':[/bold]")
180
+
181
+ for i, run in enumerate(runs, 1):
182
+ ts = run['timestamp']
183
+ subtitle = f"Run #{i} · {ts}" if show_all else f"Last Run · {ts}"
184
+ display_stats_table(run["stats"], title=f"Stats for {spider_name} — {subtitle}")
185
+ if i < len(runs):
186
+ console.print("─" * 60)
187
+
188
+ return 0
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: UTF-8 -*-
3
+ """
4
+ 命令行工具公共模块
5
+ 提供命令行工具的公共函数和工具
6
+ """
7
+ import sys
8
+ import configparser
9
+ from pathlib import Path
10
+ from importlib import import_module
11
+ from typing import Optional, Tuple
12
+
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.text import Text
16
+
17
+ console = Console()
18
+
19
+
20
+ def get_project_root() -> Optional[Path]:
21
+ """
22
+ 自动检测项目根目录:从当前目录向上查找 crawlo.cfg
23
+
24
+ Returns:
25
+ Path: 项目根目录路径,如果未找到返回 None
26
+ """
27
+ current = Path.cwd()
28
+ for _ in range(10): # 最多向上查找10层
29
+ cfg_file = current / "crawlo.cfg"
30
+ if cfg_file.exists():
31
+ return current
32
+ if current == current.parent:
33
+ break
34
+ current = current.parent
35
+ return None
36
+
37
+
38
+ def validate_project_environment() -> Tuple[bool, Optional[str], Optional[str]]:
39
+ """
40
+ 验证项目环境,确保在正确的 Crawlo 项目中
41
+
42
+ Returns:
43
+ Tuple[bool, Optional[str], Optional[str]]:
44
+ (是否有效, 项目包名, 错误信息)
45
+ """
46
+ # 1. 查找项目根目录
47
+ project_root = get_project_root()
48
+ if not project_root:
49
+ return False, None, "Cannot find 'crawlo.cfg'. Run this command inside your project directory."
50
+
51
+ # 2. 将项目根加入 Python 路径
52
+ project_root_str = str(project_root)
53
+ if project_root_str not in sys.path:
54
+ sys.path.insert(0, project_root_str)
55
+
56
+ # 3. 读取配置文件
57
+ cfg_file = project_root / "crawlo.cfg"
58
+ config = configparser.ConfigParser()
59
+
60
+ try:
61
+ config.read(cfg_file, encoding="utf-8")
62
+ except Exception as e:
63
+ return False, None, f"Failed to read crawlo.cfg: {e}"
64
+
65
+ if not config.has_section("settings") or not config.has_option("settings", "default"):
66
+ return False, None, "Invalid crawlo.cfg: missing [settings] section or 'default' option"
67
+
68
+ # 4. 获取项目包名
69
+ settings_module = config.get("settings", "default")
70
+ project_package = settings_module.split(".")[0]
71
+
72
+ # 5. 验证项目包是否可导入
73
+ try:
74
+ import_module(project_package)
75
+ except ImportError as e:
76
+ return False, None, f"Failed to import project package '{project_package}': {e}"
77
+
78
+ return True, project_package, None
79
+
80
+
81
+ def show_error_panel(title: str, message: str, show_json: bool = False) -> None:
82
+ """
83
+ 显示错误面板或JSON格式错误
84
+
85
+ Args:
86
+ title: 错误标题
87
+ message: 错误消息
88
+ show_json: 是否以JSON格式输出
89
+ """
90
+ if show_json:
91
+ console.print_json(data={"success": False, "error": message})
92
+ else:
93
+ console.print(Panel(
94
+ Text.from_markup(f":cross_mark: [bold red]{message}[/bold red]"),
95
+ title=f"❌ {title}",
96
+ border_style="red",
97
+ padding=(1, 2)
98
+ ))
99
+
100
+
101
+ def show_success_panel(title: str, message: str, show_json: bool = False, data: dict = None) -> None:
102
+ """
103
+ 显示成功面板或JSON格式结果
104
+
105
+ Args:
106
+ title: 成功标题
107
+ message: 成功消息
108
+ show_json: 是否以JSON格式输出
109
+ data: JSON数据(当show_json=True时)
110
+ """
111
+ if show_json:
112
+ result = {"success": True, "message": message}
113
+ if data:
114
+ result.update(data)
115
+ console.print_json(data=result)
116
+ else:
117
+ console.print(Panel(
118
+ Text.from_markup(f":white_check_mark: [bold green]{message}[/bold green]"),
119
+ title=f"✅ {title}",
120
+ border_style="green",
121
+ padding=(1, 2)
122
+ ))
123
+
124
+
125
+ def validate_spider_name(spider_name: str) -> bool:
126
+ """
127
+ 验证爬虫名称是否符合规范
128
+
129
+ Args:
130
+ spider_name: 爬虫名称
131
+
132
+ Returns:
133
+ bool: 是否有效
134
+ """
135
+ import re
136
+ # 爬虫名称应该是有效的Python标识符
137
+ return spider_name.isidentifier() and re.match(r'^[a-z][a-z0-9_]*$', spider_name)
138
+
139
+
140
+ def format_file_size(size_bytes: int) -> str:
141
+ """
142
+ 格式化文件大小
143
+
144
+ Args:
145
+ size_bytes: 字节数
146
+
147
+ Returns:
148
+ str: 格式化后的大小字符串
149
+ """
150
+ for unit in ['B', 'KB', 'MB', 'GB']:
151
+ if size_bytes < 1024.0:
152
+ return f"{size_bytes:.1f} {unit}"
153
+ size_bytes /= 1024.0
154
+ return f"{size_bytes:.1f} TB"
155
+
156
+
157
+ def truncate_text(text: str, max_length: int = 80) -> str:
158
+ """
159
+ 截断过长的文本
160
+
161
+ Args:
162
+ text: 原始文本
163
+ max_length: 最大长度
164
+
165
+ Returns:
166
+ str: 截断后的文本
167
+ """
168
+ if len(text) <= max_length:
169
+ return text
170
+ return text[:max_length-3] + "..."
171
+
172
+
173
+ def is_valid_domain(domain: str) -> bool:
174
+ """
175
+ 验证域名格式是否正确
176
+
177
+ Args:
178
+ domain: 域名
179
+
180
+ Returns:
181
+ bool: 是否有效
182
+ """
183
+ import re
184
+ pattern = re.compile(
185
+ r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$'
186
+ )
187
+ return bool(pattern.match(domain))