跟踪 X 账号的抓取程序
一个基于 twikit 的航天/卫星互联网 X 账号采集器:cookie 无头登录、按种子清单批量抓 profile 与时间线、归一化后落进本机 SQLite,既出中英对照报告,又作为 Satellite Agent 的每日信号源——两侧通过一份只读的 x.sqlite3 彻底解耦。
概述
这是什么、解决什么问题、现状与部署。
这是什么。 expert/X/ 是一个基于 twikit(非官方 X GraphQL 客户端)的航天 X 账号抓取器。它用一次性登录得到的 cookie 做无头登录,按种子清单批量拉取账号 profile 与时间线,把 twikit 的松散对象归一化后落进本机 SQLite(x.sqlite3),并可产出中英对照的 Markdown / HTML / PDF 报告。
解决什么问题。 为「Satellite Agent」(agent/,全球卫星互联网产业决策 agent)提供每日的 X 侧信号源。抓取器只负责「把推文抓进 SQLite」,agent 侧再只读增量地把新推文映射成事件(events),喂进 decide / 日报 / wiki 回写。
当前数据规模。 SQLite 已存 95 个账号 / 3123 条推文(2026-06-06 实测,NEXT_STEPS.md:3);其中 5 个种子账号报 user_not_found(handle 停用/改名)。账号按 9 大分类组织。
抓取器与 agent 入库链均为 operator 本机 macOS 资产,脚本硬编码 /Users/john/InvesResearch/expert/X。本仓库只是代码副本,data/ 全部 gitignore——x.sqlite3、cookies.json、快照、报告都不在此机。
Tier 2 整合(XSqliteSource + x-ingest)已交付 2026-06-06,端到端实测 30 条推文真入库。Tier 3(X → wiki 单向增量回写)仍待启动:x_to_wiki.py 已具备但作为手动脚本、不在调度链。
系统架构
两大块,共用底层 expert/X/data/x.sqlite3,去向不同。
accounts.load_seed_accounts()cookies.json 无头登录 → 逐账号抓 profile + 时间线(Tweets/Replies) → 归一化 _normalize_user / _normalize_tweet / _normalize_mediacli report → reports.py中英对照,HTML 自包含
原始快照,离线审计
下游 agent 侧只读 x.sqlite3,绝不重抓、绝不写 X 的表。分两条链路(沿用 x-bridge-operator-runbook.md 字母约定):
run_x_scrape.sh → export_x_events.py复用 XSqliteSource · 近30天 · 脱敏5字段
data-live/x-events-archive.jsonl → git pushrefresh_live_data→ oaf.world 看板(每6h)+ 飞书日报(08:37)
run_x_ingest.shx-ingest-daily = _action_x_ingestXSqliteSource 增量读(
first_seen_at>since) → classify(5主线)+去重 → events 表x-freshness(断供监控,|| true)daily-report(复用 weekly_report 7d)decide() · x_to_wiki.py(手动)x.sqlite3(含 cookie / 全量原文)永不进 git;GitHub Actions 只见脱敏 JSONL,服务器侧看不到本体。
jobs.py:536 注释与 cron-runbook 写的是 launchd 03:00(遗留本机时点);production-runbook.md:84 记录当前生产调度为 06:45(45 6 * * * run_job.sh x-ingest-daily),以此为准。抓取(run_x_scrape.sh)故意不进任何调度链——单次 30–60 分钟、撞 429 会 sleep 900s,与 1–2 秒的 ingest 量级不同,默认 operator 手动跑。
目录与文件地图
上游抓取库 + agent 桥接的关键文件。
| 作用 | 路径 |
|---|---|
| 包初始化 · 首先 import 补丁 | expert/X/x_agent/__init__.py |
| argparse 入口 · 6 子命令 | expert/X/x_agent/cli.py |
种子清单加载 SeedAccount | expert/X/x_agent/accounts.py |
| twikit 抓取核心 | expert/X/x_agent/scraper.py |
| twikit 2.3.3 两处 monkey-patch | expert/X/x_agent/_twikit_patch.py |
| 全部 SQLite 持久化 · 5 表 | expert/X/x_agent/db.py |
| md/html/pdf 报告(只读) | expert/X/x_agent/reports.py |
| 翻译工作流:导出待译 / 写回 | expert/X/scripts/dump_for_translate.py · import_translations.py |
| 配置模板(复制为 config.yaml) | expert/X/config.example.yaml |
项目依赖 · requires-python>=3.10 | expert/X/pyproject.toml |
| 产物根(全 gitignored) | expert/X/data/ → x.sqlite3 / cookies.json / snapshots / reports |
| 只读增量消费 → RawEvent | agent/satellite_agent/sources/x_sqlite.py |
| x-ingest / x-freshness / daily-report | agent/satellite_agent/jobs.py |
| 逐条入库 + 去重 | agent/satellite_agent/ingest_pipeline.py |
5 主线本体 Thread | agent/satellite_agent/ontology.py |
| operator scrape / launchd 链 / 可移植入口 | agent/scripts/run_x_scrape.sh · run_x_ingest.sh · run_job.sh |
| 回写 wiki / 导出桥 / occurred_at 回填 | agent/scripts/x_to_wiki.py · export_x_events.py · backfill_x_occurred_at.py |
数据模型(SQLite)
DDL 集中在 db.py:11-94,全 IF NOT EXISTS、init_schema 可反复执行。共 5 表 + 3 索引;时间戳统一由 utcnow_iso() 生成(UTC 秒级 ISO8601)。
4.1 accounts — 账号 db.py:12-29
主键 username。字段分三组:抓取回填、种子导入、运维状态。无外键,也无「建行时间」字段。
| 字段 | 类型 | 含义 |
|---|---|---|
username | TEXT PK | 去 @ 的用户名(种子导入建行的键) |
user_id | TEXT | X 数字 id(回填,COALESCE 保护不被 NULL 覆盖) |
display_name / bio | TEXT | 显示名 / 简介原文(回填自 name / description) |
followers/following/tweets_count | INTEGER | 三项计数(following = following_count or friends_count) |
verified | INTEGER | 0/1;verified OR is_blue_verified(蓝标也算) |
profile_created_at / location / url | TEXT | 资料字段(回填) |
source_category | TEXT | 种子表分类标签(9 大分类,供 agent source 标签) |
source_name / source_bio_zh | TEXT | 种子表账号名 / 中文简介 |
last_scraped_at / last_error | TEXT | 末次抓取时间 / 末次错误(截断 500 字,成功清空) |
4.2 tweets — 推文 db.py:31-55
主键 id(TEXT)。唯一外键 author_username → accounts(username)。三索引:author / created_at / type。
| 字段 | 类型 | 含义 |
|---|---|---|
id | TEXT PK | 推文 id(强转 str) |
author_username | TEXT NN | 时间线拥有者(种子账号),RT 时非原作者 |
text / created_at / lang | TEXT | full_text or text / 推文原始时间(非 ISO) / 语言码 |
tweet_type | TEXT | original|retweet|quote|reply(_classify,RT 优先) |
in_reply_to_username | TEXT | ⚠️ 名不副实:实存 in_reply_to_user_id_str(数字 id 非 handle) |
retweeted / quoted_tweet_id | TEXT | RT 原推 id / 引用推 id |
*_count ×6 | INTEGER | favorite/retweet/reply/quote/view/bookmark 互动计数 |
urls_json / hashtags_json | TEXT | json.dumps(... ensure_ascii=False) |
scraped_at | TEXT | 末见时间(每次 upsert 刷新为 now) |
first_seen_at | TEXT | 首见时间(增量判定水位,见 4.6) |
run_id | INTEGER | 首见的那次 run(不随复看刷新) |
4.3 media / 4.4 translations / 4.5 runs
- media(
db.py:61-71):复合主键(tweet_id, media_key)。无到 tweets(id) 的外键——即便foreign_keys=ON也可能写入孤儿媒体行。photo 存图片直链,video/gif 存最高码率 mp4 直链(_best_video_url)。 - translations(
db.py:73-81):复合主键(kind, ref_id),kind ∈ {tweet,bio}。db.py本身只有 DDL、无读写函数——写在import_translations.py(INSERT OR REPLACE),读在reports.py与x_sqlite.py。 - runs(
db.py:83-93):抓取运行账本。finished_at=NULL即 run 未正常收尾(中断/崩溃),据此判僵尸 run。记 total/ok/failed 与 tweets_new/seen。
4.6 first_seen_at 增量判定 核心不变量
① is_new 靠插入前一次 SELECT 1 FROM tweets WHERE id=?(db.py:230)判定,结果为 None → 新增;同连接内即使未 commit 也自见,账号内跨 tab 去重可靠。
② ON CONFLICT(id) DO UPDATE 冲突分支只更新 text + 6 个互动计数 + scraped_at=now;first_seen_at 与 run_id 刻意不在更新列表(db.py:241-249)。
③ 结果:first_seen_at 恒为首见时间、scraped_at 刷成末见时间。下游 agent 正是靠 first_seen_at > since 做开区间增量。
核心抓取逻辑
scraper.pyScrapeConfig(frozen dataclass)默认:tweets_per_account=40(每个 tab 的目标条数)、include_replies=True、sleep_between_accounts=(3,7)、rate_limit_sleep=900、max_retries=3。
5.1 登录 / cookie
两条路径,登录态与抓取解耦,凭证只在登录时用:interactive_login(scraper.py:53)一次性交互登录产出 cookie JSON(支持 totp_secret 2FA);make_client(scraper.py:42)无头路径,cookie 不存在直接 RuntimeError 提示先 cli login,不带任何明文凭证。Cookie 复用规避重复登录挑战与风控。
5.2 归一化层(松散对象 → 稳定 dict)
全字段走 getattr(..,None) / .get(default) 容错,X 掉字段也不崩:_classify 按 RT>reply>quote>original 优先级判类型;_best_video_url 挑最高 bitrate 的 mp4;_normalize_tweet 的 author_username 恒为传入种子账号(RT 时非原作者)。
5.3 分页 _fetch_timeline · scraper.py:188
首页 get_tweets(191 行)不套 try,异常向上抛;分页 result.next()(196 行)被裸 except Exception 吞掉直接 break——翻页途中撞 429 或任何错都只是「提前截断」,不触发退避重试。X 实际每页约 20,须靠 next() 翻页凑够 40。
5.4 单账号流程 scrape_account · scraper.py:205
返回 (new, seen, error):① get_user_by_screen_name 本地 try 只捕 UserNotFound/Unauthorized 成软错误(不含 429,故 429 冒泡到 run 层退避);② upsert profile;③ 抓 Tweets tab(不套 try),若 include_replies 再抓 Replies——仅此处套 try/except NotFound 兜 Replies 端点 404,保留 Tweets-only;④ 两 tab 都 fetch 完才跑写库循环(逐条 normalize+upsert_tweet+upsert_media);⑤ 写 {username}.json 快照。
5.5 run 编排 run_scrape · scraper.py:275
- --only 过滤:不在 seed 清单的账号显式 warn(修此前静默跳过的已知问题 #1);全不命中 →
SystemExit。 - 逐账号重试循环:
TooManyRequests→attempt<max_retries则 sleep 900s 后从 get_user 重跑整账号(upsert 幂等不重复);BadRequest/TwitterException与兜底Exception记错 break,绝不掀翻整轮。 - commit-per-account(347 行):每账号后
conn.commit()checkpoint,配合 WAL 让 stats/report 在 run 进行中就读到已完成账号,中途崩溃不丢前面成果。
5.6 twikit 补丁 _twikit_patch.py
由 __init__.py:2 在任何 Client 使用前 import 生效(强依赖此导入路径,绕过则每请求 Couldn't get KEY_BYTE indices 全线失败)。两处:get_indices 适配 X 2026-03 后 ondemand.s 拆 index+hash 两段(upstream #408);User.__init__ 换成全字段 .get(default) 版容忍缺字段(#417),含 following_count = legacy.friends_count 映射。
5.7 限流 / 重试的陷阱(均已核查更正)
退避覆盖范围:rate_limit_sleep=900 对任何从 scrape_account 冒泡的 429 生效(含 get_user、两 tab 首页 get_tweets),唯独分页 next() 的 429 被裸 except 吞掉。单账号被限流最坏 3×900s ≈ 45 分钟串行阻塞整轮。
限流失败不落半份推文:写库循环在两 tab fetch 完之后才跑,429 只可能在此之前抛出,故限流账号最多落 profile 行 + mark_account_error,不写任何推文。
cookie 过期无全局熔断:失效时首个调用抛 Unauthorized → 被本地捕成软错误 → 不 abort,会把整份 seed 逐个跑成 failed,浪费一整轮。
include_retweets 是死配置:已完整接线(config→cli→ScrapeConfig)但抓取逻辑内无任何分支读它(原始时间线本就含 RT),改动无行为效果。
CLI 与配置
进程唯一入口 main(cli.py:263):parse_args → _load_config → _resolve_paths → args.func(args,cfg,paths)。子命令统一签名,返回值即退出码。
| 子命令 | 参数 | 作用 |
|---|---|---|
init | — | 从 xlsx 种子表灌 accounts |
login | --username/--email/--password/--totp | 交互登录一次,产出 cookies.json |
import-cookies | path | 浏览器导出 cookie JSON 归一后写入 |
scrape | --only · --tweets-per-account | 抓全部/指定种子账号的 profile+推文 |
stats | — | 只读打印 DB 汇总 |
report | --out-dir · --no-pdf | 从 DB 渲染 md/html/(pdf) |
6.1 config.yaml(全可选,缺文件用默认)
| 配置键 | 默认 | 说明 |
|---|---|---|
data_dir | ROOT/data | 输出根(ROOT = expert/X) |
db_path / cookies_path / snapshots_root | 相对 data_dir | ⚠️ 只改 db_path 不改 data_dir 会路径分裂 |
credentials.* | — | 仅 login 用;优先级 args > config > env(X_USERNAME…) |
scrape.include_replies | 示例 false / 代码默认 True | 2026-06-08 端点 404 后示例配置关掉 |
scrape.rate_limit_sleep / max_retries | 900 / 3 | 仅对 429 生效 |
6.2 cookie 导入 & 种子加载
_extract_from_browser_export(cli.py:72)支持 3 种导出格式归一成 {name:value},按 _X_DOMAINS 过滤,必需 {auth_token, ct0}(缺则 WARN 不 fail——真失败要到 scrape 才暴露)。load_seed_accounts(accounts.py:18)读活动 sheet,首行前 4 列须严格等于 (分类,账号名称,@用户名,简介核心内容) 否则 ValueError;按去 @ 用户名去重。seed_accounts 用 ON CONFLICT DO UPDATE 只覆盖 source_* 三列,重导种子不清已抓数据。
报告生成
reports.pywrite_reports(reports.py:735):collect() 得 Report → 渲染 md + 自包含 html → 可选 headless Chrome 转 pdf。md/html 必出,pdf 可缺。
collect(唯一读库处,reports.py:118):6 条 SQL 一次性把 5 张表读进内存 shape 成 Report dataclass,渲染层不再碰 DB。派生:by_category 分桶后桶内按 followers 降序;top = 全部推文按 Tweet.score 降序取 30。
报告结构
统计头部 → 目录 → 全局 Top 30 推文 → 按分类展开(每账号 profile + 该账号全部推文按 score 降序,含媒体缩略图,中英对照)→ 失败账号表。HTML 内联 CSS+JS,含深/浅/print 三主题、搜索过滤、中英显隐、?expand=1 展开。
Tweet.score(reports.py:69)= view_count or (fav×3 + rt×5 + reply + quote×2):有/无浏览数两组量级混排;view_count==0 会误回退到加权分。
TOP_TWEETS_PER_ACCOUNT=999:实质不限量,大账号会撑爆 HTML。
分类数由数据决定(已更正):由 accounts.source_category 数据决定、非代码写死「9」;缺失兜底 (未分类)。媒体是远程 <img src> 非内嵌,在线可能 404、PDF 中被 @media print 隐藏。
PDF 靠本机 Chrome(_find_chrome 先查 macOS 四个 .app 再 shutil.which),命令 chrome --headless=new --print-to-pdf,找不到则静默降级只出 md+html。
下游集成:接入 Satellite Agent
XSqliteSource(x_sqlite.py:136)把 x.sqlite3 作为只读、增量的 Source 接进 agent 抓取层。
8.1 增量映射 XSqliteSource.fetch
- 只读打开(
_open_ro):3 段 fallbackfile:?mode=ro&immutable=1(WAL 下唯一稳,读 last commit 不抢锁)→?mode=ro→ 普通 connect;db 不存在设last_error返回 None 降级不抛。 - 增量 SQL:
text<>''恒成立,since非空再加(first_seen_at IS NULL OR first_seen_at > ?)(开区间;NULL 行永远放过,靠下游去重挡),LEFT JOIN accounts取 category/display_name,ORDER BY first_seen_at DESC NULLS LAST(老 SQLite 去词 fallback),LIMIT(source 默认 500 / job 默认 200)。 - 过滤:默认过滤 reply / retweet;可选
categories白名单;二次空文本过滤。 - 中英拼接:若 translations 有 zh 译,
text = 原文 + "\n\n[zh] 译文",让 classifier 同看中英提升中文命中。
occurred_at = _to_iso(created_at)——来自推文 created_at 而非 first_seen_at。_to_iso 把 Twitter ctime 风格 'Wed May 20 13:00:33 +0000 2026'(年在末尾,非标准 RFC 2822,仅 parsedate_to_datetime 容忍)转 ISO;解析失败留 None,decide 的 7d/30d 窗口会跳过。companies/numeric_overrides/next_indicators 全空,让下游 match_companies/extract_numeric 兜底。
8.2 9 分类 → 主线(关键设计:不映射)
KNOWN_CATEGORIES 9 大分类:全球卫星运营商 / 其他细分航天企业 / 军工航天巨头 / 各国官方航天机构 / 在轨专项航天项目账号 / 民营航天企业(美国)/ 航天从业者/宇航员 / 航天媒体&资讯博主 / 非洲航天相关。这 9 分类不映射到主线——只拼进 source 标签 X · {category} · @{handle} 供反查,主线判定完全由下游 classifier 读 text 决定(媒体博主/在轨专项常跨主线,强绑会误分)。
agent 本体 ontology.Thread 现有 5 主线——核心网 / 终端 / 芯片 / 运营支撑 / 运载发射(ontology.py:11-16,2026-06-08 新增 LAUNCH_VEHICLE)。x_sqlite.py 与 ontology.py docstring 仍写「4 主线」系遗漏更新,以 5 为准。这不影响 source 行为(它本就不做 category→主线映射)。
8.3 x-ingest cron 链 · 24h 时序
抓取是 operator 手动触发的(不在时钟上),产出 x.sqlite3 后,两条链路各自按固定时刻消费。下图按每日时钟排布 4 类执行者的调度点。
launchd(本机) 与 ECS cron(生产) 是链路 B 的两套独立部署,互不依赖;生产库靠 03:30 rsync 从本机同步。run_x_scrape.sh(建议 1–2 天一次)x.sqlite3。随后(链路 A)export_x_events.py 脱敏 → x-events-archive.jsonl → git push main。不在固定时钟上;隔太久下游每天重读 stale 库(由 x-freshness 把这种「假活」变告警)。x-events-archive.jsonl → oaf.world 看板refresh_live_data.py → docs/sample/*.json → Cloudflare Pages 部署。Actions 永远看不到 x.sqlite3 本体。limit=500 无 since,靠 (url,occurred_at) 去重重跑不双写 → 本机 agent.db events。链路各步 || true,仅主步透传退出码。x.sqlite3 → 生产服务器production-runbook.md:147)。since=auto 增量 → 服务器 eventsMAX(occurred_at) WHERE source LIKE 'X ·%' 反推水位(减 24h 缓冲)做增量,重叠由去重消化。max_age_hours=48created_at(入库时间)距今 >48h → job exit 1 +(可选)飞书红卡。注册须 --max-failures 999,否则报警 3 次自 disable。x-events-archive.jsonl,命中主线的 X · 条目进次日早报候选。各 action 明细:
| 环节 | 位置 | 做什么 |
|---|---|---|
| x-ingest 主步 | jobs.py:188,604 | _action_x_ingest:since=auto 解水位 → XSqliteSource → ingest_from_source 入 events;SQLite 缺失静默 yield 0 仍标 success |
| since 水位 | x_sqlite.py:106 | MAX(occurred_at) WHERE source LIKE 'X ·%' - margin(24h);语义偏移(seen≥created)由 margin 吸收 |
| 逐条入库+去重 | ingest_pipeline.py:29,74 | ingest_raw:classify+match_companies+extract_numeric;find_event_by_url_and_time 去重(消化重叠窗,重跑不双写) |
| x-freshness | jobs.py:451 | 按 events.created_at(入库时间)判上游断供,超 48h 告警。监控 job 须配 --max-failures 999 |
| daily-report | jobs.py:532 | 复用 weekly_report(7d) 落 markdown + 「信息源构成」表让 X 贡献可见 |
| decide 消费 | decision.py:52,507 | 不在链上,按需触发;依赖 ISO occurred_at 才能正确切窗 |
8.4 export / backfill / 回写 wiki(均手动,不在 cron)
- export_x_events.py:复用 XSqliteSource 读近 30 天 → 按
shared_keywords过滤 → 脱敏 5 字段 JSONL →data-live/x-events-archive.jsonl(原子落盘)→ push → Actions 读它。Actions 永远看不到x.sqlite3本体。 - backfill_x_occurred_at.py:一次性把历史 Twitter 格式
occurred_at回填成 ISO(幂等、--dry-run);因 decide 窗口/排序按字符串比 occurred_at,非 ISO 给错时序。 - x_to_wiki.py:读近 14 天 → 用 wiki 实体别名 matcher 做 NER → 命中 slug 整段替换目标 md 的
## X 动态段(原子写);整账号零命中 → 起草_drafts/x_new_entities/<account>.md供人审。
运维要点
- 登录 / 换 cookie:首次
cli login或import-cookies(校验 auth_token+ct0)。cookie 失效表现为整份 seed 逐个跑成unauthorized——见到即重导 fresh cookie。 - 续跑 / 省 quota:撞 429 或部分失败后用
scrape --only <handle…>只抓未成功账号(未命中会 WARN)。 - 429 应对:等 1–2 小时让限流窗口过 →
--only续跑 → 实在卡死重导 cookie。降概率:把sleep_between_accounts从[3,7]拉到[8,15](上轮在第 63 账号 429)。 - 定时窗口:抓取手动(不进调度链);ingest 链 launchd 03:00(本机)/ 生产 06:45;
x-freshness断供监控 48h,注册须--max-failures 999。 - 翻译工作流:
dump_for_translate.py(去已译、CJK≥60% 跳过、每 250 条一批)→ 人工/子 agent 译 →import_translations.py(_lenient_parse容忍未转义引号,INSERT OR REPLACE)。 - 路径:脚本硬编码
/Users/john/InvesResearch/expert/X;NEXT_STEPS.md:29残留老路径/Users/john/lichao/X,换机器要改。
已知问题与后续计划
| 问题 | 现状 / 应对 |
|---|---|
| X 端点漂移(Replies 404) | 示例配置默认关 replies;scraper.py:235 已 try/except 兜底,保留 Tweets-only |
5 个账号 user_not_found | stokespace/slingshot_aero/HTVXA/NorwegianSpaceAg/LM_Space handle 停用/改名,需在 xlsx 更正 |
| cron 假活 | operator 几天不抓 → ingest 每天重读 stale 库,表面 success 实则无增量;x-freshness 已变显式告警 |
| 首页/翻页 429 不对称 | 翻页 429 被裸 except 吞成静默少抓(scraper.py:196),无退避补偿 |
| 孤儿媒体风险 | media 表无到 tweets(id) 外键,可能写入孤儿行 |
| upsert_account_profile 纯 UPDATE | username 未先 seed 建行则回写影响 0 行、资料丢失——强依赖「种子先行」顺序 |
| Tier 3(X→wiki 回写) | x_to_wiki.py 已具备但未进调度、仍待启动 |
| 运行栈约束 | expert/X 与 agent 均 requires-python>=3.10,不能被 ECS 宿主 python 3.6 运维脚本直接 import |
关键代码索引
散落的重要 file:line 速查表。
| 主题 | 位置 | 说明 |
|---|---|---|
| run 顶层编排 | scraper.py:275 | run_scrape:--only / 重试循环 / checkpoint |
| 单账号流程 | scraper.py:205 | scrape_account → (new,seen,err) |
| 429 退避重试 | scraper.py:327 | 对所有冒泡 429 生效,最坏 3×900s |
| commit-per-account | scraper.py:347 | 每账号 checkpoint,中途 kill 不丢数据 |
| Replies 404 降级 | scraper.py:236 | 仅 Replies 套 except NotFound |
| 分页裸 except | scraper.py:196 | 翻页 429 被吞成提前截断 |
| twikit 补丁生效点 | __init__.py:2 · _twikit_patch.py:15,89 | KEY_BYTE 双段匹配 / friends_count 映射 |
| SQLite schema | db.py:11-94 | 5 表 + 3 索引 |
| is_new 判定 / first_seen 保留 | db.py:230 · db.py:241 | 插入前 SELECT 1 / 冲突分支不更新 first_seen_at |
| CLI 入口 / 路径派生 | cli.py:263 · cli.py:33 | main 分发 / 全默认路径 |
| cookie 归一 / xlsx 表头校验 | cli.py:72 · accounts.py:23 | 三格式 / 前 4 列严格匹配 |
| 报告入口 / 唯一读库处 | reports.py:735 · reports.py:118 | write_reports / collect 6 SQL |
| Tweet.score / PDF 渲染 | reports.py:69 · reports.py:708 | 量级混排坑 / headless Chrome |
| agent Source / 增量 SQL | x_sqlite.py:136 · x_sqlite.py:228 | XSqliteSource / first_seen_at>since |
| occurred_at 归一 / 只读打开 | x_sqlite.py:40,310 · x_sqlite.py:186 | _to_iso / mode=ro&immutable=1 |
| source 标签 / since=auto 水位 | x_sqlite.py:77,297 · x_sqlite.py:106 | X·分类·@handle / MAX(occurred_at)-margin |
| 5 主线本体 | ontology.py:11 | 核心网/终端/芯片/运营支撑/运载发射 |
| x-ingest / x-freshness / daily-report | jobs.py:188,604 · 451 · 532 | 三个 action + register |
| 逐条入库 + 去重 | ingest_pipeline.py:29,74 | ingest_raw + find_event_by_url_and_time |
| launchd 链 / operator scrape | run_x_ingest.sh · run_x_scrape.sh:44 | ingest→freshness→report / 透传 cli scrape |
| 导出桥 / 回填 / 回写 wiki | export_x_events.py:62 · backfill:25 · x_to_wiki.py:63,91 | 脱敏 JSONL / 幂等回填 / ## X 动态 段 |