《新闻联播》每日抓取入库:全链路 + 可移植化 + 定时任务
从 CCTV 主页抓取《新闻联播》,下载 → 转 MP3/WAV → 静音切分 → ASR 识别 → LLM 校对 → 切分为单条新闻 → 入库 MySQL。 主要内容: - 全链路:getVideo5 抓取下载、audioRead 转写、deepseek 校对与切分、newsProcess 入库 - 可移植化:配置分层,.env 只放密钥、config.yml 放模型/接入点/路由/参数 - 可换供应商:endpoints(kind/base_url/api_key_env/extra_body)+ routes 按环节选路 - 数据保真:数值事实守卫,校对改动数字/年份/届次则整片回退 ASR 原文; 识别不完整不发布该日精编,避免半天内容被当成完整一天 - 定时任务:systemd 每天 21:00,失败 21:30 / 22:00 重试; 只缺切分时只重跑切分(省掉全部 ASR),用 state/.asr_complete_* 标记判定阶段 - 隧道自愈:13306 不通时自动执行 autossh.sh(所有入口共用,systemd 托管时只等待) - 中间产物每日清理;97 项离线自检(配置/清理/事实守卫/解析/隧道)
This commit is contained in:
Executable
+52
@@ -0,0 +1,52 @@
|
||||
"""查询某天数据完成度,供重试逻辑判断"该重跑哪个阶段"
|
||||
|
||||
.venv/bin/python scripts/day_status.py 2026-09-24
|
||||
|
||||
退出码:
|
||||
0 已有精编(xwlb_daily_ext 有数据)→ 视为完成
|
||||
1 识别已全部完成(存在 .asr_complete_<日期> 标记)但有分片无精编 → 只缺切分环节
|
||||
2 其余情况(分片未入库 / 识别只跑了一半)→ 必须重跑全链路
|
||||
|
||||
注意:只有 daily 有数据**不足以**说明识别跑完了。识别中途卡死时也会留下一部分分片,
|
||||
此时若无条件走"只重跑切分",会把半天内容当成完整一天切分入库——所以用标记文件区分。
|
||||
|
||||
同时在 stdout 打印 daily=<分片数> ext=<精编条数>,便于日志排查。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from mysqlHandle import MySQLDB # noqa: E402
|
||||
from audioRead import asr_marker_path # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print('用法: day_status.py <YYYY-MM-DD|YYYYMMDD>')
|
||||
return 3
|
||||
raw = sys.argv[1]
|
||||
date10 = f"{raw[:4]}-{raw[4:6]}-{raw[6:]}" if len(raw) == 8 else raw
|
||||
|
||||
db = MySQLDB()
|
||||
try:
|
||||
daily = db.query_one('xwlb_daily', 'COUNT(*) n', 'news_days = %s', (date10,))
|
||||
ext = db.query_one('xwlb_daily_ext', 'COUNT(*) n', 'news_date = %s', (date10,))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
n_daily = (daily or {}).get('n', 0) or 0
|
||||
n_ext = (ext or {}).get('n', 0) or 0
|
||||
marker = asr_marker_path(date10)
|
||||
asr_done = os.path.exists(marker)
|
||||
print(f"daily={n_daily} ext={n_ext} asr_complete={int(asr_done)}")
|
||||
if n_ext > 0:
|
||||
return 0
|
||||
if n_daily > 0 and asr_done:
|
||||
return 1
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# 安装/更新 systemd 定时任务(需要 sudo;幂等,可反复执行)
|
||||
#
|
||||
# bash scripts/install_systemd.sh
|
||||
#
|
||||
# 做四件事:
|
||||
# 1) 把 systemd/*.{service,timer} 装到 /etc/systemd/system/
|
||||
# 2) 启用并启动 xwlb-daily.timer(每天 21:00 触发)
|
||||
# 3) 启用 xwlb-tunnel.service(autossh 隧道,随开机自启)
|
||||
# 4) 打印下一次触发时间
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SUDO=""
|
||||
if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi
|
||||
|
||||
echo "== 1/4 安装单元文件 =="
|
||||
for unit in xwlb-daily.service xwlb-daily.timer xwlb-tunnel.service; do
|
||||
$SUDO install -m 0644 "$DIR/systemd/$unit" "/etc/systemd/system/$unit"
|
||||
echo " $unit"
|
||||
done
|
||||
|
||||
echo "== 2/4 daemon-reload =="
|
||||
$SUDO systemctl daemon-reload
|
||||
|
||||
echo "== 3/4 启用定时器 =="
|
||||
$SUDO systemctl enable --now xwlb-daily.timer >/dev/null
|
||||
echo " xwlb-daily.timer 已启用"
|
||||
|
||||
echo "== 4/4 隧道服务 =="
|
||||
if ss -ltn 2>/dev/null | grep -q '127.0.0.1:13306'; then
|
||||
if systemctl is-active --quiet xwlb-tunnel.service; then
|
||||
$SUDO systemctl restart xwlb-tunnel.service
|
||||
echo " xwlb-tunnel.service 已重启"
|
||||
else
|
||||
echo " ⚠️ 13306 已被**非 systemd 托管**的 autossh 占用,暂不启用隧道服务。"
|
||||
echo " 想改由 systemd 托管(可开机自启、断了自动重连)请执行:"
|
||||
echo " pkill -f 'autossh.*13306' && sudo systemctl enable --now xwlb-tunnel.service"
|
||||
fi
|
||||
else
|
||||
$SUDO systemctl enable --now xwlb-tunnel.service >/dev/null
|
||||
echo " xwlb-tunnel.service 已启用"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== 下一次触发时间 =="
|
||||
systemctl list-timers xwlb-daily.timer --no-pager || true
|
||||
echo
|
||||
echo "查看日志: journalctl -u xwlb-daily -f (也可看 $DIR/main.log)"
|
||||
echo "立即试跑: sudo systemctl start xwlb-daily.service"
|
||||
echo "临时停用: sudo systemctl disable --now xwlb-daily.timer"
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# 每日全链路任务(由 systemd 定时器调用,也可手动执行)
|
||||
#
|
||||
# scripts/run_daily.sh # 抓取"今天"
|
||||
# scripts/run_daily.sh 20260924 # 指定日期
|
||||
#
|
||||
# 重试策略:首次失败后,在 21:30、22:00 各再执行一次(可用 XWLB_RETRY_SLOTS 覆盖)。
|
||||
# · 若当天分片已入库、只缺精编(切分失败)→ 重试只重跑切分,省掉全部 ASR 调用
|
||||
# · 若分片都没入库(下载/识别失败) → 重试重跑全链路
|
||||
#
|
||||
# 环境变量(一般不用改):
|
||||
# XWLB_RETRY_SLOTS 重试时刻,默认 "21:30 22:00"
|
||||
# XWLB_ATTEMPT_TIMEOUT 单次尝试上限秒数,默认 2400(40 分钟)
|
||||
# XWLB_ENV_FILE / XWLB_CONFIG_FILE 透传给 python 的配置覆盖
|
||||
# XWLB_FORCE=1 即使当天已入库也强制重跑
|
||||
#
|
||||
# 退出码:0=成功 1=重试用尽仍失败 2=已有实例在运行(跳过)
|
||||
set -uo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$DIR" || exit 1
|
||||
PY="$DIR/.venv/bin/python"
|
||||
LOG="$DIR/main.log"
|
||||
RETRY_SLOTS="${XWLB_RETRY_SLOTS:-21:30 22:00}"
|
||||
ATTEMPT_TIMEOUT="${XWLB_ATTEMPT_TIMEOUT:-2400}"
|
||||
DB_PORT="${XWLB_DB_PORT:-13306}"
|
||||
|
||||
log() { printf '%s [run_daily] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
|
||||
|
||||
# ---- 防重入:定时器与手动执行、或两次重试叠在一起时,避免重复烧 ASR ----
|
||||
exec 9>"$DIR/.run_daily.lock"
|
||||
if ! flock -n 9; then
|
||||
log "已有 run_daily 实例在运行,本次跳过"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ---- 日期 ----
|
||||
D8="${1:-$(date +%Y%m%d)}"
|
||||
D10="${D8:0:4}-${D8:4:2}-${D8:6:2}"
|
||||
|
||||
# ---- 隧道自愈:数据库走 autossh 本地端口,隧道断了当天必失败 ----
|
||||
# 先用 ss 快速判断(零成本);不通则交给 tunnel.py —— 那里是唯一的自愈实现,
|
||||
# 所有 Python 入口(main/getVideo5/newsProcess/...)走的是同一套逻辑。
|
||||
ensure_tunnel() {
|
||||
ss -ltn 2>/dev/null | grep -q "127.0.0.1:${DB_PORT}" && return 0
|
||||
log "数据库端口 ${DB_PORT} 不通,调用 tunnel.py 自愈(必要时执行 autossh.sh)"
|
||||
if "$PY" "$DIR/tunnel.py" >>"$LOG" 2>&1; then
|
||||
log "隧道已就绪"
|
||||
return 0
|
||||
fi
|
||||
log "⚠️ 隧道仍不通(端口 ${DB_PORT}),本次很可能失败"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 等到 HH:MM;若已过该时刻则立即返回(例如首次尝试本来就跑过了 21:30)
|
||||
wait_until() {
|
||||
local target="$1" now target_sec
|
||||
now=$(date +%s)
|
||||
target_sec=$(date -d "today ${target}" +%s 2>/dev/null) || { log "无法解析重试时刻 ${target},立即执行"; return 0; }
|
||||
if [ "$target_sec" -gt "$now" ]; then
|
||||
log "等待到 ${target} 再重试(约 $(( (target_sec - now + 59) / 60 )) 分钟)"
|
||||
sleep $(( target_sec - now ))
|
||||
else
|
||||
log "已过 ${target},立即重试"
|
||||
fi
|
||||
}
|
||||
|
||||
# 失败重试时:只缺精编就走切分环节(快、便宜),否则重跑全链路
|
||||
build_cmd() {
|
||||
local stage="$1"
|
||||
if [ "$stage" = "retry" ]; then
|
||||
"$PY" "$DIR/scripts/day_status.py" "$D10" >/dev/null 2>&1
|
||||
case $? in
|
||||
1) log "当天分片已入库、只缺精编 → 重试仅重跑切分环节"
|
||||
CMD=("$PY" "$DIR/newsProcess.py" "$D10" --force); return 0 ;;
|
||||
0) log "当天数据已完整(可能上次重试已完成),无需再跑"
|
||||
CMD=(); return 0 ;;
|
||||
*) log "当天分片未入库 → 重试重跑全链路" ;;
|
||||
esac
|
||||
fi
|
||||
CMD=("$PY" "$DIR/getVideo5.py" "$D8" "$D8")
|
||||
}
|
||||
|
||||
run_attempt() {
|
||||
local label="$1" stage="$2" rc
|
||||
build_cmd "$stage"
|
||||
if [ "${#CMD[@]}" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
log "===== ${label} 开始(${CMD[*]},上限 ${ATTEMPT_TIMEOUT}s)====="
|
||||
timeout "$ATTEMPT_TIMEOUT" "${CMD[@]}" 2>&1 | tee -a "$LOG"
|
||||
rc=${PIPESTATUS[0]}
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
log "===== ${label} 成功 ====="
|
||||
return 0
|
||||
fi
|
||||
log "===== ${label} 失败(退出码 ${rc})====="
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_tunnel || true
|
||||
|
||||
# ---- 当天已完成则跳过:ext 有数据即视为完成(识别不完整的日子不会写出 ext)----
|
||||
# 手动强制重跑:XWLB_FORCE=1 bash scripts/run_daily.sh <日期>
|
||||
if [ "${XWLB_FORCE:-0}" != "1" ]; then
|
||||
if "$PY" "$DIR/scripts/day_status.py" "$D10" >/dev/null 2>&1; then
|
||||
log "$D10 的数据已完整入库,跳过本次运行(强制重跑用 XWLB_FORCE=1 或 newsRedo.py)"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if run_attempt "首次尝试($(date +%H:%M))" full; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for slot in $RETRY_SLOTS; do
|
||||
wait_until "$slot"
|
||||
if run_attempt "重试(${slot})" retry; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
log "❌ 全部重试失败,${D8} 数据未完成"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user