Files
usa/crawler/README.md
2026-03-03 17:27:55 +08:00

17 KiB
Raw Blame History

GDELT 实时冲突服务 + 新闻爬虫

数据来源梳理

1. GDELT Project (gdelt_events)

项目 说明
API https://api.gdeltproject.org/api/v2/doc/doc
查询 query=United States Iran military(可配 GDELT_QUERY
模式 mode=ArtListformat=jsonmaxrecords=30
时间范围 未指定时默认最近 3 个月,按相关性排序,易返回较旧文章
更新频率 GDELT 约 15 分钟级,爬虫 60 秒拉一次

数据偏老原因:未传 timespansort=datedescAPI 返回 3 个月内“最相关”文章,不保证最新。

2. RSS 新闻 (situation_update) — 主事件脉络来源

项目 说明
多国主流媒体:美(Reuters/NYT)、英(BBC)、法(France 24)、俄(TASS/RT)、中(Xinhua/CGTN)、伊(Press TV)、卡塔尔(Al Jazeera)
过滤 标题/摘要需含 KEYWORDS 之一iran、usa、strike、military 等)
更新 爬虫 45 秒拉一次(RSS_INTERVAL_SEC),优先保证事件脉络
优先级 启动时先拉 RSS再拉 GDELT

GDELT 无法访问时:设置 GDELT_DISABLED=1,仅用 RSS 新闻即可维持事件脉络。部分境外源可能受网络限制。

3. AI 新闻清洗与分类(可选)

  • 清洗cleaner_ai.py 用 Ollama 提炼新闻为简洁摘要,供面板展示
  • 分类parser_ai.py 用 Ollama 替代规则做 category/severity 判定
  • 需先安装并运行 Ollamaollama run llama3.1
  • 环境变量:OLLAMA_MODEL=llama3.1PARSER_AI_DISABLED=1CLEANER_AI_DISABLED=1(禁用对应 AI

事件脉络可实时更新:爬虫抓取后 → 写入 SQLite → 调用 Node 通知 → WebSocket 广播 → 前端自动刷新。

依赖

pip install -r requirements.txt

新增 deep-translatorGDELT 与 RSS 新闻入库前自动翻译为中文。

运行(需同时启动 3 个服务)

终端 命令 说明
1 npm run api Node API + WebSocket必须
2 npm run gdelt GDELT + RSS 爬虫(事件脉络数据来源
3 npm run dev 前端开发

事件脉络不更新时:多半是未启动 npm run gdelt。只跑 npm run api 时,事件脉络会显示空或仅有缓存。

如何检查爬虫是否工作正常

按下面顺序做即可确认整条链路(爬虫 → 数据库 → Node 重载 → API/WebSocket正常。

1. 一键验证(推荐)

先启动 API再执行验证脚本可选是否顺带启动爬虫

# 终端 1必须
npm run api

# 终端 2执行验证不启动爬虫只检查当前状态
./scripts/verify-pipeline.sh

# 或:顺带启动爬虫并等首次抓取后再验证
./scripts/verify-pipeline.sh --start-crawler

脚本会检查API 健康、态势数据含 lastUpdated、爬虫服务是否可达、news_content/situation_update、战损字段、POST /api/crawler/notify 是否可用。

2. 手动快速检查

步骤 命令 / 操作 正常表现
API 是否在跑 curl -s http://localhost:3001/api/health 返回 {"ok":true}
态势是否可读 curl -s http://localhost:3001/api/situation | head -c 300 lastUpdatedusForcesrecentUpdates
RSS 能否抓到 npm run crawler:test 输出「RSS 抓取: N 条」N>0 表示有命中
爬虫服务gdelt curl -s http://localhost:8000/crawler/status 返回 JSONdb_path/db_exists
库里有无爬虫数据 sqlite3 server/data.db "SELECT COUNT(*) FROM situation_update; SELECT COUNT(*) FROM news_content;" 或访问 http://localhost:3001/api/db/dashboard situation_update、news_content 条数 > 0跑过流水线后
通知后是否重载 爬虫写库后会 POST /api/crawler/notifyNode 会 reloadFromFile 再广播 前端//api/situationlastUpdated 和内容会更新

3. 跑一轮流水线(不常驻爬虫时)

不启动 gdelt 时,可单次跑完整流水线(抓取 → 去重 → 写表 → notify

npm run api   # 保持运行
cd crawler && python3 -c "
from pipeline import run_full_pipeline
from config import DB_PATH, API_BASE
n_fetched, n_news, n_panel = run_full_pipeline(db_path=DB_PATH, api_base=API_BASE, notify=True)
print('抓取:', n_fetched, '去重新增:', n_news, '面板写入:', n_panel)
"

有网络且有关键词命中时,应看到非零数字;再查 curl -s http://localhost:3001/api/situation 或前端事件脉络是否出现新数据。

4. 仅测提取逻辑(不写库)

npm run crawler:test:extraction   # 规则/db_merge 测试
# 或按 README「快速自测命令」用示例文本调 extract_from_news 看 combat_losses_delta / key_location_updates

常见现象:抓取 0 条 → 网络/RSS 被墙或关键词未命中situation_update 为空 → 未跑流水线或去重后无新增;前端不刷新 → 未开 npm run api 或未开爬虫gdelt

5. 爬虫与面板是否联通

专门检查「爬虫写库」与「面板展示」是否一致:

./scripts/check-crawler-panel-connectivity.sh

会对比:爬虫侧的 situation_update 条数 vs 面板 API 返回的 recentUpdates 条数,并说明为何战损/基地等不一定随每条新闻变化。

爬虫与面板数据联动说明

面板展示 数据来源(表/接口) 是否由爬虫更新 说明
事件脉络 (recentUpdates) situation_update → getSituation() 每条去重后的新闻会写入 situation_updateNode 收到 notify 后重载 DB 再广播
地图冲突点 (conflictEvents) gdelt_events 或 RSS→gdelt 回填 GDELT 或 GDELT 禁用时由 situation_update 同步到 gdelt_events
战损/装备毁伤 (combatLosses) combat_losses ⚠️ 有条件 仅当 AI/规则从新闻中提取到数字如「2 名美军死亡」merge 才写入增量
基地/地点状态 (keyLocations) key_location ⚠️ 有条件 仅当提取到 key_location_updates如某基地遭袭时更新
力量摘要/指数/资产 (summary, powerIndex, assets) force_summary, power_index, force_asset 仅 seed 初始化,爬虫不写
华尔街/报复情绪 (wallStreet, retaliation) wall_street_trend, retaliation_* ⚠️ 有条件 仅当提取器输出对应字段时更新

因此:新闻很多、但战损/基地数字不动是正常现象——多数标题不含可解析的伤亡/基地数字只有事件脉络recentUpdates和地图冲突点会随每条新闻增加。若事件脉络也不更新,请确认 Node 终端在爬虫每轮抓取后是否出现 [crawler/notify] DB 已重载;若无,检查爬虫的 API_BASE 是否指向当前 API默认 http://localhost:3001)。

写库流水线(与 server/README 第五节一致)

RSS 与主入口均走统一流水线 pipeline.run_full_pipeline

  1. 抓取 → 2. AI 清洗(标题/摘要/分类)→ 3. 去重news_content.content_hash→ 4. 映射到前端库字段situation_update、combat_losses、key_location 等)→ 5. 更新表 → 6. 有新增时 POST /api/crawler/notify
  • npm run crawlermain.pynpm run gdeltrealtime_conflict_service的 RSS 分支都调用该流水线。
  • 实现见 crawler/pipeline.py

数据流

GDELT API → 抓取(60s) → SQLite (gdelt_events, conflict_stats) → POST /api/crawler/notify
RSS → 抓取 → 清洗 → 去重 → 写 news_content / situation_update / 战损等 → POST /api/crawler/notify
                                                                    ↓
                                              Node 更新 situation.updated_at + WebSocket 广播
                                                                    ↓
                                                              前端实时展示

配置

环境变量:

  • DB_PATH: SQLite 路径,默认 ../server/data.db
  • API_BASE: Node API 地址,默认 http://localhost:3001
  • DASHSCOPE_API_KEY阿里云通义DashScopeAPI Key。设置后全程使用商业模型,无需本机安装 Ollama(适合 Mac 版本较低无法跑 Ollama 的情况)。获取: 阿里云百炼 / DashScope → 创建 API-KEY复制到环境变量或项目根目录 .envDASHSCOPE_API_KEY=sk-xxx。摘要、分类、战损/基地提取均走通义。
  • GDELT_QUERY: 搜索关键词,默认 United States Iran military
  • GDELT_MAX_RECORDS: 最大条数,默认 30
  • GDELT_TIMESPAN: 时间范围,1h / 1d / 1week,默认 1d(近日资讯)
  • GDELT_DISABLED: 设为 1 则跳过 GDELT仅用 RSS 新闻GDELT 无法访问时用)
  • FETCH_INTERVAL_SEC: GDELT 抓取间隔(秒),默认 60
  • RSS_INTERVAL_SEC: RSS 抓取间隔(秒),默认 45优先保证事件脉络
  • OLLAMA_MODEL: AI 分类模型,默认 llama3.1
  • PARSER_AI_DISABLED: 设为 1 则禁用 AI 分类,仅用规则
  • CLEANER_AI_DISABLED: 设为 1 则禁用 AI 清洗,仅用规则截断
  • FETCH_FULL_ARTICLE: 设为 0 则不再抓取正文,仅用标题+摘要做 AI 提取(默认 1 抓取正文)
  • ARTICLE_FETCH_LIMIT: 每轮为多少条新资讯抓取正文,默认 10
  • ARTICLE_FETCH_TIMEOUT: 单篇正文请求超时(秒),默认 12
  • ARTICLE_MAX_BODY_CHARS: 正文最大字符数,默认 6000
  • EXTRACT_TEXT_MAX_LEN: 送入 AI 提取的原文最大长度,默认 4000

增量与地点:战损一律按增量处理——AI 只填本则报道的「本次/此次」新增数,不填累计总数;合并时与库内当前值叠加。双方攻击地点通过 key_location_updates 更新(美军基地被打击 side=us伊朗设施被打击 side=iran会写入 key_location 的 status/damage_level。


优化后验证效果示例

以下为「正文抓取 + AI 精确提取 + 增量与地点更新」优化后,单条新闻从输入到前端展示的完整示例,便于对照验证。

1. 示例输入(新闻摘要/全文片段)

伊朗向伊拉克阿萨德空军基地发射 12 枚弹道导弹,造成此次袭击中 2 名美军人员死亡、14 人受伤,
另有 1 架战机在跑道受损。乌代德基地未遭直接命中。同日以色列对伊朗伊斯法罕一处设施发动打击。

2. AI 提取输出(增量 + 攻击地点)

{
  "summary": "伊朗导弹袭击伊拉克阿萨德基地致美军 2 死 14 伤1 架战机受损;以军打击伊斯法罕。",
  "category": "alert",
  "severity": "high",
  "us_personnel_killed": 2,
  "us_personnel_wounded": 14,
  "us_aircraft": 1,
  "us_bases_damaged": 1,
  "key_location_updates": [
    { "name_keywords": "阿萨德|asad|al-asad", "side": "us", "status": "attacked", "damage_level": 2 },
    { "name_keywords": "伊斯法罕|isfahan", "side": "iran", "status": "attacked", "damage_level": 1 }
  ]
}

说明:战损为本则报道的新增数(此次 2 死、14 伤、1 架战机),不是累计总数;地点为双方遭袭设施(美军基地 side=us伊朗设施 side=iran

3. 合并后数据库变化

表/字段 合并前 本则增量 合并后
combat_losses.us.personnel_killed 127 +2 129
combat_losses.us.personnel_wounded 384 +14 398
combat_losses.us.aircraft 2 +1 3
combat_losses.us.bases_damaged 27 +1 28
key_locationname 含「阿萨德」) status=operational status=attacked, damage_level=2
key_locationname 含「伊斯法罕」) status=operational status=attacked, damage_level=1

若 AI 误提「累计 2847 人丧生」并填成 personnel_killed=2847单次合并会被上限截断如最多 +500避免一次写入导致数据剧增。

4. 前端验证效果

  • 事件脉络出现一条新条目summary 为上述 12 句概括category=alert、severity=high。
  • 装备毁伤面板:美军「阵亡」+2、「受伤」+14、「战机」+1基地毁/损数字随 bases_damaged +1 更新。
  • 地图:阿萨德基地、伊斯法罕对应点位显示为「遭袭」状态(脉冲/标色随现有地图逻辑)。
  • APIGET /api/situationusForces.combatLossesusForces.keyLocations(含 status/damage_level为更新后值lastUpdated 为合并后时间。

5. 快速自测命令

# 仅测提取逻辑(不写库):用示例文本调 AI 提取,看是否得到增量 + key_location_updates
cd crawler && python3 -c "
from extractor_ai import extract_from_news
text = '''伊朗向伊拉克阿萨德空军基地发射导弹,此次袭击造成 2 名美军死亡、14 人受伤1 架战机受损。'''
out = extract_from_news(text)
print('combat_losses_delta:', out.get('combat_losses_delta'))
print('key_location_updates:', out.get('key_location_updates'))
"

期望:combat_losses_delta.us 含 personnel_killed=2、personnel_wounded=14、aircraft=1 等增量;key_location_updates 含阿萨德 side=us 等条目。


冲突强度 (impact_score)

分数 地图效果
13 绿色点
46 橙色闪烁
710 红色脉冲扩散

API

  • GET http://localhost:8000/events返回事件列表与冲突统计Python 服务直连)
  • GET http://localhost:3001/api/events:从 Node 读取(推荐,含 WebSocket 同步)

本地验证链路

按下面任选一种方式,确认「抓取 → 清洗 → 去重 → 映射 → 写表 → 通知」整条链路正常。

方式一:最小验证(不启动前端)

  1. 启动 API必须

    npm run api
    

    保持运行,默认 http://localhost:3001

  2. 安装爬虫依赖并跑一轮流水线

    cd crawler && pip install -r requirements.txt
    python -c "
    from pipeline import run_full_pipeline
    from config import DB_PATH, API_BASE
    n_fetched, n_news, n_panel = run_full_pipeline(db_path=DB_PATH, api_base=API_BASE, translate=True, notify=True)
    print('抓取:', n_fetched, '去重新增:', n_news, '面板写入:', n_panel)
    "
    
    • 有网络且有关键词命中时,应看到非零数字;无网络或全被过滤则为 0 0 0
    • 若报错 module 'socket' has no attribute 'settimeout',已修复为 setdefaulttimeout,请拉取最新代码。
  3. 查库确认

    sqlite3 server/data.db "SELECT COUNT(*) FROM situation_update; SELECT COUNT(*) FROM news_content;"
    

    或浏览器打开 http://localhost:3001/api/db/dashboard,看 situation_updatenews_content 是否有数据。

  4. 确认态势接口

    curl -s http://localhost:3001/api/situation | head -c 500
    

    应包含 lastUpdatedrecentUpdates 等。

方式二:用现有验证脚本(推荐)

  1. 终端 1npm run api
  2. 终端 2可选npm run gdelt(会定时跑 RSS + GDELT
  3. 执行验证脚本:
    ./scripts/verify-pipeline.sh
    
    若爬虫未启动想一并测爬虫,可:
    ./scripts/verify-pipeline.sh --start-crawler
    
    脚本会检查API 健康、态势数据、爬虫状态、资讯表、战损字段、通知接口。

方式三:只测 RSS 抓取(不写库)

npm run crawler:test

输出为「RSS 抓取: N 条」。0 条时检查网络或 config.pyRSS_FEEDS / KEYWORDS

常见问题

现象 可能原因
抓取 0 条 网络不通、RSS 被墙、关键词无一命中
situation_update 为空 去重后无新增,或未跑流水线(只跑了 fetch_all 未跑 run_full_pipeline
前端事件脉络不刷新 未启动 npm run api 或 WebSocket 未连上(需通过 Vite 代理访问前端)
翻译/AI 清洗很慢或报错 TRANSLATE_DISABLED=1CLEANER_AI_DISABLED=1 可跳过,用规则兜底

故障排查

现象 可能原因 排查
事件脉络始终为空 未启动 GDELT 爬虫 另开终端运行 npm run gdelt,观察是否有 GDELT 更新 X 条事件 输出
事件脉络不刷新 WebSocket 未连上 确认 npm run api 已启动,前端需通过 npm run dev 访问Vite 会代理 /ws
GDELT 抓取失败 系统代理超时 / ProxyError 爬虫默认直连,不走代理;若需代理请设 CRAWLER_USE_PROXY=1
GDELT 抓取失败 网络 / GDELT API 限流 检查 Python 终端报错GDELT 在国外,国内网络可能较慢或超时
新闻条数为 0 RSS 源被墙或关键词不匹配 检查 crawler/config.py 中 RSS_FEEDS、KEYWORDS国内需代理
返回数据偏老 GDELT 默认 3 个月内按相关性 设置 GDELT_TIMESPAN=1d 限制为近日;加 sort=datedesc 最新优先