@crawlee/utils 4.0.0-beta.12 → 4.0.0-beta.121

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 (71) hide show
  1. package/README.md +17 -13
  2. package/index.d.ts +4 -12
  3. package/index.js +3 -11
  4. package/internal.d.ts +7 -0
  5. package/internal.js +6 -0
  6. package/internals/blocked.d.ts +0 -1
  7. package/internals/blocked.js +0 -1
  8. package/internals/cheerio.d.ts +3 -2
  9. package/internals/cheerio.js +4 -5
  10. package/internals/extract-urls.d.ts +5 -1
  11. package/internals/extract-urls.js +8 -5
  12. package/internals/general.d.ts +0 -25
  13. package/internals/general.js +2 -110
  14. package/internals/iterables.d.ts +47 -0
  15. package/internals/iterables.js +96 -0
  16. package/internals/open_graph_parser.d.ts +2 -3
  17. package/internals/open_graph_parser.js +8 -9
  18. package/internals/robots.d.ts +19 -7
  19. package/internals/robots.js +47 -41
  20. package/internals/sitemap.d.ts +65 -8
  21. package/internals/sitemap.js +222 -78
  22. package/internals/social.d.ts +1 -2
  23. package/internals/social.js +7 -5
  24. package/internals/url.d.ts +1 -2
  25. package/internals/url.js +1 -2
  26. package/package.json +7 -6
  27. package/index.d.ts.map +0 -1
  28. package/index.js.map +0 -1
  29. package/internals/blocked.d.ts.map +0 -1
  30. package/internals/blocked.js.map +0 -1
  31. package/internals/cheerio.d.ts.map +0 -1
  32. package/internals/cheerio.js.map +0 -1
  33. package/internals/chunk.d.ts +0 -2
  34. package/internals/chunk.d.ts.map +0 -1
  35. package/internals/chunk.js +0 -40
  36. package/internals/chunk.js.map +0 -1
  37. package/internals/debug.d.ts +0 -31
  38. package/internals/debug.d.ts.map +0 -1
  39. package/internals/debug.js +0 -29
  40. package/internals/debug.js.map +0 -1
  41. package/internals/extract-urls.d.ts.map +0 -1
  42. package/internals/extract-urls.js.map +0 -1
  43. package/internals/general.d.ts.map +0 -1
  44. package/internals/general.js.map +0 -1
  45. package/internals/open_graph_parser.d.ts.map +0 -1
  46. package/internals/open_graph_parser.js.map +0 -1
  47. package/internals/robots.d.ts.map +0 -1
  48. package/internals/robots.js.map +0 -1
  49. package/internals/sitemap.d.ts.map +0 -1
  50. package/internals/sitemap.js.map +0 -1
  51. package/internals/social.d.ts.map +0 -1
  52. package/internals/social.js.map +0 -1
  53. package/internals/system-info/cpu-info.d.ts +0 -64
  54. package/internals/system-info/cpu-info.d.ts.map +0 -1
  55. package/internals/system-info/cpu-info.js +0 -211
  56. package/internals/system-info/cpu-info.js.map +0 -1
  57. package/internals/system-info/memory-info.d.ts +0 -28
  58. package/internals/system-info/memory-info.d.ts.map +0 -1
  59. package/internals/system-info/memory-info.js +0 -118
  60. package/internals/system-info/memory-info.js.map +0 -1
  61. package/internals/system-info/ps-tree.d.ts +0 -18
  62. package/internals/system-info/ps-tree.d.ts.map +0 -1
  63. package/internals/system-info/ps-tree.js +0 -145
  64. package/internals/system-info/ps-tree.js.map +0 -1
  65. package/internals/typedefs.d.ts +0 -5
  66. package/internals/typedefs.d.ts.map +0 -1
  67. package/internals/typedefs.js +0 -9
  68. package/internals/typedefs.js.map +0 -1
  69. package/internals/url.d.ts.map +0 -1
  70. package/internals/url.js.map +0 -1
  71. package/tsconfig.build.tsbuildinfo +0 -1
@@ -2,29 +2,30 @@ 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 sax from 'sax';
5
+ import { FetchHttpClient } from '@crawlee/http-client';
6
6
  import MIMEType from 'whatwg-mimetype';
7
- import log from '@apify/log';
7
+ import { mergeAsyncIterables } from './iterables.js';
8
+ import { RobotsTxtFile } from './robots.js';
8
9
  class SitemapTxtParser extends Transform {
9
- decoder = new StringDecoder('utf8');
10
- buffer = '';
10
+ #decoder = new StringDecoder('utf8');
11
+ #buffer = '';
11
12
  constructor() {
12
13
  super({
13
14
  readableObjectMode: true,
14
15
  transform: (chunk, _encoding, callback) => {
15
- this.processBuffer(this.decoder.write(chunk), false);
16
+ this.processBuffer(this.#decoder.write(chunk), false);
16
17
  callback();
17
18
  },
18
19
  flush: (callback) => {
19
- this.processBuffer(this.decoder.end(), true);
20
+ this.processBuffer(this.#decoder.end(), true);
20
21
  callback();
21
22
  },
22
23
  });
23
24
  }
24
25
  processBuffer(input, finalize) {
25
- this.buffer += input;
26
- if (finalize || this.buffer.includes('\n')) {
27
- const parts = this.buffer
26
+ this.#buffer += input;
27
+ if (finalize || this.#buffer.includes('\n')) {
28
+ const parts = this.#buffer
28
29
  .split('\n')
29
30
  .map((part) => part.trim())
30
31
  .filter((part) => part.length > 0);
@@ -32,101 +33,109 @@ class SitemapTxtParser extends Transform {
32
33
  for (const url of parts) {
33
34
  this.push({ type: 'url', loc: url });
34
35
  }
35
- this.buffer = '';
36
+ this.#buffer = '';
36
37
  }
37
38
  else if (parts.length > 0) {
38
39
  for (const url of parts.slice(0, -1)) {
39
40
  this.push({ type: 'url', loc: url });
40
41
  }
41
- this.buffer = parts.at(-1);
42
+ this.#buffer = parts.at(-1);
42
43
  }
43
44
  }
44
45
  }
45
46
  }
46
47
  class SitemapXmlParser extends Transform {
47
- decoder = new StringDecoder('utf8');
48
- parser = new sax.SAXParser(true);
49
- rootTagName;
50
- currentTag = undefined;
51
- url = {};
52
- constructor() {
48
+ #decoder = new StringDecoder('utf8');
49
+ #parser;
50
+ #rootTagName;
51
+ #currentTag = undefined;
52
+ #url = {};
53
+ static async create() {
54
+ const { SAXParser } = await import('sax');
55
+ return new SitemapXmlParser(new SAXParser(true));
56
+ }
57
+ constructor(parser) {
53
58
  super({
54
59
  readableObjectMode: true,
55
60
  transform: (chunk, _encoding, callback) => {
56
- this.parser.write(this.decoder.write(chunk));
61
+ this.#parser.write(this.#decoder.write(chunk));
57
62
  callback();
58
63
  },
59
64
  flush: (callback) => {
60
- const rest = this.decoder.end();
65
+ const rest = this.#decoder.end();
61
66
  if (rest.length > 0) {
62
- this.parser.write(rest);
67
+ this.#parser.write(rest);
63
68
  }
64
- this.parser.end();
69
+ this.#parser.end();
65
70
  callback();
66
71
  },
67
72
  });
68
- this.parser.onopentag = this.onOpenTag.bind(this);
69
- this.parser.onclosetag = this.onCloseTag.bind(this);
70
- this.parser.ontext = this.onText.bind(this);
71
- this.parser.oncdata = this.onText.bind(this);
72
- this.parser.onerror = this.destroy.bind(this);
73
+ this.#parser = parser;
74
+ this.#parser.onopentag = this.onOpenTag.bind(this);
75
+ this.#parser.onclosetag = this.onCloseTag.bind(this);
76
+ this.#parser.ontext = this.onText.bind(this);
77
+ this.#parser.oncdata = this.onText.bind(this);
78
+ this.#parser.onerror = this.destroy.bind(this);
73
79
  }
74
80
  onOpenTag(node) {
75
- if (this.rootTagName !== undefined) {
81
+ if (this.#rootTagName !== undefined) {
76
82
  if (node.name === 'loc' ||
77
83
  node.name === 'lastmod' ||
78
84
  node.name === 'priority' ||
79
85
  node.name === 'changefreq') {
80
- this.currentTag = node.name;
86
+ this.#currentTag = node.name;
81
87
  }
82
88
  }
83
89
  if (node.name === 'urlset') {
84
- this.rootTagName = 'urlset';
90
+ this.#rootTagName = 'urlset';
85
91
  }
86
92
  if (node.name === 'sitemapindex') {
87
- this.rootTagName = 'sitemapindex';
93
+ this.#rootTagName = 'sitemapindex';
88
94
  }
89
95
  }
90
96
  onCloseTag(name) {
91
97
  if (name === 'loc' || name === 'lastmod' || name === 'priority' || name === 'changefreq') {
92
- this.currentTag = undefined;
98
+ this.#currentTag = undefined;
93
99
  }
94
- if (name === 'url' && this.url.loc !== undefined) {
95
- this.push({ type: 'url', ...this.url, loc: this.url.loc });
96
- this.url = {};
100
+ if (name === 'url') {
101
+ if (this.#url.loc !== undefined) {
102
+ this.push({ type: 'url', ...this.#url, loc: this.#url.loc });
103
+ }
104
+ this.#url = {};
97
105
  }
98
106
  }
99
107
  onText(text) {
100
- if (this.currentTag === 'loc') {
101
- if (this.rootTagName === 'sitemapindex') {
108
+ if (this.#currentTag === 'loc') {
109
+ if (this.#rootTagName === 'sitemapindex') {
102
110
  this.push({ type: 'sitemapUrl', url: text.trim() });
103
111
  }
104
- if (this.rootTagName === 'urlset') {
105
- this.url ??= {};
106
- this.url.loc = text.trim();
112
+ if (this.#rootTagName === 'urlset') {
113
+ this.#url ??= {};
114
+ this.#url.loc = text.trim();
107
115
  }
108
116
  }
109
117
  text = text.trim();
110
- if (this.currentTag === 'lastmod') {
111
- this.url.lastmod = new Date(text);
118
+ if (this.#currentTag === 'lastmod') {
119
+ const lastmod = new Date(text);
120
+ if (!Number.isNaN(lastmod.getTime())) {
121
+ this.#url.lastmod = lastmod;
122
+ }
112
123
  }
113
- if (this.currentTag === 'priority') {
114
- this.url.priority = Number(text);
124
+ if (this.#currentTag === 'priority') {
125
+ this.#url.priority = Number(text);
115
126
  }
116
- if (this.currentTag === 'changefreq') {
127
+ if (this.#currentTag === 'changefreq') {
117
128
  if (['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'].includes(text)) {
118
- this.url.changefreq = text;
129
+ this.#url.changefreq = text;
119
130
  }
120
131
  }
121
132
  }
122
133
  }
123
134
  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 ?? {};
135
+ const { httpClient = new FetchHttpClient(), emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, timeoutMillis: timeout = 30000, reportNetworkErrors = true, nestedSitemapFilter, logger, } = options ?? {};
127
136
  const sources = [...initialSources];
128
137
  const visitedSitemapUrls = new Set();
129
- const createParser = (contentType = '', url) => {
138
+ const createParser = async (contentType = '', url) => {
130
139
  let mimeType;
131
140
  try {
132
141
  mimeType = new MIMEType(contentType);
@@ -135,7 +144,7 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
135
144
  mimeType = null;
136
145
  }
137
146
  if (mimeType?.isXML() || url?.pathname.endsWith('.xml')) {
138
- return new SitemapXmlParser();
147
+ return SitemapXmlParser.create();
139
148
  }
140
149
  if (mimeType?.essence === 'text/plain' || url?.pathname.endsWith('.txt')) {
141
150
  return new SitemapTxtParser();
@@ -145,7 +154,6 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
145
154
  while (sources.length > 0) {
146
155
  const source = sources.shift();
147
156
  if ((source?.depth ?? 0) > maxDepth) {
148
- log.debug(`Skipping sitemap ${source.type === 'url' ? source.url : ''} because it reached max depth ${maxDepth}.`);
149
157
  continue;
150
158
  }
151
159
  let items = null;
@@ -155,23 +163,29 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
155
163
  let retriesLeft = sitemapRetries + 1;
156
164
  while (retriesLeft-- > 0) {
157
165
  try {
158
- const sitemapStream = await new Promise((resolve, reject) => {
159
- const request = gotScraping.stream({
160
- url: sitemapUrl,
161
- proxyUrl,
166
+ let sitemapResponse;
167
+ try {
168
+ sitemapResponse = await httpClient.sendRequest(new Request(sitemapUrl, {
162
169
  method: 'GET',
163
- timeout: networkTimeouts,
164
170
  headers: {
165
- accept: 'text/plain, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8',
171
+ accept: '*/*',
166
172
  },
173
+ }), {
174
+ proxyUrl,
175
+ timeoutMillis: timeout,
167
176
  });
168
- request.on('response', () => resolve(request));
169
- request.on('error', reject);
170
- });
177
+ }
178
+ catch (error) {
179
+ sitemapResponse = null;
180
+ }
171
181
  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);
182
+ if (sitemapResponse && sitemapResponse.status >= 200 && sitemapResponse.status < 300) {
183
+ let contentType = sitemapResponse.headers.get('content-type');
184
+ if (sitemapResponse.body === null) {
185
+ break;
186
+ }
187
+ const { fileTypeStream } = await import('file-type');
188
+ const streamWithType = await fileTypeStream(Readable.fromWeb(sitemapResponse.body));
175
189
  if (streamWithType.fileType !== undefined) {
176
190
  contentType = streamWithType.fileType.mime;
177
191
  }
@@ -184,29 +198,37 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
184
198
  sitemapUrl.pathname = sitemapUrl.pathname.substring(0, sitemapUrl.pathname.length - 3);
185
199
  }
186
200
  }
187
- items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), createParser(contentType, sitemapUrl), (e) => {
188
- if (e !== undefined) {
189
- error = e;
201
+ items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), await createParser(contentType ?? undefined, sitemapUrl), (e) => {
202
+ if (e !== undefined && e !== null) {
203
+ error = { type: 'parser', error: e };
190
204
  }
191
205
  });
192
206
  }
193
207
  else {
194
- error = new Error(`Failed to fetch sitemap: ${sitemapUrl}, status code: ${sitemapStream.response.statusCode}`);
208
+ error = {
209
+ type: 'fetch',
210
+ error: new Error(`Failed to fetch sitemap: ${sitemapUrl}, status code: ${sitemapResponse?.status}`),
211
+ };
195
212
  }
196
213
  if (error !== null) {
197
- throw error;
214
+ const shouldIgnoreError = error.type === 'fetch' && !reportNetworkErrors;
215
+ if (!shouldIgnoreError) {
216
+ throw error.error;
217
+ }
218
+ }
219
+ else {
220
+ break;
198
221
  }
199
- break;
200
222
  }
201
223
  catch (e) {
202
- log.warning(`Malformed sitemap content: ${sitemapUrl}, ${retriesLeft === 0 ? 'no retries left.' : 'retrying...'} (${e})`);
224
+ logger?.warning(`Malformed sitemap content: ${sitemapUrl}, ${retriesLeft === 0 ? 'no retries left.' : 'retrying...'} (${e})`);
203
225
  }
204
226
  }
205
227
  }
206
228
  else if (source.type === 'raw') {
207
- items = pipeline(Readable.from([source.content]), createParser('text/xml'), (error) => {
229
+ items = pipeline(Readable.from([source.content]), await createParser('text/xml'), (error) => {
208
230
  if (error !== undefined) {
209
- log.warning(`Malformed sitemap content: ${error}`);
231
+ logger?.warning(`Malformed sitemap content: ${error}`);
210
232
  }
211
233
  });
212
234
  }
@@ -215,6 +237,10 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
215
237
  }
216
238
  for await (const item of items) {
217
239
  if (item.type === 'sitemapUrl' && !visitedSitemapUrls.has(item.url)) {
240
+ if (nestedSitemapFilter && !nestedSitemapFilter(item.url)) {
241
+ logger?.debug(`Skipping sitemap ${item.url} due to nestedSitemapFilter.`);
242
+ continue;
243
+ }
218
244
  sources.push({ type: 'url', url: item.url, depth: (source.depth ?? 0) + 1 });
219
245
  if (emitNestedSitemaps) {
220
246
  yield { loc: item.url, originSitemapUrl: null };
@@ -254,7 +280,7 @@ export class Sitemap {
254
280
  * @param url The domain URL to fetch the sitemap for.
255
281
  * @param proxyUrl A proxy to be used for fetching the sitemap file.
256
282
  */
257
- static async tryCommonNames(url, proxyUrl) {
283
+ static async tryCommonNames(url, proxyUrl, parseSitemapOptions) {
258
284
  const sitemapUrls = [];
259
285
  const sitemapUrl = new URL(url);
260
286
  sitemapUrl.search = '';
@@ -262,7 +288,7 @@ export class Sitemap {
262
288
  sitemapUrls.push(sitemapUrl.toString());
263
289
  sitemapUrl.pathname = '/sitemap.txt';
264
290
  sitemapUrls.push(sitemapUrl.toString());
265
- return Sitemap.load(sitemapUrls, proxyUrl);
291
+ return Sitemap.load(sitemapUrls, proxyUrl, { reportNetworkErrors: false, ...parseSitemapOptions });
266
292
  }
267
293
  /**
268
294
  * Fetch sitemap content from given URL or URLs and return URLs of referenced pages.
@@ -277,8 +303,8 @@ export class Sitemap {
277
303
  * @param content XML sitemap content
278
304
  * @param proxyUrl URL of a proxy to be used for fetching sitemap contents
279
305
  */
280
- static async fromXmlString(content, proxyUrl) {
281
- return await this.parse([{ type: 'raw', content }], proxyUrl);
306
+ static async fromXmlString(content, proxyUrl, parseSitemapOptions) {
307
+ return await this.parse([{ type: 'raw', content }], proxyUrl, parseSitemapOptions);
282
308
  }
283
309
  static async parse(sources, proxyUrl, parseSitemapOptions) {
284
310
  const urls = [];
@@ -287,10 +313,128 @@ export class Sitemap {
287
313
  urls.push(item.loc);
288
314
  }
289
315
  }
290
- catch {
316
+ catch (e) {
317
+ parseSitemapOptions?.logger?.warning(`Sitemap.load: Failed to load sitemap, returning empty result. (${e})`);
291
318
  return new Sitemap([]);
292
319
  }
293
320
  return new Sitemap(urls);
294
321
  }
295
322
  }
296
- //# sourceMappingURL=sitemap.js.map
323
+ /**
324
+ * Given a list of URLs, discover related sitemap files for these domains by checking the `robots.txt` file,
325
+ * the default `sitemap.xml` & `sitemap.txt` files and the URLs themselves.
326
+ * @param `urls` The list of URLs to discover sitemaps for.
327
+ * @param `options` Options for sitemap discovery
328
+ * @returns An async iterable with the discovered sitemap URLs.
329
+ */
330
+ export async function* discoverValidSitemaps(urls, options = {}) {
331
+ const { proxyUrl, timeoutMillis = 60_000, signal: externalSignal, requestTimeoutMillis = 20_000, httpClient = new FetchHttpClient(), logger, } = options;
332
+ const controller = new AbortController();
333
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMillis);
334
+ const onExternalAbort = () => controller.abort();
335
+ if (externalSignal) {
336
+ if (externalSignal.aborted) {
337
+ controller.abort();
338
+ }
339
+ else {
340
+ externalSignal.addEventListener('abort', onExternalAbort, { once: true });
341
+ }
342
+ }
343
+ const signal = controller.signal;
344
+ const sitemapUrls = new Set();
345
+ const addSitemapUrl = (url) => {
346
+ const sizeBefore = sitemapUrls.size;
347
+ sitemapUrls.add(url);
348
+ if (sitemapUrls.size > sizeBefore) {
349
+ return url;
350
+ }
351
+ return undefined;
352
+ };
353
+ const urlExists = async (url) => {
354
+ if (!httpClient) {
355
+ return false;
356
+ }
357
+ try {
358
+ const response = await httpClient.sendRequest(new Request(url, { method: 'HEAD' }), {
359
+ proxyUrl,
360
+ timeoutMillis: requestTimeoutMillis,
361
+ signal,
362
+ });
363
+ return response.status >= 200 && response.status < 400;
364
+ }
365
+ catch {
366
+ return false;
367
+ }
368
+ };
369
+ const discoverSitemapsForDomainUrls = async function* (hostname, domainUrls) {
370
+ if (!hostname) {
371
+ return;
372
+ }
373
+ try {
374
+ const robotsFile = await RobotsTxtFile.find(domainUrls[0], {
375
+ proxyUrl,
376
+ timeoutMillis: requestTimeoutMillis,
377
+ signal,
378
+ httpClient,
379
+ logger,
380
+ });
381
+ for (const sitemapUrl of robotsFile.getSitemaps()) {
382
+ if (addSitemapUrl(sitemapUrl)) {
383
+ yield sitemapUrl;
384
+ }
385
+ }
386
+ }
387
+ catch (err) {
388
+ logger?.warning(`Failed to fetch robots.txt file for ${hostname}`, { error: err });
389
+ }
390
+ const sitemapUrl = domainUrls.find((url) => /sitemap\.(?:xml|txt)(?:\.gz)?$/i.test(url));
391
+ if (sitemapUrl !== undefined) {
392
+ if (addSitemapUrl(sitemapUrl)) {
393
+ yield sitemapUrl;
394
+ }
395
+ }
396
+ else {
397
+ const firstUrl = new URL(domainUrls[0]);
398
+ const possibleSitemapPathnames = ['/sitemap.xml', '/sitemap.txt', '/sitemap_index.xml'];
399
+ const candidateSitemapUrls = possibleSitemapPathnames.map((pathname) => {
400
+ firstUrl.pathname = pathname;
401
+ return firstUrl.toString();
402
+ });
403
+ const candidateResults = await Promise.allSettled(candidateSitemapUrls.map(urlExists));
404
+ for (const [index, result] of candidateResults.entries()) {
405
+ const candidateSitemapUrl = candidateSitemapUrls[index];
406
+ if (result.status === 'fulfilled') {
407
+ if (result.value && addSitemapUrl(candidateSitemapUrl)) {
408
+ yield candidateSitemapUrl;
409
+ }
410
+ }
411
+ else {
412
+ logger?.debug(`Failed to check sitemap candidate ${candidateSitemapUrl} for ${hostname}`, {
413
+ error: result.reason,
414
+ });
415
+ }
416
+ }
417
+ }
418
+ };
419
+ const groupedUrls = urls.reduce((acc, url) => {
420
+ const hostname = new URL(url)?.hostname ?? '';
421
+ acc[hostname] ??= [];
422
+ acc[hostname].push(url);
423
+ return acc;
424
+ }, {});
425
+ const iterables = Object.entries(groupedUrls).map(([hostname, domainUrls]) => discoverSitemapsForDomainUrls(hostname, domainUrls));
426
+ const discoveredUrls = new Set();
427
+ try {
428
+ for await (const url of mergeAsyncIterables(...iterables)) {
429
+ if (discoveredUrls.has(url)) {
430
+ continue;
431
+ }
432
+ discoveredUrls.add(url);
433
+ yield url;
434
+ }
435
+ }
436
+ finally {
437
+ clearTimeout(timeoutHandle);
438
+ externalSignal?.removeEventListener('abort', onExternalAbort);
439
+ }
440
+ }
@@ -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.12",
3
+ "version": "4.0.0-beta.121",
4
4
  "description": "A set of shared utilities that can be used by crawlers",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -8,6 +8,7 @@
8
8
  "type": "module",
9
9
  "exports": {
10
10
  ".": "./index.js",
11
+ "./internal": "./internal.js",
11
12
  "./package.json": "./package.json"
12
13
  },
13
14
  "keywords": [
@@ -35,19 +36,19 @@
35
36
  },
36
37
  "homepage": "https://crawlee.dev",
37
38
  "scripts": {
38
- "build": "yarn clean && yarn compile && yarn copy",
39
+ "build": "pnpm clean && pnpm compile && pnpm copy",
39
40
  "clean": "rimraf ./dist",
40
41
  "compile": "tsc -p tsconfig.build.json",
41
42
  "copy": "tsx ../../scripts/copy.ts"
42
43
  },
43
44
  "dependencies": {
44
- "@apify/log": "^2.5.18",
45
45
  "@apify/ps-tree": "^1.2.0",
46
- "@crawlee/types": "4.0.0-beta.12",
46
+ "@crawlee/http-client": "4.0.0-beta.121",
47
+ "@crawlee/types": "4.0.0-beta.121",
47
48
  "@types/sax": "^1.2.7",
48
49
  "cheerio": "^1.0.0",
50
+ "domhandler": "^5.0.3",
49
51
  "file-type": "^21.0.0",
50
- "got-scraping": "^4.1.1",
51
52
  "ow": "^2.0.0",
52
53
  "robots-parser": "^3.0.1",
53
54
  "sax": "^1.4.1",
@@ -61,5 +62,5 @@
61
62
  }
62
63
  }
63
64
  },
64
- "gitHead": "06431733e5cfa6cc3a4c105427592f87424037ca"
65
+ "gitHead": "5027317de626f5ba6de5047ae9341a898258cc5a"
65
66
  }
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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"}
@@ -1,2 +0,0 @@
1
- export declare function chunk<T>(array: readonly T[], chunkSize: number): T[][];
2
- //# sourceMappingURL=chunk.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"chunk.d.ts","sourceRoot":"","sources":["../../src/internals/chunk.ts"],"names":[],"mappings":"AA2BA,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,EAAE,EAAE,CAQtE"}