@crawlee/utils 4.0.0-beta.10 → 4.0.0-beta.101

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.
Files changed (69) hide show
  1. package/README.md +17 -13
  2. package/index.d.ts +1 -7
  3. package/index.js +1 -6
  4. package/internals/blocked.d.ts +0 -1
  5. package/internals/blocked.js +0 -1
  6. package/internals/cheerio.d.ts +3 -2
  7. package/internals/cheerio.js +4 -5
  8. package/internals/extract-urls.d.ts +5 -1
  9. package/internals/extract-urls.js +8 -5
  10. package/internals/general.d.ts +0 -25
  11. package/internals/general.js +2 -110
  12. package/internals/iterables.d.ts +47 -0
  13. package/internals/iterables.js +96 -0
  14. package/internals/open_graph_parser.d.ts +2 -3
  15. package/internals/open_graph_parser.js +8 -9
  16. package/internals/robots.d.ts +14 -5
  17. package/internals/robots.js +31 -35
  18. package/internals/sitemap.d.ts +65 -8
  19. package/internals/sitemap.js +174 -34
  20. package/internals/social.d.ts +1 -2
  21. package/internals/social.js +7 -5
  22. package/internals/url.d.ts +1 -2
  23. package/internals/url.js +1 -2
  24. package/package.json +6 -6
  25. package/index.d.ts.map +0 -1
  26. package/index.js.map +0 -1
  27. package/internals/blocked.d.ts.map +0 -1
  28. package/internals/blocked.js.map +0 -1
  29. package/internals/cheerio.d.ts.map +0 -1
  30. package/internals/cheerio.js.map +0 -1
  31. package/internals/chunk.d.ts +0 -2
  32. package/internals/chunk.d.ts.map +0 -1
  33. package/internals/chunk.js +0 -40
  34. package/internals/chunk.js.map +0 -1
  35. package/internals/debug.d.ts +0 -31
  36. package/internals/debug.d.ts.map +0 -1
  37. package/internals/debug.js +0 -29
  38. package/internals/debug.js.map +0 -1
  39. package/internals/extract-urls.d.ts.map +0 -1
  40. package/internals/extract-urls.js.map +0 -1
  41. package/internals/general.d.ts.map +0 -1
  42. package/internals/general.js.map +0 -1
  43. package/internals/open_graph_parser.d.ts.map +0 -1
  44. package/internals/open_graph_parser.js.map +0 -1
  45. package/internals/robots.d.ts.map +0 -1
  46. package/internals/robots.js.map +0 -1
  47. package/internals/sitemap.d.ts.map +0 -1
  48. package/internals/sitemap.js.map +0 -1
  49. package/internals/social.d.ts.map +0 -1
  50. package/internals/social.js.map +0 -1
  51. package/internals/system-info/cpu-info.d.ts +0 -64
  52. package/internals/system-info/cpu-info.d.ts.map +0 -1
  53. package/internals/system-info/cpu-info.js +0 -211
  54. package/internals/system-info/cpu-info.js.map +0 -1
  55. package/internals/system-info/memory-info.d.ts +0 -28
  56. package/internals/system-info/memory-info.d.ts.map +0 -1
  57. package/internals/system-info/memory-info.js +0 -118
  58. package/internals/system-info/memory-info.js.map +0 -1
  59. package/internals/system-info/ps-tree.d.ts +0 -18
  60. package/internals/system-info/ps-tree.d.ts.map +0 -1
  61. package/internals/system-info/ps-tree.js +0 -145
  62. package/internals/system-info/ps-tree.js.map +0 -1
  63. package/internals/typedefs.d.ts +0 -5
  64. package/internals/typedefs.d.ts.map +0 -1
  65. package/internals/typedefs.js +0 -9
  66. package/internals/typedefs.js.map +0 -1
  67. package/internals/url.d.ts.map +0 -1
  68. package/internals/url.js.map +0 -1
  69. package/tsconfig.build.tsbuildinfo +0 -1
@@ -1,7 +1,6 @@
1
- import { gotScraping } from 'got-scraping';
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
- constructor(robots, proxyUrl) {
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 [proxyUrl] a proxy to be used for fetching the robots.txt file
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, proxyUrl) {
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(), proxyUrl);
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, proxyUrl) {
52
- if (!HTTPError) {
53
- HTTPError = (await import('got-scraping')).HTTPError;
54
- }
55
- try {
56
- const response = await gotScraping({
57
- url,
58
- proxyUrl,
59
- method: 'GET',
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
- catch (e) {
66
- if (e instanceof HTTPError && e.response.statusCode === 404) {
67
- return new RobotsTxtFile({
68
- isAllowed() {
69
- return true;
70
- },
71
- getSitemaps() {
72
- return [];
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
@@ -1,5 +1,4 @@
1
- // @ts-ignore optional peer dependency or compatibility with es2022
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
- * Network timeouts for sitemap fetching. See [Got documentation](https://github.com/sindresorhus/got/blob/main/documentation/6-timeout.md) for more details.
38
+ * Timeout settings for network requests when fetching sitemaps. By default this is `30000` milliseconds (30 seconds).
40
39
  */
41
- networkTimeouts?: Delays;
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
- protected static parse(sources: SitemapSource[], proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
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
@@ -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 log from '@apify/log';
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' && this.url.loc !== undefined) {
95
- this.push({ type: 'url', ...this.url, loc: this.url.loc });
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
- this.url.lastmod = new Date(text);
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 { gotScraping } = await import('got-scraping');
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
- const sitemapStream = await new Promise((resolve, reject) => {
159
- const request = gotScraping.stream({
160
- url: sitemapUrl,
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: 'text/plain, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8',
168
+ accept: '*/*',
166
169
  },
170
+ }), {
171
+ proxyUrl,
172
+ timeoutMillis: timeout,
167
173
  });
168
- request.on('response', () => resolve(request));
169
- request.on('error', reject);
170
- });
174
+ }
175
+ catch (error) {
176
+ sitemapResponse = null;
177
+ }
171
178
  let error = null;
172
- if (sitemapStream.response.statusCode >= 200 && sitemapStream.response.statusCode < 300) {
173
- let contentType = sitemapStream.response.headers['content-type'];
174
- const streamWithType = await fileTypeStream(sitemapStream);
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 = new Error(`Failed to fetch sitemap: ${sitemapUrl}, status code: ${sitemapStream.response.statusCode}`);
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
- throw error;
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
- log.warning(`Malformed sitemap content: ${sitemapUrl}, ${retriesLeft === 0 ? 'no retries left.' : 'retrying...'} (${e})`);
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
- log.warning(`Malformed sitemap content: ${error}`);
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
- //# sourceMappingURL=sitemap.js.map
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
+ }
@@ -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>;
@@ -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
- const EMAIL_REGEX_STRING = '(?:[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[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])+)\\])';
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
@@ -1,4 +1,4 @@
1
- export type SearchParams = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>;
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(newSearchParams)) {
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.10",
3
+ "version": "4.0.0-beta.101",
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": "yarn clean && yarn compile && yarn copy",
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/types": "4.0.0-beta.10",
45
+ "@crawlee/http-client": "4.0.0-beta.101",
46
+ "@crawlee/types": "4.0.0-beta.101",
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": "b35240c5780d87a6d96046a286e0c9cf78be9e4f"
64
+ "gitHead": "1fd886ee601c053d6fa8cada24b751f9cbe0a539"
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"}
@@ -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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"cheerio.js","sourceRoot":"","sources":["../../src/internals/cheerio.ts"],"names":[],"mappings":"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"}