@cloud411716/fancy-webnovel 0.1.78 → 0.1.79
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
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
**故障排查(书名全是 `bookId:xxx` / `(标题待解析)`)**:
|
|
95
95
|
- 看文件头 `数据质量`:标 `[标题解析异常]` 说明详情页解码失败率高。
|
|
96
96
|
- 多为详情页结构变动或被登录/验证页拦截。在已登录的 Chrome 里手动打开任一 `https://fanqienovel.com/page/{bookId}` 确认页面正常、非验证页。
|
|
97
|
-
- 控制台若报 `
|
|
97
|
+
- 控制台若报 ` playwright-core 无法启动浏览器`,说明系统缺少 Chrome/Edge 或浏览器路径不对,用 `--login-wait` 等待手动登录后重试。
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## 环境要求
|
|
16
16
|
|
|
17
17
|
> ⚠️ **所有 scraper 现在使用 playwright-core 自己管理浏览器,不再依赖 agent-browser 和 Chrome CDP 端口。**
|
|
18
18
|
> 每个 scraper 独立 launch/close 浏览器进程,采集完成后自动关闭。
|
|
@@ -32,20 +32,20 @@
|
|
|
32
32
|
|
|
33
33
|
| 篇幅 | Scraper | 采集模式 | 示例命令 |
|
|
34
34
|
|------|---------|---------|---------|
|
|
35
|
-
| 长篇 | fanqie-rank-scraper.
|
|
35
|
+
| 长篇 | fanqie-rank-scraper.cjs | playwright-native | `node fanqie-rank-scraper.cjs --channel all --type all --outdir {out}` |
|
|
36
36
|
|
|
37
37
|
### 晋江(jinjiang)
|
|
38
38
|
|
|
39
39
|
| 篇幅 | Scraper | 采集模式 | 示例命令 |
|
|
40
40
|
|------|---------|---------|---------|
|
|
41
|
-
| 长篇 | jjwxc-rank-scraper.
|
|
41
|
+
| 长篇 | jjwxc-rank-scraper.cjs | playwright-native | `node jjwxc-rank-scraper.cjs --type 12 --outdir {out}` |
|
|
42
42
|
|
|
43
43
|
### 知乎(zhihu)
|
|
44
44
|
|
|
45
45
|
| 篇幅 | Scraper | 采集模式 | 示例命令 |
|
|
46
46
|
|------|---------|---------|---------|
|
|
47
|
-
| 长篇 | zhihu-rank-scraper.
|
|
48
|
-
| 短篇 | zhihu-rank-scraper.
|
|
47
|
+
| 长篇 | zhihu-rank-scraper.cjs | playwright-native | `node zhihu-rank-scraper.cjs --length long --outdir {out}` |
|
|
48
|
+
| 短篇 | zhihu-rank-scraper.cjs | playwright-native | `node zhihu-rank-scraper.cjs --length short --outdir {out}` |
|
|
49
49
|
|
|
50
50
|
入口 URL:`https://www.zhihu.com/fiore/h5/vip-web`
|
|
51
51
|
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
|
|
59
59
|
| 篇幅 | Scraper | 采集模式 | 示例命令 |
|
|
60
60
|
|------|---------|---------|---------|
|
|
61
|
-
| 长篇 | qimao-rank-scraper.
|
|
62
|
-
| 短篇 | qimao-rank-scraper.
|
|
61
|
+
| 长篇 | qimao-rank-scraper.cjs | playwright-native | `node qimao-rank-scraper.cjs --type hot --period day --outdir {out}` |
|
|
62
|
+
| 短篇 | qimao-rank-scraper.cjs | playwright-native | `node qimao-rank-scraper.cjs --type hot --period day --gender f --outdir {out}` |
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
@@ -82,12 +82,12 @@ fancy-scan/
|
|
|
82
82
|
├── run-scan.py ← Python 编排层
|
|
83
83
|
├── scraper-registry.md ← 本文件
|
|
84
84
|
├── scan-output-format.md
|
|
85
|
-
├── cdp-utils.
|
|
85
|
+
├── cdp-utils.cjs
|
|
86
86
|
├── qidian-rank-scraper.js
|
|
87
|
-
├── fanqie-rank-scraper.
|
|
88
|
-
├── jjwxc-rank-scraper.
|
|
89
|
-
├── zhihu-rank-scraper.
|
|
90
|
-
└── qimao-rank-scraper.
|
|
87
|
+
├── fanqie-rank-scraper.cjs
|
|
88
|
+
├── jjwxc-rank-scraper.cjs
|
|
89
|
+
├── zhihu-rank-scraper.cjs
|
|
90
|
+
└── qimao-rank-scraper.cjs
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
> ⚠️ 如果 `{SKILL_DIR}/references/` 下没有某个 scraper,run-scan.py 回退到"内置知识"模式,输出占位符报告并标注 `[内置知识-未采集]`。
|
|
@@ -2,188 +2,23 @@
|
|
|
2
2
|
* 工具函数 — 各平台采集脚本的公共依赖
|
|
3
3
|
*
|
|
4
4
|
* 提供:
|
|
5
|
-
* getArg()
|
|
5
|
+
* getArg() — 命令行参数解析
|
|
6
6
|
* localDateStamp() — 本地日期戳(YYYYMMDD)
|
|
7
|
-
* runCli()
|
|
8
|
-
* sleep() — 同步阻塞等待(Atomics.wait)
|
|
9
|
-
*
|
|
10
|
-
* 注意:cdp-utils 不再依赖 agent-browser/CDP 端口。浏览器生命周期由各 scraper 自己管理。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { execFileSync } = require("child_process");
|
|
14
|
-
const fs = require("fs");
|
|
15
|
-
const path = require("path");
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* On Windows `agent-browser` is an npm shim (agent-browser.cmd/.ps1) that
|
|
19
|
-
* forwards to the real target — the native agent-browser-win32-*.exe or a
|
|
20
|
-
* bundled Node CLI. Node refuses to execFile the `.cmd` without a shell
|
|
21
|
-
* (CVE-2024-27980), and routing the argv array through a shell mangles it: the
|
|
22
|
-
* `.cmd`'s `%*` is re-tokenized by cmd.exe (splitting on spaces, breaking on
|
|
23
|
-
* & | ^), and calling the shim by bare name from powershell.exe collapses the
|
|
24
|
-
* whole array into a single space-joined argument. The exact locus differs by
|
|
25
|
-
* runtime, so instead of hardening any one shell path we bypass shells entirely:
|
|
26
|
-
* read the `.cmd` shim, recover the real program plus its fixed leading args,
|
|
27
|
-
* and execFile that target directly with the argv array — verbatim, no shell.
|
|
7
|
+
* runCli() — CLI 入口包装(含进程超时和信号处理)
|
|
28
8
|
*/
|
|
29
|
-
function resolveWindowsAgentBrowser(argv) {
|
|
30
|
-
const dirs = String(process.env.PATH || "").split(path.delimiter);
|
|
31
|
-
let cmdPath = null;
|
|
32
|
-
for (const dir of dirs) {
|
|
33
|
-
if (!dir) continue;
|
|
34
|
-
const candidate = path.join(dir, "agent-browser.cmd");
|
|
35
|
-
if (fs.existsSync(candidate)) {
|
|
36
|
-
cmdPath = candidate;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (!cmdPath) return { file: "agent-browser", args: argv };
|
|
41
|
-
const dir = path.dirname(cmdPath);
|
|
42
|
-
const forwardLine =
|
|
43
|
-
fs
|
|
44
|
-
.readFileSync(cmdPath, "utf8")
|
|
45
|
-
.split(/\r?\n/)
|
|
46
|
-
.find((line) => line.includes("%*")) || "";
|
|
47
|
-
const tokens = [...forwardLine.matchAll(/"([^"]*)"/g)]
|
|
48
|
-
.map((m) => m[1])
|
|
49
|
-
.map((t) =>
|
|
50
|
-
t
|
|
51
|
-
.replace(/%~dp0/gi, () => dir + path.sep)
|
|
52
|
-
.replace(/%dp0%/gi, () => dir + path.sep)
|
|
53
|
-
);
|
|
54
|
-
const jsIndex = tokens.findIndex((t) => /\.[cm]?js$/i.test(t));
|
|
55
|
-
if (jsIndex >= 0) {
|
|
56
|
-
return { file: process.execPath, args: [...tokens.slice(jsIndex), ...argv] };
|
|
57
|
-
}
|
|
58
|
-
if (tokens.length > 0) {
|
|
59
|
-
return { file: tokens[0], args: [...tokens.slice(1), ...argv] };
|
|
60
|
-
}
|
|
61
|
-
return { file: "agent-browser", args: argv };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Build a shell-free invocation. POSIX runs the native `agent-browser` binary
|
|
66
|
-
* directly; Windows resolves the npm `.cmd` shim to that native target so the
|
|
67
|
-
* argument array is passed verbatim, never routed through cmd.exe/PowerShell.
|
|
68
|
-
*/
|
|
69
|
-
function buildAgentBrowserInvocation(port, args, platform = process.platform) {
|
|
70
|
-
const argv = ["--cdp", String(port), ...args.map(String)];
|
|
71
|
-
if (platform !== "win32") {
|
|
72
|
-
return { file: "agent-browser", args: argv };
|
|
73
|
-
}
|
|
74
|
-
return resolveWindowsAgentBrowser(argv);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
// agent-browser 工具函数
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 调用 agent-browser CLI
|
|
83
|
-
* @param {number} port - CDP 端口
|
|
84
|
-
* @param {...string} args - agent-browser 参数
|
|
85
|
-
* @returns {string} stdout(trim 后)
|
|
86
|
-
*/
|
|
87
|
-
function ab(port, ...args) {
|
|
88
|
-
const invocation = buildAgentBrowserInvocation(port, args);
|
|
89
|
-
try {
|
|
90
|
-
return execFileSync(
|
|
91
|
-
invocation.file,
|
|
92
|
-
invocation.args,
|
|
93
|
-
{
|
|
94
|
-
encoding: "utf-8",
|
|
95
|
-
timeout: 20000,
|
|
96
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
97
|
-
windowsHide: true,
|
|
98
|
-
}
|
|
99
|
-
).trim();
|
|
100
|
-
} catch (error) {
|
|
101
|
-
const stderr = error && error.stderr ? String(error.stderr).trim() : "";
|
|
102
|
-
const stdout = error && error.stdout ? String(error.stdout).trim() : "";
|
|
103
|
-
const detail = stderr || stdout || (error && error.message) || "unknown error";
|
|
104
|
-
throw new Error(`agent-browser failed: ${detail}`, { cause: error });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** 等待 ms 毫秒(跨平台,不依赖系统 sleep 命令) */
|
|
109
|
-
function sleep(ms) {
|
|
110
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function parseJSONResult(raw) {
|
|
114
|
-
if (!raw || raw === "ERR") {
|
|
115
|
-
throw new Error("agent-browser returned no JSON result");
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
let parsed = JSON.parse(raw);
|
|
119
|
-
if (typeof parsed === "string") {
|
|
120
|
-
try { parsed = JSON.parse(parsed); } catch {}
|
|
121
|
-
}
|
|
122
|
-
return parsed;
|
|
123
|
-
} catch (error) {
|
|
124
|
-
throw new Error(`agent-browser returned invalid JSON: ${String(raw).slice(0, 160)}`, {
|
|
125
|
-
cause: error,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 在浏览器内执行 JS,并解析 JSON 返回值。
|
|
132
|
-
* 一律走 base64(-b):正文提取用的 JS 常含引号、反斜杠等,作为命令行参数时在 Windows 上
|
|
133
|
-
* 无法逐字透传(.cmd 的 %* 与 PowerShell 都会二次解析)。base64 让参数只含 [A-Za-z0-9+/=],
|
|
134
|
-
* 和各采集脚本已在用的 evalJSONBase64 走同一条安全通道。
|
|
135
|
-
*/
|
|
136
|
-
function evalJSON(port, js) {
|
|
137
|
-
return evalJSONBase64(port, js);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 通过 agent-browser 的 base64 参数执行复杂 JS,避免命令行转义和参数边界问题。
|
|
142
|
-
*/
|
|
143
|
-
function evalJSONBase64(port, js) {
|
|
144
|
-
const encoded = Buffer.from(String(js), "utf8").toString("base64");
|
|
145
|
-
return parseJSONResult(ab(port, "eval", "-b", encoded));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* 安全地将值插入浏览器 eval 字符串。
|
|
150
|
-
* 使用 JSON.stringify 确保值不会因特殊字符(引号、反斜杠等)破坏 eval 字符串。
|
|
151
|
-
* @param {*} val - 要插入的值
|
|
152
|
-
* @returns {string} JSON 字符串表示(含引号)
|
|
153
|
-
*/
|
|
154
|
-
function safeStr(val) {
|
|
155
|
-
return JSON.stringify(String(val));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 滚动页面加载更多内容
|
|
160
|
-
* @param {number} port - CDP 端口
|
|
161
|
-
* @param {number} times - 滚动次数
|
|
162
|
-
* @param {number} [interval=1000] - 每次滚动间隔(ms)
|
|
163
|
-
*/
|
|
164
|
-
function scrollLoad(port, times, interval = 1000) {
|
|
165
|
-
for (let i = 0; i < times; i++) {
|
|
166
|
-
ab(port, "eval", "window.scrollBy(0, window.innerHeight)");
|
|
167
|
-
sleep(interval);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
9
|
|
|
171
10
|
/** 解析 --xxx 参数 */
|
|
172
11
|
function getArg(args, name) {
|
|
173
12
|
const i = args.indexOf(name);
|
|
174
13
|
if (i >= 0) return i + 1 < args.length ? args[i + 1] : null;
|
|
175
14
|
const prefix = `${name}=`;
|
|
176
|
-
const inline = args.find(
|
|
15
|
+
const inline = args.find(arg => String(arg).startsWith(prefix));
|
|
177
16
|
return inline === undefined ? null : String(inline).slice(prefix.length);
|
|
178
17
|
}
|
|
179
18
|
|
|
180
19
|
/**
|
|
181
|
-
* 输出文件名用的日期戳(YYYYMMDD
|
|
20
|
+
* 输出文件名用的日期戳(YYYYMMDD),一律取本地日历日。
|
|
182
21
|
* 不能用 new Date().toISOString().slice(0,10):那是 UTC 日期,比 UTC+8 晚 8 小时。
|
|
183
|
-
* 文件名是各采集脚本唯一的去重键(一个榜单一天一份),北京时间 00:00-08:00 之间的采集
|
|
184
|
-
* 会退回「昨天」的文件名,静默覆盖前一晚采到的同名报告,且这份数据被标成前一天。
|
|
185
|
-
* @param {Date} [date] - 默认当前时间
|
|
186
|
-
* @returns {string} YYYYMMDD
|
|
187
22
|
*/
|
|
188
23
|
function localDateStamp(date) {
|
|
189
24
|
const d = date instanceof Date ? date : new Date();
|
|
@@ -194,14 +29,15 @@ function localDateStamp(date) {
|
|
|
194
29
|
}
|
|
195
30
|
|
|
196
31
|
/**
|
|
197
|
-
* Run a scraper entrypoint
|
|
198
|
-
*
|
|
199
|
-
*
|
|
32
|
+
* Run a scraper entrypoint 并将结果转换为机器可读的退出码。
|
|
33
|
+
* 返回值支持:
|
|
34
|
+
* 整数 — 直接作为 written 计数
|
|
35
|
+
* 对象 — { planned, written, failed, partial, partialReasons }
|
|
200
36
|
*/
|
|
201
37
|
function runCli(main, label) {
|
|
202
38
|
Promise.resolve()
|
|
203
39
|
.then(main)
|
|
204
|
-
.then(
|
|
40
|
+
.then(result => {
|
|
205
41
|
const outcome = Number.isInteger(result)
|
|
206
42
|
? { planned: result, written: result, failed: 0, partial: false, partialReasons: [] }
|
|
207
43
|
: result;
|
|
@@ -223,22 +59,11 @@ function runCli(main, label) {
|
|
|
223
59
|
process.exitCode = 2;
|
|
224
60
|
}
|
|
225
61
|
})
|
|
226
|
-
.catch(
|
|
62
|
+
.catch(error => {
|
|
227
63
|
const message = error && error.message ? error.message : String(error);
|
|
228
64
|
console.error(`${label} failed: ${message}`);
|
|
229
65
|
process.exitCode = 1;
|
|
230
66
|
});
|
|
231
67
|
}
|
|
232
68
|
|
|
233
|
-
module.exports = {
|
|
234
|
-
ab,
|
|
235
|
-
sleep,
|
|
236
|
-
evalJSON,
|
|
237
|
-
evalJSONBase64,
|
|
238
|
-
buildAgentBrowserInvocation,
|
|
239
|
-
safeStr,
|
|
240
|
-
scrollLoad,
|
|
241
|
-
getArg,
|
|
242
|
-
localDateStamp,
|
|
243
|
-
runCli,
|
|
244
|
-
};
|
|
69
|
+
module.exports = { getArg, localDateStamp, runCli };
|