Files
simon 1d3ba02703 feat: 日志轮转(保留 14 天)
main.log 无轮转、已涨到 34MB,且每晚任务都会继续追加。

- logrotate/xwlb 模板 + scripts/install_logrotate.sh(幂等,装到 /etc/logrotate.d/xwlb)
- 每天轮转、保留 14 份、立即压缩 → main.log-YYYYMMDD.gz
- 必须用 copytruncate:脚本以 >>main.log 长开句柄,改名式轮转会
  让当晚日志写进归档、新文件空着
- journald 同步设 MaxRetentionSec=14day + SystemMaxUse=500M
- 安装脚本自带 logrotate -d 校验(这条立刻抓到"指令行尾注释"被当成参数值的错误)
- tests/test_logrotate.py 19 项,锁住保留天数、行尾注释、占位符替换

实测:34MB → 2.9MB,归档 32453 行与轮转前完全一致
2026-09-25 12:50:11 +08:00

31 KiB
Raw Permalink Blame History

xwlb —— 央视《新闻联播》自动转写与结构化入库流水线

把央视网《新闻联播》完整版视频自动抓取下来,转写成文字,再交给大模型切分成一条条独立新闻(含标题、正文)写进 MySQL/MariaDB。

全流程一条命令跑完,可幂等重跑、失败可定位、跑完自动清理中间产物。

内容版权归中央广播电视总台所有。本项目仅用于个人学习与研究用途,请勿用于商业分发。


目录


一、功能

1.1 主流程(python main.py 一条命令走完)

步骤 做什么 产出
① 链接发现 抓 tv.cctv.com/lm/xwlb/day/YYYYMMDD.shtml,匹配出当天「完整版《新闻联播》」的 VID 详情页 视频页 URL
② 下载与抽音 yt-dlp 下载 mp4,ffmpeg 抽出 mp3(已存在则跳过下载) xwlb_video/YYYYMMDD.mp4 / .mp3
③ 转写落库 mp3 → 16kHz 单声道 wav → 按静音切成 ~11 片 → 逐片语音识别 → LLM 校对 → 写入 xwlb_daily 分片级原文 + 校对文本
④ 结构化精编 把当天全部分片文本拼起来交给 DeepSeek,切分成独立新闻并起标题 xwlb_daily_ext 多行(标题 + 正文)
⑤ 自动清理 当天全部成功后才清空 mp3/mp4/wav 释放磁盘(实测单日约 121MB)

1.2 工程能力

  • 幂等重跑:xwlb_daily 按 (日期, 分片号) 先删后插;xwlb_daily_ext 按日期整体替换。同一天跑多少遍结果都一致,不会产生重复行。
  • 绝不丢已识别文本:校对环节任何失败(超时、限流、返回空)都自动回退 ASR 原文,不会因为"润色"把整片文字丢掉。
  • 数值事实守卫:LLM 校对若改动了数字/年份/届次/规划期等事实(实测出现过 2027→2024、十五五→十四五、第十一届→第九届),该分片整片回退 ASR 原文。
  • 不写脏数据:识别为空的分片不写库;失败当天不删除已有数据,留给下次重跑。
  • 可重跑的分步入口:只重做转写、只重做切分、强制重切,都有独立命令。
  • 配置分层:口令/Key 在 .env,其余(含三个模型名与各供应商 base url)在 config.yml;换模型、换供应商都不用动代码。
  • 失败说得清楚:缺 Key、连不上库(含隧道提示)、模型返回非 JSON 等都有明确日志与修复指引。
  • 116 项离线自检:配置、清理、事实守卫、LLM 返回解析、隧道自愈、日志轮转六个测试套件,不依赖网络与真实 API。

1.3 当前数据规模(2026-09-25 实测)

表 行数 覆盖天数 日期范围
xwlb_daily(分片级原文) 7,973 725 2024-09-26 ~ 2026-09-23
xwlb_daily_ext(单条新闻) 11,494 523 2024-09-26 ~ 2026-09-23

单日耗时约 25~35 分钟(下载 + 抽音 + 11 片识别 + 校对 + 切分),全程顺序执行、无并发。


二、技术架构

2.1 数据流

                    main.py(当天)
                         │  start_date = end_date = %Y%m%d
                         ▼
  ┌───────────────────────────────────────────────────────────────┐
  │ ① 链接发现   getVideo5.get_all_video_links(start, end)         │
  │    xwlb_urls()           → tv.cctv.com/lm/xwlb/day/YYYYMMDD.shtml
  │    get_xwlb_video_link() → 匹配「完整版《新闻联播》」得到 VID 页  │
  ├───────────────────────────────────────────────────────────────┤
  │ ② 下载与抽音 download_and_extract_audio(url, date, dir)        │
  │    yt-dlp  best[ext=mp4]/best        → xwlb_video/YYYYMMDD.mp4 │
  │    ffmpeg  -c:a libmp3lame -q:a 0    → xwlb_video/YYYYMMDD.mp3 │
  ├───────────────────────────────────────────────────────────────┤
  │ ③ 转写落库   audioRead.process_long_audio(mp3, out_dir, date)  │
  │    convert_mp3_to_wav()      16kHz / 单声道 → input_YYYYMMDD.wav│
  │    split_audio_by_smart_silence(700ms, -40dBFS, keep 400ms)    │
  │        → 贪心合并为 ≤3 分钟分片 chunk_i.wav(实测约 11 片/天)   │
  │    for each chunk:                                            │
  │        transcribe_audio()          dashscope paraformer ASR    │
  │        analyze_and_correct_text()  LLM 校对 + 数值事实守卫      │
  │        _upsert_daily_chunk()       → xwlb_daily(先删后插)     │
  ├───────────────────────────────────────────────────────────────┤
  │ ④ 切分入库   newsProcess.news_to_db(date)                      │
  │    读当天 news_improve 按分片号拼接 → DeepSeek(json_object)    │
  │    → 解析 [{news_id, news_title, news_content}]                │
  │    → 删除该日期旧记录 → 批量写入 xwlb_daily_ext                 │
  ├───────────────────────────────────────────────────────────────┤
  │ ⑤ 清理       cleanup.maybe_cleanup_after_run(date, day_ok)     │
  │    成功才清空 xwlb_video/*.mp3|*.mp4 与 audio_processing/*.wav │
  └───────────────────────────────────────────────────────────────┘

2.2 技术栈

层 选型 用途
语言/运行 Python 3.13 + .venv 本机为 externally-managed 环境,必须用虚拟环境
网页抓取 requests + beautifulsoup4 解析央视按天页面,定位完整版视频
视频下载 yt-dlp 下载 mp4
音频处理 ffmpeg(系统依赖)+ pydub + audioop-lts 抽音、16k 单声道转换、静音切分(3.13 已移除 audioop,需补丁包)
语音识别 阿里云 DashScope paraformer-realtime-v2 中文长音频转写(接入点可配)
文本校对 阿里云 DashScope qwen3.7-max 修正 ASR 的错别字/断句(受事实守卫约束)
新闻切分 DeepSeek deepseek-chat(json_object) 切分为独立新闻 + 起标题
LLM 接入 可配置的 endpoints + routes base url / 协议类型 / 密钥变量名都在 config.yml,可换供应商
数据库 MySQL / MariaDB 10.11 + mysql-connector-python 结果入库
配置 PyYAML(config.yml)+ 自研 dotenv 解析(.env) 敏感/非敏感分开
远程访问 autossh 本地 13306 → 远端 3306 隧道

2.3 目录结构

xwlb/
├── main.py               # 每日入口(当天)
├── getVideo5.py          # 链接发现 + 下载 + 主流程编排
├── audioRead.py          # 转写 + 校对 + 分片写库
├── newsProcess.py        # DeepSeek 切分 + 写 xwlb_daily_ext
├── deepseek.py           # DeepSeek 客户端(重试 / 退避 / json_object)
├── cleanup.py            # 中间产物清理(自动钩子 + 手动命令)
├── config.py / config.yml# 非敏感配置加载器 / 配置本体(含三个模型名)
├── env.py                # 敏感配置(.env)加载与缺失检查
├── mysql_handler.py      # 数据库层(项目内置,不再依赖父项目)
├── mysqlHandle.py        # 转发 mysql_handler.MySQLDB
├── newsRedo.py           # 手动重跑某天(跳过 / 只切分 / 全流程)
├── main_videos.py        # 补缺失日期(区间写在文件内)
├── .env / .env.example   # 敏感项 / 模板
├── requirements.txt
├── autossh.sh            # SSH 隧道脚本
├── xwlb_video/           # 中间产物:YYYYMMDD.mp4 / .mp3(成功后清空)
├── audio_processing/     # 中间产物:input_*.wav / chunk_*.wav(成功后清空)
├── docs/                 # ARCHITECTURE.md(技术说明)、BUGS.md(缺陷清单)
└── tests/                # 4 个离线测试套件

2.4 数据库结构

xwlb_daily —— 音频分片级文本(一天的完整版被切成约 11 片,每片一行)

字段 类型 说明
nid int, PK, auto_increment 自增主键
news_days date 日期(YYYY-MM-DD)
daily_sub_id int 分片序号,从 0 开始
news_raw text ASR 原始识别文本
news_improve text 校对后文本(关闭校对或守卫回退时等于 news_raw)
news_title text 保留字段(当前流程未使用)

表中无 (news_days, daily_sub_id) 唯一索引,幂等性由代码「先删后插」保证。

建表语句(新环境可直接执行;库名默认 myquant)

CREATE TABLE IF NOT EXISTS xwlb_daily (
  nid           int(11)     NOT NULL AUTO_INCREMENT,
  news_days     date        NOT NULL,
  daily_sub_id  int(11)     NOT NULL,
  news_raw      text        NOT NULL,
  news_improve  text        NOT NULL,
  news_title    text        NOT NULL,
  PRIMARY KEY (nid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE IF NOT EXISTS xwlb_daily_ext (
  extid         int(11)        NOT NULL AUTO_INCREMENT,
  news_date     date           NOT NULL,
  sub_id        tinyint(4)     NOT NULL,
  news_title    varchar(256)   NOT NULL,
  news_content  text           NOT NULL,
  PRIMARY KEY (extid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

xwlb_daily_ext —— 最终产物:单条新闻

字段 类型 说明
extid int, PK, auto_increment 自增主键
news_date date 日期(YYYY-MM-DD)
sub_id tinyint 当天第几条新闻,从 1 开始
news_title varchar(256) 新闻标题(模型生成)
news_content text 新闻正文

2.5 三个模型的职责

环节 模型配置项 走哪个接入点(routes.*) 默认模型 输入 → 输出
语音识别 models.asr_model asr → dashscope paraformer-realtime-v2 单片 wav → 该片文本
文本校对 models.correct_model correct → dashscope qwen3.7-max 单片 ASR 文本 → 校对后文本(同一分片粒度)
新闻切分 models.split_model split → deepseek deepseek-chat 当天全文 → JSON 数组(标题 + 正文)

模型名与接入点是分开配置的:换供应商时改 routes + endpoints,再同步改模型名,代码不动(见 3.4 更换 LLM 供应商)。

为什么要两道 LLM:ASR 只保证"字"对上,标点、断句、同音错字仍需修;而切分属于理解任务——需要判断新闻边界、给每条起标题,所以交给擅长长文本理解的 DeepSeek。两道 LLM 的分工边界清晰,任一道失败都不会污染另一道的结果。

2.6 关键设计取舍

取舍 做法 原因
校对会改错事实 数值事实守卫:数值签名不一致就整片回退原文 宁保留可信的 ASR 原文,也不接受"通顺但错误"的文本
重跑会重复写库 xwlb_daily 先删后插;xwlb_daily_ext 按日期替换 无需唯一索引即可幂等,重跑不产生脏数据
分片识别失败 记 ERROR、不写空行、其余分片照常 空行曾导致下游拼出残缺文本(历史库中有 101 行空白)
切分返回非 JSON 剥代码围栏 + raw_decode 兜底 + 强化提示词重试一次 历史上有 22 天因此得到 0 条精编
中间产物占用磁盘 当天全部成功才清理,失败保留 成功即无价值,可释放磁盘;失败则保留供重跑
顺序执行、不并发 逐片串行 单机负载可控、日志与失败定位简单;单日 25~35 分钟已满足离线需求

三、使用说明

3.1 环境要求

  • Linux(本机 Raspberry Pi / Debian)
  • Python 3.11 ~ 3.13(本项目在 3.13.5 上验证)
  • 系统依赖:ffmpeg;autossh(远程数据库时)
  • 一个可用的 MySQL/MariaDB 库(两张表需已存在,见 2.4)
  • 阿里云 DashScope API Key(语音识别 + 校对)、DeepSeek API Key(切分)

3.2 安装

cd /home/pi/project/xwlb

# 1) 端口隧道:本地 13306 → 远端 3306(数据库不在本机时需要,通了可跳过)
bash autossh.sh

# 2) 虚拟环境(本机 Python 为 externally-managed,必须用 venv)
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

# 3) 配置
cp .env.example .env       # 填 3 个敏感项
vim config.yml             # 核对数据库地址、目录、模型

Python 3.13 已移除标准库 audioop,pydub 需要 requirements.txt 中的 audioop-lts,已自动按版本条件安装。

3.3 配置

(1).env —— 只放敏感项(已被 .gitignore 排除)

MYSQL_PASSWORD=...
DASHSCOPE_API_KEY=sk-...
DEEPSEEK_API_KEY=sk-...

(2)config.yml —— 其余全部配置

mysql:      { host: localhost, port: 13306, user: myquant, database: myquant }
paths:      { video_dir: xwlb_video, audio_dir: audio_processing }
models:
  asr_model: paraformer-realtime-v2   # 语音识别
  correct_model: qwen3.7-max          # ASR 文本校对
  split_model: deepseek-chat          # 新闻切分 + 标题

# 接入点(base url):换供应商 / 换区域 / 走代理只改这一段
endpoints:
  dashscope:
    kind: dashscope                                       # dashscope=用 SDK,openai=OpenAI 兼容
    http_base_url: https://dashscope.aliyuncs.com/api/v1  # 文本生成走 HTTP
    websocket_base_url: wss://dashscope.aliyuncs.com/api-ws/v1/inference  # 实时 ASR 走 WS
    api_key_env: DASHSCOPE_API_KEY                        # 密钥在 .env 里的变量名
  deepseek:
    kind: openai
    base_url: https://api.deepseek.com/v1
    chat_completions_path: /chat/completions
    api_key_env: DEEPSEEK_API_KEY

# 每个环节走哪个接入点(值是 endpoints 下的名字)
routes: { asr: dashscope, correct: dashscope, split: deepseek }

llm_correct:
  enabled: 1                          # 1=开启校对(默认);0=直接用 ASR 原文
cleanup:
  after_daily_run: 1                  # 当天成功结束后自动清理中间产物
  only_on_success: 1                  # 仅成功才清理;0=无论成败都清理
  remove_video: 1                     # 删除 mp3 / mp4
  remove_audio: 1                     # 删除 wav

优先级:进程环境变量 > config.yml > 代码内置默认值。 所以临时试验可以不改文件:

DASHSCOPE_LLM_MODEL=qwen-max .venv/bin/python audioRead.py 20260904
LLM_CORRECT_ENABLED=0 .venv/bin/python getVideo5.py 20260904 20260904
XWLB_ROUTE_CORRECT=deepseek .venv/bin/python newsProcess.py 2026-09-04   # 临时换校对供应商

启动时会打印生效配置(不含敏感值),含每个环节实际用的接入点:

配置来源: /home/pi/project/xwlb/config.yml | MySQL myquant@localhost:13306/myquant |
模型 asr=paraformer-realtime-v2 correct=qwen3.7-max split=deepseek-chat | 校对=开启 | 完成后清理=开启
接入点 asr     -> dashscope  kind=dashscope wss://dashscope.aliyuncs.com/api-ws/v1/inference(密钥变量 DASHSCOPE_API_KEY)
接入点 correct -> dashscope  kind=dashscope https://dashscope.aliyuncs.com/api/v1(密钥变量 DASHSCOPE_API_KEY)
接入点 split   -> deepseek   kind=openai    https://api.deepseek.com/v1/chat/completions(密钥变量 DEEPSEEK_API_KEY)

3.4 更换 LLM 供应商

供应商不写死在代码里:地址在 endpoints,走哪条线在 routes,密钥变量名在 api_key_env。

想做什么 改哪里
换区域(如 DashScope 国际站) endpoints.dashscope 的两个 url
走公司网关 / 代理 / 自建推理 改对应接入点的 base_url(OpenAI 兼容)
换校对供应商(如月之暗面、智谱、Kimi) ① routes.correct 指向新接入点 ② 在 endpoints 加一段 ③ 改 models.correct_model ④ .env 加对应 Key
换切分供应商 同上,改 routes.split + models.split_model
换 ASR 供应商 ⚠️ 实时语音识别只有 kind: dashscope 一种适配器;换厂商需要新增适配器(换区域/网关仍可,改 url 即可)

示例:把校对从通义千问换成月之暗面

routes: { asr: dashscope, correct: moonshot, split: deepseek }
endpoints:
  moonshot:
    kind: openai
    base_url: https://api.moonshot.cn/v1
    chat_completions_path: /chat/completions
    api_key_env: MOONSHOT_API_KEY
models:
  correct_model: kimi-k2-0905-preview
# .env
MOONSHOT_API_KEY=sk-...

要点:

  • kind 只有两种:dashscope(用 dashscope SDK)和 openai(OpenAI 兼容的 /chat/completions,覆盖 DeepSeek / Kimi / 智谱 / vLLM / 各类网关)。
  • 请求地址 = base_url + chat_completions_path,所以不带版本号的网关地址(如 https://my-gateway/llm)也能直接填。
  • 缺 base_url、routes 指向不存在的接入点、kind 拼错,都会在启动日志与调用时报出可读错误,不会静默走错地址。
  • 换供应商不影响数值事实守卫:无论哪家的校对结果,改动数字/年份/届次一样会被拦下并回退 ASR 原文(已用 DeepSeek 作为校对供应商实测)。
  • 运行时必需哪些 Key 是按路由推导的:只有被 routes 用到的接入点才要求配 Key,未被使用的不会报缺失。

3.4.1 推理模型(thinking)注意

现在的模型多是"推理模型":回答前先生成一大段思维链,推理 token 按输出计费。 实测(详见 docs/REPORT_raw_vs_improve.md):

环节 关掉思考 开着思考 结论
校对 312 token / 4~6 秒,输出几乎一致 5,932 token 关(省约 13 倍,机械任务不需要推理)
切分 4,656 token,正文覆盖率 95.34%、20 条 19,077 token,覆盖率 99.67%、27 条 开(关掉会丢 4.7% 正文、少切 7 条)

关思考的参数名因供应商而异,写错会被静默忽略(不报错但也不生效),所以配在接入点上:

endpoints:
  qwen:      { extra_body: {enable_thinking: false} }   # 通义/百炼兼容模式
  deepseek:  { extra_body: {thinking: {type: disabled}} }  # DeepSeek 推理模型

也可在 llm_correct / llm_split 段写 extra_body 覆盖(优先级更高)。 使用推理模型时 llm_split.max_tokens 要给足(现为 60000):实测推理 token 在 14k~24k 之间波动,上限 20000 会截断,甚至整个回复为空。

3.5 运行

场景 命令 说明
日常(当天) .venv/bin/python main.py 抓当天 → 下载 → 转写 → 切分 → 清理
指定某天 .venv/bin/python getVideo5.py 20260904 20260904 日期格式 YYYYMMDD
指定区间 .venv/bin/python getVideo5.py 20260901 20260907 逐日全流程
补缺失日期 .venv/bin/python main_videos.py 反查库中缺失日期再补跑(区间写在文件内,需按需修改)
重跑某天 .venv/bin/python newsRedo.py 2026-09-04 已有精编 → 跳过;有原文 → 只重做切分;都没有 → 全流程
强制重切 .venv/bin/python newsRedo.py 2026-09-04 --force 忽略已有记录,重新切分并替换该日 xwlb_daily_ext
只重做切分 .venv/bin/python newsProcess.py 2026-09-04 [--force] 不碰音频,直接用库中文本重切
只重做转写 .venv/bin/python audioRead.py 20260904 用已有 xwlb_video/20260904.mp3 重跑分割+识别+落库
手动清理 .venv/bin/python cleanup.py [--dry-run] 清空两个中间产物目录,--dry-run 只列不删

注意:默认 cleanup.remove_video: 1,当天成功后 mp3 会被删掉。因此要重做某天的转写(audioRead.py)时,需要先重新下载该天的 mp3——最简单是把 cleanup.remove_video 临时设为 0,或直接跑 getVideo5.py 重下再重跑。

3.6 自检与测试

六个测试套件共 116 项断言,全部离线运行(不消耗 API 额度、不触碰真实文件):

.venv/bin/python tests/test_config.py          # 37 项:配置加载、优先级、接入点/路由、敏感项隔离
.venv/bin/python tests/test_cleanup.py         # 10 项:清理开关五种组合(临时目录)
.venv/bin/python tests/test_fidelity_guard.py  # 14 项:数值事实守卫(含真实篡改样例)
.venv/bin/python tests/test_news_parse.py      # 16 项:LLM 返回解析(含真实坏返回)
.venv/bin/python tests/test_tunnel.py          # 20 项:隧道自愈各分支(临时端口,不碰真实隧道)
.venv/bin/python tests/test_logrotate.py       # 19 项:轮转指令、保留天数、行尾注释、占位符替换

其中隧道测试不会碰真实的 13306 与 autossh.sh:它用临时 config.yml + 临时端口的假脚本, 验证"端口已通时绝不执行脚本""systemd 托管时只等待不抢端口"等关键约定。

连通性自检:

# 打印生效配置并校验 Key 是否齐全
.venv/bin/python -c "import config; config.log_summary()"

# 数据库连通
.venv/bin/python -c "from mysqlHandle import MySQLDB; d=MySQLDB(); print(d.query_one('xwlb_daily','COUNT(*) c')); d.close()"

3.7 定时任务(systemd)

已配置为 systemd 定时器:每天 21:00 触发全链路,失败后 21:30、22:00 各自动重试一次。

bash scripts/install_systemd.sh          # 安装/更新单元(幂等,可反复执行)
systemctl list-timers xwlb-daily.timer   # 看下次触发时间
journalctl -u xwlb-daily -f              # 实时看日志(也在 main.log)
sudo systemctl start xwlb-daily.service  # 立即试跑一次
sudo systemctl disable --now xwlb-daily.timer   # 临时停用

涉及三个单元(源码在 systemd/):

单元 作用
xwlb-daily.timer 每天 21:00 触发;Persistent=true,树莓派关机错过时刻会在开机后补跑
xwlb-daily.service oneshot,调用 scripts/run_daily.sh;TimeoutStartSec=10800 以容忍内部等待重试
xwlb-tunnel.service autossh 隧道托管(开机自启、断线自动重连),xwlb-daily 依赖它

重试不是简单地"再跑一遍":

  • 只缺切分(识别已全部完成)→ 只重跑切分环节,省掉全部 ASR 调用(约十几分钟与相应费用)
  • 识别没跑完 → 重跑全链路
  • 判断依据是识别全部完成后写的标记 state/.asr_complete_<日期>; 只看"库里有没有分片"是不够的——识别中途卡死也会留下部分分片, 那样会被误判成"只缺切分",把半天内容当成完整一天入库(scripts/day_status.py 专门防这个)
  • 识别不完整的那一天不会执行切分(不发布半天的 ext),宁可让重试补全, 也不产生"看起来完整"的一天;分片原文仍在 xwlb_daily 里,不会丢
  • 标记放在 state/ 且不随清理删除:它是长期凭证,删了就分不清 "已完成"与"识别只跑了一半但切分照样写了 ext" (cleanup 只清 xwlb_video/*.mp3|*.mp4 与 audio_processing/*.wav)
  • 单次尝试有 40 分钟上限(XWLB_ATTEMPT_TIMEOUT),卡死的 ASR 不会拖垮整晚
  • 有文件锁防重入,手动执行与定时触发叠在一起也不会重复烧 ASR

退出码:0 成功;1 重试用尽仍失败(会出现在 systemctl --failed);2 已有实例在运行。

用 cron 也行,但那样拿不到"超时上限、重试、防重入、开机补跑"这些保障: 40 20 * * * cd /home/pi/project/xwlb && .venv/bin/python main.py >> main.log 2>&1

3.8 数据库隧道自愈

数据库在内网,靠 autossh.sh 把本地 13306 转发到远端 3306。隧道断掉时, 任何入口都会自己把它拉起来——不只是定时任务,手动跑 main.py / getVideo5.py / newsProcess.py / day_status.py 都一样(实现挂在数据库连接层 MySQLDB.connect())。

行为(tunnel.py,所有入口共用一套逻辑):

情况 动作
端口可连 什么都不做(正常路径零开销,不会多起进程)
端口不通,且 systemd 隧道单元 active 只等待它自动重连——不另起 autossh 抢同一个端口(那会让 systemd 单元因端口被占反复重启失败)
端口不通,systemd 单元未在运行 执行 config.yml 里 mysql.tunnel.script(默认 autossh.sh),最多等 30 秒
仍不通 如实报错,不再假装成功
python tunnel.py            # 手动探测 + 按需修复,退出码 0=通 / 1=仍不通
python tunnel.py --dry-run  # 只看会做什么,不执行

配置:

mysql:
  tunnel:
    enabled: 1
    script: autossh.sh                    # 相对项目根
    systemd_unit: xwlb-tunnel.service     # 该单元 active 时只等待,不抢端口
    wait_seconds: 30
    connect_timeout: 2

(直连远端数据库时,host 不是本机地址 → 判定为"不适用隧道",不会去跑 autossh.sh。)

3.9 日志轮转

bash scripts/install_logrotate.sh      # 安装/更新(幂等,可反复执行)

两处一起收口,都是保留 14 天:

日志 位置 策略
任务日志 main.log 项目根(>>main.log 追加) logrotate:每天轮转、保留 14 份、立即压缩 → main.log-20260925.gz
systemd journal /var/log/journal journald:MaxRetentionSec=14day + SystemMaxUse=500M

实测效果:34MB 的 main.log 轮转后成 2.9MB 压缩归档,归档行数与轮转前完全一致(32453 行),磁盘立即回收。

配置里有两个容易踩的点:

  • 必须用 copytruncate:脚本以 >>main.log 方式持续写入,一次任务跑几十分钟、句柄一直开着。 默认的"改名+新建"会让运行中的进程继续写旧文件,当晚日志被切进归档、新文件却空着。
  • 指令后面不能写注释:logrotate 会把注释当成参数值(实测报 bad rotation count '14 # 保留 14 份'),注释只能单独成行。 tests/test_logrotate.py 专门锁住了这条。

轮转由系统自带的 logrotate.timer(每天 00:46)驱动,无需额外定时任务。

sudo logrotate -d /etc/logrotate.d/xwlb   # 干跑校验配置
sudo logrotate -f /etc/logrotate.d/xwlb   # 立即轮转一次
zcat main.log-20260925.gz | less          # 看归档
ls -lh main.log*                          # 看当前归档与占用

3.10 故障排查

现象 原因 / 处理
MySQL 连接失败 ... 隧道不通。现在会自动恢复:连接失败时探测端口,不通则执行 autossh.sh 并等待(见 3.9 隧道自愈)。仍失败再手动 bash autossh.sh,并确认 config.yml 的 mysql.port 是隧道端口 13306
缺少敏感配置 ... .env 未填 MYSQL_PASSWORD / DASHSCOPE_API_KEY / DEEPSEEK_API_KEY
externally-managed-environment 不要用系统 pip,用 .venv/bin/pip
No module named 'audioop' Python 3.13 需装 audioop-lts(已在 requirements 中)
某天识别成功但精编为 0 条 切分返回非 JSON;现会自动用强化提示词重试一次,仍失败则保留原文不写库,可 newsProcess.py <日期> --force 重试
校对结果与原文数字不一致 事实守卫已拦截并回退原文,日志会打印 ⚠️ 校对改动了数值事实,已回退 ASR 原文
想完全关掉 LLM 校对 config.yml 设 llm_correct.enabled: 0
接入点配置错误: ... routes.* 指向了不存在的接入点、kind 拼错、或缺 base_url;按日志提示补 endpoints 字段
ASR 接入点 ... kind=openai:实时语音识别仅支持 kind=dashscope ASR 换厂商需新增适配器;换区域 / 网关请保留 kind: dashscope 只改 url
换供应商后报 401 / 403 .env 里没有该接入点 api_key_env 指定的那个变量(不是把新 Key 塞进旧变量名)
换供应商后报模型不存在 models.correct_model / models.split_model 还是旧供应商的模型名,需同步修改
getVideo5 报某天 404 该天页面尚未上线(当天节目过期或未发布),日志会明确跳过
磁盘被中间产物占满 python cleanup.py --dry-run 查看,再 python cleanup.py
main.log 越来越大 已配 logrotate 保留 14 天(见 3.9);未装则 bash scripts/install_logrotate.sh
日志轮转报 bad rotation count 指令行写了行尾注释;logrotate 不支持,注释要单独成行
某天识别到一半卡死(无新日志、无 CPU) ASR 的 Recognition.call 没有超时参数,长连接可能悬挂;定时任务有 40 分钟上限并在 21:30/22:00 重试。手动跑请自行 timeout
定时任务没跑 systemctl list-timers xwlb-daily.timer;systemctl status xwlb-daily.service;日志 journalctl -u xwlb-daily -n 100
定时任务报数据库连接失败 隧道服务:systemctl status xwlb-tunnel;手动兜底 bash autossh.sh

四、相关文档

文档 内容
docs/ARCHITECTURE.md 完整技术说明:数据流、模块与函数索引、数据库结构、配置项全表、外部调用实测参数、时序图
docs/REPORT_raw_vs_improve.md 校对效果评估:7973 条真实数据统计、实测 token 开销、合并/关闭校对的对比与结论
docs/BUGS.md 15 项缺陷清单(含严重度、位置、影响、证据、修复),以及本次修复的实测验证记录与后续待办

已知待办(详见 docs/BUGS.md):

  • main_videos.py 的日期区间仍写死在文件内,尚未改为命令行参数;
  • 历史数据治理(截至 2026-09-25):xwlb_daily 有 100 行空白分片、202 天缺 xwlb_daily_ext,可用上述分步入口补齐;
  • 项目尚未 git init(.gitignore 已就绪);main.log 无轮转(历史 34MB 作为证据保留)。