@cloud411716/fancy-webnovel 0.3.15 → 1.0.0
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/commands/bootstrap/index.js +138 -0
- package/commands/bootstrap/init-project.js +121 -0
- package/commands/scan/index.js +226 -0
- package/commands/scan/platforms/fanqie.js +148 -0
- package/commands/scan/platforms/jinjiang.js +143 -0
- package/commands/scan/platforms/qidian.js +106 -0
- package/commands/scan/platforms/qimao.js +127 -0
- package/commands/scan/scraper.js +195 -0
- package/cordis.patch.yml +14 -0
- package/events.js +53 -0
- package/index.js +40 -6
- package/infra.js +83 -102
- package/package.json +3 -11
- package/plugins/fancy-bootstrap/index.js +0 -100
- package/plugins/fancy-bootstrap/scripts/init.js +0 -111
- package/plugins/fancy-bootstrap/scripts/path-utils.js +0 -22
- package/plugins/fancy-bootstrap/templates.js +0 -41
- package/plugins/fancy-scan/index.js +0 -195
- package/plugins/fancy-scan/scripts/run-scan.js +0 -282
- package/plugins/fancy-scan/scripts/scan-output-format.md +0 -188
- package/plugins/fancy-scan/scripts/scraper-registry.md +0 -104
- package/plugins/fancy-scan/scripts/scrapers/cdp-utils.cjs +0 -75
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.cjs +0 -441
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.cjs +0 -384
- package/plugins/fancy-scan/scripts/scrapers/package-lock.json +0 -27
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.cjs +0 -430
- package/plugins/fancy-scan/templates.js +0 -158
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* 晋江文学城排行榜采集脚本
|
|
4
|
-
*
|
|
5
|
-
* 使用 playwright-core 自己管理浏览器。
|
|
6
|
-
* 采集策略:
|
|
7
|
-
* 布局 A(表格型):orderstr=5/7/4/15 等,表头含"序号/作者/作品",直接解析表格。
|
|
8
|
-
* 布局 B(交替行型):orderstr=12/16/17 等,按频道分段、书名/作者交替行解析。
|
|
9
|
-
* 详情补采:从书名 anchor 取 novelid,逐本进 onebook.php 补核心指标
|
|
10
|
-
* (收藏数/营养液/积分/字数/状态),满足规范对晋江的硬性要求。
|
|
11
|
-
* 晋江页面为 gb18030 编码:详情页用 fetch+arrayBuffer+TextDecoder('gb18030') 解码。
|
|
12
|
-
*
|
|
13
|
-
* 用法:
|
|
14
|
-
* node jjwxc-rank-scraper.js --type 12 # 收入金榜(默认含详情)
|
|
15
|
-
* node jjwxc-rank-scraper.js --type 12 --top 15 # 每频道补采前 15 本
|
|
16
|
-
* node jjwxc-rank-scraper.js --type 12 --detail-limit 60 # 详情总量上限 60
|
|
17
|
-
* node jjwxc-rank-scraper.js --type 12 --list-only # 只采列表(快,无核心指标)
|
|
18
|
-
* node jjwxc-rank-scraper.js --type all # 全部榜单
|
|
19
|
-
* node jjwxc-rank-scraper.js --login-wait 60 # 等待手动登录
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
const fs = require("fs");
|
|
23
|
-
const path = require("path");
|
|
24
|
-
const { chromium } = require("playwright-core");
|
|
25
|
-
const { getArg, localDateStamp, runCli } = require("./cdp-utils.cjs");
|
|
26
|
-
|
|
27
|
-
const BASE_URL = "https://www.jjwxc.net/topten.php";
|
|
28
|
-
const DETAIL_BASE = "https://www.jjwxc.net/onebook.php";
|
|
29
|
-
|
|
30
|
-
const RANK_TYPES = [
|
|
31
|
-
{ id: "5", label: "月榜" },
|
|
32
|
-
{ id: "7", label: "总分榜" },
|
|
33
|
-
{ id: "4", label: "季度榜" },
|
|
34
|
-
{ id: "12", label: "收入金榜" },
|
|
35
|
-
{ id: "16", label: "完结金榜" },
|
|
36
|
-
{ id: "15", label: "勤奋指数榜" },
|
|
37
|
-
{ id: "17", label: "新手金榜" },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const DETAIL_CHUNK = 6;
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// 工具函数
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
function sleep(ms) {
|
|
47
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function fmtWan(s, unit) {
|
|
51
|
-
if (s == null || s === "") return "";
|
|
52
|
-
const n = parseInt(String(s).replace(/[^0-9]/g, ""), 10);
|
|
53
|
-
if (isNaN(n)) return "";
|
|
54
|
-
if (n >= 10000) return (n / 10000).toFixed(1) + "万" + (unit || "");
|
|
55
|
-
return n + (unit || "");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// 详情页(Node.js fetch + gb18030 解码)
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/** 单次详情请求(Node.js fetch,gb18030 解码) */
|
|
63
|
-
async function fetchOneDetail(novelid) {
|
|
64
|
-
try {
|
|
65
|
-
const res = await fetch(`${DETAIL_BASE}?novelid=${novelid}`);
|
|
66
|
-
if (!res.ok) return { id: novelid, err: `HTTP ${res.status}` };
|
|
67
|
-
const buf = await res.arrayBuffer();
|
|
68
|
-
const h = new TextDecoder("gb18030").decode(new Uint8Array(buf));
|
|
69
|
-
function prop(n) {
|
|
70
|
-
const m = h.match(new RegExp(`itemprop="${n}"[^>]*>([^<]*)<`));
|
|
71
|
-
return m ? m[1].trim() : "";
|
|
72
|
-
}
|
|
73
|
-
const status =
|
|
74
|
-
(h.match(/itemprop="updataStatus"[^>]*>\s*([^\s<]{1,6})/) || [, ""])[1] ||
|
|
75
|
-
(h.match(/(连载中|已完结|完结)/) || [, ""])[1] ||
|
|
76
|
-
"";
|
|
77
|
-
return {
|
|
78
|
-
id: novelid,
|
|
79
|
-
collect: prop("collectedCount"),
|
|
80
|
-
nutrition: prop("nutritionCount"),
|
|
81
|
-
score: prop("scoreCount"),
|
|
82
|
-
review: prop("reviewCount"),
|
|
83
|
-
words: prop("wordCount"),
|
|
84
|
-
status,
|
|
85
|
-
};
|
|
86
|
-
} catch (e) {
|
|
87
|
-
return { id: novelid, err: String(e && e.message || e) };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** 分批补采详情 */
|
|
92
|
-
async function fetchDetails(ids) {
|
|
93
|
-
const map = {};
|
|
94
|
-
let failedChunks = 0;
|
|
95
|
-
for (let i = 0; i < ids.length; i += DETAIL_CHUNK) {
|
|
96
|
-
const chunk = ids.slice(i, i + DETAIL_CHUNK);
|
|
97
|
-
const results = await Promise.all(chunk.map(id => fetchOneDetail(id)));
|
|
98
|
-
for (const r of results) map[r.id] = r;
|
|
99
|
-
sleep(400);
|
|
100
|
-
}
|
|
101
|
-
return map;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// 主流程
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
async function scrapeRank(rankTypeId, channelId) {
|
|
109
|
-
const rt = RANK_TYPES.find(r => r.id === rankTypeId);
|
|
110
|
-
if (!rt) { console.log(` ⚠ 未知榜单类型: ${rankTypeId}`); return null; }
|
|
111
|
-
|
|
112
|
-
const url = `${BASE_URL}?orderstr=${rankTypeId}&t=${channelId}`;
|
|
113
|
-
const chLabel = channelId === "0" ? "全站" : `频道${channelId}`;
|
|
114
|
-
console.log(`\n→ 采集 晋江${rt.label}(${chLabel})...`);
|
|
115
|
-
console.log(` URL: ${url}`);
|
|
116
|
-
|
|
117
|
-
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage", "--start-maximized"] });
|
|
118
|
-
const context = await browser.newContext();
|
|
119
|
-
const page = await context.newPage();
|
|
120
|
-
|
|
121
|
-
// CDP 强制窗口最大化
|
|
122
|
-
try {
|
|
123
|
-
const cdp = await context.newCDPSession(page);
|
|
124
|
-
const { windowId } = await cdp.send('Browser.getWindowForTarget', { target: page.target() });
|
|
125
|
-
await cdp.send('Browser.setWindowBounds', { windowId, bounds: { state: 'maximized' } });
|
|
126
|
-
} catch (_) {}
|
|
127
|
-
|
|
128
|
-
let data = null;
|
|
129
|
-
try {
|
|
130
|
-
await page.goto(url, { waitUntil: "networkidle" });
|
|
131
|
-
await page.waitForTimeout(3000);
|
|
132
|
-
|
|
133
|
-
// 连通性自检
|
|
134
|
-
const host = page.url();
|
|
135
|
-
if (host.indexOf("jjwxc") === -1) {
|
|
136
|
-
console.error(` ✗ 当前页面非晋江(url=${host}),可能被重定向,已跳过。`);
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// 布局检测:先找含"序号"/"作者"/"作品"表头的表格,有则为布局A(表格),无为布局B(交替行)
|
|
141
|
-
data = await page.evaluate(() => {
|
|
142
|
-
// ── 布局 A:表格型(表头在第一行 <tr> 的 <td> 里,含"序号""作者""作品") ──
|
|
143
|
-
var tables = document.querySelectorAll('table');
|
|
144
|
-
for (var ti = 0; ti < tables.length; ti++) {
|
|
145
|
-
var firstRow = tables[ti].querySelector('tr');
|
|
146
|
-
if (!firstRow) continue;
|
|
147
|
-
var headerCells = firstRow.querySelectorAll('td,th');
|
|
148
|
-
var headerText = Array.from(headerCells).map(function(c) { return c.innerText.trim(); }).join('');
|
|
149
|
-
if (headerText.indexOf('序号') >= 0 && headerText.indexOf('作者') >= 0 && headerText.indexOf('作品') >= 0) {
|
|
150
|
-
var rows = tables[ti].querySelectorAll('tr');
|
|
151
|
-
var books = [];
|
|
152
|
-
for (var ri = 1; ri < rows.length; ri++) {
|
|
153
|
-
var cells = rows[ri].querySelectorAll('td');
|
|
154
|
-
if (cells.length < 3) continue;
|
|
155
|
-
var rankCell = cells[0].innerText.trim();
|
|
156
|
-
if (!/^\d+$/.test(rankCell)) continue;
|
|
157
|
-
var author = cells[1].innerText.trim();
|
|
158
|
-
var titleLink = cells[2].querySelector('a');
|
|
159
|
-
var title = titleLink ? titleLink.innerText.trim() : cells[2].innerText.trim();
|
|
160
|
-
var novelid = '';
|
|
161
|
-
if (titleLink) {
|
|
162
|
-
var m = (titleLink.getAttribute('href') || '').match(/novelid=([0-9]+)/);
|
|
163
|
-
if (m) novelid = m[1];
|
|
164
|
-
}
|
|
165
|
-
var typeText = cells.length > 3 ? cells[3].innerText.trim() : '';
|
|
166
|
-
var progress = cells.length > 4 ? cells[4].innerText.trim() : '';
|
|
167
|
-
var words = cells.length > 5 ? cells[5].innerText.trim() : '';
|
|
168
|
-
var score = cells.length > 6 ? cells[6].innerText.trim() : '';
|
|
169
|
-
var pubtime = cells.length > 7 ? cells[7].innerText.trim() : '';
|
|
170
|
-
books.push({ rank: parseInt(rankCell), title: title, author: author, novelid: novelid, type: typeText, progress: progress, words: words, score: score, pubtime: pubtime });
|
|
171
|
-
}
|
|
172
|
-
return { layout: 'A', channels: [{ name: '全站', books: books }] };
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// ── 布局 B:交替行型(频道分段,书名/作者交替) ──
|
|
176
|
-
var result = { layout: 'B', channels: [] };
|
|
177
|
-
var text = document.body.innerText || "";
|
|
178
|
-
var lines = text.split(/\n/).map(function(l) { return l.trim(); }).filter(Boolean);
|
|
179
|
-
|
|
180
|
-
var idMap = {};
|
|
181
|
-
Array.from(document.querySelectorAll("a")).forEach(function(a) {
|
|
182
|
-
var hm = (a.getAttribute("href") || "").match(/novelid=([0-9]+)/);
|
|
183
|
-
if (!hm) return;
|
|
184
|
-
var t = (a.innerText || a.textContent || "").trim();
|
|
185
|
-
if (!t || t.indexOf("向《") > -1 || t.indexOf("投") > -1 || t.length > 30) return;
|
|
186
|
-
if (!idMap[t]) idMap[t] = hm[1];
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
var channels = ["古代言情","现代言情","古代穿越","现代都市纯爱","现代幻想纯爱","古代纯爱","衍生纯爱","幻想现言","奇幻言情","未来游戏悬疑","百合","无CP","二次元言情","衍生言情","衍生无cp","未来幻想纯爱","原创轻小说","多元"];
|
|
190
|
-
var channelSet = {};
|
|
191
|
-
channels.forEach(function(c) { channelSet[c] = true; });
|
|
192
|
-
|
|
193
|
-
var curChannel = "";
|
|
194
|
-
var channelBooks = {};
|
|
195
|
-
var expectTitle = true;
|
|
196
|
-
var pendingTitle = "";
|
|
197
|
-
|
|
198
|
-
for (var i = 0; i < lines.length; i++) {
|
|
199
|
-
var line = lines[i];
|
|
200
|
-
if (/上榜天数记录|榜单说明/.test(line)) { break; }
|
|
201
|
-
if (/^(免费强推|vip强推|新晋作者|月榜|季榜|半年榜|长生殿|总分榜|字数榜|收入金榜|霸王票|霸王总榜|勤奋指数|完结金榜|新手金榜|栽培月榜|驻站|完结高分|千字金榜|完结全订榜)$/.test(line)) { continue; }
|
|
202
|
-
if (line.length > 30 && line.indexOf("·") > 0) continue;
|
|
203
|
-
if (channelSet[line]) {
|
|
204
|
-
if (curChannel && channelBooks[curChannel]) channelBooks[curChannel]._finished = true;
|
|
205
|
-
curChannel = line;
|
|
206
|
-
if (!channelBooks[curChannel]) channelBooks[curChannel] = { books: [] };
|
|
207
|
-
expectTitle = true;
|
|
208
|
-
pendingTitle = "";
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (!curChannel) continue;
|
|
212
|
-
if (expectTitle) {
|
|
213
|
-
pendingTitle = line;
|
|
214
|
-
expectTitle = false;
|
|
215
|
-
} else {
|
|
216
|
-
if (pendingTitle) {
|
|
217
|
-
channelBooks[curChannel].books.push({ title: pendingTitle, author: line, novelid: idMap[pendingTitle] || "" });
|
|
218
|
-
}
|
|
219
|
-
expectTitle = true;
|
|
220
|
-
pendingTitle = "";
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
for (var name in channelBooks) {
|
|
225
|
-
if (channelBooks[name].books.length > 0) {
|
|
226
|
-
result.channels.push({ name: name, books: channelBooks[name].books });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return result;
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
console.log(` ✓ 布局检测:${data.layout === 'A' ? '表格型' : '交替行型'}`);
|
|
233
|
-
|
|
234
|
-
if (!data?.channels?.length) {
|
|
235
|
-
console.error(`[jjwxc] 采集失败:未解析到榜单(页面结构可能变动或未加载)。请人工打开 ${url} 确认。`);
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
} finally {
|
|
239
|
-
await browser.close();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
let totalBooks = 0;
|
|
243
|
-
data.channels.forEach(ch => {
|
|
244
|
-
totalBooks += ch.books.length;
|
|
245
|
-
const authors = new Set(ch.books.map(b => b.author));
|
|
246
|
-
if (ch.books.length >= 5 && authors.size / ch.books.length < 0.2) {
|
|
247
|
-
console.log(` ⚠ ${ch.name}:${ch.books.length} 本只有 ${authors.size} 个唯一作者,可能提取有误`);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
console.log(` ✓ 列表:${data.channels.length} 个频道,共 ${totalBooks} 本`);
|
|
251
|
-
|
|
252
|
-
// 补采详情
|
|
253
|
-
let detailMap = {};
|
|
254
|
-
let detailPlanned = 0;
|
|
255
|
-
let detailOk = 0;
|
|
256
|
-
let detailFailedChunks = 0;
|
|
257
|
-
if (!LIST_ONLY) {
|
|
258
|
-
const picked = [];
|
|
259
|
-
for (const ch of data.channels) {
|
|
260
|
-
let n = 0;
|
|
261
|
-
for (const b of ch.books) {
|
|
262
|
-
if (picked.length >= DETAIL_LIMIT) break;
|
|
263
|
-
if (n >= TOP) break;
|
|
264
|
-
if (b.novelid) { picked.push(b.novelid); n++; }
|
|
265
|
-
}
|
|
266
|
-
if (picked.length >= DETAIL_LIMIT) break;
|
|
267
|
-
}
|
|
268
|
-
detailPlanned = picked.length;
|
|
269
|
-
if (picked.length) {
|
|
270
|
-
console.log(` → 补采详情 ${picked.length} 本...`);
|
|
271
|
-
detailMap = await fetchDetails(picked);
|
|
272
|
-
detailFailedChunks = Object.values(detailMap).filter(d => d.err).length;
|
|
273
|
-
detailOk = Object.values(detailMap).filter(d => d && d.collect).length;
|
|
274
|
-
console.log(` ✓ 详情命中收藏数 ${detailOk}/${picked.length}`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
let quality = "[OK]";
|
|
279
|
-
const detailPartial = !LIST_ONLY && detailPlanned > 0 && (detailFailedChunks > 0 || detailOk < detailPlanned);
|
|
280
|
-
if (!LIST_ONLY && detailPlanned > 0 && detailOk === 0) {
|
|
281
|
-
quality = "[详情解析异常/登录态缺失]";
|
|
282
|
-
console.error(` ⚠ 详情全部无收藏数:可能页面结构变动或需登录。`);
|
|
283
|
-
} else if (detailPartial) {
|
|
284
|
-
quality = "[部分详情缺失]";
|
|
285
|
-
console.error(` ⚠ 详情仅命中 ${detailOk}/${detailPlanned},已按部分结果标注。`);
|
|
286
|
-
} else if (LIST_ONLY) {
|
|
287
|
-
quality = "[仅列表-无核心指标]";
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const now = new Date().toISOString();
|
|
291
|
-
const lines = [
|
|
292
|
-
`# 晋江 · ${rt.label}`,
|
|
293
|
-
"",
|
|
294
|
-
`- 来源:${url}`,
|
|
295
|
-
`- 抓取时间:${now}`,
|
|
296
|
-
`- 条目数:${totalBooks}`,
|
|
297
|
-
"",
|
|
298
|
-
"---",
|
|
299
|
-
"",
|
|
300
|
-
];
|
|
301
|
-
|
|
302
|
-
for (const ch of data.channels) {
|
|
303
|
-
for (let i = 0; i < ch.books.length; i++) {
|
|
304
|
-
const b = ch.books[i];
|
|
305
|
-
var genre = '';
|
|
306
|
-
if (data.layout === 'A' && b.type) {
|
|
307
|
-
var parts = b.type.split('-');
|
|
308
|
-
genre = parts[1] || '';
|
|
309
|
-
} else {
|
|
310
|
-
genre = ch.name;
|
|
311
|
-
}
|
|
312
|
-
lines.push(`书名:${b.title || ""}`);
|
|
313
|
-
lines.push(`题材:${genre}`);
|
|
314
|
-
lines.push(`作者:${b.author || ""}`);
|
|
315
|
-
if (b.novelid) lines.push(`作品页:https://www.jjwxc.net/onebook.php?novelid=${b.novelid}`);
|
|
316
|
-
lines.push("");
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
content: lines.join("\n"),
|
|
322
|
-
partial: detailPartial,
|
|
323
|
-
partialReason: detailPartial ? `${rt.label}: detail ${detailOk}/${detailPlanned}` : "",
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ---------------------------------------------------------------------------
|
|
328
|
-
// 入口
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
|
|
331
|
-
const args = process.argv.slice(2);
|
|
332
|
-
const OUTDIR = getArg(args, "--outdir") || ".";
|
|
333
|
-
const RANKTYPE = getArg(args, "--type") || "12";
|
|
334
|
-
const CHANNEL = getArg(args, "--channel") || "0";
|
|
335
|
-
const TOP = parseInt(getArg(args, "--top") || "10", 10);
|
|
336
|
-
const DETAIL_LIMIT = parseInt(getArg(args, "--detail-limit") || "100", 10);
|
|
337
|
-
const LIST_ONLY = args.includes("--list-only");
|
|
338
|
-
const LOGIN_WAIT = parseInt(getArg(args, "--login-wait") || "0", 10);
|
|
339
|
-
|
|
340
|
-
async function main() {
|
|
341
|
-
const rankTypeIds = (RANKTYPE || "12").split(",").map(t => t.trim());
|
|
342
|
-
const invalid = rankTypeIds.find(id => id !== "all" && !RANK_TYPES.some(r => r.id === id));
|
|
343
|
-
if (invalid) {
|
|
344
|
-
throw new Error(`未知 --type: ${invalid}`);
|
|
345
|
-
}
|
|
346
|
-
if (CHANNEL !== "0") {
|
|
347
|
-
throw new Error(`未知 --channel: ${CHANNEL}(当前仅支持 0=全站)`);
|
|
348
|
-
}
|
|
349
|
-
const rankTypes = RANKTYPE === "all" ? RANK_TYPES.map(r => r.id) : rankTypeIds;
|
|
350
|
-
let written = 0, failed = 0, partial = false;
|
|
351
|
-
const partialReasons = [];
|
|
352
|
-
|
|
353
|
-
for (const rt of rankTypes) {
|
|
354
|
-
try {
|
|
355
|
-
const result = await scrapeRank(rt, CHANNEL);
|
|
356
|
-
if (!result) { failed++; continue; }
|
|
357
|
-
if (result.partial) {
|
|
358
|
-
partial = true;
|
|
359
|
-
if (result.partialReason) partialReasons.push(result.partialReason);
|
|
360
|
-
}
|
|
361
|
-
const rtInfo = RANK_TYPES.find(r => r.id === rt);
|
|
362
|
-
const date = localDateStamp();
|
|
363
|
-
const filename = `晋江${rtInfo.label}_全站_${date}.md`;
|
|
364
|
-
fs.mkdirSync(OUTDIR, { recursive: true });
|
|
365
|
-
const filepath = path.join(OUTDIR, filename);
|
|
366
|
-
fs.writeFileSync(filepath, result.content, "utf-8");
|
|
367
|
-
written++;
|
|
368
|
-
console.log(` ✓ 已保存: ${filepath}`);
|
|
369
|
-
} catch (rankErr) {
|
|
370
|
-
failed++;
|
|
371
|
-
const message = rankErr && rankErr.message ? rankErr.message : String(rankErr);
|
|
372
|
-
partialReasons.push(`${rt}: ${message}`);
|
|
373
|
-
console.error(`[jjwxc] ${rt} 采集失败,跳过: ${message}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return { planned: rankTypes.length, written, failed, partial: partial || failed > 0, partialReasons };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (require.main === module) {
|
|
381
|
-
runCli(main, "晋江采集");
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
module.exports = { fmtWan };
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|