From 221b1f8addbd4c9bfdd3bc9cddcd1f41a0e970c9 Mon Sep 17 00:00:00 2001 From: cheney Date: Wed, 4 Nov 2026 15:12:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=95=E5=85=A5=20so-novel=20submodu?= =?UTF-8?q?le=20=E5=B9=B6=E6=8F=90=E4=BE=9B=E9=9D=9E=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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: 使用说明与下载原理分析 --- .gitignore | 16 + .gitmodules | 3 + README.md | 253 ++++++++++++++ SKILL.md | 94 ++++++ bin/novel.cmd | 10 + bin/novel.ps1 | 229 +++++++++++++ tools/build.ps1 | 114 +++++++ tools/config.ini.template | 43 +++ tools/lib.ps1 | 127 +++++++ tools/src/skills/novel/ToolMain.java | 482 +++++++++++++++++++++++++++ vendor/so-novel | 1 + 11 files changed, 1372 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 SKILL.md create mode 100644 bin/novel.cmd create mode 100644 bin/novel.ps1 create mode 100644 tools/build.ps1 create mode 100644 tools/config.ini.template create mode 100644 tools/lib.ps1 create mode 100644 tools/src/skills/novel/ToolMain.java create mode 160000 vendor/so-novel diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d599aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# build artifacts / local runtime +/tools/runtime/ +/tools/build/ +/tools/.jdk/ + +# downloaded books and logs +/books/ +/logs/ +*.log + +# editor noise +.vscode/ +.idea/ +*.iml +Thumbs.db +.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..43574bf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/so-novel"] + path = vendor/so-novel + url = https://github.com/freeok/so-novel diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2de22c --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +# ebook-skill + +把开源项目 [so-novel](https://github.com/freeok/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 +``` + +--- + +## 快速开始 + +```powershell +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` 命中 `