用AI读《前汉演义》

2026-04-18

前段时间开始读蔡东藩先生的《中国历代通俗演义》,其中《前汉演义》读了差不多半年了,读到第68回的时候,突然奇想,与其每次遇到不懂的文言文查询,不如试试AI一键获取会怎么样呢?因此采用Cursor的Auto模式开始进行批量生成。 当然由于内容过多,目前生成的效果还来不及二次校验(只校验了一小部分),但后续会继续阅读时会一直校验,先以本博文记录下AI生成及目录。

代码生成过程

整体流程:维基文库 API 拉 HTML → BeautifulSoup 取回目与正文段 → zhconv 转简体 → 写入 Jekyll Markdown# 译文 占位符后续由 Cursor 中 AI(可拆多批/子智能体)按第 68 回式述要补全。

环境与依赖

在仓库根目录执行(示例为 Python 3.9+):

pip install zhconv beautifulsoup4 lxml

说明:

  • zhconv:繁体转大陆简体(zh-cn)。
  • beautifulsoup4 + lxml:解析 action=parse 返回的 HTML。
  • 请求维基 API 时必须带 非空 User-Agent(脚本中已写固定标识),否则易被 403。

脚本位置与用法

仓库内脚本路径:scripts/fetch_qianhan_chapters.py

# 默认仅拉第 26~67 回(与早期批处理一致,避免误覆盖全书)
python3 scripts/fetch_qianhan_chapters.py

# 指定区间(示例:全书 1~100 回;会覆盖已存在的同名 md)
python3 scripts/fetch_qianhan_chapters.py --start 1 --end 100

# 仅补某一回
python3 scripts/fetch_qianhan_chapters.py --start 69 --end 69

写入路径:_posts/前汉演义/2026-04-18-前汉演义XX.mdXX 为两位回次)。再次运行会整文件覆盖,若已手写译文请先备份或改脚本逻辑。

完整脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Fetch 前漢演義 chapters from zh.wikisource.org, convert to zh-cn, write Jekyll posts."""

from __future__ import annotations

import argparse
import json
import re
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
from typing import List, Optional, Tuple

from bs4 import BeautifulSoup
import zhconv

UA = "KwanWaiPangBlogBot/1.0 (https://github.com/KwanWaiPang/kwanwaipang.github.io; zhconv+read-only)"

ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "_posts" / "前汉演义"


def api_parse(page: str) -> str:
    q = urllib.parse.urlencode(
        {"action": "parse", "page": page, "prop": "text", "format": "json"}
    )
    url = f"https://zh.wikisource.org/w/api.php?{q}"
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=60) as resp:
        data = json.loads(resp.read().decode("utf-8"))
    if "error" in data:
        raise RuntimeError(data["error"])
    return data["parse"]["text"]["*"]


def _parse_nav_subtitle(nav_text: str) -> Optional[str]:
    """从导航块文字解析副标题;维基文库各回版式略有出入。"""
    flat = re.sub(r"\s+", " ", nav_text.replace("\n", " "))
    patterns = [
        r"前漢演義\s*(第.+?回)\s*(.+?)\s*作者",
        r"前漢演義\s*(第.+?回)\s*(.+?)(?=\s*[◄►])",
        r"前漢演義\s*(第.+?回)\s*(.+?)(?=\s*→|\s*←)",
        r"前漢演義\s*(第.+?回)\s*(.+?)(?=\s*姊妹计划)",
    ]
    for pat in patterns:
        m = re.search(pat, flat)
        if m:
            return m.group(2).strip()
    return None


def extract_title_and_paragraphs(html: str) -> Tuple[str, List[str]]:
    soup = BeautifulSoup(html, "lxml")
    po = soup.select_one(".mw-parser-output")
    if not po:
        raise RuntimeError("no .mw-parser-output")

    subtitle = ""
    paragraphs: List[str] = []

    for child in po.children:
        name = getattr(child, "name", None)
        if not name:
            continue
        if name in ("div", "table") and not subtitle:
            nav_text = child.get_text(" ", strip=True)
            if "前漢演義" in nav_text and "回" in nav_text:
                sub = _parse_nav_subtitle(nav_text)
                if sub:
                    subtitle = sub
            continue
        if name == "p":
            t = child.get_text()
            if not t or not t.strip():
                continue
            paragraphs.append(t.strip())

    if not subtitle:
        blob = po.get_text(" ", strip=True)[:1200]
        subtitle = _parse_nav_subtitle(blob) or ""

    if not subtitle:
        raise RuntimeError("subtitle not found")

    return subtitle, paragraphs


def to_cn(s: str) -> str:
    return zhconv.convert(s, "zh-cn")


def cn_num(n: int) -> str:
    """阿拉伯数字转中文数字(用于 Jekyll title 中的回次,1–100)。"""
    digits = "零一二三四五六七八九"
    if n < 10:
        return digits[n]
    if n == 10:
        return "十"
    if n < 20:
        return "十" + digits[n - 10]
    if n < 100:
        tens, ones = divmod(n, 10)
        return digits[tens] + "十" + (digits[ones] if ones else "")
    if n == 100:
        return "一百"
    return str(n)


def build_markdown(chapter: int, subtitle_tw: str, paras_tw: List[str]) -> str:
    subtitle = to_cn(subtitle_tw)
    body_lines = []
    for p in paras_tw:
        body_lines.append(to_cn(p))
    body = "\n\n".join("  " + ln.replace("\n", "") for ln in body_lines)

    title = f'读书笔记之——《前汉演义》第{cn_num(chapter)}回:{subtitle}'
    wikilink = f"https://zh.wikisource.org/wiki/%E5%89%8D%E6%BC%A2%E6%BC%94%E7%BE%A9/%E7%AC%AC{chapter:03d}%E5%9B%9E"

    return f"""---
layout: post
title: "{title}"
date: 2026-04-18
tags: [Books]
comments: true
author: kwanwaipang
# toc: false
excerpt: ""
---

<!-- * 参考资料:[蔡东藩《前汉演义》](https://zh.wikisource.org/wiki/%E5%89%8D%E6%BC%A2%E6%BC%94%E7%BE%A9) · [维基文库:本回全文]({wikilink}) -->


---

# 原文

{body}

---

# 译文

(译文生成中)
"""


def main(argv=None) -> None:
    p = argparse.ArgumentParser(description="Fetch 前漢演義 chapters from zh.wikisource.org")
    p.add_argument(
        "--start",
        type=int,
        default=26,
        help="起始回次(含)。默认 26 与历史批处理一致;全书请用 1",
    )
    p.add_argument(
        "--end",
        type=int,
        default=67,
        help="结束回次(含)。默认 67;全书至 100 请写 100",
    )
    args = p.parse_args(argv)
    start, end = args.start, args.end
    if start > end:
        print("error: --start must be <= --end", file=sys.stderr)
        sys.exit(1)

    OUT_DIR.mkdir(parents=True, exist_ok=True)
    for n in range(start, end + 1):
        page = f"前漢演義/第{n:03d}回"
        print("fetch", page)
        html = api_parse(page)
        sub, paras = extract_title_and_paragraphs(html)
        md = build_markdown(n, sub, paras)
        path = OUT_DIR / f"2026-04-18-前汉演义{n:02d}.md"
        path.write_text(md, encoding="utf-8")
        time.sleep(0.35)


if __name__ == "__main__":
    main()

抓取逻辑摘要

步骤 说明
API https://zh.wikisource.org/w/api.php?action=parse&page=前漢演義/第NNN回&prop=text&format=json
回目副题 .mw-parser-output 首块 divtable 导航文字中正则提取;部分回次无「作者」字样时换用 ◄► 等分隔模式;仍失败则在前 1200 字内再扫
正文 收集导航块之后所有顶层 <p>,段首统一加全角空格   
简体 副题与正文均经 zhconv.convert(..., "zh-cn")
节流 每回 sleep(0.35),减轻对维基服务器压力

译文部分(AI)

脚本生成的 # 译文 初值为 (译文生成中)。实际生产时在 Cursor 用 Auto / Agent,按回次分批撰写白话述要(对齐 _posts/前汉演义/2026-04-18-前汉演义68.md:开篇说明、若干 ##、诗联要点、## 回末史论(白话)),再写回对应 md。与抓取脚本解耦,避免一次对话超长。

多智能体(可选)

大批量回次时,可在 Cursor 中并行派发多个子任务,各负责一段回次区间,分别改文件。示意截图仍如下(界面随版本可能略有差异):

Cursor 中并行任务示意

书籍目录及索引