@crawlee/utils 4.0.0-rc.0 → 4.0.0-rc.1
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 +1 -1
- package/index.d.ts +5 -3
- package/index.js +4 -3
- package/internal.d.ts +3 -2
- package/internal.js +2 -1
- package/internals/cheerio.d.ts +1 -2
- package/internals/cheerio.js +5 -3
- package/internals/extract-microdata.d.ts +24 -0
- package/internals/extract-microdata.js +118 -0
- package/internals/open_graph_parser.js +1 -1
- package/internals/robots.d.ts +0 -1
- package/internals/robots.js +9 -7
- package/internals/schemas.d.ts +2 -2
- package/internals/schemas.js +8 -7
- package/internals/sitemap.d.ts +1 -1
- package/internals/sitemap.js +20 -15
- package/internals/social.d.ts +1 -1
- package/internals/social.js +2 -2
- package/internals/validation.d.ts +6 -22
- package/internals/validation.js +12 -136
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Crawlee is available as the [`crawlee`](https://www.npmjs.com/package/crawlee) N
|
|
|
34
34
|
|
|
35
35
|
We recommend visiting the [Introduction tutorial](https://crawlee.dev/js/docs/introduction) in Crawlee documentation for more information.
|
|
36
36
|
|
|
37
|
-
> Crawlee requires **Node.js
|
|
37
|
+
> Crawlee requires **Node.js 22.13 or higher**.
|
|
38
38
|
|
|
39
39
|
### With Crawlee CLI
|
|
40
40
|
|
package/index.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ export { htmlToText } from './internals/cheerio.js';
|
|
|
2
2
|
export { downloadListOfUrls, extractUrls } from './internals/extract-urls.js';
|
|
3
3
|
export { EnqueueStrategy } from './internals/url.js';
|
|
4
4
|
export type { DownloadListOfUrlsOptions, ExtractUrlsOptions } from './internals/extract-urls.js';
|
|
5
|
-
export { sleep
|
|
5
|
+
export { sleep } from './internals/general.js';
|
|
6
6
|
export * as social from './internals/social.js';
|
|
7
|
+
export * from './internals/extract-microdata.js';
|
|
7
8
|
export * from './internals/open_graph_parser.js';
|
|
8
9
|
export * from './internals/robots.js';
|
|
9
|
-
export
|
|
10
|
-
export
|
|
10
|
+
export { discoverValidSitemaps, Sitemap } from './internals/sitemap.js';
|
|
11
|
+
export type { ParseSitemapOptions } from './internals/sitemap.js';
|
|
12
|
+
export { ArgumentValidationError } from './internals/validation.js';
|
package/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { htmlToText } from './internals/cheerio.js';
|
|
2
2
|
export { downloadListOfUrls, extractUrls } from './internals/extract-urls.js';
|
|
3
3
|
export { EnqueueStrategy } from './internals/url.js';
|
|
4
|
-
export { sleep
|
|
4
|
+
export { sleep } from './internals/general.js';
|
|
5
5
|
export * as social from './internals/social.js';
|
|
6
|
+
export * from './internals/extract-microdata.js';
|
|
6
7
|
export * from './internals/open_graph_parser.js';
|
|
7
8
|
export * from './internals/robots.js';
|
|
8
|
-
export
|
|
9
|
-
export
|
|
9
|
+
export { discoverValidSitemaps, Sitemap } from './internals/sitemap.js';
|
|
10
|
+
export { ArgumentValidationError } from './internals/validation.js';
|
package/internal.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export * from './internals/blocked.js';
|
|
2
|
-
export type {
|
|
2
|
+
export type { CheerioAPI, Cheerio, Element } from './internals/cheerio.js';
|
|
3
3
|
export { extractUrlsFromCheerio } from './internals/cheerio.js';
|
|
4
4
|
export { tryAbsoluteURL } from './internals/extract-urls.js';
|
|
5
|
-
export { URL_NO_COMMAS_REGEX, URL_WITH_COMMAS_REGEX } from './internals/general.js';
|
|
5
|
+
export { expandShadowRoots, URL_NO_COMMAS_REGEX, URL_WITH_COMMAS_REGEX } from './internals/general.js';
|
|
6
6
|
export * from './internals/iterables.js';
|
|
7
7
|
export * from './internals/url.js';
|
|
8
8
|
export * from './internals/validation.js';
|
|
9
9
|
export * as schemas from './internals/schemas.js';
|
|
10
|
+
export { parseSitemap, type SitemapUrl } from './internals/sitemap.js';
|
package/internal.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from './internals/blocked.js';
|
|
2
2
|
export { extractUrlsFromCheerio } from './internals/cheerio.js';
|
|
3
3
|
export { tryAbsoluteURL } from './internals/extract-urls.js';
|
|
4
|
-
export { URL_NO_COMMAS_REGEX, URL_WITH_COMMAS_REGEX } from './internals/general.js';
|
|
4
|
+
export { expandShadowRoots, URL_NO_COMMAS_REGEX, URL_WITH_COMMAS_REGEX } from './internals/general.js';
|
|
5
5
|
export * from './internals/iterables.js';
|
|
6
6
|
export * from './internals/url.js';
|
|
7
7
|
export * from './internals/validation.js';
|
|
8
8
|
export * as schemas from './internals/schemas.js';
|
|
9
|
+
export { parseSitemap } from './internals/sitemap.js';
|
package/internals/cheerio.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CheerioAPI } from 'cheerio';
|
|
2
|
-
export type CheerioRoot = CheerioAPI;
|
|
3
2
|
export type { CheerioAPI, Cheerio } from 'cheerio';
|
|
4
3
|
export type { Element } from 'domhandler';
|
|
5
4
|
/**
|
|
@@ -29,7 +28,7 @@ export type { Element } from 'domhandler';
|
|
|
29
28
|
* @param htmlOrCheerioElement HTML text or parsed HTML represented using a [cheerio](https://www.npmjs.com/package/cheerio) function.
|
|
30
29
|
* @return Plain text
|
|
31
30
|
*/
|
|
32
|
-
export declare function htmlToText(htmlOrCheerioElement: string |
|
|
31
|
+
export declare function htmlToText(htmlOrCheerioElement: string | CheerioAPI): Promise<string>;
|
|
33
32
|
/**
|
|
34
33
|
* Extracts URLs from a given Cheerio object.
|
|
35
34
|
*
|
package/internals/cheerio.js
CHANGED
|
@@ -30,7 +30,7 @@ const BLOCK_TAGS_REGEX = /^(p|h1|h2|h3|h4|h5|h6|ol|ul|li|pre|address|blockquote|
|
|
|
30
30
|
* @return Plain text
|
|
31
31
|
*/
|
|
32
32
|
export async function htmlToText(htmlOrCheerioElement) {
|
|
33
|
-
const { load } = await import('cheerio');
|
|
33
|
+
const { load } = await import('cheerio/slim');
|
|
34
34
|
if (!htmlOrCheerioElement)
|
|
35
35
|
return '';
|
|
36
36
|
const $ = typeof htmlOrCheerioElement === 'function' ? htmlOrCheerioElement : load(htmlOrCheerioElement);
|
|
@@ -42,8 +42,10 @@ export async function htmlToText(htmlOrCheerioElement) {
|
|
|
42
42
|
if (elem.type === 'text') {
|
|
43
43
|
// Compress spaces, unless we're inside <pre> element
|
|
44
44
|
let compr;
|
|
45
|
-
if (elem.parent?.tagName === 'pre')
|
|
46
|
-
|
|
45
|
+
if (elem.parent?.tagName === 'pre') {
|
|
46
|
+
// Strip single newline right after `<pre>` that htmlparser2 keeps
|
|
47
|
+
compr = elem.parent.children?.[0] === elem ? elem.data.replace(/^\r?\n/, '') : elem.data;
|
|
48
|
+
}
|
|
47
49
|
else
|
|
48
50
|
compr = elem.data.replace(/\s+/g, ' ');
|
|
49
51
|
// If text is empty or ends with a whitespace, don't add the leading whitespace
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CheerioAPI } from 'cheerio';
|
|
2
|
+
/** The value of a microdata property: either text or a nested item. */
|
|
3
|
+
export type MicrodataValue = string | MicrodataItem;
|
|
4
|
+
/** A single schema.org item extracted from a document. */
|
|
5
|
+
export interface MicrodataItem {
|
|
6
|
+
/** Tokens of the item's `itemtype` attribute. */
|
|
7
|
+
type?: string[];
|
|
8
|
+
/** The item's `itemid` attribute. */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** Values keyed by `itemprop` name, an array where the property repeats. */
|
|
11
|
+
properties: Record<string, MicrodataValue | MicrodataValue[]>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Easily parse all schema.org microdata from a page with just a `CheerioAPI` object or raw HTML,
|
|
15
|
+
* following the [microdata processing model](https://html.spec.whatwg.org/multipage/microdata.html#microdata).
|
|
16
|
+
*
|
|
17
|
+
* Text values are trimmed and their inner whitespace collapsed. URL-valued attributes are returned
|
|
18
|
+
* verbatim rather than resolved against the document's base URL.
|
|
19
|
+
*
|
|
20
|
+
* @param htmlOrCheerioElement A `CheerioAPI` object, or a string of raw HTML.
|
|
21
|
+
* @returns The document's top-level items. Nested items are the property values of their parent.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractMicrodata(raw: string): Promise<MicrodataItem[]>;
|
|
24
|
+
export declare function extractMicrodata($: CheerioAPI): Promise<MicrodataItem[]>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { isTag } from 'domhandler';
|
|
2
|
+
export async function extractMicrodata(htmlOrCheerioElement) {
|
|
3
|
+
// Dynamic so that importing `@crawlee/utils` does not pull in cheerio - see #3836.
|
|
4
|
+
const { load } = await import('cheerio/slim');
|
|
5
|
+
const $ = typeof htmlOrCheerioElement === 'string' ? load(htmlOrCheerioElement) : htmlOrCheerioElement;
|
|
6
|
+
const context = { $ };
|
|
7
|
+
return $('[itemscope]')
|
|
8
|
+
.toArray()
|
|
9
|
+
.filter((element) => !('itemprop' in element.attribs))
|
|
10
|
+
.map((element) => parseItem(context, element, new Set()));
|
|
11
|
+
}
|
|
12
|
+
function parseItem(context, element, ancestors) {
|
|
13
|
+
const item = { properties: {} };
|
|
14
|
+
const type = uniqueTokens(element.attribs.itemtype);
|
|
15
|
+
const id = element.attribs.itemid;
|
|
16
|
+
if (type.length > 0) {
|
|
17
|
+
item.type = type;
|
|
18
|
+
}
|
|
19
|
+
if (id) {
|
|
20
|
+
item.id = id.trim();
|
|
21
|
+
}
|
|
22
|
+
ancestors.add(element);
|
|
23
|
+
for (const propertyElement of collectPropertyElements(context, element)) {
|
|
24
|
+
const value = getPropertyValue(context, propertyElement, ancestors);
|
|
25
|
+
for (const name of uniqueTokens(propertyElement.attribs.itemprop)) {
|
|
26
|
+
addProperty(item.properties, name, value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
ancestors.delete(element);
|
|
30
|
+
return item;
|
|
31
|
+
}
|
|
32
|
+
function collectPropertyElements(context, scope) {
|
|
33
|
+
const elements = [];
|
|
34
|
+
collectFromNodes(scope.children, elements);
|
|
35
|
+
for (const id of uniqueTokens(scope.attribs.itemref)) {
|
|
36
|
+
const referenced = (context.idIndex ??= indexIds(context.$)).get(id);
|
|
37
|
+
if (referenced) {
|
|
38
|
+
collectFromNodes([referenced], elements);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return elements;
|
|
42
|
+
}
|
|
43
|
+
function collectFromNodes(nodes, elements) {
|
|
44
|
+
for (const node of nodes) {
|
|
45
|
+
if (!isTag(node)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if ('itemprop' in node.attribs) {
|
|
49
|
+
elements.push(node);
|
|
50
|
+
}
|
|
51
|
+
// A nested item owns everything below it, so its subtree is not part of the enclosing item.
|
|
52
|
+
if (!('itemscope' in node.attribs)) {
|
|
53
|
+
collectFromNodes(node.children, elements);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function indexIds($) {
|
|
58
|
+
const index = new Map();
|
|
59
|
+
for (const element of $('[id]').toArray()) {
|
|
60
|
+
// Duplicate ids are invalid HTML; `getElementById` resolves them to the first element.
|
|
61
|
+
if (!index.has(element.attribs.id)) {
|
|
62
|
+
index.set(element.attribs.id, element);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return index;
|
|
66
|
+
}
|
|
67
|
+
function getPropertyValue(context, element, ancestors) {
|
|
68
|
+
if ('itemscope' in element.attribs) {
|
|
69
|
+
// `itemref` can point back at an enclosing item, which the spec treats as an error.
|
|
70
|
+
return ancestors.has(element) ? { properties: {} } : parseItem(context, element, ancestors);
|
|
71
|
+
}
|
|
72
|
+
const { attribs } = element;
|
|
73
|
+
switch (element.tagName.toLowerCase()) {
|
|
74
|
+
case 'meta':
|
|
75
|
+
return attribs.content ?? '';
|
|
76
|
+
case 'audio':
|
|
77
|
+
case 'embed':
|
|
78
|
+
case 'iframe':
|
|
79
|
+
case 'img':
|
|
80
|
+
case 'source':
|
|
81
|
+
case 'track':
|
|
82
|
+
case 'video':
|
|
83
|
+
return attribs.src ?? '';
|
|
84
|
+
case 'a':
|
|
85
|
+
case 'area':
|
|
86
|
+
case 'link':
|
|
87
|
+
return attribs.href ?? '';
|
|
88
|
+
case 'object':
|
|
89
|
+
return attribs.data ?? '';
|
|
90
|
+
case 'data':
|
|
91
|
+
case 'meter':
|
|
92
|
+
return attribs.value ?? '';
|
|
93
|
+
case 'time':
|
|
94
|
+
return attribs.datetime ?? context.$(element).text().replace(/\s+/g, ' ').trim();
|
|
95
|
+
default:
|
|
96
|
+
return context.$(element).text().replace(/\s+/g, ' ').trim();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function addProperty(properties, name, value) {
|
|
100
|
+
const existing = properties[name];
|
|
101
|
+
if (existing === undefined) {
|
|
102
|
+
properties[name] = value;
|
|
103
|
+
}
|
|
104
|
+
else if (Array.isArray(existing)) {
|
|
105
|
+
existing.push(value);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
properties[name] = [existing, value];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** `itemtype`, `itemprop` and `itemref` are all unordered sets of unique space-separated tokens. */
|
|
112
|
+
function uniqueTokens(value) {
|
|
113
|
+
if (!value) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const tokens = value.split(/\s+/).filter(Boolean);
|
|
117
|
+
return tokens.length > 1 ? [...new Set(tokens)] : tokens;
|
|
118
|
+
}
|
|
@@ -366,7 +366,7 @@ const parseOpenGraphProperty = (property, $) => {
|
|
|
366
366
|
};
|
|
367
367
|
};
|
|
368
368
|
export async function parseOpenGraph(item, additionalProperties) {
|
|
369
|
-
const { load } = await import('cheerio');
|
|
369
|
+
const { load } = await import('cheerio/slim');
|
|
370
370
|
const $ = typeof item === 'string' ? load(item) : item;
|
|
371
371
|
return [...(additionalProperties || []), ...OPEN_GRAPH_PROPERTIES].reduce((acc, curr) => {
|
|
372
372
|
return {
|
package/internals/robots.d.ts
CHANGED
|
@@ -53,7 +53,6 @@ export declare class RobotsTxtFile {
|
|
|
53
53
|
* @param [proxyUrl] a proxy to be used for fetching the robots.txt file
|
|
54
54
|
*/
|
|
55
55
|
static from(url: string, content: string, proxyUrl?: string): RobotsTxtFile;
|
|
56
|
-
private static load;
|
|
57
56
|
/**
|
|
58
57
|
* Get crawl delay for a given user agent.
|
|
59
58
|
* @param [userAgent] relevant user agent, default to `*`
|
package/internals/robots.js
CHANGED
|
@@ -43,7 +43,7 @@ export class RobotsTxtFile {
|
|
|
43
43
|
const robotsTxtFileUrl = new URL(url);
|
|
44
44
|
robotsTxtFileUrl.pathname = '/robots.txt';
|
|
45
45
|
robotsTxtFileUrl.search = '';
|
|
46
|
-
return RobotsTxtFile
|
|
46
|
+
return RobotsTxtFile.#load(robotsTxtFileUrl.toString(), options);
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
49
|
* Allows providing the URL and robots.txt content explicitly instead of loading it from the target site.
|
|
@@ -55,20 +55,19 @@ export class RobotsTxtFile {
|
|
|
55
55
|
// @ts-ignore
|
|
56
56
|
return new RobotsTxtFile(url, robotsParser(url, content), proxyUrl);
|
|
57
57
|
}
|
|
58
|
-
static async load(url, options) {
|
|
58
|
+
static async #load(url, options) {
|
|
59
59
|
const { proxyUrl, logger, httpClient = new FetchHttpClient() } = options || {};
|
|
60
60
|
const response = await httpClient.sendRequest(new Request(url, { method: 'GET' }), {
|
|
61
61
|
proxyUrl,
|
|
62
62
|
timeoutMillis: options?.timeoutMillis,
|
|
63
63
|
signal: options?.signal,
|
|
64
64
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (response.status === 404) {
|
|
65
|
+
// RFC 9309: an unavailable (4xx) robots.txt allows everything, an unreachable (5xx) one disallows everything.
|
|
66
|
+
if (response.status >= 400 && response.status < 600) {
|
|
67
|
+
const allowed = response.status < 500;
|
|
69
68
|
return new RobotsTxtFile(url, {
|
|
70
69
|
isAllowed() {
|
|
71
|
-
return
|
|
70
|
+
return allowed;
|
|
72
71
|
},
|
|
73
72
|
getSitemaps() {
|
|
74
73
|
return [];
|
|
@@ -78,6 +77,9 @@ export class RobotsTxtFile {
|
|
|
78
77
|
},
|
|
79
78
|
}, proxyUrl, logger);
|
|
80
79
|
}
|
|
80
|
+
if (response.status < 200 || response.status >= 300) {
|
|
81
|
+
throw new Error(`Failed to load robots.txt from ${url}: HTTP ${response.status}`);
|
|
82
|
+
}
|
|
81
83
|
// @ts-ignore
|
|
82
84
|
return new RobotsTxtFile(url, robotsParser(url.toString(), await response.text()), proxyUrl, logger);
|
|
83
85
|
}
|
package/internals/schemas.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare function objectWithKeys(keys: string[], message?: string): z.ZodT
|
|
|
30
30
|
* Accepts only instances of {@link BaseHttpClient} (all Crawlee HTTP clients extend it).
|
|
31
31
|
* @internal
|
|
32
32
|
*/
|
|
33
|
-
export declare const httpClient: z.
|
|
33
|
+
export declare const httpClient: z.ZodInstanceOf<BaseHttpClient>;
|
|
34
34
|
/**
|
|
35
35
|
* Accepts any object implementing the CrawleeLogger interface.
|
|
36
36
|
* @internal
|
|
@@ -47,7 +47,7 @@ export declare const typedArray: z.ZodCustom<NodeJS.TypedArray<ArrayBufferLike>,
|
|
|
47
47
|
*/
|
|
48
48
|
export declare const plainObject: z.ZodCustom<Record<string, unknown>, Record<string, unknown>>;
|
|
49
49
|
/**
|
|
50
|
-
* Shape of a request stored in a request queue.
|
|
50
|
+
* Shape of a request stored in a request queue. Compiled: storage clients validate every request with it.
|
|
51
51
|
* @internal
|
|
52
52
|
*/
|
|
53
53
|
export declare const storageRequest: z.ZodObject<{
|
package/internals/schemas.js
CHANGED
|
@@ -54,11 +54,7 @@ export const typedArray = z.custom((value) => ArrayBuffer.isView(value) && !(val
|
|
|
54
54
|
* @internal
|
|
55
55
|
*/
|
|
56
56
|
export const plainObject = z.custom((value) => typeof value === 'object' && value !== null && !Array.isArray(value), { message: 'Invalid input: expected an object' });
|
|
57
|
-
|
|
58
|
-
* Shape of a request stored in a request queue.
|
|
59
|
-
* @internal
|
|
60
|
-
*/
|
|
61
|
-
export const storageRequest = z.looseObject({
|
|
57
|
+
const storageRequestShape = z.looseObject({
|
|
62
58
|
id: z.string(),
|
|
63
59
|
url: z.url({ protocol: /^https?$/ }),
|
|
64
60
|
uniqueKey: z.string(),
|
|
@@ -66,11 +62,16 @@ export const storageRequest = z.looseObject({
|
|
|
66
62
|
retryCount: z.number().int().optional(),
|
|
67
63
|
handledAt: z.union([z.string(), z.date()]).optional(),
|
|
68
64
|
});
|
|
65
|
+
/**
|
|
66
|
+
* Shape of a request stored in a request queue. Compiled: storage clients validate every request with it.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export const storageRequest = z.compile(storageRequestShape);
|
|
69
70
|
/**
|
|
70
71
|
* {@link storageRequest} before an id is assigned.
|
|
71
72
|
* @internal
|
|
72
73
|
*/
|
|
73
|
-
export const storageRequestWithoutId =
|
|
74
|
+
export const storageRequestWithoutId = z.compile(storageRequestShape.omit({ id: true }));
|
|
74
75
|
/**
|
|
75
76
|
* `z.array(item)` whose top-level type error names the element type — ``expected an array of numbers`` —
|
|
76
77
|
* instead of zod's bare `expected array`. Element failures keep zod's per-index messages, and `elements`
|
|
@@ -86,7 +87,7 @@ export function arrayOf(item, elements) {
|
|
|
86
87
|
* Batch of {@link storageRequestWithoutId}.
|
|
87
88
|
* @internal
|
|
88
89
|
*/
|
|
89
|
-
export const storageRequestBatch = arrayOf(storageRequestWithoutId, 'requests');
|
|
90
|
+
export const storageRequestBatch = z.compile(arrayOf(storageRequestWithoutId, 'requests'));
|
|
90
91
|
/**
|
|
91
92
|
* Options of request queue add/update operations.
|
|
92
93
|
* @internal
|
package/internals/sitemap.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export declare function parseSitemap<T extends ParseSitemapOptions>(initialSourc
|
|
|
82
82
|
* ```
|
|
83
83
|
*/
|
|
84
84
|
export declare class Sitemap {
|
|
85
|
+
#private;
|
|
85
86
|
readonly urls: string[];
|
|
86
87
|
constructor(urls: string[]);
|
|
87
88
|
/**
|
|
@@ -103,7 +104,6 @@ export declare class Sitemap {
|
|
|
103
104
|
* @param proxyUrl URL of a proxy to be used for fetching sitemap contents
|
|
104
105
|
*/
|
|
105
106
|
static fromXmlString(content: string, proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
|
|
106
|
-
private static parse;
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* Given a list of URLs, discover related sitemap files for these domains by checking the `robots.txt` file,
|
package/internals/sitemap.js
CHANGED
|
@@ -3,7 +3,7 @@ import { PassThrough, pipeline, Readable, Transform } from 'node:stream';
|
|
|
3
3
|
import { StringDecoder } from 'node:string_decoder';
|
|
4
4
|
import { createGunzip } from 'node:zlib';
|
|
5
5
|
import { FetchHttpClient } from '@crawlee/http-client';
|
|
6
|
-
import MIMEType from 'whatwg-mimetype';
|
|
6
|
+
import { MIMEType } from 'whatwg-mimetype';
|
|
7
7
|
import { mergeAsyncIterables } from './iterables.js';
|
|
8
8
|
import { RobotsTxtFile } from './robots.js';
|
|
9
9
|
import { filterUrl } from './url.js';
|
|
@@ -14,16 +14,16 @@ class SitemapTxtParser extends Transform {
|
|
|
14
14
|
super({
|
|
15
15
|
readableObjectMode: true,
|
|
16
16
|
transform: (chunk, _encoding, callback) => {
|
|
17
|
-
this
|
|
17
|
+
this.#processBuffer(this.#decoder.write(chunk), false);
|
|
18
18
|
callback();
|
|
19
19
|
},
|
|
20
20
|
flush: (callback) => {
|
|
21
|
-
this
|
|
21
|
+
this.#processBuffer(this.#decoder.end(), true);
|
|
22
22
|
callback();
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
processBuffer(input, finalize) {
|
|
26
|
+
#processBuffer(input, finalize) {
|
|
27
27
|
this.#buffer += input;
|
|
28
28
|
if (finalize || this.#buffer.includes('\n')) {
|
|
29
29
|
const parts = this.#buffer
|
|
@@ -52,7 +52,7 @@ class SitemapXmlParser extends Transform {
|
|
|
52
52
|
#currentTag = undefined;
|
|
53
53
|
#url = {};
|
|
54
54
|
static async create() {
|
|
55
|
-
const { SAXParser } = await import('sax');
|
|
55
|
+
const { SAXParser } = (await import('sax')).default;
|
|
56
56
|
return new SitemapXmlParser(new SAXParser(true));
|
|
57
57
|
}
|
|
58
58
|
constructor(parser) {
|
|
@@ -72,13 +72,13 @@ class SitemapXmlParser extends Transform {
|
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
this.#parser = parser;
|
|
75
|
-
this.#parser.onopentag = this
|
|
76
|
-
this.#parser.onclosetag = this
|
|
77
|
-
this.#parser.ontext = this
|
|
78
|
-
this.#parser.oncdata = this
|
|
75
|
+
this.#parser.onopentag = this.#onOpenTag.bind(this);
|
|
76
|
+
this.#parser.onclosetag = this.#onCloseTag.bind(this);
|
|
77
|
+
this.#parser.ontext = this.#onText.bind(this);
|
|
78
|
+
this.#parser.oncdata = this.#onText.bind(this);
|
|
79
79
|
this.#parser.onerror = this.destroy.bind(this);
|
|
80
80
|
}
|
|
81
|
-
onOpenTag(node) {
|
|
81
|
+
#onOpenTag(node) {
|
|
82
82
|
if (this.#rootTagName !== undefined) {
|
|
83
83
|
if (node.name === 'loc' ||
|
|
84
84
|
node.name === 'lastmod' ||
|
|
@@ -94,7 +94,7 @@ class SitemapXmlParser extends Transform {
|
|
|
94
94
|
this.#rootTagName = 'sitemapindex';
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
onCloseTag(name) {
|
|
97
|
+
#onCloseTag(name) {
|
|
98
98
|
if (name === 'loc' || name === 'lastmod' || name === 'priority' || name === 'changefreq') {
|
|
99
99
|
this.#currentTag = undefined;
|
|
100
100
|
}
|
|
@@ -105,7 +105,7 @@ class SitemapXmlParser extends Transform {
|
|
|
105
105
|
this.#url = {};
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
onText(text) {
|
|
108
|
+
#onText(text) {
|
|
109
109
|
if (this.#currentTag === 'loc') {
|
|
110
110
|
if (this.#rootTagName === 'sitemapindex') {
|
|
111
111
|
this.push({ type: 'sitemapUrl', url: text.trim() });
|
|
@@ -323,7 +323,7 @@ export class Sitemap {
|
|
|
323
323
|
* @param proxyUrl URL of a proxy to be used for fetching sitemap contents
|
|
324
324
|
*/
|
|
325
325
|
static async load(urls, proxyUrl, parseSitemapOptions) {
|
|
326
|
-
return await this
|
|
326
|
+
return await this.#parse((Array.isArray(urls) ? urls : [urls]).map((url) => ({ type: 'url', url })), proxyUrl, parseSitemapOptions);
|
|
327
327
|
}
|
|
328
328
|
/**
|
|
329
329
|
* Parse XML sitemap content from a string and return URLs of referenced pages. If the sitemap references other sitemaps, they will be loaded via HTTP.
|
|
@@ -331,9 +331,9 @@ export class Sitemap {
|
|
|
331
331
|
* @param proxyUrl URL of a proxy to be used for fetching sitemap contents
|
|
332
332
|
*/
|
|
333
333
|
static async fromXmlString(content, proxyUrl, parseSitemapOptions) {
|
|
334
|
-
return await this
|
|
334
|
+
return await this.#parse([{ type: 'raw', content }], proxyUrl, parseSitemapOptions);
|
|
335
335
|
}
|
|
336
|
-
static async parse(sources, proxyUrl, parseSitemapOptions) {
|
|
336
|
+
static async #parse(sources, proxyUrl, parseSitemapOptions) {
|
|
337
337
|
const urls = [];
|
|
338
338
|
try {
|
|
339
339
|
for await (const item of parseSitemap(sources, proxyUrl, parseSitemapOptions)) {
|
|
@@ -423,6 +423,11 @@ export async function* discoverValidSitemaps(urls, options = {}) {
|
|
|
423
423
|
}
|
|
424
424
|
else {
|
|
425
425
|
const firstUrl = new URL(domainUrls[0]);
|
|
426
|
+
// The guessed candidates are well-known paths on the same origin, so the query string
|
|
427
|
+
// and fragment of the input URL are not part of them - carrying them over probes (and
|
|
428
|
+
// yields) URLs like `/sitemap.xml?page=2`, which servers may well answer with a 404.
|
|
429
|
+
firstUrl.search = '';
|
|
430
|
+
firstUrl.hash = '';
|
|
426
431
|
const possibleSitemapPathnames = ['/sitemap.xml', '/sitemap.txt', '/sitemap_index.xml'];
|
|
427
432
|
const candidateSitemapUrls = possibleSitemapPathnames.map((pathname) => {
|
|
428
433
|
firstUrl.pathname = pathname;
|
package/internals/social.d.ts
CHANGED
|
@@ -225,7 +225,7 @@ export declare const TWITTER_REGEX: RegExp;
|
|
|
225
225
|
* ```
|
|
226
226
|
* import { social } from 'crawlee';
|
|
227
227
|
*
|
|
228
|
-
* const matches = text.match(social.
|
|
228
|
+
* const matches = text.match(social.TWITTER_REGEX_GLOBAL);
|
|
229
229
|
* if (matches) console.log(`${matches.length} Twitter profiles found!`);
|
|
230
230
|
* ```
|
|
231
231
|
*/
|
package/internals/social.js
CHANGED
|
@@ -326,7 +326,7 @@ export const TWITTER_REGEX = new RegExp(`^${TWITTER_REGEX_STRING}$`, 'i');
|
|
|
326
326
|
* ```
|
|
327
327
|
* import { social } from 'crawlee';
|
|
328
328
|
*
|
|
329
|
-
* const matches = text.match(social.
|
|
329
|
+
* const matches = text.match(social.TWITTER_REGEX_GLOBAL);
|
|
330
330
|
* if (matches) console.log(`${matches.length} Twitter profiles found!`);
|
|
331
331
|
* ```
|
|
332
332
|
*/
|
|
@@ -590,7 +590,7 @@ export const DISCORD_REGEX_GLOBAL = new RegExp(DISCORD_REGEX_STRING, 'ig');
|
|
|
590
590
|
* @return An object with the social handles.
|
|
591
591
|
*/
|
|
592
592
|
export async function parseHandlesFromHtml(html, data = null) {
|
|
593
|
-
const cheerio = await import('cheerio');
|
|
593
|
+
const cheerio = await import('cheerio/slim');
|
|
594
594
|
const result = {
|
|
595
595
|
emails: [],
|
|
596
596
|
phones: [],
|
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Dictionary } from '@crawlee/types';
|
|
2
|
+
export { ArgumentValidationError, parseArgument } from '@apify/validations';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* value it received (rather than a raw JSON dump). The structured
|
|
7
|
-
* {@link https://zod.dev | zod} issues are available on `issues`, and the
|
|
8
|
-
* original `ZodError` on `cause`, for programmatic inspection.
|
|
9
|
-
*/
|
|
10
|
-
export declare class ArgumentValidationError extends Error {
|
|
11
|
-
/** Structured issues from the underlying schema check. */
|
|
12
|
-
readonly issues: z.ZodError['issues'];
|
|
13
|
-
/** The raw zod error that triggered this. */
|
|
14
|
-
readonly cause: z.ZodError;
|
|
15
|
-
constructor(error: z.ZodError, value: unknown, label?: string);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Parses `value` with `schema`, returning the typed result (with schema defaults applied).
|
|
19
|
-
* Throws {@link ArgumentValidationError} on failure.
|
|
20
|
-
*
|
|
21
|
-
* The optional `label` names the interface being validated and is appended to every error line
|
|
22
|
-
* (e.g. ``… at `maxRequestRetries` in `BasicCrawlerOptions` ``).
|
|
4
|
+
* Rejects options that exist only to configure the browser pool the crawler would have built for itself.
|
|
5
|
+
* Accepting them alongside a pre-built `browserPool` and quietly ignoring them is how `browserPoolOptions` grew
|
|
6
|
+
* into a second, half-working way of configuring the same pool.
|
|
23
7
|
* @internal
|
|
24
8
|
*/
|
|
25
|
-
export declare function
|
|
9
|
+
export declare function assertBrowserPoolNotConfigured(crawlerName: string, ignoredOptions: Dictionary): void;
|
package/internals/validation.js
CHANGED
|
@@ -1,140 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
function formatIssuePath(path) {
|
|
3
|
-
let out = '';
|
|
4
|
-
for (const key of path) {
|
|
5
|
-
if (typeof key === 'number')
|
|
6
|
-
out += `[${key}]`;
|
|
7
|
-
else
|
|
8
|
-
out += out ? `.${String(key)}` : String(key);
|
|
9
|
-
}
|
|
10
|
-
return out;
|
|
11
|
-
}
|
|
12
|
-
/** Reads the value at `path` from the validated input, to include in the error. */
|
|
13
|
-
function valueAtPath(root, path) {
|
|
14
|
-
let current = root;
|
|
15
|
-
for (const key of path) {
|
|
16
|
-
if (current === null || typeof current !== 'object')
|
|
17
|
-
return undefined;
|
|
18
|
-
current = current[key];
|
|
19
|
-
}
|
|
20
|
-
return current;
|
|
21
|
-
}
|
|
22
|
-
/** Names the runtime type of `value` the way zod's own messages do (`null`, `array`, `string`, …). */
|
|
23
|
-
function describeType(value) {
|
|
24
|
-
if (value === null)
|
|
25
|
-
return 'null';
|
|
26
|
-
if (Array.isArray(value))
|
|
27
|
-
return 'array';
|
|
28
|
-
return typeof value;
|
|
29
|
-
}
|
|
30
|
-
/** The bare custom-schema messages that stop at the expected type, e.g. `Invalid input: expected number`. */
|
|
31
|
-
const BARE_EXPECTED_TYPE_MESSAGE = /^Invalid input: expected (an array of .+|a typed array|an object|object|array|function|number|string|boolean)$/;
|
|
32
|
-
/** Longest received string rendered in an error; the rest is elided. */
|
|
33
|
-
const MAX_RENDERED_STRING_LENGTH = 200;
|
|
34
|
-
/** Renders a primitive received value for an error; skips objects/Dates (noisy). */
|
|
35
|
-
function describeReceived(value) {
|
|
36
|
-
switch (typeof value) {
|
|
37
|
-
case 'string':
|
|
38
|
-
// An empty string would render as bare backticks — make it visible.
|
|
39
|
-
if (value === '')
|
|
40
|
-
return "''";
|
|
41
|
-
return value.length > MAX_RENDERED_STRING_LENGTH
|
|
42
|
-
? `${value.slice(0, MAX_RENDERED_STRING_LENGTH)}… (${value.length - MAX_RENDERED_STRING_LENGTH} more characters)`
|
|
43
|
-
: value;
|
|
44
|
-
case 'number':
|
|
45
|
-
case 'boolean':
|
|
46
|
-
case 'bigint':
|
|
47
|
-
return String(value);
|
|
48
|
-
default:
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/** Renders the received side of a sentence: ``received the string `abc` ``, `received NaN`, `received array`. */
|
|
53
|
-
function describeReceivedClause(value) {
|
|
54
|
-
if (typeof value === 'number' && Number.isNaN(value))
|
|
55
|
-
return 'received NaN';
|
|
56
|
-
if (value === '')
|
|
57
|
-
return 'received an empty string';
|
|
58
|
-
const rendered = describeReceived(value);
|
|
59
|
-
return rendered === undefined
|
|
60
|
-
? `received ${describeType(value)}`
|
|
61
|
-
: `received the ${describeType(value)} \`${rendered}\``;
|
|
62
|
-
}
|
|
63
|
-
/** Renders one issue as a line each; a union expands into a line per failed arm. */
|
|
64
|
-
function formatIssue(issue, root, basePath) {
|
|
65
|
-
const path = [...basePath, ...issue.path];
|
|
66
|
-
// A union's own message is a bare "Invalid input" — the useful part is in `errors`,
|
|
67
|
-
// whose paths are relative to the union, hence passing `path` down as the base.
|
|
68
|
-
if (issue.code === 'invalid_union') {
|
|
69
|
-
return issue.errors.flatMap((arm) => arm.flatMap((nested) => formatIssue(nested, root, path)));
|
|
70
|
-
}
|
|
71
|
-
const location = path.length ? ` at \`${formatIssuePath(path)}\`` : '';
|
|
72
|
-
const value = valueAtPath(root, path);
|
|
73
|
-
const rendered = describeReceived(value);
|
|
74
|
-
// ow named the received type ("expected `number` but received type `string`"). The received value is
|
|
75
|
-
// folded into that clause (``received the string `3` ``) rather than dangling after the location: our
|
|
76
|
-
// custom schemas stop at the expected type, so the clause is appended; zod's built-in messages already
|
|
77
|
-
// end with `, received <type>`, so that tail is replaced with the enriched one.
|
|
78
|
-
let { message } = issue;
|
|
79
|
-
let got = '';
|
|
80
|
-
const bareExpected = BARE_EXPECTED_TYPE_MESSAGE.exec(message);
|
|
81
|
-
const zodReceived = /, received (\S+)$/.exec(message);
|
|
82
|
-
// `arrayOf` messages name the element type — their expected runtime type is `array`.
|
|
83
|
-
const expectedType = bareExpected?.[1].startsWith('an array of') ? 'array' : bareExpected?.[1];
|
|
84
|
-
if (bareExpected && expectedType !== (Number.isNaN(value) ? 'NaN' : describeType(value))) {
|
|
85
|
-
message += `, ${describeReceivedClause(value)}`;
|
|
86
|
-
}
|
|
87
|
-
else if (zodReceived && zodReceived[1] === describeType(value) && rendered !== undefined) {
|
|
88
|
-
message = `${message.slice(0, zodReceived.index)}, ${describeReceivedClause(value)}`;
|
|
89
|
-
}
|
|
90
|
-
else if (rendered !== undefined && !message.endsWith(`received ${rendered}`)) {
|
|
91
|
-
// Messages that never name a received type (regex, min/max, enums) keep the plain value suffix.
|
|
92
|
-
got = `, got \`${rendered}\``;
|
|
93
|
-
}
|
|
94
|
-
return [`${message}${location}${got}`];
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Formats a `ZodError` as a plain, human-readable message that names the
|
|
98
|
-
* offending field *and* the value it received (e.g. ``must match pattern
|
|
99
|
-
* /^[A-Z]{2}$/ at `countryCode`, got `CZE` ``) — closer to the old `ow` errors
|
|
100
|
-
* than zod's default, which omits the received value.
|
|
101
|
-
*/
|
|
102
|
-
function formatZodError(error, root, label) {
|
|
103
|
-
const lines = error.issues.flatMap((issue) => formatIssue(issue, root, []));
|
|
104
|
-
// The label names the validated interface, the way ow's errors ended with "in object `X`".
|
|
105
|
-
return (label ? lines.map((line) => `${line} in \`${label}\``) : lines).join('\n');
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Thrown when an argument fails schema validation.
|
|
109
|
-
*
|
|
110
|
-
* Its `message` is a human-readable sentence naming the offending field and the
|
|
111
|
-
* value it received (rather than a raw JSON dump). The structured
|
|
112
|
-
* {@link https://zod.dev | zod} issues are available on `issues`, and the
|
|
113
|
-
* original `ZodError` on `cause`, for programmatic inspection.
|
|
114
|
-
*/
|
|
115
|
-
export class ArgumentValidationError extends Error {
|
|
116
|
-
/** Structured issues from the underlying schema check. */
|
|
117
|
-
issues;
|
|
118
|
-
/** The raw zod error that triggered this. */
|
|
119
|
-
cause;
|
|
120
|
-
constructor(error, value, label) {
|
|
121
|
-
super(formatZodError(error, value, label), { cause: error });
|
|
122
|
-
this.name = 'ArgumentValidationError';
|
|
123
|
-
this.issues = error.issues;
|
|
124
|
-
this.cause = error;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
1
|
+
export { ArgumentValidationError, parseArgument } from '@apify/validations';
|
|
127
2
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* The optional `label` names the interface being validated and is appended to every error line
|
|
132
|
-
* (e.g. ``… at `maxRequestRetries` in `BasicCrawlerOptions` ``).
|
|
3
|
+
* Rejects options that exist only to configure the browser pool the crawler would have built for itself.
|
|
4
|
+
* Accepting them alongside a pre-built `browserPool` and quietly ignoring them is how `browserPoolOptions` grew
|
|
5
|
+
* into a second, half-working way of configuring the same pool.
|
|
133
6
|
* @internal
|
|
134
7
|
*/
|
|
135
|
-
export function
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
8
|
+
export function assertBrowserPoolNotConfigured(crawlerName, ignoredOptions) {
|
|
9
|
+
const names = Object.keys(ignoredOptions).filter((name) => ignoredOptions[name] !== undefined);
|
|
10
|
+
if (names.length === 0) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`${crawlerName}: ${names.map((name) => `\`${name}\``).join(', ')} cannot be combined with \`browserPool\`, ` +
|
|
14
|
+
`${names.length > 1 ? 'they configure' : 'it configures'} the browser pool the crawler would build for ` +
|
|
15
|
+
'itself. Configure the pool you pass in instead.');
|
|
140
16
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/utils",
|
|
3
|
-
"version": "4.0.0-rc.
|
|
3
|
+
"version": "4.0.0-rc.1",
|
|
4
4
|
"description": "A set of shared utilities that can be used by crawlers",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=22.
|
|
6
|
+
"node": ">=22.13.0"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"exports": {
|
|
@@ -42,19 +42,19 @@
|
|
|
42
42
|
"copy": "tsx ../../scripts/copy.ts"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@apify/
|
|
46
|
-
"@crawlee/http-client": "4.0.0-rc.
|
|
47
|
-
"@crawlee/types": "4.0.0-rc.
|
|
45
|
+
"@apify/validations": "^1.1.0",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-rc.1",
|
|
47
|
+
"@crawlee/types": "4.0.0-rc.1",
|
|
48
48
|
"@types/sax": "^1.2.7",
|
|
49
49
|
"cheerio": "^1.0.0",
|
|
50
|
-
"domhandler": "^
|
|
50
|
+
"domhandler": "^6.0.0",
|
|
51
51
|
"file-type": "^21.0.0",
|
|
52
52
|
"robots-parser": "^3.0.1",
|
|
53
53
|
"sax": "^1.4.1",
|
|
54
54
|
"tldts": "^7.0.6",
|
|
55
55
|
"tslib": "^2.8.1",
|
|
56
|
-
"whatwg-mimetype": "^
|
|
57
|
-
"zod": "^4.4
|
|
56
|
+
"whatwg-mimetype": "^5.0.0",
|
|
57
|
+
"zod": "^4.5.4"
|
|
58
58
|
},
|
|
59
59
|
"lerna": {
|
|
60
60
|
"command": {
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
|
|
67
67
|
}
|