BaiFu
Committed by GitHub

Merge pull request #601 from DoiiarX/mindspider/fix-2026-03-07

MindSpider: 修复多项运行时错误,提升初始化健壮性
@@ -59,6 +59,8 @@ class DeepSentimentCrawling: @@ -59,6 +59,8 @@ class DeepSentimentCrawling:
59 59
60 if not summary['has_data']: 60 if not summary['has_data']:
61 print("⚠️ 没有找到话题数据,无法进行爬取") 61 print("⚠️ 没有找到话题数据,无法进行爬取")
  62 + print("💡 请先运行以下命令获取今日话题数据:")
  63 + print(" uv run main.py --broad-topic")
62 return {"success": False, "error": "没有话题数据"} 64 return {"success": False, "error": "没有话题数据"}
63 65
64 # 2. 获取关键词(不分配,所有平台使用相同关键词) 66 # 2. 获取关键词(不分配,所有平台使用相同关键词)
@@ -33,9 +33,13 @@ class PlatformCrawler: @@ -33,9 +33,13 @@ class PlatformCrawler:
33 self.supported_platforms = ['xhs', 'dy', 'ks', 'bili', 'wb', 'tieba', 'zhihu'] 33 self.supported_platforms = ['xhs', 'dy', 'ks', 'bili', 'wb', 'tieba', 'zhihu']
34 self.crawl_stats = {} 34 self.crawl_stats = {}
35 35
36 - # 确保MediaCrawler目录存在  
37 - if not self.mediacrawler_path.exists():  
38 - raise FileNotFoundError(f"MediaCrawler目录不存在: {self.mediacrawler_path}") 36 + # 确保MediaCrawler子模块已初始化
  37 + db_config_path = self.mediacrawler_path / "config" / "db_config.py"
  38 + if not self.mediacrawler_path.exists() or not db_config_path.exists():
  39 + logger.error("MediaCrawler子模块未初始化或不完整")
  40 + logger.error("请在项目根目录运行以下命令初始化子模块:")
  41 + logger.error(" git submodule update --init --recursive")
  42 + raise FileNotFoundError("MediaCrawler子模块未初始化,请先运行: git submodule update --init --recursive")
39 43
40 logger.info(f"初始化平台爬虫管理器,MediaCrawler路径: {self.mediacrawler_path}") 44 logger.info(f"初始化平台爬虫管理器,MediaCrawler路径: {self.mediacrawler_path}")
41 45
@@ -122,19 +126,19 @@ mongodb_config = {{ @@ -122,19 +126,19 @@ mongodb_config = {{
122 "db_name": MONGODB_DB_NAME, 126 "db_name": MONGODB_DB_NAME,
123 }} 127 }}
124 128
125 -# postgresql config - 使用MindSpider的数据库配置(如果DB_DIALECT是postgresql)或环境变量  
126 -POSTGRESQL_DB_PWD = os.getenv("POSTGRESQL_DB_PWD", "{pg_password}")  
127 -POSTGRESQL_DB_USER = os.getenv("POSTGRESQL_DB_USER", "{pg_user}")  
128 -POSTGRESQL_DB_HOST = os.getenv("POSTGRESQL_DB_HOST", "{pg_host}")  
129 -POSTGRESQL_DB_PORT = os.getenv("POSTGRESQL_DB_PORT", "{pg_port}")  
130 -POSTGRESQL_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "{pg_db_name}")  
131 -  
132 -postgresql_db_config = {{  
133 - "user": POSTGRESQL_DB_USER,  
134 - "password": POSTGRESQL_DB_PWD,  
135 - "host": POSTGRESQL_DB_HOST,  
136 - "port": POSTGRESQL_DB_PORT,  
137 - "db_name": POSTGRESQL_DB_NAME, 129 +# postgres config - 使用MindSpider的数据库配置(如果DB_DIALECT是postgresql)或环境变量
  130 +POSTGRES_DB_PWD = os.getenv("POSTGRES_DB_PWD", "{pg_password}")
  131 +POSTGRES_DB_USER = os.getenv("POSTGRES_DB_USER", "{pg_user}")
  132 +POSTGRES_DB_HOST = os.getenv("POSTGRES_DB_HOST", "{pg_host}")
  133 +POSTGRES_DB_PORT = os.getenv("POSTGRES_DB_PORT", "{pg_port}")
  134 +POSTGRES_DB_NAME = os.getenv("POSTGRES_DB_NAME", "{pg_db_name}")
  135 +
  136 +postgres_db_config = {{
  137 + "user": POSTGRES_DB_USER,
  138 + "password": POSTGRES_DB_PWD,
  139 + "host": POSTGRES_DB_HOST,
  140 + "port": POSTGRES_DB_PORT,
  141 + "db_name": POSTGRES_DB_NAME,
138 }} 142 }}
139 143
140 ''' 144 '''
@@ -169,7 +173,7 @@ postgresql_db_config = {{ @@ -169,7 +173,7 @@ postgresql_db_config = {{
169 # 判断数据库类型,确定 SAVE_DATA_OPTION 173 # 判断数据库类型,确定 SAVE_DATA_OPTION
170 db_dialect = (config.settings.DB_DIALECT or "mysql").lower() 174 db_dialect = (config.settings.DB_DIALECT or "mysql").lower()
171 is_postgresql = db_dialect in ("postgresql", "postgres") 175 is_postgresql = db_dialect in ("postgresql", "postgres")
172 - save_data_option = "postgresql" if is_postgresql else "db" 176 + save_data_option = "postgres" if is_postgresql else "db"
173 177
174 base_config_path = self.mediacrawler_path / "config" / "base_config.py" 178 base_config_path = self.mediacrawler_path / "config" / "base_config.py"
175 179
@@ -181,26 +185,42 @@ postgresql_db_config = {{ @@ -181,26 +185,42 @@ postgresql_db_config = {{
181 content = f.read() 185 content = f.read()
182 186
183 # 修改关键配置项 187 # 修改关键配置项
  188 + # skip_until_paren: 当原始行是多行赋值(以"("结尾)被替换为单行后,
  189 + # 需要跳过后续续行直到遇到配对的")"
184 lines = content.split('\n') 190 lines = content.split('\n')
185 new_lines = [] 191 new_lines = []
  192 + skip_until_paren = False
186 193
187 for line in lines: 194 for line in lines:
  195 + # 跳过多行赋值的续行
  196 + if skip_until_paren:
  197 + if line.strip() == ')':
  198 + skip_until_paren = False
  199 + continue
  200 +
  201 + replaced = None
188 if line.startswith('PLATFORM = '): 202 if line.startswith('PLATFORM = '):
189 - new_lines.append(f'PLATFORM = "{platform}" # 平台,xhs | dy | ks | bili | wb | tieba | zhihu') 203 + replaced = f'PLATFORM = "{platform}" # 平台,xhs | dy | ks | bili | wb | tieba | zhihu'
190 elif line.startswith('KEYWORDS = '): 204 elif line.startswith('KEYWORDS = '):
191 - new_lines.append(f'KEYWORDS = "{keywords_str}" # 关键词搜索配置,以英文逗号分隔') 205 + replaced = f'KEYWORDS = "{keywords_str}" # 关键词搜索配置,以英文逗号分隔'
192 elif line.startswith('CRAWLER_TYPE = '): 206 elif line.startswith('CRAWLER_TYPE = '):
193 - new_lines.append(f'CRAWLER_TYPE = "{crawler_type}" # 爬取类型,search(关键词搜索) | detail(帖子详情)| creator(创作者主页数据)') 207 + replaced = f'CRAWLER_TYPE = "{crawler_type}" # 爬取类型,search(关键词搜索) | detail(帖子详情)| creator(创作者主页数据)'
194 elif line.startswith('SAVE_DATA_OPTION = '): 208 elif line.startswith('SAVE_DATA_OPTION = '):
195 - new_lines.append(f'SAVE_DATA_OPTION = "{save_data_option}" # csv or db or json or sqlite or postgresql') 209 + replaced = f'SAVE_DATA_OPTION = "{save_data_option}" # csv or db or json or sqlite or postgres'
196 elif line.startswith('CRAWLER_MAX_NOTES_COUNT = '): 210 elif line.startswith('CRAWLER_MAX_NOTES_COUNT = '):
197 - new_lines.append(f'CRAWLER_MAX_NOTES_COUNT = {max_notes}') 211 + replaced = f'CRAWLER_MAX_NOTES_COUNT = {max_notes}'
198 elif line.startswith('ENABLE_GET_COMMENTS = '): 212 elif line.startswith('ENABLE_GET_COMMENTS = '):
199 - new_lines.append('ENABLE_GET_COMMENTS = True') 213 + replaced = 'ENABLE_GET_COMMENTS = True'
200 elif line.startswith('CRAWLER_MAX_COMMENTS_COUNT_SINGLENOTES = '): 214 elif line.startswith('CRAWLER_MAX_COMMENTS_COUNT_SINGLENOTES = '):
201 - new_lines.append('CRAWLER_MAX_COMMENTS_COUNT_SINGLENOTES = 20') 215 + replaced = 'CRAWLER_MAX_COMMENTS_COUNT_SINGLENOTES = 20'
202 elif line.startswith('HEADLESS = '): 216 elif line.startswith('HEADLESS = '):
203 - new_lines.append('HEADLESS = True') # 使用无头模式 217 + replaced = 'HEADLESS = True'
  218 +
  219 + if replaced is not None:
  220 + new_lines.append(replaced)
  221 + # 若原始行是多行赋值开头(以"("结尾),跳过后续续行
  222 + if line.rstrip().endswith('('):
  223 + skip_until_paren = True
204 else: 224 else:
205 new_lines.append(line) 225 new_lines.append(line)
206 226
@@ -253,7 +273,7 @@ postgresql_db_config = {{ @@ -253,7 +273,7 @@ postgresql_db_config = {{
253 # 判断数据库类型,确定 save_data_option 273 # 判断数据库类型,确定 save_data_option
254 db_dialect = (config.settings.DB_DIALECT or "mysql").lower() 274 db_dialect = (config.settings.DB_DIALECT or "mysql").lower()
255 is_postgresql = db_dialect in ("postgresql", "postgres") 275 is_postgresql = db_dialect in ("postgresql", "postgres")
256 - save_data_option = "postgresql" if is_postgresql else "db" 276 + save_data_option = "postgres" if is_postgresql else "db"
257 277
258 # 构建命令 278 # 构建命令
259 cmd = [ 279 cmd = [
@@ -261,7 +281,8 @@ postgresql_db_config = {{ @@ -261,7 +281,8 @@ postgresql_db_config = {{
261 "--platform", platform, 281 "--platform", platform,
262 "--lt", login_type, 282 "--lt", login_type,
263 "--type", "search", 283 "--type", "search",
264 - "--save_data_option", save_data_option 284 + "--save_data_option", save_data_option,
  285 + "--headless", "false"
265 ] 286 ]
266 287
267 logger.info(f"执行命令: {' '.join(cmd)}") 288 logger.info(f"执行命令: {' '.join(cmd)}")
@@ -25,7 +25,7 @@ MindSpider 运行示例 @@ -25,7 +25,7 @@ MindSpider 运行示例
25 - **编程语言**: Python 3.9+ 25 - **编程语言**: Python 3.9+
26 - **AI框架**: 默认Deepseek,可以接入多种api (话题提取与分析) 26 - **AI框架**: 默认Deepseek,可以接入多种api (话题提取与分析)
27 - **爬虫框架**: Playwright (浏览器自动化) 27 - **爬虫框架**: Playwright (浏览器自动化)
28 -- **数据库**: MySQL (数据持久化存储) 28 +- **数据库**: MySQL / PostgreSQL (数据持久化存储)
29 - **并发处理**: AsyncIO (异步并发爬取) 29 - **并发处理**: AsyncIO (异步并发爬取)
30 30
31 ## 项目结构 31 ## 项目结构
@@ -223,7 +223,7 @@ cd BettaFish/MindSpider @@ -223,7 +223,7 @@ cd BettaFish/MindSpider
223 git submodule update --init --recursive 223 git submodule update --init --recursive
224 ``` 224 ```
225 225
226 -> **注意**:MediaCrawler 的 Python 依赖会在首次运行 `python main.py` 时由系统自动检测并静默安装到当前环境。 226 +> **注意**:MediaCrawler 的 Python 依赖会在首次运行 `uv run main.py --deep-sentiment` 时由系统自动检测并安装到当前环境。
227 227
228 ### 2. 创建并激活环境 228 ### 2. 创建并激活环境
229 229
@@ -275,13 +275,18 @@ playwright install @@ -275,13 +275,18 @@ playwright install
275 复制.env.example文件为.env文件,放置在项目根目录。编辑 `.env` 文件,设置数据库和API配置: 275 复制.env.example文件为.env文件,放置在项目根目录。编辑 `.env` 文件,设置数据库和API配置:
276 276
277 ```python 277 ```python
278 -# MySQL数据库配置 278 +# 数据库配置(MySQL 示例)
  279 +DB_DIALECT = "mysql" # mysql 或 postgresql
279 DB_HOST = "your_database_host" 280 DB_HOST = "your_database_host"
280 DB_PORT = 3306 281 DB_PORT = 3306
281 DB_USER = "your_username" 282 DB_USER = "your_username"
282 DB_PASSWORD = "your_password" 283 DB_PASSWORD = "your_password"
283 DB_NAME = "mindspider" 284 DB_NAME = "mindspider"
284 -DB_CHARSET = "utf8mb4" 285 +DB_CHARSET = "utf8mb4" # PostgreSQL 时可省略此项
  286 +
  287 +# PostgreSQL 示例(将上方 DB_DIALECT 改为 postgresql,DB_PORT 改为 5432)
  288 +# DB_DIALECT = "postgresql"
  289 +# DB_PORT = 5432
285 290
286 # MINDSPIDER API密钥 291 # MINDSPIDER API密钥
287 MINDSPIDER_BASE_URL=your_api_base_url 292 MINDSPIDER_BASE_URL=your_api_base_url
@@ -294,6 +299,8 @@ MINDSPIDER_MODEL_NAME=deepseek-chat @@ -294,6 +299,8 @@ MINDSPIDER_MODEL_NAME=deepseek-chat
294 ```bash 299 ```bash
295 # 检查系统状态 300 # 检查系统状态
296 python main.py --status 301 python main.py --status
  302 +# 或
  303 +uv run main.py --status
297 ``` 304 ```
298 305
299 ## 使用指南 306 ## 使用指南
@@ -303,12 +310,18 @@ python main.py --status @@ -303,12 +310,18 @@ python main.py --status
303 ```bash 310 ```bash
304 # 1. 运行话题提取(获取热点新闻和关键词) 311 # 1. 运行话题提取(获取热点新闻和关键词)
305 python main.py --broad-topic 312 python main.py --broad-topic
  313 +# 或
  314 +uv run main.py --broad-topic
306 315
307 # 2. 运行爬虫(基于关键词爬取各平台内容) 316 # 2. 运行爬虫(基于关键词爬取各平台内容)
308 python main.py --deep-sentiment --test 317 python main.py --deep-sentiment --test
  318 +# 或
  319 +uv run main.py --deep-sentiment --test
309 320
310 # 或者一次性运行完整流程 321 # 或者一次性运行完整流程
311 python main.py --complete --test 322 python main.py --complete --test
  323 +# 或
  324 +uv run main.py --complete --test
312 ``` 325 ```
313 326
314 ### 单独使用模块 327 ### 单独使用模块
@@ -316,12 +329,18 @@ python main.py --complete --test @@ -316,12 +329,18 @@ python main.py --complete --test
316 ```bash 329 ```bash
317 # 只获取今日热点和关键词 330 # 只获取今日热点和关键词
318 python main.py --broad-topic 331 python main.py --broad-topic
  332 +# 或
  333 +uv run main.py --broad-topic
319 334
320 # 只爬取特定平台 335 # 只爬取特定平台
321 python main.py --deep-sentiment --platforms xhs dy --test 336 python main.py --deep-sentiment --platforms xhs dy --test
  337 +# 或
  338 +uv run main.py --deep-sentiment --platforms xhs dy --test
322 339
323 # 指定日期 340 # 指定日期
324 python main.py --broad-topic --date 2024-01-15 341 python main.py --broad-topic --date 2024-01-15
  342 +# 或
  343 +uv run main.py --broad-topic --date 2024-01-15
325 ``` 344 ```
326 345
327 ## 爬虫配置(重要) 346 ## 爬虫配置(重要)
@@ -334,6 +353,8 @@ python main.py --broad-topic --date 2024-01-15 @@ -334,6 +353,8 @@ python main.py --broad-topic --date 2024-01-15
334 ```bash 353 ```bash
335 # 测试小红书爬取(会弹出二维码) 354 # 测试小红书爬取(会弹出二维码)
336 python main.py --deep-sentiment --platforms xhs --test 355 python main.py --deep-sentiment --platforms xhs --test
  356 +# 或
  357 +uv run main.py --deep-sentiment --platforms xhs --test
337 # 用小红书APP扫码登录,登录成功后会自动保存状态 358 # 用小红书APP扫码登录,登录成功后会自动保存状态
338 ``` 359 ```
339 360
@@ -341,25 +362,27 @@ python main.py --deep-sentiment --platforms xhs --test @@ -341,25 +362,27 @@ python main.py --deep-sentiment --platforms xhs --test
341 ```bash 362 ```bash
342 # 测试抖音爬取 363 # 测试抖音爬取
343 python main.py --deep-sentiment --platforms dy --test 364 python main.py --deep-sentiment --platforms dy --test
  365 +# 或
  366 +uv run main.py --deep-sentiment --platforms dy --test
344 # 用抖音APP扫码登录 367 # 用抖音APP扫码登录
345 ``` 368 ```
346 369
347 3. **其他平台同理** 370 3. **其他平台同理**
348 ```bash 371 ```bash
349 # 快手 372 # 快手
350 -python main.py --deep-sentiment --platforms ks --test 373 +uv run main.py --deep-sentiment --platforms ks --test
351 374
352 # B站 375 # B站
353 -python main.py --deep-sentiment --platforms bili --test 376 +uv run main.py --deep-sentiment --platforms bili --test
354 377
355 # 微博 378 # 微博
356 -python main.py --deep-sentiment --platforms wb --test 379 +uv run main.py --deep-sentiment --platforms wb --test
357 380
358 # 贴吧 381 # 贴吧
359 -python main.py --deep-sentiment --platforms tieba --test 382 +uv run main.py --deep-sentiment --platforms tieba --test
360 383
361 # 知乎 384 # 知乎
362 -python main.py --deep-sentiment --platforms zhihu --test 385 +uv run main.py --deep-sentiment --platforms zhihu --test
363 ``` 386 ```
364 387
365 ### 登录问题排除 388 ### 登录问题排除
@@ -385,9 +408,13 @@ https://github.com/666ghj/BettaFish/issues/185 @@ -385,9 +408,13 @@ https://github.com/666ghj/BettaFish/issues/185
385 ```bash 408 ```bash
386 # 小规模测试(推荐先这样测试) 409 # 小规模测试(推荐先这样测试)
387 python main.py --complete --test 410 python main.py --complete --test
  411 +# 或
  412 +uv run main.py --complete --test
388 413
389 # 调整爬取数量 414 # 调整爬取数量
390 python main.py --complete --max-keywords 20 --max-notes 30 415 python main.py --complete --max-keywords 20 --max-notes 30
  416 +# 或
  417 +uv run main.py --complete --max-keywords 20 --max-notes 30
391 ``` 418 ```
392 419
393 ### 高级功能 420 ### 高级功能
@@ -396,18 +423,26 @@ python main.py --complete --max-keywords 20 --max-notes 30 @@ -396,18 +423,26 @@ python main.py --complete --max-keywords 20 --max-notes 30
396 ```bash 423 ```bash
397 # 提取指定日期的话题 424 # 提取指定日期的话题
398 python main.py --broad-topic --date 2024-01-15 425 python main.py --broad-topic --date 2024-01-15
  426 +# 或
  427 +uv run main.py --broad-topic --date 2024-01-15
399 428
400 # 爬取指定日期的内容 429 # 爬取指定日期的内容
401 python main.py --deep-sentiment --date 2024-01-15 430 python main.py --deep-sentiment --date 2024-01-15
  431 +# 或
  432 +uv run main.py --deep-sentiment --date 2024-01-15
402 ``` 433 ```
403 434
404 #### 2. 指定平台爬取 435 #### 2. 指定平台爬取
405 ```bash 436 ```bash
406 # 只爬取B站和抖音 437 # 只爬取B站和抖音
407 python main.py --deep-sentiment --platforms bili dy --test 438 python main.py --deep-sentiment --platforms bili dy --test
  439 +# 或
  440 +uv run main.py --deep-sentiment --platforms bili dy --test
408 441
409 # 爬取所有平台的特定数量内容 442 # 爬取所有平台的特定数量内容
410 python main.py --deep-sentiment --max-keywords 30 --max-notes 20 443 python main.py --deep-sentiment --max-keywords 30 --max-notes 20
  444 +# 或
  445 +uv run main.py --deep-sentiment --max-keywords 30 --max-notes 20
411 ``` 446 ```
412 447
413 ## 常用参数 448 ## 常用参数
@@ -443,12 +478,16 @@ HEADLESS = False @@ -443,12 +478,16 @@ HEADLESS = False
443 478
444 # 重新运行登录 479 # 重新运行登录
445 python main.py --deep-sentiment --platforms xhs --test 480 python main.py --deep-sentiment --platforms xhs --test
  481 +# 或
  482 +uv run main.py --deep-sentiment --platforms xhs --test
446 ``` 483 ```
447 484
448 ### 2. 数据库连接失败 485 ### 2. 数据库连接失败
449 ```bash 486 ```bash
450 # 检查配置 487 # 检查配置
451 python main.py --status 488 python main.py --status
  489 +# 或
  490 +uv run main.py --status
452 491
453 # 检查config.py中的数据库配置是否正确 492 # 检查config.py中的数据库配置是否正确
454 ``` 493 ```
@@ -8,6 +8,8 @@ MindSpider - AI爬虫项目主程序 @@ -8,6 +8,8 @@ MindSpider - AI爬虫项目主程序
8 import os 8 import os
9 import sys 9 import sys
10 import argparse 10 import argparse
  11 +import difflib
  12 +import re
11 from datetime import date, datetime 13 from datetime import date, datetime
12 from pathlib import Path 14 from pathlib import Path
13 import subprocess 15 import subprocess
@@ -73,7 +75,7 @@ class MindSpider: @@ -73,7 +75,7 @@ class MindSpider:
73 75
74 def build_async_url() -> str: 76 def build_async_url() -> str:
75 dialect = (settings.DB_DIALECT or "mysql").lower() 77 dialect = (settings.DB_DIALECT or "mysql").lower()
76 - if dialect == "postgresql": 78 + if dialect in ("postgresql", "postgres"):
77 return f"postgresql+asyncpg://{settings.DB_USER}:{quote_plus(settings.DB_PASSWORD)}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}" 79 return f"postgresql+asyncpg://{settings.DB_USER}:{quote_plus(settings.DB_PASSWORD)}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"
78 # 默认使用 mysql 异步驱动 asyncmy 80 # 默认使用 mysql 异步驱动 asyncmy
79 return ( 81 return (
@@ -104,7 +106,7 @@ class MindSpider: @@ -104,7 +106,7 @@ class MindSpider:
104 106
105 def build_async_url() -> str: 107 def build_async_url() -> str:
106 dialect = (settings.DB_DIALECT or "mysql").lower() 108 dialect = (settings.DB_DIALECT or "mysql").lower()
107 - if dialect == "postgresql": 109 + if dialect in ("postgresql", "postgres"):
108 return f"postgresql+asyncpg://{settings.DB_USER}:{quote_plus(settings.DB_PASSWORD)}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}" 110 return f"postgresql+asyncpg://{settings.DB_USER}:{quote_plus(settings.DB_PASSWORD)}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"
109 return ( 111 return (
110 f"mysql+asyncmy://{settings.DB_USER}:{quote_plus(settings.DB_PASSWORD)}" 112 f"mysql+asyncmy://{settings.DB_USER}:{quote_plus(settings.DB_PASSWORD)}"
@@ -230,21 +232,25 @@ class MindSpider: @@ -230,21 +232,25 @@ class MindSpider:
230 return True 232 return True
231 233
232 logger.info("正在安装MediaCrawler依赖...") 234 logger.info("正在安装MediaCrawler依赖...")
  235 + install_commands = [
  236 + [sys.executable, "-m", "pip", "install", "-r", str(mediacrawler_req), "-q"],
  237 + ["uv", "pip", "install", "-r", str(mediacrawler_req), "-q"],
  238 + ]
233 try: 239 try:
  240 + for cmd in install_commands:
234 result = subprocess.run( 241 result = subprocess.run(
235 - [sys.executable, "-m", "pip", "install", "-r", str(mediacrawler_req), "-q"], 242 + cmd,
236 capture_output=True, 243 capture_output=True,
237 text=True, 244 text=True,
238 timeout=300 # 5分钟超时 245 timeout=300 # 5分钟超时
239 ) 246 )
240 -  
241 if result.returncode == 0: 247 if result.returncode == 0:
242 - # 创建标记文件  
243 marker_file.touch() 248 marker_file.touch()
244 - logger.info("MediaCrawler依赖安装成功") 249 + logger.info(f"MediaCrawler依赖安装成功 (via {cmd[0]})")
245 return True 250 return True
246 - else:  
247 - logger.error(f"MediaCrawler依赖安装失败: {result.stderr}") 251 + logger.debug(f"{cmd[0]} 安装失败,尝试下一种方式: {result.stderr.strip()}")
  252 +
  253 + logger.error("MediaCrawler依赖安装失败:所有安装方式均不可用")
248 return False 254 return False
249 255
250 except subprocess.TimeoutExpired: 256 except subprocess.TimeoutExpired:
@@ -303,6 +309,9 @@ class MindSpider: @@ -303,6 +309,9 @@ class MindSpider:
303 if not self._ensure_database_ready(): 309 if not self._ensure_database_ready():
304 return False 310 return False
305 311
  312 + # 自动安装MediaCrawler依赖
  313 + self._install_mediacrawler_dependencies()
  314 +
306 if not target_date: 315 if not target_date:
307 target_date = date.today() 316 target_date = date.today()
308 317
@@ -430,9 +439,42 @@ class MindSpider: @@ -430,9 +439,42 @@ class MindSpider:
430 logger.info("MindSpider项目初始化完成!") 439 logger.info("MindSpider项目初始化完成!")
431 return True 440 return True
432 441
  442 +PLATFORM_CHOICES = ['xhs', 'dy', 'ks', 'bili', 'wb', 'tieba', 'zhihu']
  443 +
  444 +PLATFORM_ALIASES = {
  445 + 'weibo': 'wb', 'webo': 'wb', '微博': 'wb',
  446 + 'douyin': 'dy', '抖音': 'dy',
  447 + 'kuaishou': 'ks', '快手': 'ks',
  448 + 'bilibili': 'bili', 'b站': 'bili', 'bstation': 'bili',
  449 + 'xiaohongshu': 'xhs', '小红书': 'xhs', 'redbook': 'xhs',
  450 + 'zhihu': 'zhihu', '知乎': 'zhihu',
  451 + 'tieba': 'tieba', '贴吧': 'tieba',
  452 +}
  453 +
  454 +class SuggestiveArgumentParser(argparse.ArgumentParser):
  455 + """在参数错误时给出相似候选项提示"""
  456 +
  457 + def error(self, message: str):
  458 + match = re.search(r"invalid choice: '([^']+)'", message)
  459 + if match:
  460 + bad = match.group(1)
  461 + alias = PLATFORM_ALIASES.get(bad.lower())
  462 + suggestions = difflib.get_close_matches(bad, PLATFORM_CHOICES, n=3, cutoff=0.3)
  463 + if alias:
  464 + print(f"错误: '{bad}' 不是合法的平台代码。您是否想输入 '{alias}'?", file=sys.stderr)
  465 + elif suggestions:
  466 + print(f"错误: '{bad}' 不是合法的平台代码。最接近的选项: {suggestions}", file=sys.stderr)
  467 + else:
  468 + print(f"错误: '{bad}' 不是合法的平台代码。合法平台: {PLATFORM_CHOICES}", file=sys.stderr)
  469 + print(f"完整错误: {message}", file=sys.stderr)
  470 + else:
  471 + print(f"错误: {message}", file=sys.stderr)
  472 + self.print_usage(sys.stderr)
  473 + sys.exit(2)
  474 +
433 def main(): 475 def main():
434 """命令行入口""" 476 """命令行入口"""
435 - parser = argparse.ArgumentParser(description="MindSpider - AI爬虫项目主程序") 477 + parser = SuggestiveArgumentParser(description="MindSpider - AI爬虫项目主程序")
436 478
437 # 基本操作 479 # 基本操作
438 parser.add_argument("--setup", action="store_true", help="初始化项目设置") 480 parser.add_argument("--setup", action="store_true", help="初始化项目设置")
@@ -447,7 +489,7 @@ def main(): @@ -447,7 +489,7 @@ def main():
447 # 参数配置 489 # 参数配置
448 parser.add_argument("--date", type=str, help="目标日期 (YYYY-MM-DD),默认为今天") 490 parser.add_argument("--date", type=str, help="目标日期 (YYYY-MM-DD),默认为今天")
449 parser.add_argument("--platforms", type=str, nargs='+', 491 parser.add_argument("--platforms", type=str, nargs='+',
450 - choices=['xhs', 'dy', 'ks', 'bili', 'wb', 'tieba', 'zhihu'], 492 + choices=PLATFORM_CHOICES,
451 help="指定爬取平台") 493 help="指定爬取平台")
452 parser.add_argument("--keywords-count", type=int, default=100, help="话题提取的关键词数量") 494 parser.add_argument("--keywords-count", type=int, default=100, help="话题提取的关键词数量")
453 parser.add_argument("--max-keywords", type=int, default=50, help="每个平台最大关键词数量") 495 parser.add_argument("--max-keywords", type=int, default=50, help="每个平台最大关键词数量")