@cloud411716/fancy-webnovel 0.1.76 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infra.js +26 -21
- package/package.json +1 -1
- package/plugins/fancy-bootstrap/index.js +4 -2
- package/plugins/fancy-scan/index.js +1 -2
- package/plugins/fancy-scan/scripts/run-scan.js +22 -44
- package/plugins/fancy-scan/scripts/scraper-registry.md +3 -5
- package/plugins/fancy-scan/scripts/scrapers/cdp-utils.cjs +7 -5
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.cjs +282 -253
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.cjs +193 -253
- package/plugins/fancy-scan/scripts/scrapers/node_modules/.package-lock.json +20 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/LICENSE +202 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/NOTICE +5 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/README.md +3 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/browsers.json +75 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/cli.js +21 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/index.d.ts +17 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/index.js +17 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/index.mjs +28 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/bootstrap.js +88 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/coreBundle.js +74830 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/mcp.js +10 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/package.js +50 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/server/electron/loader.js +118 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/serverRegistry.js +7347 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +354 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/help.json +708 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/output.js +343 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/program.js +404 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/registry.js +176 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/cli-client/session.js +258 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +420 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +171 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/utils/extension.js +101 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/utilsBundle.js +90764 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +2179 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-rXmQmLUY.js +32 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B-dXF5JN.js +181 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-BuZfJS5v.js +7 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/index.KZ4wOW1K.js +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Dzuouizj.js +5 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/lib/xdg-open +1267 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/package.json +34 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/types/protocol.d.ts +24842 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/types/structs.d.ts +52 -0
- package/plugins/fancy-scan/scripts/scrapers/node_modules/playwright-core/types/types.d.ts +26188 -0
- package/plugins/fancy-scan/scripts/scrapers/package-lock.json +27 -0
- package/plugins/fancy-scan/scripts/scrapers/package.json +9 -0
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.cjs +200 -297
- package/plugins/fancy-scan/scripts/scrapers/zhihu-rank-scraper.cjs +80 -80
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 番茄小说排行榜采集脚本
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 使用 playwright-core 自己管理浏览器生命周期。
|
|
6
6
|
* 采集策略:从榜单页 __INITIAL_STATE__ 取结构化列表,再逐本请求详情页解码真实
|
|
7
7
|
* 书名/作者/简介/题材/标签(番茄列表页有字体反爬,详情页 HTML 里是明文)。
|
|
8
8
|
* 输出 Markdown 格式匹配 scan-output-format.md 规范。
|
|
@@ -10,170 +10,197 @@
|
|
|
10
10
|
* 用法:
|
|
11
11
|
* node fanqie-rank-scraper.js --channel 1 --type 2 # 男频阅读榜
|
|
12
12
|
* node fanqie-rank-scraper.js --channel 0 --type 1 # 女频新书榜
|
|
13
|
-
* node fanqie-rank-scraper.js --channel 1 --type 2 --outdir ./ # 指定输出目录
|
|
14
13
|
* node fanqie-rank-scraper.js --channel all # 全部采集
|
|
15
14
|
* node fanqie-rank-scraper.js --channel 1 --top 15 # 每题材只取前 15 本
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* node {SKILL_DIR}/browser-cdp/scripts/setup-cdp-chrome.js 9222
|
|
15
|
+
* node fanqie-rank-scraper.js --login-wait 60 # 打开浏览器后等待用户登录(秒)
|
|
16
|
+
* node fanqie-rank-scraper.js --outdir ./ # 指定输出目录
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
const fs = require("fs");
|
|
22
20
|
const path = require("path");
|
|
23
|
-
const {
|
|
21
|
+
const { chromium } = require("playwright-core");
|
|
22
|
+
const { getArg, localDateStamp, runCli } = require("./cdp-utils.cjs");
|
|
24
23
|
|
|
25
|
-
//
|
|
26
|
-
// cdp-utils 里 ab() 的 20s 超时;超时会显式失败,这里分批是为了避免整个题材被中断。
|
|
24
|
+
// 一次详情请求的并发批大小
|
|
27
25
|
const DETAIL_CHUNK = 5;
|
|
28
26
|
|
|
29
27
|
// ---------------------------------------------------------------------------
|
|
30
|
-
//
|
|
28
|
+
// 工具函数
|
|
31
29
|
// ---------------------------------------------------------------------------
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
port,
|
|
37
|
-
"JSON.stringify({host:location.host,hasState:!!window.__INITIAL_STATE__})"
|
|
38
|
-
);
|
|
31
|
+
function sleep(ms) {
|
|
32
|
+
// 同步等待:Atomics.wait 在主线程阻塞,不占用事件循环
|
|
33
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if(seen[href])return;seen[href]=1;
|
|
52
|
-
out.push({name:name,href:href});
|
|
53
|
-
});
|
|
54
|
-
return out;
|
|
55
|
-
})())`;
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// 页面提取
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/** 连通性 + 页面就绪自检 */
|
|
41
|
+
async function probePage(page) {
|
|
42
|
+
return page.evaluate(() => ({
|
|
43
|
+
host: location.host,
|
|
44
|
+
hasState: !!window.__INITIAL_STATE__,
|
|
45
|
+
}));
|
|
56
46
|
}
|
|
57
47
|
|
|
58
48
|
/** 提取侧边菜单品类链接 */
|
|
59
|
-
function extractCategories(
|
|
49
|
+
async function extractCategories(page, channel, type) {
|
|
60
50
|
const prefix = `/rank/${channel}_${type}_`;
|
|
61
|
-
|
|
51
|
+
const cats = await page.evaluate((p) => {
|
|
52
|
+
const out = [];
|
|
53
|
+
const seen = {};
|
|
54
|
+
document.querySelectorAll("a").forEach((a) => {
|
|
55
|
+
const href = a.getAttribute("href") || "";
|
|
56
|
+
if (href.indexOf(p) === -1) return;
|
|
57
|
+
const name = (a.innerText || a.textContent || "").trim();
|
|
58
|
+
if (!name) return;
|
|
59
|
+
if (seen[href]) return;
|
|
60
|
+
seen[href] = 1;
|
|
61
|
+
out.push({ name, href });
|
|
62
|
+
});
|
|
63
|
+
return out;
|
|
64
|
+
}, prefix);
|
|
65
|
+
return cats;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
s.
|
|
73
|
-
s.
|
|
68
|
+
/** 从 __INITIAL_STATE__ 提取当前品类页的作品列表 */
|
|
69
|
+
async function extractBookList(page) {
|
|
70
|
+
return page.evaluate(() => {
|
|
71
|
+
const s = window.__INITIAL_STATE__ || {};
|
|
72
|
+
const cands = [
|
|
73
|
+
s.rank && s.rank.book_list,
|
|
74
|
+
s.rank && s.rank.bookList,
|
|
75
|
+
s.rank && s.rank.rankList,
|
|
76
|
+
s.rankData && s.rankData.book_list,
|
|
77
|
+
s.page && s.page.book_list,
|
|
74
78
|
];
|
|
75
|
-
var list=null;
|
|
76
|
-
for(var i=0;i<cands.length;i++)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
var list = null;
|
|
80
|
+
for (var i = 0; i < cands.length; i++) {
|
|
81
|
+
if (Array.isArray(cands[i]) && cands[i].length) {
|
|
82
|
+
list = cands[i];
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!list) {
|
|
87
|
+
var found = null;
|
|
88
|
+
(function walk(o, d) {
|
|
89
|
+
if (found || !o || d > 6) return;
|
|
90
|
+
if (Array.isArray(o)) {
|
|
91
|
+
if (o.length && o[0] && typeof o[0] === "object" && (o[0].bookId || o[0].book_id)) {
|
|
92
|
+
found = o;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (var j = 0; j < o.length && !found; j++) walk(o[j], d + 1);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (typeof o === "object") {
|
|
99
|
+
for (var k in o) {
|
|
100
|
+
if (found) break;
|
|
101
|
+
try {
|
|
102
|
+
walk(o[k], d + 1);
|
|
103
|
+
} catch (e) {}
|
|
104
|
+
}
|
|
84
105
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
list=found||[];
|
|
106
|
+
})(s, 0);
|
|
107
|
+
list = found || [];
|
|
88
108
|
}
|
|
89
|
-
return list.map(
|
|
90
|
-
bookId:String(b.bookId||b.book_id||
|
|
91
|
-
read_count:b.read_count||b.readCount||b.read||
|
|
92
|
-
wordNumber:b.wordNumber||b.word_number||b.wordCount||
|
|
93
|
-
creationStatus:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
return list.map((b) => ({
|
|
110
|
+
bookId: String(b.bookId || b.book_id || ""),
|
|
111
|
+
read_count: b.read_count || b.readCount || b.read || "",
|
|
112
|
+
wordNumber: b.wordNumber || b.word_number || b.wordCount || "",
|
|
113
|
+
creationStatus:
|
|
114
|
+
b.creationStatus != null
|
|
115
|
+
? b.creationStatus
|
|
116
|
+
: b.creation_status != null
|
|
117
|
+
? b.creation_status
|
|
118
|
+
: b.status,
|
|
119
|
+
lastChapterTitle: b.lastChapterTitle || b.last_chapter_title || b.lastChapter || "",
|
|
120
|
+
category: b.category || b.categoryName || b.category_name || "",
|
|
121
|
+
})).filter((b) => b.bookId);
|
|
122
|
+
});
|
|
98
123
|
}
|
|
99
124
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function pick(h,res){for(var i=0;i<res.length;i++){var m=h.match(res[i]);if(m&&m[1])return m[1].trim();}return '';}
|
|
117
|
-
for(var k=0;k<ids.length;k++){
|
|
118
|
-
var id=ids[k];
|
|
119
|
-
try{
|
|
120
|
-
var x=new XMLHttpRequest();
|
|
121
|
-
x.open('GET','/page/'+id,false);
|
|
125
|
+
/** 批量解码详情:逐本同步 XHR 请求 /page/{id} */
|
|
126
|
+
async function fetchDetailsChunk(page, ids) {
|
|
127
|
+
return page.evaluate((ids) => {
|
|
128
|
+
var map = {};
|
|
129
|
+
function pick(h, res) {
|
|
130
|
+
for (var i = 0; i < res.length; i++) {
|
|
131
|
+
var m = h.match(res[i]);
|
|
132
|
+
if (m && m[1]) return m[1].trim();
|
|
133
|
+
}
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
for (var k = 0; k < ids.length; k++) {
|
|
137
|
+
var id = ids[k];
|
|
138
|
+
try {
|
|
139
|
+
var x = new XMLHttpRequest();
|
|
140
|
+
x.open("GET", "/page/" + id, false);
|
|
122
141
|
x.send();
|
|
123
|
-
var h=x.responseText||
|
|
124
|
-
var title=pick(h,[
|
|
125
|
-
/"bookName"
|
|
142
|
+
var h = x.responseText || "";
|
|
143
|
+
var title = pick(h, [
|
|
144
|
+
/"bookName"\s*:\s*"([^"]+)"/,
|
|
126
145
|
/<title>([^<]*?)(?:完整版|最新章节|在线阅读|_番茄小说|-番茄小说|_番茄|-番茄)/,
|
|
127
146
|
/<meta[^>]+property="og:title"[^>]+content="([^"]+)"/,
|
|
128
|
-
/<title>([^<|_]{1,40})
|
|
129
|
-
]);
|
|
130
|
-
var author=pick(h,[
|
|
131
|
-
/"author"\\s*:\\s*"([^"]+)"/,
|
|
132
|
-
/"authorName"\\s*:\\s*"([^"]+)"/,
|
|
133
|
-
/<meta[^>]+property="og:novel:author"[^>]+content="([^"]+)"/
|
|
147
|
+
/<title>([^<|_]{1,40})/,
|
|
134
148
|
]);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
/<meta[^>]+name="description"[^>]+content="([^"]+)"/,
|
|
140
|
-
/<meta[^>]+property="og:description"[^>]+content="([^"]+)"/
|
|
149
|
+
var author = pick(h, [
|
|
150
|
+
/"author"\s*:\s*"([^"]+)"/,
|
|
151
|
+
/"authorName"\s*:\s*"([^"]+)"/,
|
|
152
|
+
/<meta[^>]+property="og:novel:author"[^>]+content="([^"]+)"/,
|
|
141
153
|
]);
|
|
142
|
-
|
|
143
|
-
var
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
var abs = pick(h, [/"abstract"\s*:\s*"([^"]{6,}?)"/]);
|
|
155
|
+
var desc =
|
|
156
|
+
abs ||
|
|
157
|
+
pick(h, [
|
|
158
|
+
/<meta[^>]+name="description"[^>]+content="([^"]+)"/,
|
|
159
|
+
/<meta[^>]+property="og:description"[^>]+content="([^"]+)"/,
|
|
160
|
+
]);
|
|
161
|
+
var category = pick(h, [
|
|
162
|
+
/"categoryV2":"\\[\{[\\s\\S]*?\\"Name\\":\\"([^"\\]+)/,
|
|
163
|
+
/"category"\s*:\s*"([^"]{1,20})"/,
|
|
164
|
+
/<meta[^>]+property="og:novel:category"[^>]+content="([^"]+)"/,
|
|
147
165
|
]);
|
|
148
|
-
|
|
149
|
-
var
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
var tags = "";
|
|
167
|
+
var bm = (abs || desc || "").match(/【\[]([^】\]]{2,40})[】\]]/);
|
|
168
|
+
if (bm) {
|
|
169
|
+
tags = bm[1]
|
|
170
|
+
.split(/[+、,\/\s]+/)
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.slice(0, 6)
|
|
173
|
+
.join("、");
|
|
174
|
+
}
|
|
175
|
+
map[id] = { title, author, desc, category, tags };
|
|
176
|
+
} catch (e) {
|
|
177
|
+
map[id] = { title: "", author: "", desc: "", category: "", tags: "", err: String(e && e.message || e) };
|
|
155
178
|
}
|
|
156
179
|
}
|
|
157
180
|
return map;
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function fetchDetailsChunk(port, ids) {
|
|
162
|
-
return evalJSONBase64(port, buildDetailJS(ids)) || {};
|
|
181
|
+
}, ids);
|
|
163
182
|
}
|
|
164
183
|
|
|
165
|
-
/** 分批解码,避免单次 eval
|
|
166
|
-
function fetchDetails(
|
|
184
|
+
/** 分批解码,避免单次 eval 超时 */
|
|
185
|
+
async function fetchDetails(page, bookIds) {
|
|
167
186
|
const map = {};
|
|
168
187
|
for (let i = 0; i < bookIds.length; i += DETAIL_CHUNK) {
|
|
169
188
|
const chunk = bookIds.slice(i, i + DETAIL_CHUNK);
|
|
170
|
-
const part = fetchDetailsChunk(
|
|
189
|
+
const part = await fetchDetailsChunk(page, chunk);
|
|
171
190
|
Object.assign(map, part);
|
|
172
191
|
sleep(300);
|
|
173
192
|
}
|
|
174
193
|
return map;
|
|
175
194
|
}
|
|
176
195
|
|
|
196
|
+
/** 滚动页面加载更多内容 */
|
|
197
|
+
async function scrollLoad(page, times, interval = 1000) {
|
|
198
|
+
for (let i = 0; i < times; i++) {
|
|
199
|
+
await page.evaluate(() => window.scrollBy(0, window.innerHeight));
|
|
200
|
+
sleep(interval);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
177
204
|
// ---------------------------------------------------------------------------
|
|
178
205
|
// 格式化
|
|
179
206
|
// ---------------------------------------------------------------------------
|
|
@@ -201,11 +228,9 @@ function fmtStatus(s) {
|
|
|
201
228
|
return s ? String(s) : "未知";
|
|
202
229
|
}
|
|
203
230
|
|
|
204
|
-
/** 清洗简介:去平台模板文本 → 折叠空白 → 句末截断 100 字 */
|
|
205
231
|
function cleanDesc(raw) {
|
|
206
232
|
if (!raw) return "";
|
|
207
233
|
let d = String(raw)
|
|
208
|
-
// 简介取自 JSON 字符串原文,先还原常见转义(\n \uXXXX \" 等)
|
|
209
234
|
.replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)))
|
|
210
235
|
.replace(/\\[nrt]/g, " ")
|
|
211
236
|
.replace(/\\"/g, '"')
|
|
@@ -215,7 +240,7 @@ function cleanDesc(raw) {
|
|
|
215
240
|
.trim();
|
|
216
241
|
if (d.length <= 100) return d;
|
|
217
242
|
const cut = d.slice(0, 100);
|
|
218
|
-
const m = cut.match(
|
|
243
|
+
const m = cut.match(/[\s\S]*[。!?]/);
|
|
219
244
|
return (m ? m[0] : cut) + "...";
|
|
220
245
|
}
|
|
221
246
|
|
|
@@ -224,7 +249,7 @@ function cleanDesc(raw) {
|
|
|
224
249
|
// ---------------------------------------------------------------------------
|
|
225
250
|
|
|
226
251
|
const args = process.argv.slice(2);
|
|
227
|
-
const
|
|
252
|
+
const LOGIN_WAIT = parseInt(getArg(args, "--login-wait") || "0", 10);
|
|
228
253
|
const OUTDIR = getArg(args, "--outdir") || ".";
|
|
229
254
|
const CHANNEL = getArg(args, "--channel") || "1";
|
|
230
255
|
const TYPE = getArg(args, "--type") || "2";
|
|
@@ -238,155 +263,162 @@ function typeLabel(t) {
|
|
|
238
263
|
return t === "2" ? "阅读榜" : "新书榜";
|
|
239
264
|
}
|
|
240
265
|
|
|
241
|
-
function scrapeChannel(ch, type) {
|
|
266
|
+
async function scrapeChannel(ch, type) {
|
|
242
267
|
const chLabel = channelLabel(ch);
|
|
243
268
|
const tyLabel = typeLabel(type);
|
|
244
269
|
console.log(`\n→ 采集 ${chLabel}${tyLabel}...`);
|
|
245
270
|
|
|
246
|
-
// 用已知品类 ID 作为入口,确保菜单只显示当前频道/类型的品类
|
|
247
271
|
const initCatId = ch === "1" ? "1141" : "1139"; // 男频:西方奇幻 / 女频:古风世情
|
|
248
272
|
const initUrl = `https://fanqienovel.com/rank/${ch}_${type}_${initCatId}`;
|
|
249
|
-
ab(PORT, "open", initUrl);
|
|
250
|
-
sleep(3000);
|
|
251
|
-
|
|
252
|
-
// 连通性自检:把"静默写出一堆 bookId"变成可操作的报错
|
|
253
|
-
const probe = probePage(PORT);
|
|
254
|
-
if (!probe) {
|
|
255
|
-
console.error(
|
|
256
|
-
` ✗ CDP 无响应。请确认已用 browser-cdp 启动 Chrome(端口 ${PORT}),且 agent-browser 可用。`
|
|
257
|
-
);
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
if (probe.host && probe.host.indexOf("fanqie") === -1) {
|
|
261
|
-
console.error(
|
|
262
|
-
` ✗ 当前页面非番茄(host=${probe.host}),可能被重定向到登录/验证页,已跳过。`
|
|
263
|
-
);
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
if (!probe.hasState) {
|
|
267
|
-
console.error(` ⚠ 页面未挂载 __INITIAL_STATE__,将尝试兜底扫描,结果可能不完整。`);
|
|
268
|
-
}
|
|
269
273
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
scrollLoad(PORT, 2);
|
|
274
|
-
sleep(1000);
|
|
275
|
-
categories = extractCategories(PORT, ch, type);
|
|
276
|
-
}
|
|
277
|
-
if (!categories.length) {
|
|
278
|
-
// 仍失败:降级为只采当前入口页,至少产出数据而不是空跑
|
|
279
|
-
console.log(` ⚠ 未提取到品类菜单,降级为单题材采集(入口页)`);
|
|
280
|
-
categories = [{ name: "全部(入口页)", href: `/rank/${ch}_${type}_${initCatId}` }];
|
|
281
|
-
} else {
|
|
282
|
-
console.log(` 发现 ${categories.length} 个品类`);
|
|
283
|
-
}
|
|
274
|
+
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"] });
|
|
275
|
+
const context = await browser.newContext();
|
|
276
|
+
const page = await context.newPage();
|
|
284
277
|
|
|
285
|
-
|
|
286
|
-
const lines = [
|
|
287
|
-
`# 番茄 · ${chLabel}${tyLabel} · 全 ${categories.length} 题材`,
|
|
288
|
-
"",
|
|
289
|
-
`- 频道参数:channel=${ch},type=${type}`,
|
|
290
|
-
`- 抓取时间:${now}`,
|
|
291
|
-
`- 每题材上限 ≈ ${TOP}`,
|
|
292
|
-
"",
|
|
293
|
-
"---",
|
|
294
|
-
"",
|
|
295
|
-
];
|
|
296
|
-
|
|
297
|
-
let totalBooks = 0;
|
|
298
|
-
let resolvedTitles = 0;
|
|
299
|
-
const bodyLines = [];
|
|
300
|
-
|
|
301
|
-
for (let ci = 0; ci < categories.length; ci++) {
|
|
302
|
-
const cat = categories[ci];
|
|
303
|
-
console.log(` [${ci + 1}/${categories.length}] ${cat.name}`);
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
ab(PORT, "open", `https://fanqienovel.com${cat.href}`);
|
|
307
|
-
sleep(2500);
|
|
308
|
-
scrollLoad(PORT, 2);
|
|
309
|
-
|
|
310
|
-
let books = extractBookList(PORT);
|
|
311
|
-
if (!Array.isArray(books) || !books.length) {
|
|
312
|
-
bodyLines.push(`## ${cat.name} — 0 本`, "", "---", "");
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
if (books.length > TOP) books = books.slice(0, TOP);
|
|
278
|
+
let scraped = null;
|
|
316
279
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
280
|
+
try {
|
|
281
|
+
// 打开入口页
|
|
282
|
+
await page.goto(initUrl, { waitUntil: "networkidle" });
|
|
320
283
|
|
|
321
|
-
|
|
284
|
+
// 等待登录(用户可在此时手动登录)
|
|
285
|
+
if (LOGIN_WAIT > 0) {
|
|
286
|
+
console.log(` ⏳ 等待 ${LOGIN_WAIT}s 供用户登录...`);
|
|
287
|
+
await page.waitForTimeout(LOGIN_WAIT * 1000);
|
|
288
|
+
}
|
|
322
289
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
290
|
+
// 连通性自检
|
|
291
|
+
const probe = await probePage(page);
|
|
292
|
+
if (!probe.host || probe.host.indexOf("fanqie") === -1) {
|
|
293
|
+
console.error(
|
|
294
|
+
` ✗ 当前页面非番茄(host=${probe.host}),可能被重定向到登录/验证页,已跳过。`
|
|
295
|
+
);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (!probe.hasState) {
|
|
299
|
+
console.log(` ⚠ 页面未挂载 __INITIAL_STATE__,将尝试兜底扫描,结果可能不完整。`);
|
|
300
|
+
}
|
|
329
301
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
302
|
+
// 提取品类菜单
|
|
303
|
+
let categories = await extractCategories(page, ch, type);
|
|
304
|
+
if (!categories.length) {
|
|
305
|
+
await scrollLoad(page, 2);
|
|
306
|
+
sleep(1000);
|
|
307
|
+
categories = await extractCategories(page, ch, type);
|
|
308
|
+
}
|
|
309
|
+
if (!categories.length) {
|
|
310
|
+
console.log(` ⚠ 未提取到品类菜单,降级为单题材采集(入口页)`);
|
|
311
|
+
categories = [{ name: "全部(入口页)", href: `/rank/${ch}_${type}_${initCatId}` }];
|
|
312
|
+
} else {
|
|
313
|
+
console.log(` 发现 ${categories.length} 个品类`);
|
|
314
|
+
}
|
|
334
315
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
316
|
+
const now = new Date().toISOString();
|
|
317
|
+
const lines = [
|
|
318
|
+
`# 番茄 · ${chLabel}${tyLabel} · 全 ${categories.length} 题材`,
|
|
319
|
+
"",
|
|
320
|
+
`- 频道参数:channel=${ch},type=${type}`,
|
|
321
|
+
`- 抓取时间:${now}`,
|
|
322
|
+
`- 每题材上限 ≈ ${TOP}`,
|
|
323
|
+
"",
|
|
324
|
+
"---",
|
|
325
|
+
"",
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
let totalBooks = 0;
|
|
329
|
+
let resolvedTitles = 0;
|
|
330
|
+
const bodyLines = [];
|
|
331
|
+
|
|
332
|
+
for (let ci = 0; ci < categories.length; ci++) {
|
|
333
|
+
const cat = categories[ci];
|
|
334
|
+
console.log(` [${ci + 1}/${categories.length}] ${cat.name}`);
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
await page.goto(`https://fanqienovel.com${cat.href}`, { waitUntil: "networkidle" });
|
|
338
|
+
await page.waitForTimeout(2500);
|
|
339
|
+
await scrollLoad(page, 2);
|
|
340
|
+
|
|
341
|
+
let books = await extractBookList(page);
|
|
342
|
+
if (!Array.isArray(books) || !books.length) {
|
|
343
|
+
bodyLines.push(`## ${cat.name} — 0 本`, "", "---", "");
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (books.length > TOP) books = books.slice(0, TOP);
|
|
347
|
+
|
|
348
|
+
const bookIds = books.map((b) => String(b.bookId));
|
|
349
|
+
const details = await fetchDetails(page, bookIds);
|
|
350
|
+
|
|
351
|
+
bodyLines.push(`## ${cat.name} — ${books.length} 本`, "");
|
|
352
|
+
|
|
353
|
+
for (let i = 0; i < books.length; i++) {
|
|
354
|
+
const b = books[i];
|
|
355
|
+
const info = details[String(b.bookId)] || {};
|
|
356
|
+
totalBooks++;
|
|
357
|
+
const resolved = !!info.title;
|
|
358
|
+
if (resolved) resolvedTitles++;
|
|
359
|
+
|
|
360
|
+
const title = info.title || "(标题待解析)";
|
|
361
|
+
const author = info.author || "未知";
|
|
362
|
+
const category = info.category || b.category || "";
|
|
363
|
+
const catSeg = category ? ` · ${category}` : "";
|
|
364
|
+
|
|
365
|
+
bodyLines.push(`### #${i + 1} ${title}`);
|
|
366
|
+
bodyLines.push(
|
|
367
|
+
`*${author}${catSeg} · ${fmtStatus(b.creationStatus)} · ${fmtReads(b.read_count)} 在读 · ${fmtWords(b.wordNumber)}字*`
|
|
368
|
+
);
|
|
369
|
+
if (info.tags) bodyLines.push(`**标签:** ${info.tags}`);
|
|
370
|
+
bodyLines.push(`**最新更新:** ${b.lastChapterTitle || "未知"}`);
|
|
371
|
+
bodyLines.push(`**bookId:** ${b.bookId}`);
|
|
372
|
+
if (info.err) bodyLines.push(`⚠️ 详情:${info.err}`);
|
|
373
|
+
bodyLines.push(`[作品页](https://fanqienovel.com/page/${b.bookId})`);
|
|
374
|
+
const desc = cleanDesc(info.desc);
|
|
375
|
+
if (desc) {
|
|
376
|
+
bodyLines.push("");
|
|
377
|
+
bodyLines.push("**简介**");
|
|
378
|
+
bodyLines.push("");
|
|
379
|
+
bodyLines.push(desc);
|
|
380
|
+
}
|
|
348
381
|
bodyLines.push("");
|
|
349
|
-
bodyLines.push(desc);
|
|
350
382
|
}
|
|
351
|
-
|
|
383
|
+
|
|
384
|
+
bodyLines.push("---", "");
|
|
385
|
+
} catch (catErr) {
|
|
386
|
+
console.error(
|
|
387
|
+
` [fanqie] 品类 ${cat.name} 处理出错,跳过: ${catErr && catErr.message ? catErr.message : catErr}`
|
|
388
|
+
);
|
|
389
|
+
bodyLines.push(`## ${cat.name} — 采集失败`, "", "---", "");
|
|
352
390
|
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const ratio = totalBooks ? resolvedTitles / totalBooks : 0;
|
|
394
|
+
const quality =
|
|
395
|
+
totalBooks === 0 ? "[无数据]" : ratio < 0.5 ? "[标题解析异常]" : "[OK]";
|
|
396
|
+
lines.splice(
|
|
397
|
+
5,
|
|
398
|
+
0,
|
|
399
|
+
`- 标题解析:成功 ${resolvedTitles} / 共 ${totalBooks}`,
|
|
400
|
+
`- 数据质量:${quality}`
|
|
401
|
+
);
|
|
353
402
|
|
|
354
|
-
|
|
355
|
-
|
|
403
|
+
if (totalBooks > 0 && resolvedTitles === 0) {
|
|
404
|
+
console.error(
|
|
405
|
+
` ✗ ${chLabel}${tyLabel}:${totalBooks} 本全部标题解析失败。请在浏览器内手动打开任一 https://fanqienovel.com/page/{bookId} 确认页面正常。`
|
|
406
|
+
);
|
|
407
|
+
} else if (ratio < 0.5) {
|
|
356
408
|
console.error(
|
|
357
|
-
`
|
|
409
|
+
` ⚠ ${chLabel}${tyLabel}:标题解析率偏低(${resolvedTitles}/${totalBooks}),结果质量已标注。`
|
|
358
410
|
);
|
|
359
|
-
bodyLines.push(`## ${cat.name} — 采集失败`, "", "---", "");
|
|
360
411
|
}
|
|
361
|
-
}
|
|
362
412
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
? "[无数据]"
|
|
367
|
-
: ratio < 0.5
|
|
368
|
-
? "[标题解析异常]"
|
|
369
|
-
: "[OK]";
|
|
370
|
-
lines.splice(5, 0,
|
|
371
|
-
`- 标题解析:成功 ${resolvedTitles} / 共 ${totalBooks}`,
|
|
372
|
-
`- 数据质量:${quality}`
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
if (totalBooks > 0 && resolvedTitles === 0) {
|
|
376
|
-
console.error(
|
|
377
|
-
` ✗ ${chLabel}${tyLabel}:${totalBooks} 本全部标题解析失败。多为详情页结构变动或登录/验证拦截,` +
|
|
378
|
-
`请在 Chrome 内手动打开任一 https://fanqienovel.com/page/{bookId} 确认页面正常。`
|
|
379
|
-
);
|
|
380
|
-
} else if (ratio < 0.5) {
|
|
381
|
-
console.error(
|
|
382
|
-
` ⚠ ${chLabel}${tyLabel}:标题解析率偏低(${resolvedTitles}/${totalBooks}),结果质量已标注。`
|
|
383
|
-
);
|
|
413
|
+
scraped = lines.concat(bodyLines).join("\n");
|
|
414
|
+
} finally {
|
|
415
|
+
await browser.close();
|
|
384
416
|
}
|
|
385
417
|
|
|
386
|
-
return
|
|
418
|
+
return scraped;
|
|
387
419
|
}
|
|
388
420
|
|
|
389
|
-
function main() {
|
|
421
|
+
async function main() {
|
|
390
422
|
if (!["0", "1", "all"].includes(CHANNEL)) {
|
|
391
423
|
throw new Error(`未知 --channel: ${CHANNEL}`);
|
|
392
424
|
}
|
|
@@ -400,7 +432,7 @@ function main() {
|
|
|
400
432
|
for (const ch of channels) {
|
|
401
433
|
for (const ty of types) {
|
|
402
434
|
try {
|
|
403
|
-
const content = scrapeChannel(ch, ty);
|
|
435
|
+
const content = await scrapeChannel(ch, ty);
|
|
404
436
|
if (!content) continue;
|
|
405
437
|
|
|
406
438
|
const date = localDateStamp();
|
|
@@ -424,11 +456,8 @@ if (require.main === module) {
|
|
|
424
456
|
runCli(main, "番茄采集");
|
|
425
457
|
}
|
|
426
458
|
|
|
427
|
-
// 导出纯函数/JS
|
|
459
|
+
// 导出纯函数/JS 构建器
|
|
428
460
|
module.exports = {
|
|
429
|
-
buildCategoriesJS,
|
|
430
|
-
buildBookListJS,
|
|
431
|
-
buildDetailJS,
|
|
432
461
|
fmtReads,
|
|
433
462
|
fmtWords,
|
|
434
463
|
fmtStatus,
|