@crawlee/utils 4.0.0-beta.99 → 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 +9 -7
- package/index.js +7 -7
- package/internal.d.ts +10 -0
- package/internal.js +9 -0
- 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/extract-urls.d.ts +1 -1
- package/internals/extract-urls.js +17 -15
- package/internals/open_graph_parser.js +1 -1
- package/internals/robots.d.ts +26 -10
- package/internals/robots.js +56 -26
- package/internals/schemas.d.ts +114 -0
- package/internals/schemas.js +115 -0
- package/internals/sitemap.d.ts +11 -2
- package/internals/sitemap.js +98 -61
- package/internals/social.d.ts +1 -1
- package/internals/social.js +2 -2
- package/internals/url.d.ts +69 -0
- package/internals/url.js +119 -0
- package/internals/validation.d.ts +9 -0
- package/internals/validation.js +16 -0
- package/package.json +11 -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
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
export { htmlToText } from './internals/cheerio.js';
|
|
2
|
+
export { downloadListOfUrls, extractUrls } from './internals/extract-urls.js';
|
|
3
|
+
export { EnqueueStrategy } from './internals/url.js';
|
|
4
|
+
export type { DownloadListOfUrlsOptions, ExtractUrlsOptions } from './internals/extract-urls.js';
|
|
5
|
+
export { sleep } from './internals/general.js';
|
|
5
6
|
export * as social from './internals/social.js';
|
|
7
|
+
export * from './internals/extract-microdata.js';
|
|
6
8
|
export * from './internals/open_graph_parser.js';
|
|
7
9
|
export * from './internals/robots.js';
|
|
8
|
-
export
|
|
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,10 +1,10 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
export { htmlToText } from './internals/cheerio.js';
|
|
2
|
+
export { downloadListOfUrls, extractUrls } from './internals/extract-urls.js';
|
|
3
|
+
export { EnqueueStrategy } from './internals/url.js';
|
|
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
|
|
10
|
-
export * from './internals/url.js';
|
|
9
|
+
export { discoverValidSitemaps, Sitemap } from './internals/sitemap.js';
|
|
10
|
+
export { ArgumentValidationError } from './internals/validation.js';
|
package/internal.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './internals/blocked.js';
|
|
2
|
+
export type { CheerioAPI, Cheerio, Element } from './internals/cheerio.js';
|
|
3
|
+
export { extractUrlsFromCheerio } from './internals/cheerio.js';
|
|
4
|
+
export { tryAbsoluteURL } from './internals/extract-urls.js';
|
|
5
|
+
export { expandShadowRoots, URL_NO_COMMAS_REGEX, URL_WITH_COMMAS_REGEX } from './internals/general.js';
|
|
6
|
+
export * from './internals/iterables.js';
|
|
7
|
+
export * from './internals/url.js';
|
|
8
|
+
export * from './internals/validation.js';
|
|
9
|
+
export * as schemas from './internals/schemas.js';
|
|
10
|
+
export { parseSitemap, type SitemapUrl } from './internals/sitemap.js';
|
package/internal.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './internals/blocked.js';
|
|
2
|
+
export { extractUrlsFromCheerio } from './internals/cheerio.js';
|
|
3
|
+
export { tryAbsoluteURL } from './internals/extract-urls.js';
|
|
4
|
+
export { expandShadowRoots, URL_NO_COMMAS_REGEX, URL_WITH_COMMAS_REGEX } from './internals/general.js';
|
|
5
|
+
export * from './internals/iterables.js';
|
|
6
|
+
export * from './internals/url.js';
|
|
7
|
+
export * from './internals/validation.js';
|
|
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
|
+
}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { FetchHttpClient } from '@crawlee/http-client';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import { URL_NO_COMMAS_REGEX } from './general.js';
|
|
4
|
+
import { httpClient as httpClientSchema } from './schemas.js';
|
|
5
|
+
import { parseArgument } from './validation.js';
|
|
6
|
+
const downloadListOfUrlsOptionsSchema = z.strictObject({
|
|
7
|
+
url: z.url(),
|
|
8
|
+
encoding: z.string().default('utf8'),
|
|
9
|
+
urlRegExp: z.instanceof(RegExp).default(URL_NO_COMMAS_REGEX),
|
|
10
|
+
proxyUrl: z.string().optional(),
|
|
11
|
+
httpClient: httpClientSchema.default(() => new FetchHttpClient()),
|
|
12
|
+
});
|
|
13
|
+
const extractUrlsOptionsSchema = z.strictObject({
|
|
14
|
+
string: z.string(),
|
|
15
|
+
urlRegExp: z.instanceof(RegExp).default(URL_NO_COMMAS_REGEX),
|
|
16
|
+
});
|
|
4
17
|
/**
|
|
5
18
|
* Returns a promise that resolves to an array of urls parsed from the resource available at the provided url.
|
|
6
19
|
* Optionally, custom regular expression and encoding may be provided.
|
|
7
20
|
*/
|
|
8
21
|
export async function downloadListOfUrls(options) {
|
|
9
|
-
|
|
10
|
-
url: ow.string.url,
|
|
11
|
-
encoding: ow.optional.string,
|
|
12
|
-
urlRegExp: ow.optional.regExp,
|
|
13
|
-
proxyUrl: ow.optional.string,
|
|
14
|
-
httpClient: ow.optional.object,
|
|
15
|
-
}));
|
|
16
|
-
const { url, encoding = 'utf8', urlRegExp = URL_NO_COMMAS_REGEX, proxyUrl, httpClient = new FetchHttpClient(), } = options;
|
|
22
|
+
const { url, encoding, urlRegExp, proxyUrl, httpClient } = parseArgument(options, downloadListOfUrlsOptionsSchema);
|
|
17
23
|
// Try to detect wrong urls and fix them. Currently, detects only sharing url instead of csv download one.
|
|
18
24
|
const match = /^(https:\/\/docs\.google\.com\/spreadsheets\/d\/(?:\w|-)+)\/?/.exec(url);
|
|
19
25
|
let fixedUrl = url;
|
|
@@ -30,13 +36,9 @@ export async function downloadListOfUrls(options) {
|
|
|
30
36
|
* Collects all URLs in an arbitrary string to an array, optionally using a custom regular expression.
|
|
31
37
|
*/
|
|
32
38
|
export function extractUrls(options) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
urlRegExp: ow.optional.regExp,
|
|
36
|
-
}));
|
|
37
|
-
const lines = options.string.split('\n');
|
|
39
|
+
const { string, urlRegExp } = parseArgument(options, extractUrlsOptionsSchema);
|
|
40
|
+
const lines = string.split('\n');
|
|
38
41
|
const result = [];
|
|
39
|
-
const urlRegExp = options.urlRegExp ?? URL_NO_COMMAS_REGEX;
|
|
40
42
|
for (const line of lines) {
|
|
41
43
|
result.push(...(line.match(urlRegExp) ?? []));
|
|
42
44
|
}
|
|
@@ -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
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import type { BaseHttpClient
|
|
1
|
+
import type { BaseHttpClient } from '@crawlee/http-client';
|
|
2
|
+
import type { CrawleeLogger } from '@crawlee/types';
|
|
2
3
|
import { Sitemap } from './sitemap.js';
|
|
4
|
+
import { type EnqueueStrategy } from './url.js';
|
|
5
|
+
export interface RobotsTxtFileSitemapsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Keep only sitemap URLs matching this strategy relative to the robots.txt host; non-`http(s)` schemes
|
|
8
|
+
* are always dropped. Pass `'all'` to disable host filtering.
|
|
9
|
+
* @default 'same-hostname'
|
|
10
|
+
*/
|
|
11
|
+
enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Loads and queries information from a [robots.txt file](https://en.wikipedia.org/wiki/Robots.txt).
|
|
5
15
|
*
|
|
@@ -19,9 +29,7 @@ import { Sitemap } from './sitemap.js';
|
|
|
19
29
|
* ```
|
|
20
30
|
*/
|
|
21
31
|
export declare class RobotsTxtFile {
|
|
22
|
-
private
|
|
23
|
-
private proxyUrl?;
|
|
24
|
-
private logger?;
|
|
32
|
+
#private;
|
|
25
33
|
private constructor();
|
|
26
34
|
/**
|
|
27
35
|
* Determine the location of a robots.txt file for a URL and fetch it.
|
|
@@ -45,7 +53,11 @@ export declare class RobotsTxtFile {
|
|
|
45
53
|
* @param [proxyUrl] a proxy to be used for fetching the robots.txt file
|
|
46
54
|
*/
|
|
47
55
|
static from(url: string, content: string, proxyUrl?: string): RobotsTxtFile;
|
|
48
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Get crawl delay for a given user agent.
|
|
58
|
+
* @param [userAgent] relevant user agent, default to `*`
|
|
59
|
+
*/
|
|
60
|
+
getCrawlDelay(userAgent?: string): number | undefined;
|
|
49
61
|
/**
|
|
50
62
|
* Check if a URL should be crawled by robots.
|
|
51
63
|
* @param url the URL to check against the rules in robots.txt
|
|
@@ -53,15 +65,19 @@ export declare class RobotsTxtFile {
|
|
|
53
65
|
*/
|
|
54
66
|
isAllowed(url: string, userAgent?: string): boolean;
|
|
55
67
|
/**
|
|
56
|
-
* Get URLs of sitemaps referenced in the robots file.
|
|
68
|
+
* Get URLs of sitemaps referenced in the robots file, filtered by `options.enqueueStrategy` relative to
|
|
69
|
+
* the robots.txt host (default `'same-hostname'`; pass `'all'` to disable). Non-`http(s)` schemes are
|
|
70
|
+
* always dropped.
|
|
57
71
|
*/
|
|
58
|
-
getSitemaps(): string[];
|
|
72
|
+
getSitemaps(options?: RobotsTxtFileSitemapsOptions): string[];
|
|
59
73
|
/**
|
|
60
|
-
* Parse all the sitemaps referenced in the robots file.
|
|
74
|
+
* Parse all the sitemaps referenced in the robots file. `options` are forwarded to `getSitemaps`
|
|
75
|
+
* and the sitemap parser.
|
|
61
76
|
*/
|
|
62
|
-
parseSitemaps(): Promise<Sitemap>;
|
|
77
|
+
parseSitemaps(options?: RobotsTxtFileSitemapsOptions): Promise<Sitemap>;
|
|
63
78
|
/**
|
|
64
79
|
* Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
|
|
80
|
+
* `options` are forwarded to `parseSitemaps`.
|
|
65
81
|
*/
|
|
66
|
-
parseUrlsFromSitemaps(): Promise<string[]>;
|
|
82
|
+
parseUrlsFromSitemaps(options?: RobotsTxtFileSitemapsOptions): Promise<string[]>;
|
|
67
83
|
}
|
package/internals/robots.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FetchHttpClient } from '@crawlee/http-client';
|
|
2
2
|
import robotsParser from 'robots-parser';
|
|
3
3
|
import { Sitemap } from './sitemap.js';
|
|
4
|
+
import { filterUrl } from './url.js';
|
|
4
5
|
/**
|
|
5
6
|
* Loads and queries information from a [robots.txt file](https://en.wikipedia.org/wiki/Robots.txt).
|
|
6
7
|
*
|
|
@@ -20,13 +21,15 @@ import { Sitemap } from './sitemap.js';
|
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
22
23
|
export class RobotsTxtFile {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this
|
|
29
|
-
this
|
|
24
|
+
#url;
|
|
25
|
+
#robots;
|
|
26
|
+
#proxyUrl;
|
|
27
|
+
#logger;
|
|
28
|
+
constructor(url, robots, proxyUrl, logger) {
|
|
29
|
+
this.#url = url;
|
|
30
|
+
this.#robots = robots;
|
|
31
|
+
this.#proxyUrl = proxyUrl;
|
|
32
|
+
this.#logger = logger;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* Determine the location of a robots.txt file for a URL and fetch it.
|
|
@@ -40,7 +43,7 @@ export class RobotsTxtFile {
|
|
|
40
43
|
const robotsTxtFileUrl = new URL(url);
|
|
41
44
|
robotsTxtFileUrl.pathname = '/robots.txt';
|
|
42
45
|
robotsTxtFileUrl.search = '';
|
|
43
|
-
return RobotsTxtFile
|
|
46
|
+
return RobotsTxtFile.#load(robotsTxtFileUrl.toString(), options);
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
49
|
* Allows providing the URL and robots.txt content explicitly instead of loading it from the target site.
|
|
@@ -50,30 +53,42 @@ export class RobotsTxtFile {
|
|
|
50
53
|
*/
|
|
51
54
|
static from(url, content, proxyUrl) {
|
|
52
55
|
// @ts-ignore
|
|
53
|
-
return new RobotsTxtFile(robotsParser(url, content), proxyUrl);
|
|
56
|
+
return new RobotsTxtFile(url, robotsParser(url, content), proxyUrl);
|
|
54
57
|
}
|
|
55
|
-
static async load(url, options) {
|
|
58
|
+
static async #load(url, options) {
|
|
56
59
|
const { proxyUrl, logger, httpClient = new FetchHttpClient() } = options || {};
|
|
57
60
|
const response = await httpClient.sendRequest(new Request(url, { method: 'GET' }), {
|
|
58
61
|
proxyUrl,
|
|
59
62
|
timeoutMillis: options?.timeoutMillis,
|
|
60
63
|
signal: options?.signal,
|
|
61
64
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return new RobotsTxtFile({
|
|
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;
|
|
68
|
+
return new RobotsTxtFile(url, {
|
|
67
69
|
isAllowed() {
|
|
68
|
-
return
|
|
70
|
+
return allowed;
|
|
69
71
|
},
|
|
70
72
|
getSitemaps() {
|
|
71
73
|
return [];
|
|
72
74
|
},
|
|
75
|
+
getCrawlDelay() {
|
|
76
|
+
return undefined;
|
|
77
|
+
},
|
|
73
78
|
}, proxyUrl, logger);
|
|
74
79
|
}
|
|
80
|
+
if (response.status < 200 || response.status >= 300) {
|
|
81
|
+
throw new Error(`Failed to load robots.txt from ${url}: HTTP ${response.status}`);
|
|
82
|
+
}
|
|
75
83
|
// @ts-ignore
|
|
76
|
-
return new RobotsTxtFile(robotsParser(url.toString(), await response.text()), proxyUrl, logger);
|
|
84
|
+
return new RobotsTxtFile(url, robotsParser(url.toString(), await response.text()), proxyUrl, logger);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get crawl delay for a given user agent.
|
|
88
|
+
* @param [userAgent] relevant user agent, default to `*`
|
|
89
|
+
*/
|
|
90
|
+
getCrawlDelay(userAgent = '*') {
|
|
91
|
+
return this.#robots.getCrawlDelay(userAgent);
|
|
77
92
|
}
|
|
78
93
|
/**
|
|
79
94
|
* Check if a URL should be crawled by robots.
|
|
@@ -81,24 +96,39 @@ export class RobotsTxtFile {
|
|
|
81
96
|
* @param [userAgent] relevant user agent, default to `*`
|
|
82
97
|
*/
|
|
83
98
|
isAllowed(url, userAgent = '*') {
|
|
84
|
-
return this
|
|
99
|
+
return this.#robots.isAllowed(url, userAgent) ?? true; // `undefined` means that there is no explicit rule for the requested URL - assume it's allowed
|
|
85
100
|
}
|
|
86
101
|
/**
|
|
87
|
-
* Get URLs of sitemaps referenced in the robots file.
|
|
102
|
+
* Get URLs of sitemaps referenced in the robots file, filtered by `options.enqueueStrategy` relative to
|
|
103
|
+
* the robots.txt host (default `'same-hostname'`; pass `'all'` to disable). Non-`http(s)` schemes are
|
|
104
|
+
* always dropped.
|
|
88
105
|
*/
|
|
89
|
-
getSitemaps() {
|
|
90
|
-
|
|
106
|
+
getSitemaps(options = {}) {
|
|
107
|
+
const { enqueueStrategy = 'same-hostname' } = options;
|
|
108
|
+
const sitemaps = [];
|
|
109
|
+
for (const sitemapUrl of this.#robots.getSitemaps()) {
|
|
110
|
+
// `filterUrl` tolerates an unparseable origin (returns not-allowed) rather than throwing.
|
|
111
|
+
const { allowed, reason } = filterUrl(sitemapUrl, this.#url, enqueueStrategy);
|
|
112
|
+
if (!allowed) {
|
|
113
|
+
this.#logger?.warning(`Skipping sitemap ${sitemapUrl} listed in robots.txt at ${this.#url}: ${reason}.`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
sitemaps.push(sitemapUrl);
|
|
117
|
+
}
|
|
118
|
+
return sitemaps;
|
|
91
119
|
}
|
|
92
120
|
/**
|
|
93
|
-
* Parse all the sitemaps referenced in the robots file.
|
|
121
|
+
* Parse all the sitemaps referenced in the robots file. `options` are forwarded to `getSitemaps`
|
|
122
|
+
* and the sitemap parser.
|
|
94
123
|
*/
|
|
95
|
-
async parseSitemaps() {
|
|
96
|
-
return Sitemap.load(this.
|
|
124
|
+
async parseSitemaps(options = {}) {
|
|
125
|
+
return Sitemap.load(this.getSitemaps(options), this.#proxyUrl, { ...options, logger: this.#logger });
|
|
97
126
|
}
|
|
98
127
|
/**
|
|
99
128
|
* Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
|
|
129
|
+
* `options` are forwarded to `parseSitemaps`.
|
|
100
130
|
*/
|
|
101
|
-
async parseUrlsFromSitemaps() {
|
|
102
|
-
return (await this.parseSitemaps()).urls;
|
|
131
|
+
async parseUrlsFromSitemaps(options = {}) {
|
|
132
|
+
return (await this.parseSitemaps(options)).urls;
|
|
103
133
|
}
|
|
104
134
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { BaseHttpClient } from '@crawlee/http-client';
|
|
2
|
+
import type { Dictionary } from '@crawlee/types';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
/**
|
|
5
|
+
* Accepts any object (including arrays and functions).
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare const anyObject: z.ZodCustom<Dictionary, Dictionary>;
|
|
9
|
+
/**
|
|
10
|
+
* Accepts any array without validating its items (cheap for huge arrays).
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare const anyArray: z.ZodCustom<unknown[], unknown[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Accepts any function.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare const anyFunction: z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Mirrors `ow.number`: `Infinity` is a valid number, `NaN` is not.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare const anyNumber: z.ZodCustom<number, number>;
|
|
24
|
+
/**
|
|
25
|
+
* Accepts any object (including functions) that has all the given keys, own or inherited.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function objectWithKeys(keys: string[], message?: string): z.ZodType<Dictionary>;
|
|
29
|
+
/**
|
|
30
|
+
* Accepts only instances of {@link BaseHttpClient} (all Crawlee HTTP clients extend it).
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export declare const httpClient: z.ZodInstanceOf<BaseHttpClient>;
|
|
34
|
+
/**
|
|
35
|
+
* Accepts any object implementing the CrawleeLogger interface.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export declare const logger: z.ZodType<Dictionary, unknown, z.core.$ZodTypeInternals<Dictionary, unknown>>;
|
|
39
|
+
/**
|
|
40
|
+
* Accepts any typed array (`Uint8Array`, `Float64Array`, ...), but not a `DataView`.
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export declare const typedArray: z.ZodCustom<NodeJS.TypedArray<ArrayBufferLike>, NodeJS.TypedArray<ArrayBufferLike>>;
|
|
44
|
+
/**
|
|
45
|
+
* Accepts any non-null, non-array object.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export declare const plainObject: z.ZodCustom<Record<string, unknown>, Record<string, unknown>>;
|
|
49
|
+
/**
|
|
50
|
+
* Shape of a request stored in a request queue. Compiled: storage clients validate every request with it.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export declare const storageRequest: z.ZodObject<{
|
|
54
|
+
id: z.ZodString;
|
|
55
|
+
url: z.ZodURL;
|
|
56
|
+
uniqueKey: z.ZodString;
|
|
57
|
+
method: z.ZodOptional<z.ZodString>;
|
|
58
|
+
retryCount: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
handledAt: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodDate]>>;
|
|
60
|
+
}, z.core.$loose>;
|
|
61
|
+
/**
|
|
62
|
+
* {@link storageRequest} before an id is assigned.
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export declare const storageRequestWithoutId: z.ZodObject<{
|
|
66
|
+
url: z.ZodURL;
|
|
67
|
+
uniqueKey: z.ZodString;
|
|
68
|
+
method: z.ZodOptional<z.ZodString>;
|
|
69
|
+
retryCount: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
handledAt: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodDate]>>;
|
|
71
|
+
}, z.core.$loose>;
|
|
72
|
+
/**
|
|
73
|
+
* `z.array(item)` whose top-level type error names the element type — ``expected an array of numbers`` —
|
|
74
|
+
* instead of zod's bare `expected array`. Element failures keep zod's per-index messages, and `elements`
|
|
75
|
+
* is a human-readable plural (`'numbers'`, `'URL patterns'`), since element types cannot be introspected.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export declare function arrayOf<TItem extends z.ZodType>(item: TItem, elements: string): z.ZodArray<TItem>;
|
|
79
|
+
/**
|
|
80
|
+
* Batch of {@link storageRequestWithoutId}.
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export declare const storageRequestBatch: z.ZodArray<z.ZodObject<{
|
|
84
|
+
url: z.ZodURL;
|
|
85
|
+
uniqueKey: z.ZodString;
|
|
86
|
+
method: z.ZodOptional<z.ZodString>;
|
|
87
|
+
retryCount: z.ZodOptional<z.ZodNumber>;
|
|
88
|
+
handledAt: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodDate]>>;
|
|
89
|
+
}, z.core.$loose>>;
|
|
90
|
+
/**
|
|
91
|
+
* Options of request queue add/update operations.
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
export declare const requestQueueOperationOptions: z.ZodObject<{
|
|
95
|
+
forefront: z.ZodOptional<z.ZodBoolean>;
|
|
96
|
+
}, z.core.$strip>;
|
|
97
|
+
/**
|
|
98
|
+
* Options of key-value store `listKeys`.
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
export declare const keyValueStoreListKeysOptions: z.ZodObject<{
|
|
102
|
+
prefix: z.ZodOptional<z.ZodString>;
|
|
103
|
+
exclusiveStartKey: z.ZodOptional<z.ZodString>;
|
|
104
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
105
|
+
}, z.core.$strip>;
|
|
106
|
+
/**
|
|
107
|
+
* Options of dataset item listing.
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
export declare const datasetListItemsOptions: z.ZodObject<{
|
|
111
|
+
desc: z.ZodOptional<z.ZodBoolean>;
|
|
112
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
113
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
114
|
+
}, z.core.$strip>;
|