Files
ebook-skill/README.md
T
cheney 221b1f8add feat: 引入 so-novel submodule 并提供非交互下载工具
- vendor/so-novel: 上游 v1.12.0 (规则驱动的书源抓取/下载引擎)
- tools/src: ToolMain 封装 search/download/toc/sources,stdout 只输出 JSON
- tools/build.ps1: 自动定位/下载 JDK21,按平台显式激活 javet 原生库 profile
- bin/novel.ps1|.cmd: 命令行入口(人读输出或 -Json)
- SKILL.md/README.md: 使用说明与下载原理分析
2026-11-04 15:12:10 +08:00

14 KiB
Raw Blame History

ebook-skill

把开源项目 so-novel(通用网页小说抓取器)以 git submodule 形式引入, 并在它外面包一层非交互 CLI,让 agent / 脚本可以直接「搜索 → 选源 → 下载 → 拿到文件」, 不部署 Web 服务、不跑 TUI 交互菜单。

ebook-skill/
├─ vendor/so-novel/          # git submodule,上游引擎(Java 21 + Maven)
├─ tools/
│  ├─ src/skills/novel/ToolMain.java   # 封装层:search / download / toc / sources
│  ├─ build.ps1              # 构建引擎 jar + 编译封装层 + 生成运行时配置
│  ├─ lib.ps1                # 公共函数(定位 JDK21、路径、jar 工具)
│  ├─ config.ini.template    # 运行时配置模板
│  └─ runtime/               # 生成物(gitignore):app.jar、rules/、config.ini、logs/
├─ bin/
│  ├─ novel.ps1              # 人读表格 + JSON 两种输出
│  └─ novel.cmd              # cmd.exe 入口(自动选 pwsh / powershell)
├─ books/                    # 下载落盘目录(gitignore)
├─ SKILL.md                  # 给 agent 的使用说明
└─ README.md

快速开始

git submodule update --init --recursive     # 首次
pwsh tools\build.ps1                        # 首次构建(可自动下载 JDK 21)

pwsh bin\novel.ps1 sources                  # 看书源
pwsh bin\novel.ps1 search 诡秘之主           # 聚合搜索
pwsh bin\novel.ps1 toc   <详情页链接>        # 只解析目录,验证完整性
pwsh bin\novel.ps1 download <详情页链接> -Ext txt

依赖:JDK 21(没有则构建脚本从清华镜像下载便携版)、Maven(仅构建时需要)、Git。 下载成品在 books/书名(作者).txt|epub|html|pdf。


一、so-novel 是怎么下载小说的

上游不是「针对某个站点写死的爬虫」,而是规则驱动 + 通用抓取引擎: 每个书源是一段 JSON(vendor/so-novel/bundle/rules/main.json 等),引擎按规则抓页面。

1. 书源规则(Rule)

一条规则对应一个站点,字段分五段:

段 作用 关键字段
url / name / disabled / needProxy 站点标识 url 同时是「按详情页 URL 前缀路由到书源」的依据
search 搜索接口 url(支持 %s 或 @js:)、method/data(GET/POST 表单)、result(结果条目选择器)、bookName/author/latestChapter…、nextPage
book 详情页 bookName/author/intro/coverUrl/latestChapter/lastUpdateTime/status
toc 目录页 url(目录可能不在详情页)、list(先取容器 HTML)、item(章节条目)、desc(倒序)、nextPage(分页)
chapter 正文页 content、nextPage(正文分页)、nextPageInJs、nextChapterLink(末页判定正则)、filterTxt/filterTag(广告清洗)
crawl 站点级抓取参数 concurrency、min/maxInterval、maxAttempts 等覆盖全局配置

选择器同时支持 CSS 与 XPath(以 /、//、( 开头判定为 XPath), 元素选择器@href / @src / @content 表示取属性;meta[property="og:novel:book_name"] 这类缺省值让「标准 meta」站点无需逐字段配置。

选择器后面还能挂 DSL:选择器@js:代码 / @java:代码 / 多段串联(a@js:...;@js:...)。 HtmlExtractor 先用选择器取原始文本,再交给 StepEngine 依次执行 JS(Javet V8 引擎池)或 Java 代码做二次加工 ——这也是 javet 依赖和启动时预热 V8 的原因。

2. 搜索(SearchParser)

  1. 用 search.url 拼搜索地址:普通 %s 字符串格式化,或 @js: 动态生成(签名、加密参数等);
  2. GET/POST(data 是 JSON,其中 %s 按顺序填参数),带随机 UA + Referer,可选 Cookie;
  3. result 选出结果条目,逐条提取书名/作者/最新章节等,链接取 @href 的绝对地址;
  4. 若有 nextPage,一次取全部翻页链接并发补齐(去重合并),再按 search-limit 截断;
  5. 若结果条目为空但页面本身是详情页,则退化为「把搜索页当详情页」处理(部分源会直接跳转);
  6. 聚合搜索(AggregatedSearchAction)对所有可搜索书源并发跑一遍,最后 SearchResultsHandler 用字符串相似度(StrUtil.similar)判断关键字更像书名还是作者,过滤低分(<0.25)、按相似度排序。
  7. 遇 Cloudflare 挑战页(标题白名单判定)时可走外部 cf-bypass 服务取真实 HTML。

3. 详情 + 目录(BookParser / TocParser)

  • 详情页解析书名、作者、简介、封面、最新章节、更新时间;规则没写的字段回退到 og:novel:* meta。
  • 目录页并非总在详情页:toc.url 可用 %s 填详情页里的书籍 id(book.url 正则提取)。
  • 目录分页有两种形态:nextPage 命中 <option> 下拉(一次性拿到所有分页 URL),或「下一页」按钮递归跟随。
  • 多目录页用 5 个虚拟线程并发抓取,再按页序拼成章节列表;desc 表示站点是倒序,需要反转。

4. 正文抓取(ChapterParser / Crawler)

  • 章节级并发:Java 21 虚拟线程 + VirtualThreadLimiter,默认并发 min(50, 章节数)(配置 concurrency 可改,上限 100)。
  • 每章请求前 sleep 一个随机间隔(默认 200–400ms,站点规则可覆盖),随机 UA + Referer。
  • 正文页有分页时按 nextPage 循环拼接,用 nextChapterLink 正则 / 通用「_数字.html + 下一章文案」判定末页。
  • 失败重试 max-retries(默认 3)次,重试间隔在 retry-min/max-interval 内随机并逐次递增; 重试用尽则记录日志跳过该章(enable-retry=0 时直接中断)。
  • 每章先落成独立文件(0001_章节名.html 等),避免长任务中途失败全丢。

5. 清洗与合成

ChapterFilter:去不可见字符/私用区字符、去 HTML 实体、按 filterTxt 正则和 filterTag 选择器删广告、 去正文开头重复的章节名、1. 标题 归一成 第1章 标题。 ChapterFormatter:把 <br> 等转成 <p> 段落。 之后按格式合成:

格式 处理器 产物
txt TxtMergeHandler 单文件,开头书名/作者/简介,正文全角缩进;可用 txt-encoding 指定 GBK 等
epub EpubMergeHandler 带封面、metadata、分章的 epub
html HtmlTocHandler 带目录的单页 HTML
pdf PdfMergeHandler openhtmltopdf 生成 PDF

中间章节目录默认在合成后删除(preserve-chapter-cache=1 可保留)。 输出命名:中间目录 书名 (作者) TXT,成品 书名(作者).txt,均在 download-path 下。

6. 反爬与网络层

随机 UA(RandomUA)、Referer、随机间隔、限流重试、可选 HTTP 代理([proxy])、 起点 Cookie([cookie] qidian,用于拿最新/正版章节)、Cloudflare 时借助 cf-bypass 服务。 Main 在启动时创建并销毁一个 V8 runtime,绕开 Windows 上 Javet 首次创建 SIGSEGV 的问题。


二、这层封装做了什么(以及为什么需要)

上游三种形态都不适合当工具:

形态 问题
TUI(默认) 交互菜单,无法脚本化
Web(-Dmode=web,端口 7765) 需要常驻服务,与「不部署服务」冲突
CLI(-u <详情页>) 只能下已知 URL 的单本,没有搜索;输出是彩色文本,无机器可读结果

所以 tools/src/skills/novel/ToolMain.java 直接复用上游库类,补上缺的部分:

  • search:并发调用各书源 SearchParser,复用上游的相似度过滤/排序,输出 JSON;附带每个源的失败统计。
  • download:与原 CLI 同一条路径(SourceUtils.getRule(url) 路由书源 → BookParser → Crawler), 但支持 --out 指定目录、返回成品文件路径与大小。
  • toc:只解析目录并给首尾章节,用来先验证书源质量(章节数、是否缺章)再决定下载。
  • sources:列出激活书源(id、主页、是否支持搜索)。

工程细节:

  • stdout 只有一份 JSON:库的彩色日志在进程内被重定向截留,--log 时以 logTail 返回最后 40 行, 便于排查「源站 403 / Cloudflare / 目录为空」。
  • JSON 中非 ASCII 全部转成 \uXXXX:Windows 控制台代码页(936)不会把中文搞成乱码, ConvertFrom-Json / Python json.loads 都会还原成正确文本。
  • 配置即参数:以 -Dconfig.file 指向生成的 tools/runtime/config.ini,书源规则走绝对路径, 与当前工作目录无关;日志落 tools/runtime/logs/。
  • 首次使用自动构建:bin/novel.ps1 发现 app.jar 或 class 缺失会先调 tools/build.ps1。

三、命令参考

# 搜索(-Limit 每个源最多取多少条,默认 30;-Source 只搜某个源;-Timeout 秒)
pwsh bin\novel.ps1 search 关键词 [-Limit 30] [-Source 8] [-Timeout 180] [-Log] [-Json]

# 下载(-Ext txt|epub|html|pdf,默认 txt;-Out 指定目录;URL 也可用 -Url)
pwsh bin\novel.ps1 download <详情页链接> [-Ext txt] [-Out D:\books] [-Log] [-Json]

# 只解析目录
pwsh bin\novel.ps1 toc <详情页链接> [-Json]

# 书源列表
pwsh bin\novel.ps1 sources [-Json]

-Json 输出契约(节选):

{
  "ok": true,
  "command": "search",
  "keyword": "诡秘之主",
  "count": 2,
  "results": [
    { "index": 1, "sourceId": 10, "sourceName": "笔趣阁52", "bookName": "诡秘之主",
      "author": "爱潜水的乌贼", "latestChapter": "新书已发", "lastUpdateTime": "09-26",
      "url": "https://www.52bqg.com/book_2279/" }
  ],
  "sources": { "total": 13, "returned": 20, "empty": 3, "failed": [] },
  "tookMs": 12000
}
{ "ok": true, "command": "download", "bookName": "诡秘之主", "author": "爱潜水的乌贼",
  "sourceId": 10, "sourceName": "笔趣阁52", "ext": "txt",
  "file": "D:\\workbench\\ebook-skill\\books\\诡秘之主(爱潜水的乌贼).txt",
  "bytes": 1234567, "seconds": 42.1 }

失败时 ok=false 并带 error(附 --log 可看上游日志尾部)。


四、运行时配置(tools/runtime/config.ini)

构建时由 tools/config.ini.template 生成,仓库位置变化后删除该文件再跑一次 tools\build.ps1 即可重建。

配置 说明
[download] download-path 默认 books/,可用 -Out 单次覆盖
[download] extname 默认格式,命令行 -Ext 优先
[download] txt-encoding txt 编码,老设备可设 GBK
[download] preserve-chapter-cache 1 = 保留中间章节文件
[source] active-rules 激活的规则文件(默认 runtime/rules/main.json,13 个源)
[source] search-limit 每个源搜索结果上限
[source] search-filter 1 = 按相似度过滤低质量结果
[crawl] concurrency 章节并发,空 = 自动(≤50,上限 100)
[crawl] min/max-interval 章节请求随机间隔(毫秒)
[crawl] max-retries 单章重试次数
[proxy] 需要代理才能访问的书源:enabled=1 + host/port
[cookie] qidian 起点 w_tsfp,用于部分源的最新章节
[global] cf-bypass Cloudflare 绕过服务地址(如 https://github.com/sarperavci/CloudflareBypassForScraping 部署的实例)

runtime/rules/ 下的其他规则包(cloudflare.json、proxy-required.json、rate-limit.json、no-search.json) 默认未激活:需要时把 active-rules 指向对应文件(或把这些源合并进一个 json)。


五、维护

# 更新上游代码(GitHub 直连不通时,本仓库已在 .git/config 配了 ghfast.top 镜像 insteadOf)
git -C vendor/so-novel fetch --tags
git -C vendor/so-novel checkout <tag>
git add vendor/so-novel && git commit -m "bump so-novel"

# 重建(引擎变更时必须;规则包更新可只刷规则)
pwsh tools\build.ps1 -Force -RefreshRules

已知坑:

  • Javet 原生库:so-novel 依赖 JS 规则(@js:),需要平台对应的 javet-v8-* 原生库。 它的 Maven profile 用 x86_64 匹配架构,而 Windows JDK 报 amd64,自动激活会失效, 症状是「Javet library /libjavet-v8-windows-x86_64...dll is not found」、依赖 JS 规则的书源全部空结果。 tools/build.ps1 已按平台显式传 -P windows-x64|linux-x64|…,重建后 app.jar 约 36 MB(含原生库)。
  • GitHub 被重置:仓库本地配置了 url."https://ghfast.top/https://github.com/".insteadOf, 删除用 git config --unset url."https://ghfast.top/https://github.com/".insteadOf。
  • JDK 21 必需(虚拟线程 + 上游 class 版本 65);Find-Jdk21 会依次找 EBOOK_NOVEL_JAVA_HOME → JAVA_HOME → tools/.jdk → 常见安装路径(含 IntelliJ 自带 JBR)。
  • Maven 只在构建时需要;Maven Central 走内网 nexus / 阿里云镜像均可。
  • 规则失效是最常见的失败原因(站点改版/关站):search 出来为空、toc 章节数异常、 下载 txt 只有简介——此时换一个书源,或更新子模块/规则。
  • 目录质量比源数量重要:toc 里首章不是第 1 章(分页缺失)或顺序反了(站点倒序但规则没标 desc), 都会产出残书/反序书,直接换源。
  • 别把并发调太高:这些站点普遍限流,concurrency 40–60、间隔 200–400ms 是上游默认值,够用。

免责声明

本项目仅用于个人学习与技术研究,请遵守目标站点服务条款与著作权法规,不要用于商业分发。