@crawlee/utils 4.0.0-beta.10 → 4.0.0-beta.100
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/README.md +17 -13
- package/index.d.ts +1 -7
- package/index.js +1 -6
- package/internals/blocked.d.ts +0 -1
- package/internals/blocked.js +0 -1
- package/internals/cheerio.d.ts +3 -2
- package/internals/cheerio.js +4 -5
- package/internals/extract-urls.d.ts +5 -1
- package/internals/extract-urls.js +8 -5
- package/internals/general.d.ts +0 -25
- package/internals/general.js +2 -110
- package/internals/iterables.d.ts +47 -0
- package/internals/iterables.js +96 -0
- package/internals/open_graph_parser.d.ts +2 -3
- package/internals/open_graph_parser.js +8 -9
- package/internals/robots.d.ts +14 -5
- package/internals/robots.js +31 -35
- package/internals/sitemap.d.ts +65 -8
- package/internals/sitemap.js +174 -34
- package/internals/social.d.ts +1 -2
- package/internals/social.js +7 -5
- package/internals/url.d.ts +1 -2
- package/internals/url.js +1 -2
- package/package.json +6 -6
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/internals/blocked.d.ts.map +0 -1
- package/internals/blocked.js.map +0 -1
- package/internals/cheerio.d.ts.map +0 -1
- package/internals/cheerio.js.map +0 -1
- package/internals/chunk.d.ts +0 -2
- package/internals/chunk.d.ts.map +0 -1
- package/internals/chunk.js +0 -40
- package/internals/chunk.js.map +0 -1
- package/internals/debug.d.ts +0 -31
- package/internals/debug.d.ts.map +0 -1
- package/internals/debug.js +0 -29
- package/internals/debug.js.map +0 -1
- package/internals/extract-urls.d.ts.map +0 -1
- package/internals/extract-urls.js.map +0 -1
- package/internals/general.d.ts.map +0 -1
- package/internals/general.js.map +0 -1
- package/internals/open_graph_parser.d.ts.map +0 -1
- package/internals/open_graph_parser.js.map +0 -1
- package/internals/robots.d.ts.map +0 -1
- package/internals/robots.js.map +0 -1
- package/internals/sitemap.d.ts.map +0 -1
- package/internals/sitemap.js.map +0 -1
- package/internals/social.d.ts.map +0 -1
- package/internals/social.js.map +0 -1
- package/internals/system-info/cpu-info.d.ts +0 -64
- package/internals/system-info/cpu-info.d.ts.map +0 -1
- package/internals/system-info/cpu-info.js +0 -211
- package/internals/system-info/cpu-info.js.map +0 -1
- package/internals/system-info/memory-info.d.ts +0 -28
- package/internals/system-info/memory-info.d.ts.map +0 -1
- package/internals/system-info/memory-info.js +0 -118
- package/internals/system-info/memory-info.js.map +0 -1
- package/internals/system-info/ps-tree.d.ts +0 -18
- package/internals/system-info/ps-tree.d.ts.map +0 -1
- package/internals/system-info/ps-tree.js +0 -145
- package/internals/system-info/ps-tree.js.map +0 -1
- package/internals/typedefs.d.ts +0 -5
- package/internals/typedefs.d.ts.map +0 -1
- package/internals/typedefs.js +0 -9
- package/internals/typedefs.js.map +0 -1
- package/internals/url.d.ts.map +0 -1
- package/internals/url.js.map +0 -1
- package/tsconfig.build.tsbuildinfo +0 -1
package/internals/robots.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FetchHttpClient } from '@crawlee/http-client';
|
|
2
2
|
import robotsParser from 'robots-parser';
|
|
3
3
|
import { Sitemap } from './sitemap.js';
|
|
4
|
-
let HTTPError;
|
|
5
4
|
/**
|
|
6
5
|
* Loads and queries information from a [robots.txt file](https://en.wikipedia.org/wiki/Robots.txt).
|
|
7
6
|
*
|
|
@@ -23,20 +22,25 @@ let HTTPError;
|
|
|
23
22
|
export class RobotsTxtFile {
|
|
24
23
|
robots;
|
|
25
24
|
proxyUrl;
|
|
26
|
-
|
|
25
|
+
logger;
|
|
26
|
+
constructor(robots, proxyUrl, logger) {
|
|
27
27
|
this.robots = robots;
|
|
28
28
|
this.proxyUrl = proxyUrl;
|
|
29
|
+
this.logger = logger;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* Determine the location of a robots.txt file for a URL and fetch it.
|
|
32
33
|
* @param url the URL to fetch robots.txt for
|
|
33
|
-
* @param [
|
|
34
|
+
* @param [options] additional options
|
|
35
|
+
* @param [options.signal] an AbortSignal to cancel the request
|
|
36
|
+
* @param [options.timeoutMillis] timeout in milliseconds for the request
|
|
37
|
+
* @param [options.proxyUrl] a proxy to be used for fetching the robots.txt file
|
|
34
38
|
*/
|
|
35
|
-
static async find(url,
|
|
39
|
+
static async find(url, options) {
|
|
36
40
|
const robotsTxtFileUrl = new URL(url);
|
|
37
41
|
robotsTxtFileUrl.pathname = '/robots.txt';
|
|
38
42
|
robotsTxtFileUrl.search = '';
|
|
39
|
-
return RobotsTxtFile.load(robotsTxtFileUrl.toString(),
|
|
43
|
+
return RobotsTxtFile.load(robotsTxtFileUrl.toString(), options);
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
42
46
|
* Allows providing the URL and robots.txt content explicitly instead of loading it from the target site.
|
|
@@ -48,33 +52,28 @@ export class RobotsTxtFile {
|
|
|
48
52
|
// @ts-ignore
|
|
49
53
|
return new RobotsTxtFile(robotsParser(url, content), proxyUrl);
|
|
50
54
|
}
|
|
51
|
-
static async load(url,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
responseType: 'text',
|
|
61
|
-
});
|
|
62
|
-
// @ts-ignore
|
|
63
|
-
return new RobotsTxtFile(robotsParser(url.toString(), response.body), proxyUrl);
|
|
55
|
+
static async load(url, options) {
|
|
56
|
+
const { proxyUrl, logger, httpClient = new FetchHttpClient() } = options || {};
|
|
57
|
+
const response = await httpClient.sendRequest(new Request(url, { method: 'GET' }), {
|
|
58
|
+
proxyUrl,
|
|
59
|
+
timeoutMillis: options?.timeoutMillis,
|
|
60
|
+
signal: options?.signal,
|
|
61
|
+
});
|
|
62
|
+
if (response.status < 200 || response.status >= 300) {
|
|
63
|
+
throw new Error(`Failed to load robots.txt from ${url}: HTTP ${response.status}`);
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}, proxyUrl);
|
|
75
|
-
}
|
|
76
|
-
throw e;
|
|
65
|
+
if (response.status === 404) {
|
|
66
|
+
return new RobotsTxtFile({
|
|
67
|
+
isAllowed() {
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
getSitemaps() {
|
|
71
|
+
return [];
|
|
72
|
+
},
|
|
73
|
+
}, proxyUrl, logger);
|
|
77
74
|
}
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
return new RobotsTxtFile(robotsParser(url.toString(), await response.text()), proxyUrl, logger);
|
|
78
77
|
}
|
|
79
78
|
/**
|
|
80
79
|
* Check if a URL should be crawled by robots.
|
|
@@ -94,7 +93,7 @@ export class RobotsTxtFile {
|
|
|
94
93
|
* Parse all the sitemaps referenced in the robots file.
|
|
95
94
|
*/
|
|
96
95
|
async parseSitemaps() {
|
|
97
|
-
return Sitemap.load(this.robots.getSitemaps(), this.proxyUrl);
|
|
96
|
+
return Sitemap.load(this.robots.getSitemaps(), this.proxyUrl, { logger: this.logger });
|
|
98
97
|
}
|
|
99
98
|
/**
|
|
100
99
|
* Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
|
|
@@ -103,6 +102,3 @@ export class RobotsTxtFile {
|
|
|
103
102
|
return (await this.parseSitemaps()).urls;
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
|
-
// to stay backwards compatible
|
|
107
|
-
export { RobotsTxtFile as RobotsFile };
|
|
108
|
-
//# sourceMappingURL=robots.js.map
|
package/internals/sitemap.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import type { Delays } from 'got-scraping';
|
|
1
|
+
import type { BaseHttpClient, CrawleeLogger } from '@crawlee/types';
|
|
3
2
|
interface SitemapUrlData {
|
|
4
3
|
loc: string;
|
|
5
4
|
lastmod?: Date;
|
|
@@ -36,9 +35,29 @@ export interface ParseSitemapOptions {
|
|
|
36
35
|
*/
|
|
37
36
|
sitemapRetries?: number;
|
|
38
37
|
/**
|
|
39
|
-
*
|
|
38
|
+
* Timeout settings for network requests when fetching sitemaps. By default this is `30000` milliseconds (30 seconds).
|
|
40
39
|
*/
|
|
41
|
-
|
|
40
|
+
timeoutMillis?: number;
|
|
41
|
+
/**
|
|
42
|
+
* If true, the parser will log a warning if it fails to fetch a sitemap due to a network error
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
reportNetworkErrors?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Custom HTTP client to be used for fetching sitemaps.
|
|
48
|
+
*/
|
|
49
|
+
httpClient?: BaseHttpClient;
|
|
50
|
+
/**
|
|
51
|
+
* Optional filter for nested sitemap URLs discovered in sitemap index files.
|
|
52
|
+
* Called with the URL of each child sitemap before it is fetched.
|
|
53
|
+
* Return `true` to include the sitemap, `false` to skip it.
|
|
54
|
+
* If not provided, all nested sitemaps are followed.
|
|
55
|
+
*/
|
|
56
|
+
nestedSitemapFilter?: (sitemapUrl: string) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Optional logger for reporting warnings during sitemap parsing.
|
|
59
|
+
*/
|
|
60
|
+
logger?: CrawleeLogger;
|
|
42
61
|
}
|
|
43
62
|
export declare function parseSitemap<T extends ParseSitemapOptions>(initialSources: SitemapSource[], proxyUrl?: string, options?: T): AsyncIterable<T['emitNestedSitemaps'] extends true ? SitemapUrl | NestedSitemap : SitemapUrl>;
|
|
44
63
|
/**
|
|
@@ -62,7 +81,7 @@ export declare class Sitemap {
|
|
|
62
81
|
* @param url The domain URL to fetch the sitemap for.
|
|
63
82
|
* @param proxyUrl A proxy to be used for fetching the sitemap file.
|
|
64
83
|
*/
|
|
65
|
-
static tryCommonNames(url: string, proxyUrl?: string): Promise<Sitemap>;
|
|
84
|
+
static tryCommonNames(url: string, proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
|
|
66
85
|
/**
|
|
67
86
|
* Fetch sitemap content from given URL or URLs and return URLs of referenced pages.
|
|
68
87
|
* @param urls sitemap URL(s)
|
|
@@ -74,8 +93,46 @@ export declare class Sitemap {
|
|
|
74
93
|
* @param content XML sitemap content
|
|
75
94
|
* @param proxyUrl URL of a proxy to be used for fetching sitemap contents
|
|
76
95
|
*/
|
|
77
|
-
static fromXmlString(content: string, proxyUrl?: string): Promise<Sitemap>;
|
|
78
|
-
|
|
96
|
+
static fromXmlString(content: string, proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
|
|
97
|
+
private static parse;
|
|
79
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Given a list of URLs, discover related sitemap files for these domains by checking the `robots.txt` file,
|
|
101
|
+
* the default `sitemap.xml` & `sitemap.txt` files and the URLs themselves.
|
|
102
|
+
* @param `urls` The list of URLs to discover sitemaps for.
|
|
103
|
+
* @param `options` Options for sitemap discovery
|
|
104
|
+
* @returns An async iterable with the discovered sitemap URLs.
|
|
105
|
+
*/
|
|
106
|
+
export declare function discoverValidSitemaps(urls: string[], options?: {
|
|
107
|
+
/**
|
|
108
|
+
* Proxy URL to be used for network requests.
|
|
109
|
+
*/
|
|
110
|
+
proxyUrl?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Timeout in milliseconds for the entire `discoverValidSitemaps` call.
|
|
113
|
+
* An `AbortController` is created internally and its signal is passed to every HTTP request,
|
|
114
|
+
* so the whole discovery operation is cancelled once the timeout elapses.
|
|
115
|
+
* Defaults to `60_000` ms (60 seconds) to prevent indefinite hangs.
|
|
116
|
+
*/
|
|
117
|
+
timeoutMillis?: number;
|
|
118
|
+
/**
|
|
119
|
+
* An external `AbortSignal` to cancel the entire discovery operation.
|
|
120
|
+
* If both `signal` and `timeout` are provided, the operation is cancelled
|
|
121
|
+
* when either the signal is aborted or the timeout elapses (whichever comes first).
|
|
122
|
+
*/
|
|
123
|
+
signal?: AbortSignal;
|
|
124
|
+
/**
|
|
125
|
+
* Timeout in milliseconds for each individual HTTP request during discovery.
|
|
126
|
+
* Defaults to `20000` ms (20 seconds).
|
|
127
|
+
*/
|
|
128
|
+
requestTimeoutMillis?: number;
|
|
129
|
+
/**
|
|
130
|
+
* HTTP client to be used for network requests.
|
|
131
|
+
*/
|
|
132
|
+
httpClient?: BaseHttpClient;
|
|
133
|
+
/**
|
|
134
|
+
* Optional logger for reporting warnings during sitemap discovery.
|
|
135
|
+
*/
|
|
136
|
+
logger?: CrawleeLogger;
|
|
137
|
+
}): AsyncIterable<string>;
|
|
80
138
|
export {};
|
|
81
|
-
//# sourceMappingURL=sitemap.d.ts.map
|
package/internals/sitemap.js
CHANGED
|
@@ -2,9 +2,12 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { PassThrough, pipeline, Readable, Transform } from 'node:stream';
|
|
3
3
|
import { StringDecoder } from 'node:string_decoder';
|
|
4
4
|
import { createGunzip } from 'node:zlib';
|
|
5
|
+
import { FetchHttpClient } from '@crawlee/http-client';
|
|
6
|
+
import { fileTypeStream } from 'file-type';
|
|
5
7
|
import sax from 'sax';
|
|
6
8
|
import MIMEType from 'whatwg-mimetype';
|
|
7
|
-
import
|
|
9
|
+
import { mergeAsyncIterables } from './iterables.js';
|
|
10
|
+
import { RobotsTxtFile } from './robots.js';
|
|
8
11
|
class SitemapTxtParser extends Transform {
|
|
9
12
|
decoder = new StringDecoder('utf8');
|
|
10
13
|
buffer = '';
|
|
@@ -91,8 +94,10 @@ class SitemapXmlParser extends Transform {
|
|
|
91
94
|
if (name === 'loc' || name === 'lastmod' || name === 'priority' || name === 'changefreq') {
|
|
92
95
|
this.currentTag = undefined;
|
|
93
96
|
}
|
|
94
|
-
if (name === 'url'
|
|
95
|
-
|
|
97
|
+
if (name === 'url') {
|
|
98
|
+
if (this.url.loc !== undefined) {
|
|
99
|
+
this.push({ type: 'url', ...this.url, loc: this.url.loc });
|
|
100
|
+
}
|
|
96
101
|
this.url = {};
|
|
97
102
|
}
|
|
98
103
|
}
|
|
@@ -108,7 +113,10 @@ class SitemapXmlParser extends Transform {
|
|
|
108
113
|
}
|
|
109
114
|
text = text.trim();
|
|
110
115
|
if (this.currentTag === 'lastmod') {
|
|
111
|
-
|
|
116
|
+
const lastmod = new Date(text);
|
|
117
|
+
if (!Number.isNaN(lastmod.getTime())) {
|
|
118
|
+
this.url.lastmod = lastmod;
|
|
119
|
+
}
|
|
112
120
|
}
|
|
113
121
|
if (this.currentTag === 'priority') {
|
|
114
122
|
this.url.priority = Number(text);
|
|
@@ -121,9 +129,7 @@ class SitemapXmlParser extends Transform {
|
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
131
|
export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
124
|
-
const {
|
|
125
|
-
const { fileTypeStream } = await import('file-type');
|
|
126
|
-
const { emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, networkTimeouts } = options ?? {};
|
|
132
|
+
const { httpClient = new FetchHttpClient(), emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, timeoutMillis: timeout = 30000, reportNetworkErrors = true, nestedSitemapFilter, logger, } = options ?? {};
|
|
127
133
|
const sources = [...initialSources];
|
|
128
134
|
const visitedSitemapUrls = new Set();
|
|
129
135
|
const createParser = (contentType = '', url) => {
|
|
@@ -145,7 +151,6 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
145
151
|
while (sources.length > 0) {
|
|
146
152
|
const source = sources.shift();
|
|
147
153
|
if ((source?.depth ?? 0) > maxDepth) {
|
|
148
|
-
log.debug(`Skipping sitemap ${source.type === 'url' ? source.url : ''} because it reached max depth ${maxDepth}.`);
|
|
149
154
|
continue;
|
|
150
155
|
}
|
|
151
156
|
let items = null;
|
|
@@ -155,23 +160,28 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
155
160
|
let retriesLeft = sitemapRetries + 1;
|
|
156
161
|
while (retriesLeft-- > 0) {
|
|
157
162
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
proxyUrl,
|
|
163
|
+
let sitemapResponse;
|
|
164
|
+
try {
|
|
165
|
+
sitemapResponse = await httpClient.sendRequest(new Request(sitemapUrl, {
|
|
162
166
|
method: 'GET',
|
|
163
|
-
timeout: networkTimeouts,
|
|
164
167
|
headers: {
|
|
165
|
-
accept: '
|
|
168
|
+
accept: '*/*',
|
|
166
169
|
},
|
|
170
|
+
}), {
|
|
171
|
+
proxyUrl,
|
|
172
|
+
timeoutMillis: timeout,
|
|
167
173
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
sitemapResponse = null;
|
|
177
|
+
}
|
|
171
178
|
let error = null;
|
|
172
|
-
if (
|
|
173
|
-
let contentType =
|
|
174
|
-
|
|
179
|
+
if (sitemapResponse && sitemapResponse.status >= 200 && sitemapResponse.status < 300) {
|
|
180
|
+
let contentType = sitemapResponse.headers.get('content-type');
|
|
181
|
+
if (sitemapResponse.body === null) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
const streamWithType = await fileTypeStream(Readable.fromWeb(sitemapResponse.body));
|
|
175
185
|
if (streamWithType.fileType !== undefined) {
|
|
176
186
|
contentType = streamWithType.fileType.mime;
|
|
177
187
|
}
|
|
@@ -184,29 +194,37 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
184
194
|
sitemapUrl.pathname = sitemapUrl.pathname.substring(0, sitemapUrl.pathname.length - 3);
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
|
-
items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), createParser(contentType, sitemapUrl), (e) => {
|
|
188
|
-
if (e !== undefined) {
|
|
189
|
-
error = e;
|
|
197
|
+
items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), createParser(contentType ?? undefined, sitemapUrl), (e) => {
|
|
198
|
+
if (e !== undefined && e !== null) {
|
|
199
|
+
error = { type: 'parser', error: e };
|
|
190
200
|
}
|
|
191
201
|
});
|
|
192
202
|
}
|
|
193
203
|
else {
|
|
194
|
-
error =
|
|
204
|
+
error = {
|
|
205
|
+
type: 'fetch',
|
|
206
|
+
error: new Error(`Failed to fetch sitemap: ${sitemapUrl}, status code: ${sitemapResponse?.status}`),
|
|
207
|
+
};
|
|
195
208
|
}
|
|
196
209
|
if (error !== null) {
|
|
197
|
-
|
|
210
|
+
const shouldIgnoreError = error.type === 'fetch' && !reportNetworkErrors;
|
|
211
|
+
if (!shouldIgnoreError) {
|
|
212
|
+
throw error.error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
break;
|
|
198
217
|
}
|
|
199
|
-
break;
|
|
200
218
|
}
|
|
201
219
|
catch (e) {
|
|
202
|
-
|
|
220
|
+
logger?.warning(`Malformed sitemap content: ${sitemapUrl}, ${retriesLeft === 0 ? 'no retries left.' : 'retrying...'} (${e})`);
|
|
203
221
|
}
|
|
204
222
|
}
|
|
205
223
|
}
|
|
206
224
|
else if (source.type === 'raw') {
|
|
207
225
|
items = pipeline(Readable.from([source.content]), createParser('text/xml'), (error) => {
|
|
208
226
|
if (error !== undefined) {
|
|
209
|
-
|
|
227
|
+
logger?.warning(`Malformed sitemap content: ${error}`);
|
|
210
228
|
}
|
|
211
229
|
});
|
|
212
230
|
}
|
|
@@ -215,6 +233,10 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
215
233
|
}
|
|
216
234
|
for await (const item of items) {
|
|
217
235
|
if (item.type === 'sitemapUrl' && !visitedSitemapUrls.has(item.url)) {
|
|
236
|
+
if (nestedSitemapFilter && !nestedSitemapFilter(item.url)) {
|
|
237
|
+
logger?.debug(`Skipping sitemap ${item.url} due to nestedSitemapFilter.`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
218
240
|
sources.push({ type: 'url', url: item.url, depth: (source.depth ?? 0) + 1 });
|
|
219
241
|
if (emitNestedSitemaps) {
|
|
220
242
|
yield { loc: item.url, originSitemapUrl: null };
|
|
@@ -254,7 +276,7 @@ export class Sitemap {
|
|
|
254
276
|
* @param url The domain URL to fetch the sitemap for.
|
|
255
277
|
* @param proxyUrl A proxy to be used for fetching the sitemap file.
|
|
256
278
|
*/
|
|
257
|
-
static async tryCommonNames(url, proxyUrl) {
|
|
279
|
+
static async tryCommonNames(url, proxyUrl, parseSitemapOptions) {
|
|
258
280
|
const sitemapUrls = [];
|
|
259
281
|
const sitemapUrl = new URL(url);
|
|
260
282
|
sitemapUrl.search = '';
|
|
@@ -262,7 +284,7 @@ export class Sitemap {
|
|
|
262
284
|
sitemapUrls.push(sitemapUrl.toString());
|
|
263
285
|
sitemapUrl.pathname = '/sitemap.txt';
|
|
264
286
|
sitemapUrls.push(sitemapUrl.toString());
|
|
265
|
-
return Sitemap.load(sitemapUrls, proxyUrl);
|
|
287
|
+
return Sitemap.load(sitemapUrls, proxyUrl, { reportNetworkErrors: false, ...parseSitemapOptions });
|
|
266
288
|
}
|
|
267
289
|
/**
|
|
268
290
|
* Fetch sitemap content from given URL or URLs and return URLs of referenced pages.
|
|
@@ -277,8 +299,8 @@ export class Sitemap {
|
|
|
277
299
|
* @param content XML sitemap content
|
|
278
300
|
* @param proxyUrl URL of a proxy to be used for fetching sitemap contents
|
|
279
301
|
*/
|
|
280
|
-
static async fromXmlString(content, proxyUrl) {
|
|
281
|
-
return await this.parse([{ type: 'raw', content }], proxyUrl);
|
|
302
|
+
static async fromXmlString(content, proxyUrl, parseSitemapOptions) {
|
|
303
|
+
return await this.parse([{ type: 'raw', content }], proxyUrl, parseSitemapOptions);
|
|
282
304
|
}
|
|
283
305
|
static async parse(sources, proxyUrl, parseSitemapOptions) {
|
|
284
306
|
const urls = [];
|
|
@@ -287,10 +309,128 @@ export class Sitemap {
|
|
|
287
309
|
urls.push(item.loc);
|
|
288
310
|
}
|
|
289
311
|
}
|
|
290
|
-
catch {
|
|
312
|
+
catch (e) {
|
|
313
|
+
parseSitemapOptions?.logger?.warning(`Sitemap.load: Failed to load sitemap, returning empty result. (${e})`);
|
|
291
314
|
return new Sitemap([]);
|
|
292
315
|
}
|
|
293
316
|
return new Sitemap(urls);
|
|
294
317
|
}
|
|
295
318
|
}
|
|
296
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Given a list of URLs, discover related sitemap files for these domains by checking the `robots.txt` file,
|
|
321
|
+
* the default `sitemap.xml` & `sitemap.txt` files and the URLs themselves.
|
|
322
|
+
* @param `urls` The list of URLs to discover sitemaps for.
|
|
323
|
+
* @param `options` Options for sitemap discovery
|
|
324
|
+
* @returns An async iterable with the discovered sitemap URLs.
|
|
325
|
+
*/
|
|
326
|
+
export async function* discoverValidSitemaps(urls, options = {}) {
|
|
327
|
+
const { proxyUrl, timeoutMillis = 60_000, signal: externalSignal, requestTimeoutMillis = 20_000, httpClient = new FetchHttpClient(), logger, } = options;
|
|
328
|
+
const controller = new AbortController();
|
|
329
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMillis);
|
|
330
|
+
const onExternalAbort = () => controller.abort();
|
|
331
|
+
if (externalSignal) {
|
|
332
|
+
if (externalSignal.aborted) {
|
|
333
|
+
controller.abort();
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const signal = controller.signal;
|
|
340
|
+
const sitemapUrls = new Set();
|
|
341
|
+
const addSitemapUrl = (url) => {
|
|
342
|
+
const sizeBefore = sitemapUrls.size;
|
|
343
|
+
sitemapUrls.add(url);
|
|
344
|
+
if (sitemapUrls.size > sizeBefore) {
|
|
345
|
+
return url;
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
};
|
|
349
|
+
const urlExists = async (url) => {
|
|
350
|
+
if (!httpClient) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const response = await httpClient.sendRequest(new Request(url, { method: 'HEAD' }), {
|
|
355
|
+
proxyUrl,
|
|
356
|
+
timeoutMillis: requestTimeoutMillis,
|
|
357
|
+
signal,
|
|
358
|
+
});
|
|
359
|
+
return response.status >= 200 && response.status < 400;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const discoverSitemapsForDomainUrls = async function* (hostname, domainUrls) {
|
|
366
|
+
if (!hostname) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const robotsFile = await RobotsTxtFile.find(domainUrls[0], {
|
|
371
|
+
proxyUrl,
|
|
372
|
+
timeoutMillis: requestTimeoutMillis,
|
|
373
|
+
signal,
|
|
374
|
+
httpClient,
|
|
375
|
+
logger,
|
|
376
|
+
});
|
|
377
|
+
for (const sitemapUrl of robotsFile.getSitemaps()) {
|
|
378
|
+
if (addSitemapUrl(sitemapUrl)) {
|
|
379
|
+
yield sitemapUrl;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
logger?.warning(`Failed to fetch robots.txt file for ${hostname}`, { error: err });
|
|
385
|
+
}
|
|
386
|
+
const sitemapUrl = domainUrls.find((url) => /sitemap\.(?:xml|txt)(?:\.gz)?$/i.test(url));
|
|
387
|
+
if (sitemapUrl !== undefined) {
|
|
388
|
+
if (addSitemapUrl(sitemapUrl)) {
|
|
389
|
+
yield sitemapUrl;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
const firstUrl = new URL(domainUrls[0]);
|
|
394
|
+
const possibleSitemapPathnames = ['/sitemap.xml', '/sitemap.txt', '/sitemap_index.xml'];
|
|
395
|
+
const candidateSitemapUrls = possibleSitemapPathnames.map((pathname) => {
|
|
396
|
+
firstUrl.pathname = pathname;
|
|
397
|
+
return firstUrl.toString();
|
|
398
|
+
});
|
|
399
|
+
const candidateResults = await Promise.allSettled(candidateSitemapUrls.map(urlExists));
|
|
400
|
+
for (const [index, result] of candidateResults.entries()) {
|
|
401
|
+
const candidateSitemapUrl = candidateSitemapUrls[index];
|
|
402
|
+
if (result.status === 'fulfilled') {
|
|
403
|
+
if (result.value && addSitemapUrl(candidateSitemapUrl)) {
|
|
404
|
+
yield candidateSitemapUrl;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
logger?.debug(`Failed to check sitemap candidate ${candidateSitemapUrl} for ${hostname}`, {
|
|
409
|
+
error: result.reason,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const groupedUrls = urls.reduce((acc, url) => {
|
|
416
|
+
const hostname = new URL(url)?.hostname ?? '';
|
|
417
|
+
acc[hostname] ??= [];
|
|
418
|
+
acc[hostname].push(url);
|
|
419
|
+
return acc;
|
|
420
|
+
}, {});
|
|
421
|
+
const iterables = Object.entries(groupedUrls).map(([hostname, domainUrls]) => discoverSitemapsForDomainUrls(hostname, domainUrls));
|
|
422
|
+
const discoveredUrls = new Set();
|
|
423
|
+
try {
|
|
424
|
+
for await (const url of mergeAsyncIterables(...iterables)) {
|
|
425
|
+
if (discoveredUrls.has(url)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
discoveredUrls.add(url);
|
|
429
|
+
yield url;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
clearTimeout(timeoutHandle);
|
|
434
|
+
externalSignal?.removeEventListener('abort', onExternalAbort);
|
|
435
|
+
}
|
|
436
|
+
}
|
package/internals/social.d.ts
CHANGED
|
@@ -488,5 +488,4 @@ export declare const DISCORD_REGEX_GLOBAL: RegExp;
|
|
|
488
488
|
* so that the caller doesn't need to parse the HTML document again, if needed.
|
|
489
489
|
* @return An object with the social handles.
|
|
490
490
|
*/
|
|
491
|
-
export declare function parseHandlesFromHtml(html: string, data?: Record<string, unknown> | null): SocialHandles
|
|
492
|
-
//# sourceMappingURL=social.d.ts.map
|
|
491
|
+
export declare function parseHandlesFromHtml(html: string, data?: Record<string, unknown> | null): Promise<SocialHandles>;
|
package/internals/social.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import * as cheerio from 'cheerio';
|
|
2
1
|
import { htmlToText } from './cheerio.js';
|
|
3
2
|
// Regex inspired by https://zapier.com/blog/extract-links-email-phone-regex/
|
|
4
|
-
|
|
3
|
+
// The dot-atom local part and domain labels use RFC 5321 length bounds ({1,64}, {0,62})
|
|
4
|
+
// instead of unbounded quantifiers to avoid quadratic backtracking (ReDoS) on long
|
|
5
|
+
// dotted or hyphenated inputs, e.g. text scraped by parseHandlesFromHtml().
|
|
6
|
+
const EMAIL_REGEX_STRING = '(?:[a-z0-9!#$%&\'*+/=?^_`{|}~-]{1,64}(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]{1,64}){0,32}|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\\])';
|
|
5
7
|
/**
|
|
6
8
|
* Regular expression to exactly match a single email address.
|
|
7
9
|
* It has the following form: `/^...$/i`.
|
|
@@ -587,7 +589,8 @@ export const DISCORD_REGEX_GLOBAL = new RegExp(DISCORD_REGEX_STRING, 'ig');
|
|
|
587
589
|
* so that the caller doesn't need to parse the HTML document again, if needed.
|
|
588
590
|
* @return An object with the social handles.
|
|
589
591
|
*/
|
|
590
|
-
export function parseHandlesFromHtml(html, data = null) {
|
|
592
|
+
export async function parseHandlesFromHtml(html, data = null) {
|
|
593
|
+
const cheerio = await import('cheerio');
|
|
591
594
|
const result = {
|
|
592
595
|
emails: [],
|
|
593
596
|
phones: [],
|
|
@@ -606,7 +609,7 @@ export function parseHandlesFromHtml(html, data = null) {
|
|
|
606
609
|
const $ = cheerio.load(html, { xml: { decodeEntities: true } });
|
|
607
610
|
if (data)
|
|
608
611
|
data.$ = $;
|
|
609
|
-
const text = htmlToText($);
|
|
612
|
+
const text = await htmlToText($);
|
|
610
613
|
if (data)
|
|
611
614
|
data.text = text;
|
|
612
615
|
// NOTE: we need to parse each text separately, orherwise we might concatenate unrelated texts
|
|
@@ -644,4 +647,3 @@ export function parseHandlesFromHtml(html, data = null) {
|
|
|
644
647
|
}
|
|
645
648
|
return result;
|
|
646
649
|
}
|
|
647
|
-
//# sourceMappingURL=social.js.map
|
package/internals/url.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import type { SearchParams } from '@crawlee/types';
|
|
2
2
|
/**
|
|
3
3
|
* Appends search (query string) parameters to a URL, replacing the original value (if any).
|
|
4
4
|
*
|
|
@@ -7,4 +7,3 @@ export type SearchParams = string | URLSearchParams | Record<string, string | nu
|
|
|
7
7
|
* @internal
|
|
8
8
|
*/
|
|
9
9
|
export declare function applySearchParams(url: URL, searchParams: SearchParams | undefined): void;
|
|
10
|
-
//# sourceMappingURL=url.d.ts.map
|
package/internals/url.js
CHANGED
|
@@ -19,7 +19,7 @@ export function applySearchParams(url, searchParams) {
|
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
21
|
newSearchParams = new URLSearchParams();
|
|
22
|
-
for (const [key, value] of Object.entries(
|
|
22
|
+
for (const [key, value] of Object.entries(searchParams)) {
|
|
23
23
|
if (value === undefined) {
|
|
24
24
|
newSearchParams.delete(key);
|
|
25
25
|
}
|
|
@@ -33,4 +33,3 @@ export function applySearchParams(url, searchParams) {
|
|
|
33
33
|
}
|
|
34
34
|
url.search = newSearchParams.toString();
|
|
35
35
|
}
|
|
36
|
-
//# sourceMappingURL=url.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/utils",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.100",
|
|
4
4
|
"description": "A set of shared utilities that can be used by crawlers",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -35,19 +35,19 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://crawlee.dev",
|
|
37
37
|
"scripts": {
|
|
38
|
-
"build": "
|
|
38
|
+
"build": "pnpm clean && pnpm compile && pnpm copy",
|
|
39
39
|
"clean": "rimraf ./dist",
|
|
40
40
|
"compile": "tsc -p tsconfig.build.json",
|
|
41
41
|
"copy": "tsx ../../scripts/copy.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@apify/log": "^2.5.18",
|
|
45
44
|
"@apify/ps-tree": "^1.2.0",
|
|
46
|
-
"@crawlee/
|
|
45
|
+
"@crawlee/http-client": "4.0.0-beta.100",
|
|
46
|
+
"@crawlee/types": "4.0.0-beta.100",
|
|
47
47
|
"@types/sax": "^1.2.7",
|
|
48
48
|
"cheerio": "^1.0.0",
|
|
49
|
+
"domhandler": "^5.0.3",
|
|
49
50
|
"file-type": "^21.0.0",
|
|
50
|
-
"got-scraping": "^4.1.1",
|
|
51
51
|
"ow": "^2.0.0",
|
|
52
52
|
"robots-parser": "^3.0.1",
|
|
53
53
|
"sax": "^1.4.1",
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "109b4c2097f7b9a26bfe761b3c9969dd626f8e38"
|
|
65
65
|
}
|
package/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,cAAc,yBAAyB,CAAC;AACxC,cAAc,kCAAkC,CAAC;AACjD,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AAEnC,OAAO,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAEnF,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
|
package/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,cAAc,yBAAyB,CAAC;AACxC,cAAc,kCAAkC,CAAC;AACjD,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AAEnC,OAAO,EAAE,oBAAoB,EAAa,MAAM,qCAAqC,CAAC;AACtF,OAAO,EAAE,aAAa,EAAc,MAAM,wCAAwC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"blocked.d.ts","sourceRoot":"","sources":["../../src/internals/blocked.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,UAA0E,CAAC;AAEtH;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAI/B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAM/B,CAAC"}
|
package/internals/blocked.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"blocked.js","sourceRoot":"","sources":["../../src/internals/blocked.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,qEAAqE,CAAC,CAAC;AAEtH;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IAC/B,GAAG,8BAA8B;IACjC,0DAA0D;IAC1D,oCAAoC;CACvC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IAC/B,YAAY;IACZ,cAAc;IACd,6BAA6B;IAC7B,8BAA8B;IAC9B,sBAAsB;CACzB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cheerio.d.ts","sourceRoot":"","sources":["../../src/internals/cheerio.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAK1C,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC;AAOrC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,UAAU,CAAC,oBAAoB,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,CAwC7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,SAAM,EAAE,OAAO,SAAK,GAAG,MAAM,EAAE,CAwB5F"}
|
package/internals/cheerio.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cheerio.js","sourceRoot":"","sources":["../../src/internals/cheerio.ts"],"names":[],"mappings":"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"}
|