# 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` 命中 `