@sovovs/bycli 2.1.29 → 2.1.31
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/clis/juejin/search.js +32 -1
- package/dist/src/output.js +26 -5
- package/package.json +1 -1
package/clis/juejin/search.js
CHANGED
|
@@ -26,6 +26,27 @@ const PAGE_SIZE = 20;
|
|
|
26
26
|
const MAX_LIMIT = 200;
|
|
27
27
|
const MAX_PAGES = 30;
|
|
28
28
|
|
|
29
|
+
// 掘金对**非 0 的时间窗**(search_type=1/2/3)会间歇性返回空首页:`data: []`
|
|
30
|
+
// 但 err_no 仍是 0 / err_msg "success",整个响应体除了 cursor 里的实例标识
|
|
31
|
+
// 之外和有数据时完全一致,只有 has_more 跟着变 false——像是部分后端分片对时间
|
|
32
|
+
// 窗查询答不出来。
|
|
33
|
+
//
|
|
34
|
+
// 单次请求的空概率实测 **76%**(query=rust/period=day,25 次里 19 次空),
|
|
35
|
+
// 且与请求间隔无关(0ms 9/12、400ms 10/12、1500ms 11/12——加延迟只会更差),
|
|
36
|
+
// 所以各次尝试相互独立、退避没有意义。改过 8 种参数组合(去 cursor / 去 limit /
|
|
37
|
+
// 去 spider / 补 version / 换 sort_type / 换 id_type / 最小参数集)空率都在
|
|
38
|
+
// 6-9/10,没有能让它确定化的参数。
|
|
39
|
+
//
|
|
40
|
+
// 首次成功所需次数实测分布(12 轮):中位数 3、p90 5、最大 7。取 12 次把
|
|
41
|
+
// 独立失败概率压到 0.76^12 ≈ 3%(4 次时是 33%,实测正好对应 4/6 的命中率)。
|
|
42
|
+
//
|
|
43
|
+
// 真实无结果(query="鿃鿄鿅鿆" / "zzzqqqxxx...")连打 12-14 次稳定是 0,所以
|
|
44
|
+
// 重试只会救回抖动,不会把空态变成假数据。注意掘金对**无匹配**的查询会退化成
|
|
45
|
+
// 返回无关的推荐文章,但那属于「有数据」分支,和这里的空首页是两回事。
|
|
46
|
+
//
|
|
47
|
+
// 只重试**首页且一行都没拿到**的情形:翻页途中的空页本来就是正常的终止信号。
|
|
48
|
+
const EMPTY_RETRY_ATTEMPTS = 12;
|
|
49
|
+
|
|
29
50
|
const TYPES = {
|
|
30
51
|
all: 0,
|
|
31
52
|
article: 2,
|
|
@@ -308,9 +329,18 @@ cli({
|
|
|
308
329
|
|
|
309
330
|
while (rows.length < limit && pages < MAX_PAGES) {
|
|
310
331
|
const url = buildUrl({ query, idType, sortType, period, cursor });
|
|
311
|
-
|
|
332
|
+
let payload = await fetchPage(url);
|
|
312
333
|
pages += 1;
|
|
313
334
|
|
|
335
|
+
// 首页空 → 可能是上面说的分片抖动,有界重试;真实空态重试后仍是空。
|
|
336
|
+
// 不加退避:实测各次尝试独立,延迟只会拖慢且略微更差。
|
|
337
|
+
if (rows.length === 0 && payload.data.length === 0) {
|
|
338
|
+
for (let attempt = 1; attempt < EMPTY_RETRY_ATTEMPTS; attempt += 1) {
|
|
339
|
+
payload = await fetchPage(url);
|
|
340
|
+
if (payload.data.length > 0) break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
314
344
|
for (const entry of payload.data) {
|
|
315
345
|
const normalized = normalizeEntry(entry);
|
|
316
346
|
if (!normalized) continue;
|
|
@@ -357,6 +387,7 @@ export const __test__ = {
|
|
|
357
387
|
PAGE_SIZE,
|
|
358
388
|
MAX_LIMIT,
|
|
359
389
|
MAX_PAGES,
|
|
390
|
+
EMPTY_RETRY_ATTEMPTS,
|
|
360
391
|
requireQuery,
|
|
361
392
|
requireChoice,
|
|
362
393
|
requireLimit,
|
package/dist/src/output.js
CHANGED
|
@@ -10,6 +10,26 @@ function normalizeRows(data) {
|
|
|
10
10
|
return [data];
|
|
11
11
|
return [{ value: data }];
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Render one cell for the flat text formats (table / plain / md / csv).
|
|
15
|
+
*
|
|
16
|
+
* Adapters occasionally expose a structured column (e.g. juejin/search's
|
|
17
|
+
* per-kind `extra`), which `String(v)` would turn into "[object Object]".
|
|
18
|
+
* JSON is lossless and still one line, so it degrades gracefully.
|
|
19
|
+
*/
|
|
20
|
+
function formatCell(v) {
|
|
21
|
+
if (v === null || v === undefined)
|
|
22
|
+
return '';
|
|
23
|
+
if (typeof v === 'object') {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(v);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return String(v);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return String(v);
|
|
32
|
+
}
|
|
13
33
|
function resolveColumns(rows, opts) {
|
|
14
34
|
return opts.columns ?? Object.keys(rows[0] ?? {});
|
|
15
35
|
}
|
|
@@ -63,8 +83,7 @@ function renderTable(data, opts) {
|
|
|
63
83
|
});
|
|
64
84
|
for (const row of rows) {
|
|
65
85
|
table.push(columns.map(c => {
|
|
66
|
-
|
|
67
|
-
return v === null || v === undefined ? '' : String(v);
|
|
86
|
+
return formatCell(row[c]);
|
|
68
87
|
}));
|
|
69
88
|
}
|
|
70
89
|
console.log();
|
|
@@ -101,7 +120,9 @@ function renderPlain(data, opts) {
|
|
|
101
120
|
}
|
|
102
121
|
}
|
|
103
122
|
rows.forEach((row, index) => {
|
|
104
|
-
const entries = Object.entries(row)
|
|
123
|
+
const entries = Object.entries(row)
|
|
124
|
+
.map(([key, value]) => [key, formatCell(value)])
|
|
125
|
+
.filter(([, value]) => value !== '');
|
|
105
126
|
entries.forEach(([key, value]) => {
|
|
106
127
|
console.log(`${key}: ${value}`);
|
|
107
128
|
});
|
|
@@ -127,7 +148,7 @@ function renderMarkdown(data, opts) {
|
|
|
127
148
|
console.log('| ' + columns.join(' | ') + ' |');
|
|
128
149
|
console.log('| ' + columns.map(() => '---').join(' | ') + ' |');
|
|
129
150
|
for (const row of rows) {
|
|
130
|
-
console.log('| ' + columns.map(c =>
|
|
151
|
+
console.log('| ' + columns.map(c => formatCell(row[c])).join(' | ') + ' |');
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
154
|
function renderCsv(data, opts) {
|
|
@@ -138,7 +159,7 @@ function renderCsv(data, opts) {
|
|
|
138
159
|
console.log(columns.join(','));
|
|
139
160
|
for (const row of rows) {
|
|
140
161
|
console.log(columns.map(c => {
|
|
141
|
-
const v =
|
|
162
|
+
const v = formatCell(row[c]);
|
|
142
163
|
return v.includes(',') || v.includes('"') || v.includes('\n') || v.includes('\r')
|
|
143
164
|
? `"${v.replace(/"/g, '""')}"` : v;
|
|
144
165
|
}).join(','));
|