22 KiB
GDELT 实时冲突服务 + 新闻爬虫
数据来源梳理
1. GDELT Project (gdelt_events)
| 项目 | 说明 |
|---|---|
| API | https://api.gdeltproject.org/api/v2/doc/doc |
| 查询 | query=United States Iran military(可配 GDELT_QUERY) |
| 模式 | mode=ArtList,format=json,maxrecords=30 |
| 时间范围 | 未指定时默认最近 3 个月,按相关性排序,易返回较旧文章 |
| 更新频率 | GDELT 约 15 分钟级,爬虫 60 秒拉一次 |
数据偏老原因:未传 timespan 和 sort=datedesc,API 返回 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 判定 - 需先安装并运行 Ollama:
ollama run llama3.1 - 环境变量:
OLLAMA_MODEL=llama3.1、PARSER_AI_DISABLED=1、CLEANER_AI_DISABLED=1(禁用对应 AI)
事件脉络可实时更新:爬虫抓取后 → 写入 SQLite → 调用 Node 通知 → WebSocket 广播 → 前端自动刷新。
依赖
pip install -r requirements.txt
新增 deep-translator:GDELT 与 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 |
含 lastUpdated、usForces、recentUpdates |
| RSS 能否抓到 | npm run crawler:test |
输出「RSS 抓取: N 条」,N>0 表示有命中 |
| 爬虫服务(gdelt) | curl -s http://localhost:8000/crawler/status |
返回 JSON,含 db_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/notify,Node 会 reloadFromFile 再广播 |
前端//api/situation 的 lastUpdated 和内容会更新 |
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 或前端事件脉络是否出现新数据。
按时间范围测试(例如 2 月 28 日 0 时至今):RSS 流水线支持只保留指定起始时间之后的条目,便于测试「从某日 0 点到现在」的数据。
# 默认从 2026-02-28 0:00 到现在
npm run crawler:once:range
# 或指定起始时间
./scripts/run-crawler-range.sh 2026-02-28T00:00:00
需设置环境变量 CRAWL_START_DATE(ISO 时间,如 2026-02-28T00:00:00)。GDELT 时间范围在启动 gdelt 服务时设置,例如:GDELT_TIMESPAN=3d npm run gdelt(最近 3 天)。
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_update,Node 收到 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:
- 抓取 → 2. AI 清洗(标题/摘要/分类)→ 3. 去重(news_content.content_hash)→ 4. 映射到前端库字段(situation_update、combat_losses、key_location 等)→ 5. 更新表 → 6. 有新增时 POST /api/crawler/notify
npm run crawler(main.py)与npm run gdelt(realtime_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.dbAPI_BASE: Node API 地址,默认http://localhost:3001DASHSCOPE_API_KEY:阿里云通义(DashScope)API Key。设置后全程使用商业模型,无需本机安装 Ollama(适合 Mac 版本较低无法跑 Ollama 的情况)。获取: 阿里云百炼 / DashScope → 创建 API-KEY,复制到环境变量或项目根目录.env中DASHSCOPE_API_KEY=sk-xxx。摘要、分类、战损/基地提取均走通义。GDELT_QUERY: 搜索关键词,默认United States Iran militaryGDELT_MAX_RECORDS: 最大条数,默认 30GDELT_TIMESPAN: 时间范围,1h/1d/1week,默认1d(近日资讯)GDELT_DISABLED: 设为1则跳过 GDELT,仅用 RSS 新闻(GDELT 无法访问时用)FETCH_INTERVAL_SEC: GDELT 抓取间隔(秒),默认 60RSS_INTERVAL_SEC: RSS 抓取间隔(秒),默认 45(优先保证事件脉络)OLLAMA_MODEL: AI 分类模型,默认llama3.1PARSER_AI_DISABLED: 设为1则禁用 AI 分类,仅用规则CLEANER_AI_DISABLED: 设为1则禁用 AI 清洗,仅用规则截断FETCH_FULL_ARTICLE: 设为0则不再抓取正文,仅用标题+摘要做 AI 提取(默认1抓取正文)ARTICLE_FETCH_LIMIT: 每轮为多少条新资讯抓取正文,默认 10ARTICLE_FETCH_TIMEOUT: 单篇正文请求超时(秒),默认 12ARTICLE_MAX_BODY_CHARS: 正文最大字符数,默认 6000EXTRACT_TEXT_MAX_LEN: 送入 AI 提取的原文最大长度,默认 4000
增量与地点:战损一律按增量处理——AI 只填本则报道的「本次/此次」新增数,不填累计总数;合并时与库内当前值叠加。双方攻击地点通过 key_location_updates 更新(美军基地被打击 side=us,伊朗设施被打击 side=iran),会写入 key_location 的 status/damage_level。
主要新闻资讯来源(RSS)
配置在 crawler/config.py 的 RSS_FEEDS,当前包含:
| 来源 | URL / 说明 |
|---|---|
| 美国 | Reuters Top News、NYT World |
| 英国 | BBC World、BBC Middle East、The Guardian World |
| 法国 | France 24 |
| 德国 | DW World |
| 俄罗斯 | TASS、RT |
| 中国 | Xinhua World、CGTN World |
| 凤凰 | 凤凰军事、凤凰国际(feedx.net 镜像) |
| 伊朗 | Press TV |
| 卡塔尔/中东 | Al Jazeera All、Al Jazeera Middle East |
单源超时由 FEED_TIMEOUT(默认 12 秒)控制;某源失败不影响其他源。
过滤:每条条目的标题+摘要必须命中 config.KEYWORDS 中至少一个关键词才会进入流水线(伊朗/美国/中东/军事/基地/霍尔木兹等,见 config.KEYWORDS)。
境内可访问情况(仅供参考,以实际网络为准)
| 通常境内可直接访问 | 说明 |
|---|---|
新华网 english.news.cn/rss/world.xml |
中国官方外文社 |
CGTN cgtn.com/rss/world |
中国国际台 |
凤凰 feedx.net/rss/ifengmil.xml、ifengworld.xml |
第三方 RSS 镜像,中文军事/国际 |
人民网 people.com.cn/rss/military.xml、world.xml |
军事、国际 |
新浪 rss.sina.com.cn 军事/新闻 |
新浪军事、新浪新闻滚动 |
中国日报 chinadaily.com.cn/rss/world_rss.xml |
国际新闻 |
中国军网 english.chinamil.com.cn/rss.xml |
解放军报英文 |
俄通社 TASS tass.com/rss/v2.xml |
俄罗斯官媒 |
RT rt.com/rss/ |
俄罗斯今日俄罗斯 |
DW rss.dw.com/xml/rss-en-world |
德国之声,部分地区/时段可访问 |
境内常需代理:Reuters、NYT、BBC、Guardian、France 24、Al Jazeera、Press TV 等境外主站 RSS,直连易超时或被墙。境内部署建议:设 CRAWLER_USE_PROXY=1 并配置代理,或仅保留上表源(可在 config.py 中注释掉不可达的 URL,减少超时等待)。
国内其他媒体(今日头条、网易、腾讯、新浪微博等):今日头条、腾讯新闻、新浪微博等多为 App/信息流产品,无官方公开 RSS。如需接入可考虑:第三方 RSS 聚合(如 FeedX、RSSHub 等若有对应频道)、或平台开放 API(若有且合规使用)。当前爬虫已加入新浪(rss.sina.com.cn)、人民网、中国日报、中国军网等有明确 RSS 的境内源;网易新闻曾有 RSS 中心页,具体栏目 XML 需在其订阅页查找后加入 config.py。
为什么爬虫一直抓不到有效信息(0 条)
常见原因与应对如下。
| 原因 | 说明 | 建议 |
|---|---|---|
| RSS 源在国内不可达 | 多数源为境外站(Reuters、BBC、NYT、Guardian、France24、DW、TASS、RT、Al Jazeera、Press TV 等),国内直连易超时或被墙。 | 使用代理:设 CRAWLER_USE_PROXY=1 并配置系统/环境 HTTP(S) 代理,或部署到海外服务器再跑爬虫。 |
| 关键词无一命中 | 只有标题或摘要里包含 KEYWORDS 中至少一个词才会保留(如 iran、usa、middle east、strike、基地 等)。若当前头条都不涉及美伊/中东,整轮会 0 条。 |
先跑 npm run crawler:test 看是否 0 条;若长期为 0 且网络正常,可在 config.py 中适当放宽或增加 KEYWORDS(如增加通用词做测试)。 |
| 单源超时导致整轮无结果 | 若所有源都在 FEED_TIMEOUT 内未返回,则每源返回空列表,汇总仍为 0 条。 |
增大 FEED_TIMEOUT(如 20);或先单独用浏览器/curl 测某条 RSS URL 是否可访问;国内建议代理后再试。 |
| 分类/清洗依赖 AI 且失败 | 每条命中关键词的条目会调 classify_and_severity(Ollama 或 DashScope)。若本机未起 Ollama、未设 DashScope,且规则兜底异常,可能影响该条。 |
设 PARSER_AI_DISABLED=1 使用纯规则分类,避免依赖 Ollama/DashScope;或配置好 DASHSCOPE_API_KEY / 本地 Ollama 再跑。 |
| 去重后无新增 | 抓到的条数 >0,但经 news_content 的 content_hash 去重后「新增」为 0,则不会写 situation_update,事件脉络不增加。 |
属正常:同一批新闻再次抓取不会重复写入。等有新头条命中关键词后才会出现新条目。 |
快速自检:
npm run crawler:test
输出「RSS 抓取: N 条」。若始终为 0,优先检查网络/代理与 KEYWORDS;若 N>0 但面板无新事件,多为去重后无新增或未调 POST /api/crawler/notify。
优化后验证效果示例
以下为「正文抓取 + 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_location(name 含「阿萨德」) | status=operational | — | status=attacked, damage_level=2 |
| key_location(name 含「伊斯法罕」) | status=operational | — | status=attacked, damage_level=1 |
若 AI 误提「累计 2847 人丧生」并填成 personnel_killed=2847,单次合并会被上限截断(如最多 +500),避免一次写入导致数据剧增。
4. 前端验证效果
- 事件脉络:出现一条新条目,summary 为上述 1–2 句概括,category=alert、severity=high。
- 装备毁伤面板:美军「阵亡」+2、「受伤」+14、「战机」+1;基地毁/损数字随 bases_damaged +1 更新。
- 地图:阿萨德基地、伊斯法罕对应点位显示为「遭袭」状态(脉冲/标色随现有地图逻辑)。
- API:
GET /api/situation中usForces.combatLosses、usForces.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)
| 分数 | 地图效果 |
|---|---|
| 1–3 | 绿色点 |
| 4–6 | 橙色闪烁 |
| 7–10 | 红色脉冲扩散 |
API
GET http://localhost:8000/events:返回事件列表与冲突统计(Python 服务直连)GET http://localhost:3001/api/events:从 Node 读取(推荐,含 WebSocket 同步)
本地验证链路
按下面任选一种方式,确认「抓取 → 清洗 → 去重 → 映射 → 写表 → 通知」整条链路正常。
方式一:最小验证(不启动前端)
-
启动 API(必须)
npm run api保持运行,默认
http://localhost:3001。 -
安装爬虫依赖并跑一轮流水线
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,请拉取最新代码。
- 有网络且有关键词命中时,应看到非零数字;无网络或全被过滤则为
-
查库确认
sqlite3 server/data.db "SELECT COUNT(*) FROM situation_update; SELECT COUNT(*) FROM news_content;"或浏览器打开
http://localhost:3001/api/db/dashboard,看situation_update、news_content是否有数据。 -
确认态势接口
curl -s http://localhost:3001/api/situation | head -c 500应包含
lastUpdated、recentUpdates等。
方式二:用现有验证脚本(推荐)
- 终端 1:
npm run api - 终端 2(可选):
npm run gdelt(会定时跑 RSS + GDELT) - 执行验证脚本:
若爬虫未启动想一并测爬虫,可:
./scripts/verify-pipeline.sh脚本会检查:API 健康、态势数据、爬虫状态、资讯表、战损字段、通知接口。./scripts/verify-pipeline.sh --start-crawler
方式三:只测 RSS 抓取(不写库)
npm run crawler:test
输出为「RSS 抓取: N 条」。0 条时检查网络或 config.py 里 RSS_FEEDS / KEYWORDS。
常见问题
| 现象 | 可能原因 |
|---|---|
| 抓取 0 条 | 网络不通、RSS 被墙、关键词无一命中 |
situation_update 为空 |
去重后无新增,或未跑流水线(只跑了 fetch_all 未跑 run_full_pipeline) |
| 前端事件脉络不刷新 | 未启动 npm run api 或 WebSocket 未连上(需通过 Vite 代理访问前端) |
| 翻译/AI 清洗很慢或报错 | 设 TRANSLATE_DISABLED=1 或 CLEANER_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 最新优先 |