@cloud411716/fancy-webnovel 0.2.15 → 0.2.16
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
|
@@ -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
|
|