@cloud411716/fancy-webnovel 0.1.77 → 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/package.json +1 -1
- 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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fancy-scan-scrapers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "fancy-scan-scrapers",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"playwright-core": "^1.40.0"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"node_modules/playwright-core": {
|
|
15
|
+
"version": "1.62.1",
|
|
16
|
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
|
17
|
+
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"bin": {
|
|
20
|
+
"playwright-core": "cli.js"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -2,170 +2,59 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 七猫小说排行榜采集脚本
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 使用 playwright-core 自己管理浏览器。
|
|
6
6
|
* 采集策略:tab 切换男生榜/女生榜和榜单类型,滚动加载后从页面文本解析结构化数据。
|
|
7
|
-
* 输出 Markdown 格式匹配 scan-output-format.md 规范。
|
|
8
7
|
*
|
|
9
8
|
* 用法:
|
|
10
9
|
* node qimao-rank-scraper.js --channel male --type hot --period day # 男生大热榜日榜
|
|
11
10
|
* node qimao-rank-scraper.js --channel male --type hot --period month # 男生大热榜月榜
|
|
12
11
|
* node qimao-rank-scraper.js --channel male --type hot --period all # 日榜+月榜
|
|
13
|
-
* node qimao-rank-scraper.js --channel female --type new
|
|
14
|
-
* node qimao-rank-scraper.js --channel all --type all
|
|
15
|
-
*
|
|
16
|
-
* 前置:
|
|
17
|
-
* node {SKILL_DIR}/browser-cdp/scripts/setup-cdp-chrome.js 9222
|
|
12
|
+
* node qimao-rank-scraper.js --channel female --type new # 女生新书榜
|
|
13
|
+
* node qimao-rank-scraper.js --channel all --type all # 全部采集
|
|
14
|
+
* node qimao-rank-scraper.js --login-wait 60 # 等待手动登录
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
17
|
const fs = require("fs");
|
|
21
18
|
const path = require("path");
|
|
22
|
-
const {
|
|
19
|
+
const { chromium } = require("playwright-core");
|
|
20
|
+
const { getArg, localDateStamp, runCli } = require("./cdp-utils.cjs");
|
|
23
21
|
|
|
24
22
|
const RANK_URL = "https://www.qimao.com/paihang";
|
|
25
23
|
|
|
26
|
-
/** 连通性 + 页面就绪自检 */
|
|
27
|
-
function probePage(port) {
|
|
28
|
-
return evalJSONBase64(
|
|
29
|
-
port,
|
|
30
|
-
"JSON.stringify({host:location.host,path:location.pathname,len:(document.body&&document.body.innerText||'').length})"
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
24
|
const CHANNELS = [
|
|
35
|
-
{ id: "male",
|
|
25
|
+
{ id: "male", label: "男频", tab: "男生榜", path: "boy" },
|
|
36
26
|
{ id: "female", label: "女频", tab: "女生榜", path: "girl" },
|
|
37
27
|
];
|
|
38
28
|
|
|
39
29
|
const RANK_TYPES = [
|
|
40
|
-
{ id: "hot",
|
|
41
|
-
{ id: "new",
|
|
30
|
+
{ id: "hot", label: "大热榜", path: "hot" },
|
|
31
|
+
{ id: "new", label: "新书榜", path: "new" },
|
|
42
32
|
{ id: "finish", label: "完结榜", path: "over" },
|
|
43
|
-
{ id: "collect",
|
|
33
|
+
{ id: "collect",label: "收藏榜", path: "collect" },
|
|
44
34
|
{ id: "update", label: "更新榜", path: "update" },
|
|
45
35
|
];
|
|
46
36
|
|
|
47
37
|
const PERIODS = [
|
|
48
|
-
{ id: "day",
|
|
38
|
+
{ id: "day", label: "日榜", path: "date" },
|
|
49
39
|
{ id: "month", label: "月榜", path: "month" },
|
|
50
40
|
];
|
|
51
41
|
|
|
52
42
|
// ---------------------------------------------------------------------------
|
|
53
|
-
//
|
|
43
|
+
// 工具函数
|
|
54
44
|
// ---------------------------------------------------------------------------
|
|
55
45
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
const rankType = RANK_TYPES.find((item) => item.id === rankTypeId);
|
|
59
|
-
const period = PERIODS.find((item) => item.id === (periodId || "day"));
|
|
60
|
-
if (!channel || !rankType || !period) return "";
|
|
61
|
-
return `${RANK_URL}/${channel.path}/${rankType.path}/${period.path}/`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 读取页面实际 active 状态;输出文件标签必须由该状态校验后才能使用。 */
|
|
65
|
-
function extractObservedSelection(port) {
|
|
66
|
-
const js = `JSON.stringify((function(){
|
|
67
|
-
function text(selector){var e=document.querySelector(selector);return e?(e.textContent||'').replace(/\\s+/g,'').trim():'';}
|
|
68
|
-
return {path:location.pathname,channel:text('.qm-switch-tab .item.active'),rankType:text('.child-tabs-item.menu-tab.active'),period:text('.date-type-tabs .tab.active')};
|
|
69
|
-
})())`;
|
|
70
|
-
return evalJSONBase64(port, js) || {};
|
|
46
|
+
function sleep(ms) {
|
|
47
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
71
48
|
}
|
|
72
49
|
|
|
73
|
-
function
|
|
74
|
-
const channel = CHANNELS.find(
|
|
75
|
-
const
|
|
76
|
-
const period =
|
|
77
|
-
if (!channel || !
|
|
78
|
-
|
|
79
|
-
if (!expectedUrl) return false;
|
|
80
|
-
const expectedPath = new URL(expectedUrl).pathname;
|
|
81
|
-
const actualPath = String(observed && observed.path || "").replace(/\/+$/, "/");
|
|
82
|
-
return !!(
|
|
83
|
-
actualPath === expectedPath &&
|
|
84
|
-
String(observed.channel || "").includes(channel.tab) &&
|
|
85
|
-
observed.rankType === rankType.label &&
|
|
86
|
-
(!period || observed.period === period.label)
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 从 DOM 获取书籍链接。每本书有多个 anchor(排名数字/书名/最近更新),
|
|
92
|
-
* 按 bookId 聚合后取最像书名的文本(非纯数字、非"最近更新"前缀、最长),
|
|
93
|
-
* 否则书名会被排名数字 anchor 覆盖,导致后续按书名回填链接全失败。
|
|
94
|
-
*/
|
|
95
|
-
function extractBookUrls(port) {
|
|
96
|
-
const js = `JSON.stringify((function(){
|
|
97
|
-
var byId={};var order=[];
|
|
98
|
-
Array.from(document.querySelectorAll('a')).forEach(function(a){
|
|
99
|
-
var h=a.getAttribute('href')||a.href||'';
|
|
100
|
-
var m=h.match(/\\/(?:shuku|book)\\/([0-9]+)/);
|
|
101
|
-
if(!m)return; var id=m[1];
|
|
102
|
-
var t=(a.innerText||a.textContent||'').replace(/\\s+/g,' ').trim();
|
|
103
|
-
if(!byId[id]){byId[id]='';order.push(id);}
|
|
104
|
-
if(t&&!/^[0-9]+$/.test(t)&&!/^(最近更新|最新章节|最新)/.test(t)){
|
|
105
|
-
if(t.length>byId[id].length)byId[id]=t;
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
return order.map(function(id){return {bookId:id,title:byId[id],url:'https://www.qimao.com/shuku/'+id+'/'};});
|
|
109
|
-
})())`;
|
|
110
|
-
return evalJSONBase64(port, js) || [];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 从页面 innerText 解析结构化书籍数据。
|
|
115
|
-
* 七猫页面文本结构固定:排名→书名→作者→题材→子分类→状态→字数→简介→更新→热度
|
|
116
|
-
*/
|
|
117
|
-
function extractBooksFromText(port) {
|
|
118
|
-
const js =
|
|
119
|
-
"JSON.stringify((()=>{" +
|
|
120
|
-
"var text=document.body.innerText||'';" +
|
|
121
|
-
// 找到榜单数据起始位置
|
|
122
|
-
"var start=-1;" +
|
|
123
|
-
"['日榜','月榜'].forEach(function(m){if(start<0)start=text.indexOf(m)});" +
|
|
124
|
-
"if(start<0)return[];" +
|
|
125
|
-
"var lines=text.substring(start).split(/\\n/);" +
|
|
126
|
-
"var books=[];var cur=null;var fieldIdx=0;" +
|
|
127
|
-
"for(var i=0;i<lines.length;i++){" +
|
|
128
|
-
" var line=lines[i].trim();" +
|
|
129
|
-
" if(!line)continue;" +
|
|
130
|
-
// 排行数据结束后的分页器/页脚必须立刻截断;否则“5 / 下一页 / 跳转 / 友情链接”
|
|
131
|
-
// 会被串成一条字段齐全的假书目。
|
|
132
|
-
" if(/^(上一页|下一页|跳转|友情链接[::]?)$/.test(line)){if(cur&&cur.title)books.push(cur);cur=null;break}" +
|
|
133
|
-
// 排名标记:独立数字 1-99
|
|
134
|
-
" if(/^\\d{1,2}$/.test(line)&&parseInt(line)<100){" +
|
|
135
|
-
" if(cur&&cur.title)books.push(cur);" +
|
|
136
|
-
" cur={rank:parseInt(line),title:'',author:'',genre:'',subGenre:'',status:'',words:'',heat:'',update:'',desc:''};" +
|
|
137
|
-
" fieldIdx=0;continue" +
|
|
138
|
-
" }" +
|
|
139
|
-
" if(!cur)continue;" +
|
|
140
|
-
// 跳过 UI 文字
|
|
141
|
-
" if(/^(加入书架|立即阅读|蝉联|榜首)/.test(line))continue;" +
|
|
142
|
-
// 热度
|
|
143
|
-
" var hm=line.match(/([\\d.]+)\\s*万\\s*热度/);" +
|
|
144
|
-
" if(hm){cur.heat=hm[1]+'万';continue}" +
|
|
145
|
-
// 最新更新
|
|
146
|
-
" if(line.indexOf('最近更新')===0){cur.update=line.replace(/^最近更新\\s*/,'');continue}" +
|
|
147
|
-
// 状态
|
|
148
|
-
" if(/^(连载中|已完结)$/.test(line)){cur.status=line;continue}" +
|
|
149
|
-
// 字数
|
|
150
|
-
" if(/^[\\d.]+万字$/.test(line)){cur.words=line;continue}" +
|
|
151
|
-
// 按序填充:书名→作者→题材→子分类
|
|
152
|
-
" if(fieldIdx===0){cur.title=line;fieldIdx=1;continue}" +
|
|
153
|
-
" if(fieldIdx===1){cur.author=line;fieldIdx=2;continue}" +
|
|
154
|
-
" if(fieldIdx===2){cur.genre=line;fieldIdx=3;continue}" +
|
|
155
|
-
" if(fieldIdx===3){cur.subGenre=line;fieldIdx=4;continue}" +
|
|
156
|
-
// 其余为简介
|
|
157
|
-
" cur.desc+=(cur.desc?' ':'')+line" +
|
|
158
|
-
"}" +
|
|
159
|
-
"if(cur&&cur.title)books.push(cur);" +
|
|
160
|
-
"return books" +
|
|
161
|
-
"})())";
|
|
162
|
-
return evalJSONBase64(port, js) || [];
|
|
50
|
+
function rankUrl(channelId, rankTypeId, periodId) {
|
|
51
|
+
const channel = CHANNELS.find(c => c.id === channelId);
|
|
52
|
+
const rt = RANK_TYPES.find(r => r.id === rankTypeId);
|
|
53
|
+
const period = PERIODS.find(p => p.id === (periodId || "day"));
|
|
54
|
+
if (!channel || !rt || !period) return "";
|
|
55
|
+
return `${RANK_URL}/${channel.path}/${rt.path}/${period.path}/`;
|
|
163
56
|
}
|
|
164
57
|
|
|
165
|
-
/**
|
|
166
|
-
* 排除分页器等被正文文本解析器误认成的伪书目。
|
|
167
|
-
* 七猫榜单尾部会出现“5 / 下一页”这类纯 UI 文本;有效条目必须同时有正排名、书名和作者。
|
|
168
|
-
*/
|
|
169
58
|
function isUsableBook(book) {
|
|
170
59
|
return !!(
|
|
171
60
|
book &&
|
|
@@ -182,188 +71,211 @@ function cleanDesc(value) {
|
|
|
182
71
|
const text = String(value || "")
|
|
183
72
|
.replace(/\s*(?:飙升|上升|下降)\s*\d+\s*名\s*$/g, "")
|
|
184
73
|
.replace(/\s*(?:上一页|下一页)\s*$/g, "")
|
|
185
|
-
.replace(/\s+/g, " ")
|
|
186
|
-
.trim();
|
|
74
|
+
.replace(/\s+/g, " ").trim();
|
|
187
75
|
if (text.length <= 100) return text;
|
|
188
76
|
const cut = text.slice(0, 100);
|
|
189
|
-
const sentence = cut.match(
|
|
77
|
+
const sentence = cut.match(/[\s\S]*[。!?]/);
|
|
190
78
|
return (sentence ? sentence[0] : cut) + "...";
|
|
191
79
|
}
|
|
192
80
|
|
|
193
81
|
function summarizeQuality(books, rawCount) {
|
|
194
|
-
const linked = books.filter(
|
|
195
|
-
const heated = books.filter(
|
|
82
|
+
const linked = books.filter(b => b.url).length;
|
|
83
|
+
const heated = books.filter(b => b.heat).length;
|
|
196
84
|
const fieldCounts = [
|
|
197
|
-
["题材", "genre"],
|
|
198
|
-
["
|
|
199
|
-
["状态", "status"],
|
|
200
|
-
["字数", "words"],
|
|
201
|
-
["热度", "heat"],
|
|
85
|
+
["题材", "genre"], ["子分类", "subGenre"],
|
|
86
|
+
["状态", "status"], ["字数", "words"], ["热度", "heat"],
|
|
202
87
|
].map(([label, field]) => ({
|
|
203
|
-
label,
|
|
204
|
-
missing: books.filter((book) => !book[field]).length,
|
|
88
|
+
label, missing: books.filter(b => !b[field]).length,
|
|
205
89
|
}));
|
|
206
90
|
const problems = [];
|
|
207
91
|
if (rawCount > books.length) problems.push(`移除无效/UI条目 ${rawCount - books.length} 条`);
|
|
208
92
|
if (linked < books.length) problems.push(`作品页链接缺失 ${books.length - linked} 条`);
|
|
209
|
-
for (const
|
|
210
|
-
if (field.missing) problems.push(`${field.label}缺失 ${field.missing} 条`);
|
|
211
|
-
}
|
|
93
|
+
for (const f of fieldCounts) { if (f.missing) problems.push(`${f.label}缺失 ${f.missing} 条`); }
|
|
212
94
|
if (books.length < 15) problems.push(`[数据稀疏] 实际采集 ${books.length} 条`);
|
|
213
|
-
return {
|
|
214
|
-
linked,
|
|
215
|
-
heated,
|
|
216
|
-
problems,
|
|
217
|
-
quality: problems.length ? "[存在问题]" : "[OK]",
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function renderMarkdown(ch, rt, period, url, books, rawCount, now = new Date().toISOString()) {
|
|
222
|
-
const periodLabel = period ? period.label : "";
|
|
223
|
-
const summary = summarizeQuality(books, rawCount);
|
|
224
|
-
const lines = [
|
|
225
|
-
`# 七猫 · ${ch.label} · ${rt.label}${periodLabel}`,
|
|
226
|
-
"",
|
|
227
|
-
`- 数据质量:${summary.quality}`,
|
|
228
|
-
`- 有效条目:${books.length} / ${rawCount}`,
|
|
229
|
-
`- 问题摘要:${summary.problems.length ? summary.problems.join(";") : "无"}`,
|
|
230
|
-
`- 作品页链接:${summary.linked} / ${books.length}`,
|
|
231
|
-
`- 热度命中:${summary.heated} / ${books.length}`,
|
|
232
|
-
`- 来源:${url}`,
|
|
233
|
-
`- 抓取时间:${now}`,
|
|
234
|
-
`- 条目数:${books.length}`,
|
|
235
|
-
"",
|
|
236
|
-
"---",
|
|
237
|
-
"",
|
|
238
|
-
];
|
|
239
|
-
|
|
240
|
-
for (const b of books) {
|
|
241
|
-
try {
|
|
242
|
-
lines.push(`### #${b.rank} ${b.title}`);
|
|
243
|
-
const meta = [
|
|
244
|
-
b.author || "[待补]",
|
|
245
|
-
b.genre || "[待补]",
|
|
246
|
-
b.subGenre || "[待补]",
|
|
247
|
-
b.status || "[待补]",
|
|
248
|
-
b.words || "[待补]",
|
|
249
|
-
b.heat ? b.heat + "热度" : "[待补]",
|
|
250
|
-
].join(" · ");
|
|
251
|
-
lines.push(`*${meta}*`);
|
|
252
|
-
if (b.update) lines.push(`**最新更新:** ${b.update}`);
|
|
253
|
-
if (b.url) lines.push(`[作品页](${b.url})`);
|
|
254
|
-
const desc = cleanDesc(b.desc);
|
|
255
|
-
if (desc) {
|
|
256
|
-
lines.push("");
|
|
257
|
-
lines.push("**简介**");
|
|
258
|
-
lines.push("");
|
|
259
|
-
lines.push(desc);
|
|
260
|
-
}
|
|
261
|
-
lines.push("", "---", "");
|
|
262
|
-
} catch (bookErr) {
|
|
263
|
-
console.error(`[qimao] ${ch.label}${rt.label} 第${b.rank}条处理出错: ${bookErr.message}`);
|
|
264
|
-
lines.push("", "---", "");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return lines.join("\n");
|
|
95
|
+
return { linked, heated, problems, quality: problems.length ? "[存在问题]" : "[OK]" };
|
|
269
96
|
}
|
|
270
97
|
|
|
271
98
|
// ---------------------------------------------------------------------------
|
|
272
|
-
//
|
|
99
|
+
// 采集
|
|
273
100
|
// ---------------------------------------------------------------------------
|
|
274
101
|
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const PERIOD = getArg(args, "--period") || "day";
|
|
102
|
+
async function scrapeRank(channelId, rankTypeId, periodId) {
|
|
103
|
+
const ch = CHANNELS.find(c => c.id === channelId);
|
|
104
|
+
const rt = RANK_TYPES.find(r => r.id === rankTypeId);
|
|
105
|
+
const period = periodId ? PERIODS.find(p => p.id === periodId) : null;
|
|
106
|
+
if (!ch || !rt) { console.log(" ⚠ 未知频道或榜单类型"); return null; }
|
|
281
107
|
|
|
282
|
-
function scrapeRank(port, channelId, rankTypeId, periodId) {
|
|
283
|
-
const ch = CHANNELS.find((c) => c.id === channelId);
|
|
284
|
-
const rt = RANK_TYPES.find((r) => r.id === rankTypeId);
|
|
285
|
-
const period = periodId ? PERIODS.find((p) => p.id === periodId) : null;
|
|
286
|
-
if (!ch || !rt) {
|
|
287
|
-
console.log(" ⚠ 未知频道或榜单类型");
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const periodLabel = period ? period.label : "";
|
|
292
108
|
const url = rankUrl(channelId, rankTypeId, periodId);
|
|
293
|
-
console.log(`\n→ 采集 七猫${ch.label}${rt.label}${
|
|
109
|
+
console.log(`\n→ 采集 七猫${ch.label}${rt.label}${period ? period.label : ""}...`);
|
|
110
|
+
|
|
111
|
+
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"] });
|
|
112
|
+
const context = await browser.newContext();
|
|
113
|
+
const page = await context.newPage();
|
|
294
114
|
|
|
295
115
|
let books, urls, rawCount;
|
|
296
116
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
117
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
118
|
+
await page.waitForTimeout(3000);
|
|
299
119
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (!probe) {
|
|
303
|
-
console.error(
|
|
304
|
-
` ✗ CDP 无响应。请确认已用 browser-cdp 启动 Chrome(端口 ${port}),且 agent-browser 可用。`
|
|
305
|
-
);
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
if (probe.host && probe.host.indexOf("qimao") === -1) {
|
|
309
|
-
console.error(` ✗ 当前页面非七猫(host=${probe.host}),可能被重定向,已跳过。`);
|
|
120
|
+
if (page.url().indexOf("qimao") === -1) {
|
|
121
|
+
console.error(` ✗ 当前页面非七猫,可能被重定向,已跳过。`);
|
|
310
122
|
return null;
|
|
311
123
|
}
|
|
312
|
-
|
|
313
|
-
|
|
124
|
+
|
|
125
|
+
// 验证页面实际选中状态
|
|
126
|
+
const observed = await page.evaluate(() => {
|
|
127
|
+
function text(sel) {
|
|
128
|
+
const e = document.querySelector(sel);
|
|
129
|
+
return e ? (e.textContent || "").replace(/\s+/g, "").trim() : "";
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
path: location.pathname,
|
|
133
|
+
channel: text(".qm-switch-tab .item.active"),
|
|
134
|
+
rankType: text(".child-tabs-item.menu-tab.active"),
|
|
135
|
+
period: text(".date-type-tabs .tab.active"),
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const expectedUrl = rankUrl(channelId, rankTypeId, periodId);
|
|
140
|
+
const expectedPath = new URL(expectedUrl).pathname;
|
|
141
|
+
const actualPath = String(observed.path || "").replace(/\/+$/, "/");
|
|
142
|
+
const periodLabel = period ? period.label : "";
|
|
143
|
+
if (
|
|
144
|
+
actualPath !== expectedPath ||
|
|
145
|
+
!(observed.channel || "").includes(ch.tab) ||
|
|
146
|
+
observed.rankType !== rt.label ||
|
|
147
|
+
(period && observed.period !== period.label)
|
|
148
|
+
) {
|
|
314
149
|
console.error(
|
|
315
|
-
` ✗ 页面实际榜单与请求不一致(请求 ${ch.tab}/${rt.label}/${periodLabel
|
|
316
|
-
`实际 ${observed.channel || "?"}/${observed.rankType || "?"}/${observed.period || "?"}
|
|
150
|
+
` ✗ 页面实际榜单与请求不一致(请求 ${ch.tab}/${rt.label}/${periodLabel},` +
|
|
151
|
+
`实际 ${observed.channel || "?"}/${observed.rankType || "?"}/${observed.period || "?"}),已跳过。`
|
|
317
152
|
);
|
|
318
153
|
return null;
|
|
319
154
|
}
|
|
320
155
|
console.log(` ✓ 已验证页面实际榜单:${observed.channel}/${observed.rankType}${observed.period ? "/" + observed.period : ""}`);
|
|
321
156
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
157
|
+
// 滚动加载
|
|
158
|
+
for (let i = 0; i < 5; i++) {
|
|
159
|
+
await page.evaluate(() => window.scrollBy(0, window.innerHeight));
|
|
160
|
+
sleep(1000);
|
|
161
|
+
}
|
|
162
|
+
await page.waitForTimeout(500);
|
|
163
|
+
|
|
164
|
+
// 提取书籍数据(文本解析)
|
|
165
|
+
const rawBooks = await page.evaluate(() => {
|
|
166
|
+
var text = document.body.innerText || "";
|
|
167
|
+
var start = -1;
|
|
168
|
+
["日榜", "月榜"].forEach(m => { if (start < 0) start = text.indexOf(m); });
|
|
169
|
+
if (start < 0) return [];
|
|
170
|
+
var lines = text.substring(start).split(/\n/);
|
|
171
|
+
var books = [];
|
|
172
|
+
var cur = null;
|
|
173
|
+
var fieldIdx = 0;
|
|
174
|
+
for (var i = 0; i < lines.length; i++) {
|
|
175
|
+
var line = lines[i].trim();
|
|
176
|
+
if (!line) continue;
|
|
177
|
+
if (/^(上一页|下一页|跳转|友情链接[::]?)$/.test(line)) { if (cur && cur.title) books.push(cur); cur = null; break; }
|
|
178
|
+
if (/^\d{1,2}$/.test(line) && parseInt(line) < 100) {
|
|
179
|
+
if (cur && cur.title) books.push(cur);
|
|
180
|
+
cur = { rank: parseInt(line), title: "", author: "", genre: "", subGenre: "", status: "", words: "", heat: "", update: "", desc: "" };
|
|
181
|
+
fieldIdx = 0; continue;
|
|
182
|
+
}
|
|
183
|
+
if (!cur) continue;
|
|
184
|
+
if (/^(加入书架|立即阅读|蝉联|榜首)/.test(line)) continue;
|
|
185
|
+
var hm = line.match(/([\d.]+)\s*万\s*热度/);
|
|
186
|
+
if (hm) { cur.heat = hm[1] + "万"; continue; }
|
|
187
|
+
if (line.indexOf("最近更新") === 0) { cur.update = line.replace(/^最近更新\s*/, ""); continue; }
|
|
188
|
+
if (/^(连载中|已完结)$/.test(line)) { cur.status = line; continue; }
|
|
189
|
+
if (/^[\d.]+万字$/.test(line)) { cur.words = line; continue; }
|
|
190
|
+
if (fieldIdx === 0) { cur.title = line; fieldIdx = 1; continue; }
|
|
191
|
+
if (fieldIdx === 1) { cur.author = line; fieldIdx = 2; continue; }
|
|
192
|
+
if (fieldIdx === 2) { cur.genre = line; fieldIdx = 3; continue; }
|
|
193
|
+
if (fieldIdx === 3) { cur.subGenre = line; fieldIdx = 4; continue; }
|
|
194
|
+
cur.desc += (cur.desc ? " " : "") + line;
|
|
195
|
+
}
|
|
196
|
+
if (cur && cur.title) books.push(cur);
|
|
197
|
+
return books;
|
|
198
|
+
});
|
|
325
199
|
|
|
326
|
-
// 文本解析获取书籍数据 + DOM 获取链接
|
|
327
|
-
const rawBooks = extractBooksFromText(port);
|
|
328
200
|
rawCount = rawBooks.length;
|
|
329
201
|
books = rawBooks.filter(isUsableBook);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
202
|
+
|
|
203
|
+
// 提取作品页链接
|
|
204
|
+
urls = await page.evaluate(() => {
|
|
205
|
+
var byId = {};
|
|
206
|
+
var order = [];
|
|
207
|
+
Array.from(document.querySelectorAll("a")).forEach(a => {
|
|
208
|
+
var h = (a.getAttribute("href") || a.href || "").match(/\/(?:shuku|book)\/([0-9]+)/);
|
|
209
|
+
if (!h) return;
|
|
210
|
+
var id = h[1];
|
|
211
|
+
var t = (a.innerText || a.textContent || "").replace(/\s+/g, " ").trim();
|
|
212
|
+
if (!byId[id]) { byId[id] = ""; order.push(id); }
|
|
213
|
+
if (t && !/^[0-9]+$/.test(t) && !/^(最近更新、最新章节、最新)/.test(t)) {
|
|
214
|
+
if (t.length > byId[id].length) byId[id] = t;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return order.map(id => ({ bookId: id, title: byId[id], url: "https://www.qimao.com/shuku/" + id + "/" }));
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
await browser.close();
|
|
334
221
|
}
|
|
335
222
|
|
|
336
223
|
if (!books.length) {
|
|
337
|
-
console.error(`[qimao]
|
|
224
|
+
console.error(`[qimao] 采集失败:页面结构可能已变。`);
|
|
338
225
|
return null;
|
|
339
226
|
}
|
|
340
227
|
|
|
341
|
-
//
|
|
342
|
-
const norm =
|
|
228
|
+
// 按书名匹配 URL
|
|
229
|
+
const norm = s => (s || "").replace(/\s+/g, "");
|
|
343
230
|
for (const b of books) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (matched) b.url = matched.url;
|
|
347
|
-
} catch (matchErr) {
|
|
348
|
-
console.error(`[qimao] URL匹配出错(#${b.rank} ${b.title}): ${matchErr.message}`);
|
|
349
|
-
}
|
|
231
|
+
const matched = urls.find(u => norm(u.title) === norm(b.title));
|
|
232
|
+
if (matched) b.url = matched.url;
|
|
350
233
|
}
|
|
351
234
|
|
|
352
235
|
const summary = summarizeQuality(books, rawCount);
|
|
353
|
-
console.log(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
236
|
+
console.log(` ✓ 提取 ${books.length} 本(链接 ${summary.linked}/${books.length},热度 ${summary.heated}/${books.length})`);
|
|
237
|
+
|
|
238
|
+
const periodLabel = period ? period.label : "";
|
|
239
|
+
const now = new Date().toISOString();
|
|
240
|
+
const lines = [
|
|
241
|
+
`# 七猫 · ${ch.label} · ${rt.label}${periodLabel}`,
|
|
242
|
+
"",
|
|
243
|
+
`- 数据质量:${summary.quality}`,
|
|
244
|
+
`- 有效条目:${books.length} / ${rawCount}`,
|
|
245
|
+
`- 问题摘要:${summary.problems.length ? summary.problems.join(";") : "无"}`,
|
|
246
|
+
`- 来源:${url}`,
|
|
247
|
+
`- 抓取时间:${now}`,
|
|
248
|
+
"",
|
|
249
|
+
"---",
|
|
250
|
+
"",
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
for (const b of books) {
|
|
254
|
+
lines.push(`### #${b.rank} ${b.title}`);
|
|
255
|
+
const meta = [
|
|
256
|
+
b.author || "[待补]", b.genre || "[待补]", b.subGenre || "[待补]",
|
|
257
|
+
b.status || "[待补]", b.words || "[待补]",
|
|
258
|
+
b.heat ? b.heat + "热度" : "[待补]",
|
|
259
|
+
].join(" · ");
|
|
260
|
+
lines.push(`*${meta}*`);
|
|
261
|
+
if (b.update) lines.push(`**最新更新:** ${b.update}`);
|
|
262
|
+
if (b.url) lines.push(`[作品页](${b.url})`);
|
|
263
|
+
const desc = cleanDesc(b.desc);
|
|
264
|
+
if (desc) lines.push("", "**简介**", "", desc);
|
|
265
|
+
lines.push("", "---", "");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return lines.join("\n");
|
|
357
269
|
}
|
|
358
270
|
|
|
359
271
|
function buildTargets(channel, rankType, period) {
|
|
360
|
-
const channels = channel === "all" ? CHANNELS.map(
|
|
361
|
-
const rankTypes = rankType === "all" ? RANK_TYPES.map(
|
|
272
|
+
const channels = channel === "all" ? CHANNELS.map(c => c.id) : [channel];
|
|
273
|
+
const rankTypes = rankType === "all" ? RANK_TYPES.map(r => r.id) : [rankType];
|
|
362
274
|
const targets = [];
|
|
363
275
|
for (const channelId of channels) {
|
|
364
276
|
for (const rankTypeId of rankTypes) {
|
|
365
277
|
if (rankTypeId === "hot") {
|
|
366
|
-
const periods = period === "all" ? PERIODS.map(
|
|
278
|
+
const periods = period === "all" ? PERIODS.map(p => p.id) : [period];
|
|
367
279
|
for (const periodId of periods) {
|
|
368
280
|
targets.push({ channel: channelId, rankType: rankTypeId, period: periodId });
|
|
369
281
|
}
|
|
@@ -376,61 +288,52 @@ function buildTargets(channel, rankType, period) {
|
|
|
376
288
|
}
|
|
377
289
|
|
|
378
290
|
function outputFilename(channelId, rankTypeId, periodId, date) {
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
const period = periodId ? PERIODS.find(
|
|
382
|
-
return `七猫${
|
|
291
|
+
const ch = CHANNELS.find(c => c.id === channelId);
|
|
292
|
+
const rt = RANK_TYPES.find(r => r.id === rankTypeId);
|
|
293
|
+
const period = periodId ? PERIODS.find(p => p.id === periodId) : null;
|
|
294
|
+
return `七猫${ch.label}${rt.label}${period ? period.label : ""}_${date}.md`;
|
|
383
295
|
}
|
|
384
296
|
|
|
385
|
-
|
|
386
|
-
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// 入口
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
const args = process.argv.slice(2);
|
|
302
|
+
const OUTDIR = getArg(args, "--outdir") || ".";
|
|
303
|
+
const CHANNEL = getArg(args, "--channel") || "male";
|
|
304
|
+
const RANKTYPE = getArg(args, "--type") || "hot";
|
|
305
|
+
const PERIOD = getArg(args, "--period") || "day";
|
|
306
|
+
const LOGIN_WAIT = parseInt(getArg(args, "--login-wait") || "0", 10);
|
|
307
|
+
|
|
308
|
+
async function main() {
|
|
309
|
+
if (CHANNEL !== "all" && !CHANNELS.some(c => c.id === CHANNEL)) {
|
|
387
310
|
throw new Error(`未知 --channel: ${CHANNEL}`);
|
|
388
311
|
}
|
|
389
|
-
if (RANKTYPE !== "all" && !RANK_TYPES.some(
|
|
312
|
+
if (RANKTYPE !== "all" && !RANK_TYPES.some(r => r.id === RANKTYPE)) {
|
|
390
313
|
throw new Error(`未知 --type: ${RANKTYPE}`);
|
|
391
314
|
}
|
|
392
|
-
if (PERIOD !== "all" && !PERIODS.some(
|
|
315
|
+
if (PERIOD !== "all" && !PERIODS.some(p => p.id === PERIOD)) {
|
|
393
316
|
throw new Error(`未知 --period: ${PERIOD}`);
|
|
394
317
|
}
|
|
395
318
|
const targets = buildTargets(CHANNEL, RANKTYPE, PERIOD);
|
|
396
|
-
let written = 0;
|
|
397
|
-
let failed = 0;
|
|
319
|
+
let written = 0, failed = 0;
|
|
398
320
|
|
|
399
321
|
for (const target of targets) {
|
|
400
|
-
const content = scrapeRank(
|
|
401
|
-
if (!content) {
|
|
402
|
-
failed++;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
|
|
322
|
+
const content = await scrapeRank(target.channel, target.rankType, target.period);
|
|
323
|
+
if (!content) { failed++; continue; }
|
|
406
324
|
const date = localDateStamp();
|
|
407
325
|
const filename = outputFilename(target.channel, target.rankType, target.period, date);
|
|
408
326
|
fs.mkdirSync(OUTDIR, { recursive: true });
|
|
409
|
-
|
|
410
|
-
fs.writeFileSync(filepath, content, "utf-8");
|
|
327
|
+
fs.writeFileSync(path.join(OUTDIR, filename), content, "utf-8");
|
|
411
328
|
written++;
|
|
412
|
-
console.log(` ✓ 已保存: ${
|
|
329
|
+
console.log(` ✓ 已保存: ${filename}`);
|
|
413
330
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
written,
|
|
417
|
-
failed,
|
|
418
|
-
partial: failed > 0,
|
|
419
|
-
partialReasons: [],
|
|
420
|
-
};
|
|
331
|
+
|
|
332
|
+
return { planned: targets.length, written, failed, partial: failed > 0, partialReasons: [] };
|
|
421
333
|
}
|
|
422
334
|
|
|
423
335
|
if (require.main === module) {
|
|
424
336
|
runCli(main, "七猫采集");
|
|
425
337
|
}
|
|
426
338
|
|
|
427
|
-
module.exports = {
|
|
428
|
-
extractBooksFromText,
|
|
429
|
-
isUsableBook,
|
|
430
|
-
cleanDesc,
|
|
431
|
-
renderMarkdown,
|
|
432
|
-
rankUrl,
|
|
433
|
-
selectionMatches,
|
|
434
|
-
buildTargets,
|
|
435
|
-
outputFilename,
|
|
436
|
-
};
|
|
339
|
+
module.exports = { rankUrl, isUsableBook, cleanDesc, buildTargets, outputFilename };
|