@mxfb/tools 1.0.3 → 1.0.4
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/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -91,25 +91,35 @@ export async function readArchivePosts(dataDirPath) {
|
|
|
91
91
|
return Array.from(postsById.values());
|
|
92
92
|
}
|
|
93
93
|
async function runCrawl(baseUrl, dataDirPath, fetcher, options) {
|
|
94
|
-
const { followedKinds = ['index', 'permalink'], pages, limit = DEFAULT_LIMIT, saveRawPages = true, resume = false, useSitemap = true, onPageCrawled } = options;
|
|
94
|
+
const { followedKinds = ['index', 'permalink'], pages, limit = DEFAULT_LIMIT, saveRawPages = true, resume = false, retryFailures = false, useSitemap = true, onPageCrawled } = options;
|
|
95
95
|
const restoredState = resume ? await readCrawlState(dataDirPath, baseUrl) : null;
|
|
96
96
|
const robots = useSitemap ? await fetchRobots(baseUrl, fetcher) : null;
|
|
97
97
|
const sitemapLocations = useSitemap
|
|
98
98
|
? await collectSitemapLocations(baseUrl, robots, fetcher)
|
|
99
99
|
: new Map();
|
|
100
100
|
const knownPostIds = new Set(restoredState?.knownPostIds ?? []);
|
|
101
|
-
const
|
|
101
|
+
const failuresByUrl = new Map((restoredState?.failures ?? []).map(failure => [failure.url, failure]));
|
|
102
|
+
const urlsToRetry = retryFailures ? Array.from(failuresByUrl.keys()) : [];
|
|
102
103
|
let blog = null;
|
|
103
104
|
let pageCount = 0;
|
|
105
|
+
// The page being reported is moved into `processed` only once the processor returns, so
|
|
106
|
+
// it is counted by hand. `foundPageCount` dedupes because the waitlist may hold a URL
|
|
107
|
+
// twice, or one already processed, and the count must never go backwards.
|
|
108
|
+
const countPages = () => ({
|
|
109
|
+
scannedPageCount: crawler.processed.size + 1,
|
|
110
|
+
foundPageCount: new Set([...crawler.processed, ...crawler.waitlist]).size
|
|
111
|
+
});
|
|
104
112
|
const reportFailure = (url, error) => {
|
|
105
113
|
const report = {
|
|
106
114
|
url,
|
|
107
115
|
kind: null,
|
|
108
116
|
postCount: 0,
|
|
109
117
|
newPostCount: 0,
|
|
110
|
-
error
|
|
118
|
+
error,
|
|
119
|
+
...countPages()
|
|
111
120
|
};
|
|
112
|
-
|
|
121
|
+
const previousCount = failuresByUrl.get(url)?.failureCount ?? 0;
|
|
122
|
+
failuresByUrl.set(url, { url, failureCount: previousCount + 1, error });
|
|
113
123
|
onPageCrawled?.(report);
|
|
114
124
|
};
|
|
115
125
|
const crawler = Crawler.create({
|
|
@@ -127,6 +137,7 @@ async function runCrawl(baseUrl, dataDirPath, fetcher, options) {
|
|
|
127
137
|
return reportFailure(url, toErrorMessage(error));
|
|
128
138
|
}
|
|
129
139
|
const page = fillLastModifiedFromSitemap(parsedPage, sitemapLocations, baseUrl);
|
|
140
|
+
failuresByUrl.delete(url);
|
|
130
141
|
pageCount = pageCount + 1;
|
|
131
142
|
blog ??= page.blog;
|
|
132
143
|
if (saveRawPages)
|
|
@@ -145,18 +156,24 @@ async function runCrawl(baseUrl, dataDirPath, fetcher, options) {
|
|
|
145
156
|
kind: page.kind,
|
|
146
157
|
postCount: page.posts.length,
|
|
147
158
|
newPostCount,
|
|
148
|
-
error: null
|
|
159
|
+
error: null,
|
|
160
|
+
...countPages()
|
|
149
161
|
});
|
|
150
162
|
}
|
|
151
163
|
});
|
|
152
|
-
|
|
164
|
+
const retriedUrls = new Set(urlsToRetry);
|
|
165
|
+
for (const processedUrl of restoredState?.processedUrls ?? []) {
|
|
166
|
+
if (retriedUrls.has(processedUrl))
|
|
167
|
+
continue;
|
|
153
168
|
crawler.processed.add(processedUrl);
|
|
169
|
+
}
|
|
154
170
|
const seedUrls = pages === undefined
|
|
155
171
|
? [baseUrl]
|
|
156
172
|
: buildListingPageUrls(baseUrl, pages.from, pages.to);
|
|
157
173
|
if (followedKinds.includes('permalink'))
|
|
158
174
|
seedUrls.push(...sitemapLocations.keys());
|
|
159
175
|
seedUrls.push(...restoredState?.waitlistUrls ?? []);
|
|
176
|
+
seedUrls.push(...urlsToRetry);
|
|
160
177
|
const pendingSeedUrls = Array.from(new Set(seedUrls
|
|
161
178
|
.map(seedUrl => normalizeUrl(seedUrl, baseUrl))
|
|
162
179
|
.filter((seedUrl) => seedUrl !== null)))
|
|
@@ -170,13 +187,14 @@ async function runCrawl(baseUrl, dataDirPath, fetcher, options) {
|
|
|
170
187
|
baseUrl,
|
|
171
188
|
processedUrls: Array.from(crawler.processed),
|
|
172
189
|
waitlistUrls: crawler.waitlist,
|
|
173
|
-
knownPostIds: Array.from(knownPostIds)
|
|
190
|
+
knownPostIds: Array.from(knownPostIds),
|
|
191
|
+
failures: Array.from(failuresByUrl.values())
|
|
174
192
|
});
|
|
175
193
|
return {
|
|
176
194
|
dataDirPath,
|
|
177
195
|
blog,
|
|
178
196
|
pageCount,
|
|
179
197
|
postCount: knownPostIds.size,
|
|
180
|
-
failures
|
|
198
|
+
failures: Array.from(failuresByUrl.values())
|
|
181
199
|
};
|
|
182
200
|
}
|
|
@@ -16,6 +16,8 @@ import type { TumblrBlog, TumblrPageKind } from '../../../agnostic/scrapers/@tum
|
|
|
16
16
|
* @property saveRawPages - Keeps each page's raw HTML under `raw/`, so a later parser fix
|
|
17
17
|
* can be replayed without fetching anything again. Defaults to `true`.
|
|
18
18
|
* @property resume - Picks up where a previous crawl of the same directory stopped.
|
|
19
|
+
* @property retryFailures - Fetches again the URLs a previous run recorded as failed,
|
|
20
|
+
* which a plain resume skips since they were marked processed. Needs `resume`.
|
|
19
21
|
* @property useSitemap - Reads `robots.txt` and `sitemap.xml` to enumerate posts without
|
|
20
22
|
* walking listings. Defaults to `true`.
|
|
21
23
|
* @property onPageCrawled - Called once per page, successes and failures alike.
|
|
@@ -35,6 +37,7 @@ export type CrawlBlogOptions = {
|
|
|
35
37
|
delayMs?: number | (() => number);
|
|
36
38
|
saveRawPages?: boolean;
|
|
37
39
|
resume?: boolean;
|
|
40
|
+
retryFailures?: boolean;
|
|
38
41
|
useSitemap?: boolean;
|
|
39
42
|
onPageCrawled?: (report: CrawledPageReport) => void;
|
|
40
43
|
fetcher?: (url: string) => Promise<string | null>;
|
|
@@ -48,6 +51,10 @@ export type CrawlBlogOptions = {
|
|
|
48
51
|
* @property newPostCount - Posts the crawl had not seen before, which is what tells a
|
|
49
52
|
* listing worth following from one that only repeats itself.
|
|
50
53
|
* @property error - Why the page was skipped, `null` on success.
|
|
54
|
+
* @property scannedPageCount - Pages read so far, this one included, failures among them.
|
|
55
|
+
* @property foundPageCount - Pages the crawl knows of, read or still queued. It only ever
|
|
56
|
+
* grows: a listing crawl discovers as it goes, so this is what has been found rather than
|
|
57
|
+
* a total to divide by.
|
|
51
58
|
*/
|
|
52
59
|
export type CrawledPageReport = {
|
|
53
60
|
url: string;
|
|
@@ -55,6 +62,21 @@ export type CrawledPageReport = {
|
|
|
55
62
|
postCount: number;
|
|
56
63
|
newPostCount: number;
|
|
57
64
|
error: string | null;
|
|
65
|
+
scannedPageCount: number;
|
|
66
|
+
foundPageCount: number;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* A URL the crawl could not read, and how often it has resisted.
|
|
70
|
+
*
|
|
71
|
+
* @property url - The URL that failed.
|
|
72
|
+
* @property failureCount - Times it has failed, across every run against this archive. A
|
|
73
|
+
* URL that later succeeds is dropped rather than kept at its last count.
|
|
74
|
+
* @property error - Why it failed the last time it was tried.
|
|
75
|
+
*/
|
|
76
|
+
export type CrawlFailure = {
|
|
77
|
+
url: string;
|
|
78
|
+
failureCount: number;
|
|
79
|
+
error: string;
|
|
58
80
|
};
|
|
59
81
|
/**
|
|
60
82
|
* What one crawl achieved.
|
|
@@ -65,14 +87,15 @@ export type CrawledPageReport = {
|
|
|
65
87
|
* could be read.
|
|
66
88
|
* @property pageCount - Pages successfully parsed.
|
|
67
89
|
* @property postCount - Distinct posts seen across them.
|
|
68
|
-
* @property failures -
|
|
90
|
+
* @property failures - URLs still unread, this run's and those a previous run left
|
|
91
|
+
* behind, with how often each has failed. Empty when the archive holds no gap.
|
|
69
92
|
*/
|
|
70
93
|
export type CrawlBlogResult = {
|
|
71
94
|
dataDirPath: string;
|
|
72
95
|
blog: TumblrBlog | null;
|
|
73
96
|
pageCount: number;
|
|
74
97
|
postCount: number;
|
|
75
|
-
failures:
|
|
98
|
+
failures: CrawlFailure[];
|
|
76
99
|
};
|
|
77
100
|
/**
|
|
78
101
|
* A crawl's progress, as `state.json` holds it between two runs.
|
|
@@ -82,10 +105,13 @@ export type CrawlBlogResult = {
|
|
|
82
105
|
* again.
|
|
83
106
|
* @property waitlistUrls - URLs still owed a fetch when the crawl stopped.
|
|
84
107
|
* @property knownPostIds - Every post id seen so far.
|
|
108
|
+
* @property failures - URLs that could not be read, kept so a later run can report them
|
|
109
|
+
* and offer to try again.
|
|
85
110
|
*/
|
|
86
111
|
export type CrawlState = {
|
|
87
112
|
baseUrl: string;
|
|
88
113
|
processedUrls: string[];
|
|
89
114
|
waitlistUrls: string[];
|
|
90
115
|
knownPostIds: string[];
|
|
116
|
+
failures: CrawlFailure[];
|
|
91
117
|
};
|
|
@@ -145,7 +145,8 @@ export async function rebuildCrawlStateFromRecords(dataDirPath, baseUrl) {
|
|
|
145
145
|
baseUrl,
|
|
146
146
|
processedUrls: Array.from(processedUrls),
|
|
147
147
|
waitlistUrls: [],
|
|
148
|
-
knownPostIds: Array.from(knownPostIds)
|
|
148
|
+
knownPostIds: Array.from(knownPostIds),
|
|
149
|
+
failures: []
|
|
149
150
|
};
|
|
150
151
|
}
|
|
151
152
|
export async function* readPageRecords(dataDirPath) {
|
|
@@ -260,9 +261,30 @@ function toCrawlStateOrNull(value) {
|
|
|
260
261
|
baseUrl,
|
|
261
262
|
processedUrls: toStringArray(record.processedUrls),
|
|
262
263
|
waitlistUrls: toStringArray(record.waitlistUrls),
|
|
263
|
-
knownPostIds: toStringArray(record.knownPostIds)
|
|
264
|
+
knownPostIds: toStringArray(record.knownPostIds),
|
|
265
|
+
failures: toCrawlFailures(record.failures)
|
|
264
266
|
};
|
|
265
267
|
}
|
|
268
|
+
function toCrawlFailures(value) {
|
|
269
|
+
if (!Array.isArray(value))
|
|
270
|
+
return [];
|
|
271
|
+
const failures = [];
|
|
272
|
+
for (const item of value) {
|
|
273
|
+
if (typeof item !== 'object' || item === null)
|
|
274
|
+
continue;
|
|
275
|
+
/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */
|
|
276
|
+
const record = item;
|
|
277
|
+
const url = record.url;
|
|
278
|
+
if (typeof url !== 'string')
|
|
279
|
+
continue;
|
|
280
|
+
failures.push({
|
|
281
|
+
url,
|
|
282
|
+
failureCount: typeof record.failureCount === 'number' ? record.failureCount : 1,
|
|
283
|
+
error: typeof record.error === 'string' ? record.error : 'unknown'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return failures;
|
|
287
|
+
}
|
|
266
288
|
function toPageRecordDigest(value) {
|
|
267
289
|
if (typeof value !== 'object' || value === null)
|
|
268
290
|
return { url: null, postIds: [] };
|