@cloud411716/fancy-webnovel 0.2.14 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud411716/fancy-webnovel",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -143,24 +143,17 @@ function renderQidianMarkdown(rankLabel, url, books) {
143
143
  `- 来源:${url}`,
144
144
  `- 抓取方式:mobile-ssr`,
145
145
  `- 抓取时间:${now}`,
146
- `- 条目数:${books.length}`,
147
146
  '',
148
147
  '---',
149
148
  '',
150
149
  ];
151
150
 
152
151
  for (const b of books) {
153
- lines.push(`## #${b.rank} ${b.title}`);
154
- const meta = [b.author, b.genre, b.status].filter(Boolean).join(' · ');
155
- if (meta) lines.push(`*${meta}*`);
156
- const req = (v) => (v === undefined || v === null || v === '') ? '[待补]' : String(v);
157
- lines.push(`**字数:** ${req(b.words)}`);
158
- if (b.rankValue) lines.push(`**榜单值:** ${b.rankValue}`);
159
- lines.push(`**总推荐:** ${req(b.totalRecommend)}`);
160
- lines.push(`**签约:** ${req(b.signStatus)}`);
161
- lines.push(`**收费:** ${req(b.vipStatus)}`);
162
- if (b.url) lines.push(`[作品页](${b.url})`);
163
- lines.push('', '---', '');
152
+ lines.push(`书名:${b.title || ''}`);
153
+ lines.push(`题材:${b.genre || ''}`);
154
+ lines.push(`作者:${b.author || ''}`);
155
+ if (b.url) lines.push(`作品页:${b.url}`);
156
+ lines.push('');
164
157
  }
165
158
 
166
159
  return lines.join('\n');
@@ -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
- let books, urls, rawCount;
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
- urls = await page.evaluate(() => {
212
- var byId = {};
213
- var order = [];
214
- Array.from(document.querySelectorAll("a")).forEach(a => {
215
- var h = (a.getAttribute("href") || a.href || "").match(/\/(?:shuku|book)\/([0-9]+)/);
216
- if (!h) return;
217
- var id = h[1];
218
- var t = (a.innerText || a.textContent || "").replace(/\s+/g, " ").trim();
219
- if (!byId[id]) { byId[id] = ""; order.push(id); }
220
- if (t && !/^[0-9]+$/.test(t) && !/^(最近更新、最新章节、最新)/.test(t)) {
221
- if (t.length > byId[id].length) byId[id] = t;
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
- return order.map(id => ({ bookId: id, title: byId[id], url: "https://www.qimao.com/shuku/" + id + "/" }));
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