@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,14 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 晋江文学城排行榜采集脚本
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 使用 playwright-core 自己管理浏览器。
|
|
6
6
|
* 采集策略:
|
|
7
7
|
* 1) topten.php 列表页(纯文本,频道名直接出现,书名/作者交替行)解出频道分组。
|
|
8
8
|
* 2) 从书名 anchor 取 novelid,逐本进 onebook.php 详情页补采核心指标
|
|
9
9
|
* (收藏数/营养液/积分/字数/状态),满足规范对晋江的硬性要求。
|
|
10
|
-
* 晋江页面为 gb18030 编码:详情页用 fetch+arrayBuffer+TextDecoder('gb18030')
|
|
11
|
-
* (同步 XHR 的 responseText 会按 UTF-8 解码导致中文乱码)。
|
|
12
|
-
* 详情采集默认开启但有上限(每频道前 N + 总量上限),用 --list-only 可只采列表。
|
|
10
|
+
* 晋江页面为 gb18030 编码:详情页用 fetch+arrayBuffer+TextDecoder('gb18030') 解码。
|
|
13
11
|
*
|
|
14
12
|
* 用法:
|
|
15
13
|
* node jjwxc-rank-scraper.js --type 12 # 收入金榜(默认含详情)
|
|
@@ -17,219 +15,196 @@
|
|
|
17
15
|
* node jjwxc-rank-scraper.js --type 12 --detail-limit 60 # 详情总量上限 60
|
|
18
16
|
* node jjwxc-rank-scraper.js --type 12 --list-only # 只采列表(快,无核心指标)
|
|
19
17
|
* node jjwxc-rank-scraper.js --type all # 全部榜单
|
|
20
|
-
*
|
|
21
|
-
* 前置:
|
|
22
|
-
* node {SKILL_DIR}/browser-cdp/scripts/setup-cdp-chrome.js 9222
|
|
18
|
+
* node jjwxc-rank-scraper.js --login-wait 60 # 等待手动登录
|
|
23
19
|
*/
|
|
24
20
|
|
|
25
21
|
const fs = require("fs");
|
|
26
22
|
const path = require("path");
|
|
27
|
-
const {
|
|
23
|
+
const { chromium } = require("playwright-core");
|
|
24
|
+
const { getArg, localDateStamp, runCli } = require("./cdp-utils.cjs");
|
|
28
25
|
|
|
29
26
|
const BASE_URL = "https://www.jjwxc.net/topten.php";
|
|
27
|
+
const DETAIL_BASE = "https://www.jjwxc.net/onebook.php";
|
|
30
28
|
|
|
31
29
|
const RANK_TYPES = [
|
|
32
30
|
{ id: "12", label: "收入金榜" },
|
|
33
|
-
{ id: "7",
|
|
34
|
-
{ id: "8",
|
|
31
|
+
{ id: "7", label: "月榜" },
|
|
32
|
+
{ id: "8", label: "季度榜" },
|
|
35
33
|
{ id: "14", label: "完结金榜" },
|
|
36
34
|
{ id: "15", label: "新手金榜" },
|
|
37
35
|
{ id: "17", label: "千字金榜" },
|
|
38
36
|
];
|
|
39
37
|
|
|
40
|
-
// 详情请求批大小(async fetch 并发,整批控制在 ab() 20s 超时内)
|
|
41
38
|
const DETAIL_CHUNK = 6;
|
|
42
39
|
|
|
43
|
-
/** 连通性 + 页面就绪自检 */
|
|
44
|
-
function probePage(port) {
|
|
45
|
-
return evalJSONBase64(
|
|
46
|
-
port,
|
|
47
|
-
"JSON.stringify({host:location.host,len:(document.body&&document.body.innerText||'').length})"
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
40
|
// ---------------------------------------------------------------------------
|
|
52
|
-
//
|
|
41
|
+
// 工具函数
|
|
53
42
|
// ---------------------------------------------------------------------------
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"var idMap={};" +
|
|
66
|
-
"Array.from(document.querySelectorAll('a')).forEach(function(a){" +
|
|
67
|
-
" var hm=(a.getAttribute('href')||'').match(/novelid=([0-9]+)/);if(!hm)return;" +
|
|
68
|
-
" var t=(a.innerText||a.textContent||'').trim();" +
|
|
69
|
-
" if(!t||t.indexOf('向《')>-1||t.indexOf('投')>-1||t.length>30)return;" +
|
|
70
|
-
" if(!idMap[t])idMap[t]=hm[1];" +
|
|
71
|
-
"});" +
|
|
72
|
-
"var channels=['古代言情','现代言情','古代穿越','现代都市纯爱','现代幻想纯爱','古代纯爱','衍生纯爱','幻想现言','奇幻言情','未来游戏悬疑','百合','无CP','二次元言情','衍生言情','衍生无cp','未来幻想纯爱','原创轻小说','多元'];" +
|
|
73
|
-
"var channelSet={};channels.forEach(function(c){channelSet[c]=true});" +
|
|
74
|
-
"var curChannel='';" +
|
|
75
|
-
"var channelBooks={};" +
|
|
76
|
-
"var expectTitle=true;" +
|
|
77
|
-
"var pendingTitle='';" +
|
|
78
|
-
"for(var i=0;i<lines.length;i++){" +
|
|
79
|
-
" var line=lines[i];" +
|
|
80
|
-
" if(/上榜天数记录|榜单说明/.test(line)){break}" +
|
|
81
|
-
" if(/^(免费强推|vip强推|新晋作者|月榜|季榜|半年榜|长生殿|总分榜|字数榜|收入金榜|霸王票|霸王总榜|勤奋指数|完结金榜|新手金榜|栽培月榜|驻站|完结高分|千字金榜|完结全订榜)$/.test(line)){continue}" +
|
|
82
|
-
" if(line.length>30&&line.indexOf('·')>0)continue;" +
|
|
83
|
-
" if(channelSet[line]){" +
|
|
84
|
-
" if(curChannel&&channelBooks[curChannel])channelBooks[curChannel]._finished=true;" +
|
|
85
|
-
" curChannel=line;" +
|
|
86
|
-
" if(!channelBooks[curChannel])channelBooks[curChannel]={books:[]};" +
|
|
87
|
-
" expectTitle=true;pendingTitle='';continue" +
|
|
88
|
-
" }" +
|
|
89
|
-
" if(!curChannel)continue;" +
|
|
90
|
-
" if(expectTitle){" +
|
|
91
|
-
" pendingTitle=line;expectTitle=false" +
|
|
92
|
-
" }else{" +
|
|
93
|
-
" if(pendingTitle){" +
|
|
94
|
-
" channelBooks[curChannel].books.push({title:pendingTitle,author:line,novelid:idMap[pendingTitle]||''})" +
|
|
95
|
-
" }" +
|
|
96
|
-
" expectTitle=true;pendingTitle=''" +
|
|
97
|
-
" }" +
|
|
98
|
-
"}" +
|
|
99
|
-
"for(var name in channelBooks){" +
|
|
100
|
-
" if(channelBooks[name].books.length>0){" +
|
|
101
|
-
" result.channels.push({name:name,books:channelBooks[name].books})" +
|
|
102
|
-
" }" +
|
|
103
|
-
"}" +
|
|
104
|
-
"return result" +
|
|
105
|
-
"})())";
|
|
106
|
-
return evalJSONBase64(port, js);
|
|
44
|
+
function sleep(ms) {
|
|
45
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fmtWan(s, unit) {
|
|
49
|
+
if (s == null || s === "") return "";
|
|
50
|
+
const n = parseInt(String(s).replace(/[^0-9]/g, ""), 10);
|
|
51
|
+
if (isNaN(n)) return "";
|
|
52
|
+
if (n >= 10000) return (n / 10000).toFixed(1) + "万" + (unit || "");
|
|
53
|
+
return n + (unit || "");
|
|
107
54
|
}
|
|
108
55
|
|
|
109
56
|
// ---------------------------------------------------------------------------
|
|
110
|
-
//
|
|
57
|
+
// 详情页(Node.js fetch + gb18030 解码)
|
|
111
58
|
// ---------------------------------------------------------------------------
|
|
112
59
|
|
|
113
|
-
/**
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
})
|
|
126
|
-
.
|
|
127
|
-
|
|
60
|
+
/** 单次详情请求(Node.js fetch,gb18030 解码) */
|
|
61
|
+
async function fetchOneDetail(novelid) {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${DETAIL_BASE}?novelid=${novelid}`);
|
|
64
|
+
if (!res.ok) return { id: novelid, err: `HTTP ${res.status}` };
|
|
65
|
+
const buf = await res.arrayBuffer();
|
|
66
|
+
const h = new TextDecoder("gb18030").decode(new Uint8Array(buf));
|
|
67
|
+
function prop(n) {
|
|
68
|
+
const m = h.match(new RegExp(`itemprop="${n}"[^>]*>([^<]*)<`));
|
|
69
|
+
return m ? m[1].trim() : "";
|
|
70
|
+
}
|
|
71
|
+
const status =
|
|
72
|
+
(h.match(/itemprop="updataStatus"[^>]*>\s*([^\s<]{1,6})/) || [, ""])[1] ||
|
|
73
|
+
(h.match(/(连载中|已完结|完结)/) || [, ""])[1] ||
|
|
74
|
+
"";
|
|
75
|
+
return {
|
|
76
|
+
id: novelid,
|
|
77
|
+
collect: prop("collectedCount"),
|
|
78
|
+
nutrition: prop("nutritionCount"),
|
|
79
|
+
score: prop("scoreCount"),
|
|
80
|
+
review: prop("reviewCount"),
|
|
81
|
+
words: prop("wordCount"),
|
|
82
|
+
status,
|
|
83
|
+
};
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return { id: novelid, err: String(e && e.message || e) };
|
|
86
|
+
}
|
|
128
87
|
}
|
|
129
88
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
* 每批单独 try/catch:整批的并发 fetch 贴着 ab() 的 20s 超时线,一次瞬时超时(或
|
|
133
|
-
* 返回非 JSON)只该丢这 6 本,不能连坐后面几十本,更不能把已解析好的列表带走。
|
|
134
|
-
*/
|
|
135
|
-
function fetchDetails(port, ids) {
|
|
89
|
+
/** 分批补采详情 */
|
|
90
|
+
async function fetchDetails(ids) {
|
|
136
91
|
const map = {};
|
|
137
92
|
let failedChunks = 0;
|
|
138
93
|
for (let i = 0; i < ids.length; i += DETAIL_CHUNK) {
|
|
139
94
|
const chunk = ids.slice(i, i + DETAIL_CHUNK);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Object.assign(map, part);
|
|
143
|
-
} catch (chunkErr) {
|
|
144
|
-
failedChunks++;
|
|
145
|
-
console.error(
|
|
146
|
-
` ⚠ 详情批次 ${Math.floor(i / DETAIL_CHUNK) + 1}(${chunk.length} 本)获取失败,跳过: ${chunkErr.message}`
|
|
147
|
-
);
|
|
148
|
-
}
|
|
95
|
+
const results = await Promise.all(chunk.map(id => fetchOneDetail(id)));
|
|
96
|
+
for (const r of results) map[r.id] = r;
|
|
149
97
|
sleep(400);
|
|
150
98
|
}
|
|
151
|
-
|
|
152
|
-
console.error(` ⚠ 共 ${failedChunks} 个详情批次失败,这部分书只有列表数据。`);
|
|
153
|
-
}
|
|
154
|
-
return { map, failedChunks };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// 格式化
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
function fmtWan(s, unit) {
|
|
162
|
-
if (s == null || s === "") return "";
|
|
163
|
-
const n = parseInt(String(s).replace(/[^0-9]/g, ""), 10);
|
|
164
|
-
if (isNaN(n)) return "";
|
|
165
|
-
if (n >= 10000) return (n / 10000).toFixed(1) + "万" + (unit || "");
|
|
166
|
-
return n + (unit || "");
|
|
99
|
+
return map;
|
|
167
100
|
}
|
|
168
101
|
|
|
169
102
|
// ---------------------------------------------------------------------------
|
|
170
103
|
// 主流程
|
|
171
104
|
// ---------------------------------------------------------------------------
|
|
172
105
|
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const RANKTYPE = getArg(args, "--type") || "12";
|
|
177
|
-
const CHANNEL = getArg(args, "--channel") || "0";
|
|
178
|
-
const TOP = parseInt(getArg(args, "--top") || "10", 10);
|
|
179
|
-
const DETAIL_LIMIT = parseInt(getArg(args, "--detail-limit") || "100", 10);
|
|
180
|
-
const LIST_ONLY = args.includes("--list-only");
|
|
181
|
-
|
|
182
|
-
function scrapeRank(port, rankTypeId, channelId) {
|
|
183
|
-
const rt = RANK_TYPES.find((r) => r.id === rankTypeId);
|
|
184
|
-
if (!rt) {
|
|
185
|
-
console.log(` ⚠ 未知榜单类型: ${rankTypeId}`);
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
106
|
+
async function scrapeRank(rankTypeId, channelId) {
|
|
107
|
+
const rt = RANK_TYPES.find(r => r.id === rankTypeId);
|
|
108
|
+
if (!rt) { console.log(` ⚠ 未知榜单类型: ${rankTypeId}`); return null; }
|
|
188
109
|
|
|
189
110
|
const url = `${BASE_URL}?orderstr=${rankTypeId}&t=${channelId}`;
|
|
190
111
|
const chLabel = channelId === "0" ? "全站" : `频道${channelId}`;
|
|
191
112
|
console.log(`\n→ 采集 晋江${rt.label}(${chLabel})...`);
|
|
192
113
|
console.log(` URL: ${url}`);
|
|
193
114
|
|
|
194
|
-
|
|
115
|
+
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"] });
|
|
116
|
+
const context = await browser.newContext();
|
|
117
|
+
const page = await context.newPage();
|
|
118
|
+
|
|
119
|
+
let data = null;
|
|
195
120
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
console.error(
|
|
203
|
-
` ✗ CDP 无响应。请确认已用 browser-cdp 启动 Chrome(端口 ${port}),且 agent-browser 可用。`
|
|
204
|
-
);
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
if (probe.host && probe.host.indexOf("jjwxc") === -1) {
|
|
208
|
-
console.error(` ✗ 当前页面非晋江(host=${probe.host}),可能被重定向,已跳过。`);
|
|
121
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
122
|
+
await page.waitForTimeout(3000);
|
|
123
|
+
|
|
124
|
+
// 连通性自检
|
|
125
|
+
const host = page.url();
|
|
126
|
+
if (host.indexOf("jjwxc") === -1) {
|
|
127
|
+
console.error(` ✗ 当前页面非晋江(url=${host}),可能被重定向,已跳过。`);
|
|
209
128
|
return null;
|
|
210
129
|
}
|
|
211
130
|
|
|
212
|
-
|
|
131
|
+
// 提取列表数据
|
|
132
|
+
data = await page.evaluate(() => {
|
|
133
|
+
var result = { channels: [] };
|
|
134
|
+
var text = document.body.innerText || "";
|
|
135
|
+
var lines = text.split(/\n/).map(function(l) { return l.trim(); }).filter(Boolean);
|
|
136
|
+
|
|
137
|
+
var idMap = {};
|
|
138
|
+
Array.from(document.querySelectorAll("a")).forEach(function(a) {
|
|
139
|
+
var hm = (a.getAttribute("href") || "").match(/novelid=([0-9]+)/);
|
|
140
|
+
if (!hm) return;
|
|
141
|
+
var t = (a.innerText || a.textContent || "").trim();
|
|
142
|
+
if (!t || t.indexOf("向《") > -1 || t.indexOf("投") > -1 || t.length > 30) return;
|
|
143
|
+
if (!idMap[t]) idMap[t] = hm[1];
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
var channels = ["古代言情","现代言情","古代穿越","现代都市纯爱","现代幻想纯爱","古代纯爱","衍生纯爱","幻想现言","奇幻言情","未来游戏悬疑","百合","无CP","二次元言情","衍生言情","衍生无cp","未来幻想纯爱","原创轻小说","多元"];
|
|
147
|
+
var channelSet = {};
|
|
148
|
+
channels.forEach(function(c) { channelSet[c] = true; });
|
|
149
|
+
|
|
150
|
+
var curChannel = "";
|
|
151
|
+
var channelBooks = {};
|
|
152
|
+
var expectTitle = true;
|
|
153
|
+
var pendingTitle = "";
|
|
154
|
+
|
|
155
|
+
for (var i = 0; i < lines.length; i++) {
|
|
156
|
+
var line = lines[i];
|
|
157
|
+
if (/上榜天数记录|榜单说明/.test(line)) { break; }
|
|
158
|
+
if (/^(免费强推|vip强推|新晋作者|月榜|季榜|半年榜|长生殿|总分榜|字数榜|收入金榜|霸王票|霸王总榜|勤奋指数|完结金榜|新手金榜|栽培月榜|驻站|完结高分|千字金榜|完结全订榜)$/.test(line)) { continue; }
|
|
159
|
+
if (line.length > 30 && line.indexOf("·") > 0) continue;
|
|
160
|
+
if (channelSet[line]) {
|
|
161
|
+
if (curChannel && channelBooks[curChannel]) channelBooks[curChannel]._finished = true;
|
|
162
|
+
curChannel = line;
|
|
163
|
+
if (!channelBooks[curChannel]) channelBooks[curChannel] = { books: [] };
|
|
164
|
+
expectTitle = true;
|
|
165
|
+
pendingTitle = "";
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!curChannel) continue;
|
|
169
|
+
if (expectTitle) {
|
|
170
|
+
pendingTitle = line;
|
|
171
|
+
expectTitle = false;
|
|
172
|
+
} else {
|
|
173
|
+
if (pendingTitle) {
|
|
174
|
+
channelBooks[curChannel].books.push({ title: pendingTitle, author: line, novelid: idMap[pendingTitle] || "" });
|
|
175
|
+
}
|
|
176
|
+
expectTitle = true;
|
|
177
|
+
pendingTitle = "";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (var name in channelBooks) {
|
|
182
|
+
if (channelBooks[name].books.length > 0) {
|
|
183
|
+
result.channels.push({ name: name, books: channelBooks[name].books });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
|
|
213
189
|
if (!data?.channels?.length) {
|
|
214
190
|
console.error(`[jjwxc] 采集失败:未解析到榜单(页面结构可能变动或未加载)。请人工打开 ${url} 确认。`);
|
|
215
191
|
return null;
|
|
216
192
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return null;
|
|
193
|
+
} finally {
|
|
194
|
+
await browser.close();
|
|
220
195
|
}
|
|
221
196
|
|
|
222
197
|
let totalBooks = 0;
|
|
223
|
-
data.channels.forEach(
|
|
198
|
+
data.channels.forEach(ch => {
|
|
224
199
|
totalBooks += ch.books.length;
|
|
225
|
-
const authors = new Set(ch.books.map(
|
|
200
|
+
const authors = new Set(ch.books.map(b => b.author));
|
|
226
201
|
if (ch.books.length >= 5 && authors.size / ch.books.length < 0.2) {
|
|
227
202
|
console.log(` ⚠ ${ch.name}:${ch.books.length} 本只有 ${authors.size} 个唯一作者,可能提取有误`);
|
|
228
203
|
}
|
|
229
204
|
});
|
|
230
205
|
console.log(` ✓ 列表:${data.channels.length} 个频道,共 ${totalBooks} 本`);
|
|
231
206
|
|
|
232
|
-
//
|
|
207
|
+
// 补采详情
|
|
233
208
|
let detailMap = {};
|
|
234
209
|
let detailPlanned = 0;
|
|
235
210
|
let detailOk = 0;
|
|
@@ -247,32 +222,19 @@ function scrapeRank(port, rankTypeId, channelId) {
|
|
|
247
222
|
}
|
|
248
223
|
detailPlanned = picked.length;
|
|
249
224
|
if (picked.length) {
|
|
250
|
-
console.log(` → 补采详情 ${picked.length}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const detailResult = fetchDetails(port, picked);
|
|
255
|
-
detailMap = detailResult.map;
|
|
256
|
-
detailFailedChunks = detailResult.failedChunks;
|
|
257
|
-
} catch (detailErr) {
|
|
258
|
-
detailMap = {};
|
|
259
|
-
detailFailedChunks = Math.max(1, Math.ceil(picked.length / DETAIL_CHUNK));
|
|
260
|
-
console.error(` ⚠ 详情补采整体失败,仅保留列表数据: ${detailErr.message}`);
|
|
261
|
-
}
|
|
262
|
-
detailOk = Object.values(detailMap).filter((d) => d && d.collect).length;
|
|
225
|
+
console.log(` → 补采详情 ${picked.length} 本...`);
|
|
226
|
+
detailMap = await fetchDetails(picked);
|
|
227
|
+
detailFailedChunks = Object.values(detailMap).filter(d => d.err).length;
|
|
228
|
+
detailOk = Object.values(detailMap).filter(d => d && d.collect).length;
|
|
263
229
|
console.log(` ✓ 详情命中收藏数 ${detailOk}/${picked.length}`);
|
|
264
230
|
}
|
|
265
231
|
}
|
|
266
232
|
|
|
267
|
-
// 质量状态:详情开启时,收藏数命中率是核心信号
|
|
268
233
|
let quality = "[OK]";
|
|
269
|
-
const detailPartial =
|
|
270
|
-
!LIST_ONLY &&
|
|
271
|
-
detailPlanned > 0 &&
|
|
272
|
-
(detailFailedChunks > 0 || detailOk < detailPlanned);
|
|
234
|
+
const detailPartial = !LIST_ONLY && detailPlanned > 0 && (detailFailedChunks > 0 || detailOk < detailPlanned);
|
|
273
235
|
if (!LIST_ONLY && detailPlanned > 0 && detailOk === 0) {
|
|
274
236
|
quality = "[详情解析异常/登录态缺失]";
|
|
275
|
-
console.error(` ⚠
|
|
237
|
+
console.error(` ⚠ 详情全部无收藏数:可能页面结构变动或需登录。`);
|
|
276
238
|
} else if (detailPartial) {
|
|
277
239
|
quality = "[部分详情缺失]";
|
|
278
240
|
console.error(` ⚠ 详情仅命中 ${detailOk}/${detailPlanned},已按部分结果标注。`);
|
|
@@ -288,7 +250,7 @@ function scrapeRank(port, rankTypeId, channelId) {
|
|
|
288
250
|
`- 抓取时间:${now}`,
|
|
289
251
|
`- 频道数:${data.channels.length}`,
|
|
290
252
|
`- 总条目数:${totalBooks}`,
|
|
291
|
-
`- 详情采集:${detailOk} / ${detailPlanned}
|
|
253
|
+
`- 详情采集:${detailOk} / ${detailPlanned}`,
|
|
292
254
|
`- 数据质量:${quality}`,
|
|
293
255
|
"",
|
|
294
256
|
"---",
|
|
@@ -296,108 +258,86 @@ function scrapeRank(port, rankTypeId, channelId) {
|
|
|
296
258
|
];
|
|
297
259
|
|
|
298
260
|
for (const ch of data.channels) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (d.score) seg.push("积分 " + d.score);
|
|
311
|
-
if (d.words) seg.push("字数 " + fmtWan(d.words, "字"));
|
|
312
|
-
if (d.status) seg.push(d.status);
|
|
313
|
-
}
|
|
314
|
-
const meta = seg.filter(Boolean).join(" · ");
|
|
315
|
-
if (meta) lines.push(`*${meta}*`);
|
|
316
|
-
if (b.novelid) lines.push(`[作品页](https://www.jjwxc.net/onebook.php?novelid=${b.novelid})`);
|
|
317
|
-
lines.push("");
|
|
318
|
-
} catch (bookErr) {
|
|
319
|
-
console.error(`[jjwxc] ${rt.label} ${ch.name} 第${i + 1}条处理出错: ${bookErr.message}`);
|
|
320
|
-
lines.push("");
|
|
321
|
-
}
|
|
261
|
+
lines.push(`## ${ch.name} — ${ch.books.length} 本`, "");
|
|
262
|
+
for (let i = 0; i < ch.books.length; i++) {
|
|
263
|
+
const b = ch.books[i];
|
|
264
|
+
const d = b.novelid ? detailMap[b.novelid] : null;
|
|
265
|
+
const seg = [b.author || ""];
|
|
266
|
+
if (d) {
|
|
267
|
+
if (d.collect) seg.push("收藏 " + fmtWan(d.collect));
|
|
268
|
+
if (d.nutrition) seg.push("营养液 " + fmtWan(d.nutrition));
|
|
269
|
+
if (d.score) seg.push("积分 " + d.score);
|
|
270
|
+
if (d.words) seg.push("字数 " + fmtWan(d.words, "字"));
|
|
271
|
+
if (d.status) seg.push(d.status);
|
|
322
272
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
273
|
+
const meta = seg.filter(Boolean).join(" · ");
|
|
274
|
+
if (meta) lines.push(`*${meta}*`);
|
|
275
|
+
if (b.novelid) lines.push(`[作品页](https://www.jjwxc.net/onebook.php?novelid=${b.novelid})`);
|
|
276
|
+
lines.push("");
|
|
326
277
|
}
|
|
278
|
+
lines.push("---", "");
|
|
327
279
|
}
|
|
328
280
|
|
|
329
281
|
return {
|
|
330
282
|
content: lines.join("\n"),
|
|
331
283
|
partial: detailPartial,
|
|
332
|
-
partialReason: detailPartial
|
|
333
|
-
? `${rt.label}: detail ${detailOk}/${detailPlanned}, failed chunks ${detailFailedChunks}`
|
|
334
|
-
: "",
|
|
284
|
+
partialReason: detailPartial ? `${rt.label}: detail ${detailOk}/${detailPlanned}` : "",
|
|
335
285
|
};
|
|
336
286
|
}
|
|
337
287
|
|
|
338
|
-
|
|
339
|
-
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// 入口
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
const args = process.argv.slice(2);
|
|
293
|
+
const OUTDIR = getArg(args, "--outdir") || ".";
|
|
294
|
+
const RANKTYPE = getArg(args, "--type") || "12";
|
|
295
|
+
const CHANNEL = getArg(args, "--channel") || "0";
|
|
296
|
+
const TOP = parseInt(getArg(args, "--top") || "10", 10);
|
|
297
|
+
const DETAIL_LIMIT = parseInt(getArg(args, "--detail-limit") || "100", 10);
|
|
298
|
+
const LIST_ONLY = args.includes("--list-only");
|
|
299
|
+
const LOGIN_WAIT = parseInt(getArg(args, "--login-wait") || "0", 10);
|
|
300
|
+
|
|
301
|
+
async function main() {
|
|
302
|
+
if (RANKTYPE !== "all" && !RANK_TYPES.some(r => r.id === RANKTYPE)) {
|
|
340
303
|
throw new Error(`未知 --type: ${RANKTYPE}`);
|
|
341
304
|
}
|
|
342
|
-
// 当前脚本只实现全站榜(t=0);不能把任意数字静默标成“频道 N”。
|
|
343
|
-
// 若后续支持分频道,先从页面提取并维护明确 ID 白名单再开放。
|
|
344
305
|
if (CHANNEL !== "0") {
|
|
345
306
|
throw new Error(`未知 --channel: ${CHANNEL}(当前仅支持 0=全站)`);
|
|
346
307
|
}
|
|
347
|
-
const rankTypes = RANKTYPE === "all" ? RANK_TYPES.map(
|
|
348
|
-
|
|
349
|
-
let written = 0;
|
|
350
|
-
let failed = 0;
|
|
351
|
-
let partial = false;
|
|
308
|
+
const rankTypes = RANKTYPE === "all" ? RANK_TYPES.map(r => r.id) : [RANKTYPE];
|
|
309
|
+
let written = 0, failed = 0, partial = false;
|
|
352
310
|
const partialReasons = [];
|
|
353
311
|
|
|
354
312
|
for (const rt of rankTypes) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const rtInfo = RANK_TYPES.find((r) => r.id === rt);
|
|
362
|
-
partialReasons.push(`${rtInfo ? rtInfo.label : rt}: no usable data`);
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
if (result.partial) {
|
|
366
|
-
partial = true;
|
|
367
|
-
if (result.partialReason) partialReasons.push(result.partialReason);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const rtInfo = RANK_TYPES.find((r) => r.id === rt);
|
|
371
|
-
const date = localDateStamp();
|
|
372
|
-
const chLabel = ch === "0" ? "全站" : `频道${ch}`;
|
|
373
|
-
const filename = `晋江${rtInfo.label}_${chLabel}_${date}.md`;
|
|
374
|
-
fs.mkdirSync(OUTDIR, { recursive: true });
|
|
375
|
-
const filepath = path.join(OUTDIR, filename);
|
|
376
|
-
fs.writeFileSync(filepath, result.content, "utf-8");
|
|
377
|
-
written++;
|
|
378
|
-
console.log(` ✓ 已保存: ${filepath}`);
|
|
379
|
-
} catch (rankErr) {
|
|
380
|
-
failed++;
|
|
381
|
-
const rtInfo = RANK_TYPES.find((r) => r.id === rt);
|
|
382
|
-
const message = rankErr && rankErr.message ? rankErr.message : String(rankErr);
|
|
383
|
-
partialReasons.push(`${rtInfo ? rtInfo.label : rt}: ${message}`);
|
|
384
|
-
console.error(
|
|
385
|
-
`[jjwxc] ${rtInfo ? rtInfo.label : rt} 采集失败,跳过: ${message}`
|
|
386
|
-
);
|
|
313
|
+
try {
|
|
314
|
+
const result = await scrapeRank(rt, CHANNEL);
|
|
315
|
+
if (!result) { failed++; continue; }
|
|
316
|
+
if (result.partial) {
|
|
317
|
+
partial = true;
|
|
318
|
+
if (result.partialReason) partialReasons.push(result.partialReason);
|
|
387
319
|
}
|
|
320
|
+
const rtInfo = RANK_TYPES.find(r => r.id === rt);
|
|
321
|
+
const date = localDateStamp();
|
|
322
|
+
const filename = `晋江${rtInfo.label}_全站_${date}.md`;
|
|
323
|
+
fs.mkdirSync(OUTDIR, { recursive: true });
|
|
324
|
+
const filepath = path.join(OUTDIR, filename);
|
|
325
|
+
fs.writeFileSync(filepath, result.content, "utf-8");
|
|
326
|
+
written++;
|
|
327
|
+
console.log(` ✓ 已保存: ${filepath}`);
|
|
328
|
+
} catch (rankErr) {
|
|
329
|
+
failed++;
|
|
330
|
+
const message = rankErr && rankErr.message ? rankErr.message : String(rankErr);
|
|
331
|
+
partialReasons.push(`${rt}: ${message}`);
|
|
332
|
+
console.error(`[jjwxc] ${rt} 采集失败,跳过: ${message}`);
|
|
388
333
|
}
|
|
389
334
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
written,
|
|
393
|
-
failed,
|
|
394
|
-
partial: partial || failed > 0,
|
|
395
|
-
partialReasons,
|
|
396
|
-
};
|
|
335
|
+
|
|
336
|
+
return { planned: rankTypes.length, written, failed, partial: partial || failed > 0, partialReasons };
|
|
397
337
|
}
|
|
398
338
|
|
|
399
339
|
if (require.main === module) {
|
|
400
340
|
runCli(main, "晋江采集");
|
|
401
341
|
}
|
|
402
342
|
|
|
403
|
-
module.exports = {
|
|
343
|
+
module.exports = { fmtWan };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fancy-scan-scrapers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"node_modules/playwright-core": {
|
|
8
|
+
"version": "1.62.1",
|
|
9
|
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
|
10
|
+
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"bin": {
|
|
13
|
+
"playwright-core": "cli.js"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|