@cloud411716/fancy-webnovel 0.2.15 → 0.2.17
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
|
@@ -199,9 +199,10 @@ function scraperArgs(platform, length, outDir) {
|
|
|
199
199
|
extra: ['--channel', 'all', '--top', '20', '--outdir', outDir],
|
|
200
200
|
};
|
|
201
201
|
case 'jinjiang':
|
|
202
|
+
// 月榜(orderstr=5) + 新手金榜(orderstr=17),--type all 扫全部
|
|
202
203
|
return {
|
|
203
204
|
script: 'jjwxc-rank-scraper.cjs',
|
|
204
|
-
extra: ['--type', '
|
|
205
|
+
extra: ['--type', '5,17', '--outdir', outDir],
|
|
205
206
|
};
|
|
206
207
|
case 'zhihu':
|
|
207
208
|
return {
|
|
@@ -59,12 +59,13 @@
|
|
|
59
59
|
|
|
60
60
|
| 榜单 | orderstr |
|
|
61
61
|
|------|----------|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
62
|
+
| 月榜 | 5 |
|
|
63
|
+
| 总分榜 | 7 |
|
|
64
64
|
| 季度榜 | 8 |
|
|
65
|
+
| 收入金榜 | 12 |
|
|
65
66
|
| 完结金榜 | 14 |
|
|
66
|
-
|
|
|
67
|
-
|
|
|
67
|
+
| 勤奋指数榜 | 15 |
|
|
68
|
+
| 新手金榜 | 17 |
|
|
68
69
|
|
|
69
70
|
### 输出模板
|
|
70
71
|
|
|
@@ -27,12 +27,13 @@ const BASE_URL = "https://www.jjwxc.net/topten.php";
|
|
|
27
27
|
const DETAIL_BASE = "https://www.jjwxc.net/onebook.php";
|
|
28
28
|
|
|
29
29
|
const RANK_TYPES = [
|
|
30
|
-
{ id: "
|
|
31
|
-
{ id: "7", label: "
|
|
30
|
+
{ id: "5", label: "月榜" },
|
|
31
|
+
{ id: "7", label: "总分榜" },
|
|
32
32
|
{ id: "8", label: "季度榜" },
|
|
33
|
+
{ id: "12", label: "收入金榜" },
|
|
33
34
|
{ id: "14", label: "完结金榜" },
|
|
34
|
-
{ id: "15", label: "
|
|
35
|
-
{ id: "17", label: "
|
|
35
|
+
{ id: "15", label: "勤奋指数榜" },
|
|
36
|
+
{ id: "17", label: "新手金榜" },
|
|
36
37
|
];
|
|
37
38
|
|
|
38
39
|
const DETAIL_CHUNK = 6;
|
|
@@ -294,13 +295,15 @@ const LIST_ONLY = args.includes("--list-only");
|
|
|
294
295
|
const LOGIN_WAIT = parseInt(getArg(args, "--login-wait") || "0", 10);
|
|
295
296
|
|
|
296
297
|
async function main() {
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
const rankTypeIds = (RANKTYPE || "12").split(",").map(t => t.trim());
|
|
299
|
+
const invalid = rankTypeIds.find(id => id !== "all" && !RANK_TYPES.some(r => r.id === id));
|
|
300
|
+
if (invalid) {
|
|
301
|
+
throw new Error(`未知 --type: ${invalid}`);
|
|
299
302
|
}
|
|
300
303
|
if (CHANNEL !== "0") {
|
|
301
304
|
throw new Error(`未知 --channel: ${CHANNEL}(当前仅支持 0=全站)`);
|
|
302
305
|
}
|
|
303
|
-
const rankTypes = RANKTYPE === "all" ? RANK_TYPES.map(r => r.id) :
|
|
306
|
+
const rankTypes = RANKTYPE === "all" ? RANK_TYPES.map(r => r.id) : rankTypeIds;
|
|
304
307
|
let written = 0, failed = 0, partial = false;
|
|
305
308
|
const partialReasons = [];
|
|
306
309
|
|
|
@@ -95,6 +95,52 @@ function summarizeQuality(books, rawCount) {
|
|
|
95
95
|
return { linked, heated, problems, quality: problems.length ? "[存在问题]" : "[OK]" };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// 遍历 JSON 对象/数组,查找含 bookId + title/author 的数组
|
|
99
|
+
function findBookArrays(obj, depth = 0) {
|
|
100
|
+
if (!obj || depth > 8) return [];
|
|
101
|
+
if (Array.isArray(obj)) {
|
|
102
|
+
for (const item of obj) {
|
|
103
|
+
if (item && typeof item === "object" && (item.bookId || item.novel_id || item.id)) {
|
|
104
|
+
if (item.title || item.bookName || item.author || item.authorName) {
|
|
105
|
+
return obj;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const item of obj) {
|
|
110
|
+
const found = findBookArrays(item, depth + 1);
|
|
111
|
+
if (found.length) return found;
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
if (typeof obj === "object") {
|
|
116
|
+
for (const key of Object.keys(obj)) {
|
|
117
|
+
const found = findBookArrays(obj[key], depth + 1);
|
|
118
|
+
if (found.length) return found;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 从 xhrBookMap 中为一本 innerText 书匹配明文数据
|
|
125
|
+
function findXhrMatch(book, xhrBookMap) {
|
|
126
|
+
// 先用归一化书名直接匹配
|
|
127
|
+
const norm = s => (s || "").replace(/\s+/g, "");
|
|
128
|
+
for (const [id, data] of xhrBookMap) {
|
|
129
|
+
if (norm(data.title) === norm(book.title)) {
|
|
130
|
+
return { ...data, id };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findIdByTitle(title, xhrBookMap) {
|
|
137
|
+
const norm = s => (s || "").replace(/\s+/g, "");
|
|
138
|
+
for (const [id, data] of xhrBookMap) {
|
|
139
|
+
if (norm(data.title) === norm(title)) return id;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
98
144
|
// ---------------------------------------------------------------------------
|
|
99
145
|
// 采集
|
|
100
146
|
// ---------------------------------------------------------------------------
|
|
@@ -119,7 +165,32 @@ async function scrapeRank(channelId, rankTypeId, periodId) {
|
|
|
119
165
|
await cdp.send('Browser.setWindowBounds', { windowId, bounds: { state: 'maximized' } });
|
|
120
166
|
} catch (_) {}
|
|
121
167
|
|
|
122
|
-
|
|
168
|
+
// 拦截 XHR 响应,从 JSON 直接拿 bookId + 明文书名/作者,绕过字体加密
|
|
169
|
+
const xhrBookMap = new Map();
|
|
170
|
+
page.on("response", async (resp) => {
|
|
171
|
+
try {
|
|
172
|
+
const respUrl = resp.url();
|
|
173
|
+
const ct = (resp.headers()["content-type"] || "").toLowerCase();
|
|
174
|
+
if (!ct.includes("json") && !respUrl.includes("/api/")) return;
|
|
175
|
+
if (/\.(css|woff|jpg|png|ico)/.test(respUrl)) return;
|
|
176
|
+
const body = await resp.text();
|
|
177
|
+
if (!body || body.length < 100) return;
|
|
178
|
+
let parsed;
|
|
179
|
+
try { parsed = JSON.parse(body); } catch { return; }
|
|
180
|
+
// 遍历 JSON 结构,找含 bookId + title/author 的数组
|
|
181
|
+
const found = findBookArrays(parsed);
|
|
182
|
+
for (const b of found) {
|
|
183
|
+
if (!b.bookId && !b.novel_id && !b.id) continue;
|
|
184
|
+
const id = String(b.bookId || b.novel_id || b.id);
|
|
185
|
+
xhrBookMap.set(id, {
|
|
186
|
+
title: b.title || b.bookName || b.name || "",
|
|
187
|
+
author: b.author || b.authorName || "",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} catch (_) {}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let books, rawCount;
|
|
123
194
|
try {
|
|
124
195
|
await page.goto(url, { waitUntil: "networkidle" });
|
|
125
196
|
await page.waitForTimeout(3000);
|
|
@@ -207,22 +278,26 @@ async function scrapeRank(channelId, rankTypeId, periodId) {
|
|
|
207
278
|
rawCount = rawBooks.length;
|
|
208
279
|
books = rawBooks.filter(isUsableBook);
|
|
209
280
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
281
|
+
// 用 XHR 数据覆盖明文书名/作者,绕过字体加密
|
|
282
|
+
if (xhrBookMap.size > 0) {
|
|
283
|
+
let updated = 0;
|
|
284
|
+
for (const b of books) {
|
|
285
|
+
// 尝试从 XHR map 中找匹配的明文数据(优先用 bookId,其次用书名归一匹配)
|
|
286
|
+
const xhrEntry = findXhrMatch(b, xhrBookMap);
|
|
287
|
+
if (xhrEntry) {
|
|
288
|
+
if (xhrEntry.title && xhrEntry.title !== b.title) {
|
|
289
|
+
b.title = xhrEntry.title;
|
|
290
|
+
updated++;
|
|
291
|
+
}
|
|
292
|
+
if (xhrEntry.author) b.author = xhrEntry.author;
|
|
293
|
+
// 用 XHR 数据里的 id 补 URL(如果有的话)
|
|
294
|
+
const id = xhrEntry.id || findIdByTitle(b.title, xhrBookMap);
|
|
295
|
+
if (id) b.url = `https://www.qimao.com/shuku/${id}/`;
|
|
222
296
|
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
}
|
|
297
|
+
}
|
|
298
|
+
console.log(` ✓ XHR 明文更新 ${updated}/${books.length} 本`);
|
|
299
|
+
}
|
|
300
|
+
|
|
226
301
|
} finally {
|
|
227
302
|
await browser.close();
|
|
228
303
|
}
|
|
@@ -232,13 +307,6 @@ async function scrapeRank(channelId, rankTypeId, periodId) {
|
|
|
232
307
|
return null;
|
|
233
308
|
}
|
|
234
309
|
|
|
235
|
-
// 按书名匹配 URL
|
|
236
|
-
const norm = s => (s || "").replace(/\s+/g, "");
|
|
237
|
-
for (const b of books) {
|
|
238
|
-
const matched = urls.find(u => norm(u.title) === norm(b.title));
|
|
239
|
-
if (matched) b.url = matched.url;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
310
|
const summary = summarizeQuality(books, rawCount);
|
|
243
311
|
console.log(` ✓ 提取 ${books.length} 本(链接 ${summary.linked}/${books.length},热度 ${summary.heated}/${books.length})`);
|
|
244
312
|
|