@crawlee/utils 4.0.0-beta.126 → 4.0.0-beta.128
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/index.d.ts +1 -0
- package/index.js +1 -0
- package/internals/robots.d.ts +18 -5
- package/internals/robots.js +30 -12
- package/internals/sitemap.d.ts +8 -0
- package/internals/sitemap.js +32 -4
- package/internals/url.d.ts +69 -0
- package/internals/url.js +119 -0
- package/package.json +5 -4
package/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { htmlToText } from './internals/cheerio.js';
|
|
2
2
|
export { downloadListOfUrls, extractUrls } from './internals/extract-urls.js';
|
|
3
|
+
export { EnqueueStrategy } from './internals/url.js';
|
|
3
4
|
export type { DownloadListOfUrlsOptions, ExtractUrlsOptions } from './internals/extract-urls.js';
|
|
4
5
|
export { sleep, expandShadowRoots } from './internals/general.js';
|
|
5
6
|
export * as social from './internals/social.js';
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { htmlToText } from './internals/cheerio.js';
|
|
2
2
|
export { downloadListOfUrls, extractUrls } from './internals/extract-urls.js';
|
|
3
|
+
export { EnqueueStrategy } from './internals/url.js';
|
|
3
4
|
export { sleep, expandShadowRoots } from './internals/general.js';
|
|
4
5
|
export * as social from './internals/social.js';
|
|
5
6
|
export * from './internals/open_graph_parser.js';
|
package/internals/robots.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { BaseHttpClient } from '@crawlee/http-client';
|
|
2
2
|
import type { CrawleeLogger } from '@crawlee/types';
|
|
3
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
|
+
}
|
|
4
13
|
/**
|
|
5
14
|
* Loads and queries information from a [robots.txt file](https://en.wikipedia.org/wiki/Robots.txt).
|
|
6
15
|
*
|
|
@@ -57,15 +66,19 @@ export declare class RobotsTxtFile {
|
|
|
57
66
|
*/
|
|
58
67
|
isAllowed(url: string, userAgent?: string): boolean;
|
|
59
68
|
/**
|
|
60
|
-
* Get URLs of sitemaps referenced in the robots file.
|
|
69
|
+
* Get URLs of sitemaps referenced in the robots file, filtered by `options.enqueueStrategy` relative to
|
|
70
|
+
* the robots.txt host (default `'same-hostname'`; pass `'all'` to disable). Non-`http(s)` schemes are
|
|
71
|
+
* always dropped.
|
|
61
72
|
*/
|
|
62
|
-
getSitemaps(): string[];
|
|
73
|
+
getSitemaps(options?: RobotsTxtFileSitemapsOptions): string[];
|
|
63
74
|
/**
|
|
64
|
-
* Parse all the sitemaps referenced in the robots file.
|
|
75
|
+
* Parse all the sitemaps referenced in the robots file. `options` are forwarded to `getSitemaps`
|
|
76
|
+
* and the sitemap parser.
|
|
65
77
|
*/
|
|
66
|
-
parseSitemaps(): Promise<Sitemap>;
|
|
78
|
+
parseSitemaps(options?: RobotsTxtFileSitemapsOptions): Promise<Sitemap>;
|
|
67
79
|
/**
|
|
68
80
|
* Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
|
|
81
|
+
* `options` are forwarded to `parseSitemaps`.
|
|
69
82
|
*/
|
|
70
|
-
parseUrlsFromSitemaps(): Promise<string[]>;
|
|
83
|
+
parseUrlsFromSitemaps(options?: RobotsTxtFileSitemapsOptions): Promise<string[]>;
|
|
71
84
|
}
|
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,10 +21,12 @@ import { Sitemap } from './sitemap.js';
|
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
22
23
|
export class RobotsTxtFile {
|
|
24
|
+
#url;
|
|
23
25
|
#robots;
|
|
24
26
|
#proxyUrl;
|
|
25
27
|
#logger;
|
|
26
|
-
constructor(robots, proxyUrl, logger) {
|
|
28
|
+
constructor(url, robots, proxyUrl, logger) {
|
|
29
|
+
this.#url = url;
|
|
27
30
|
this.#robots = robots;
|
|
28
31
|
this.#proxyUrl = proxyUrl;
|
|
29
32
|
this.#logger = logger;
|
|
@@ -50,7 +53,7 @@ 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
58
|
static async load(url, options) {
|
|
56
59
|
const { proxyUrl, logger, httpClient = new FetchHttpClient() } = options || {};
|
|
@@ -63,7 +66,7 @@ export class RobotsTxtFile {
|
|
|
63
66
|
throw new Error(`Failed to load robots.txt from ${url}: HTTP ${response.status}`);
|
|
64
67
|
}
|
|
65
68
|
if (response.status === 404) {
|
|
66
|
-
return new RobotsTxtFile({
|
|
69
|
+
return new RobotsTxtFile(url, {
|
|
67
70
|
isAllowed() {
|
|
68
71
|
return true;
|
|
69
72
|
},
|
|
@@ -76,7 +79,7 @@ export class RobotsTxtFile {
|
|
|
76
79
|
}, proxyUrl, logger);
|
|
77
80
|
}
|
|
78
81
|
// @ts-ignore
|
|
79
|
-
return new RobotsTxtFile(robotsParser(url.toString(), await response.text()), proxyUrl, logger);
|
|
82
|
+
return new RobotsTxtFile(url, robotsParser(url.toString(), await response.text()), proxyUrl, logger);
|
|
80
83
|
}
|
|
81
84
|
/**
|
|
82
85
|
* Get crawl delay for a given user agent.
|
|
@@ -94,21 +97,36 @@ export class RobotsTxtFile {
|
|
|
94
97
|
return this.#robots.isAllowed(url, userAgent) ?? true; // `undefined` means that there is no explicit rule for the requested URL - assume it's allowed
|
|
95
98
|
}
|
|
96
99
|
/**
|
|
97
|
-
* Get URLs of sitemaps referenced in the robots file.
|
|
100
|
+
* Get URLs of sitemaps referenced in the robots file, filtered by `options.enqueueStrategy` relative to
|
|
101
|
+
* the robots.txt host (default `'same-hostname'`; pass `'all'` to disable). Non-`http(s)` schemes are
|
|
102
|
+
* always dropped.
|
|
98
103
|
*/
|
|
99
|
-
getSitemaps() {
|
|
100
|
-
|
|
104
|
+
getSitemaps(options = {}) {
|
|
105
|
+
const { enqueueStrategy = 'same-hostname' } = options;
|
|
106
|
+
const sitemaps = [];
|
|
107
|
+
for (const sitemapUrl of this.#robots.getSitemaps()) {
|
|
108
|
+
// `filterUrl` tolerates an unparseable origin (returns not-allowed) rather than throwing.
|
|
109
|
+
const { allowed, reason } = filterUrl(sitemapUrl, this.#url, enqueueStrategy);
|
|
110
|
+
if (!allowed) {
|
|
111
|
+
this.#logger?.warning(`Skipping sitemap ${sitemapUrl} listed in robots.txt at ${this.#url}: ${reason}.`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
sitemaps.push(sitemapUrl);
|
|
115
|
+
}
|
|
116
|
+
return sitemaps;
|
|
101
117
|
}
|
|
102
118
|
/**
|
|
103
|
-
* Parse all the sitemaps referenced in the robots file.
|
|
119
|
+
* Parse all the sitemaps referenced in the robots file. `options` are forwarded to `getSitemaps`
|
|
120
|
+
* and the sitemap parser.
|
|
104
121
|
*/
|
|
105
|
-
async parseSitemaps() {
|
|
106
|
-
return Sitemap.load(this
|
|
122
|
+
async parseSitemaps(options = {}) {
|
|
123
|
+
return Sitemap.load(this.getSitemaps(options), this.#proxyUrl, { ...options, logger: this.#logger });
|
|
107
124
|
}
|
|
108
125
|
/**
|
|
109
126
|
* Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
|
|
127
|
+
* `options` are forwarded to `parseSitemaps`.
|
|
110
128
|
*/
|
|
111
|
-
async parseUrlsFromSitemaps() {
|
|
112
|
-
return (await this.parseSitemaps()).urls;
|
|
129
|
+
async parseUrlsFromSitemaps(options = {}) {
|
|
130
|
+
return (await this.parseSitemaps(options)).urls;
|
|
113
131
|
}
|
|
114
132
|
}
|
package/internals/sitemap.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BaseHttpClient } from '@crawlee/http-client';
|
|
2
2
|
import type { CrawleeLogger } from '@crawlee/types';
|
|
3
|
+
import { type EnqueueStrategy } from './url.js';
|
|
3
4
|
interface SitemapUrlData {
|
|
4
5
|
loc: string;
|
|
5
6
|
lastmod?: Date;
|
|
@@ -55,6 +56,13 @@ export interface ParseSitemapOptions {
|
|
|
55
56
|
* If not provided, all nested sitemaps are followed.
|
|
56
57
|
*/
|
|
57
58
|
nestedSitemapFilter?: (sitemapUrl: string) => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Keep only sitemap-derived URLs (nested `<sitemap>` and `<url>` entries) matching this strategy
|
|
61
|
+
* relative to the parent sitemap URL; non-`http(s)` schemes are always dropped. Skipped for raw string
|
|
62
|
+
* sources (no parent URL). Pass `'all'` to disable host filtering.
|
|
63
|
+
* @default 'same-hostname'
|
|
64
|
+
*/
|
|
65
|
+
enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`;
|
|
58
66
|
/**
|
|
59
67
|
* Optional logger for reporting warnings during sitemap parsing.
|
|
60
68
|
*/
|
package/internals/sitemap.js
CHANGED
|
@@ -6,6 +6,7 @@ import { FetchHttpClient } from '@crawlee/http-client';
|
|
|
6
6
|
import MIMEType from 'whatwg-mimetype';
|
|
7
7
|
import { mergeAsyncIterables } from './iterables.js';
|
|
8
8
|
import { RobotsTxtFile } from './robots.js';
|
|
9
|
+
import { filterUrl } from './url.js';
|
|
9
10
|
class SitemapTxtParser extends Transform {
|
|
10
11
|
#decoder = new StringDecoder('utf8');
|
|
11
12
|
#buffer = '';
|
|
@@ -132,7 +133,7 @@ class SitemapXmlParser extends Transform {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
135
|
-
const { httpClient = new FetchHttpClient(), emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, timeoutMillis: timeout = 30000, reportNetworkErrors = true, nestedSitemapFilter, logger, } = options ?? {};
|
|
136
|
+
const { httpClient = new FetchHttpClient(), emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, timeoutMillis: timeout = 30000, reportNetworkErrors = true, nestedSitemapFilter, enqueueStrategy = 'same-hostname', logger, } = options ?? {};
|
|
136
137
|
const sources = [...initialSources];
|
|
137
138
|
const visitedSitemapUrls = new Set();
|
|
138
139
|
const createParser = async (contentType = '', url) => {
|
|
@@ -157,8 +158,10 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
157
158
|
continue;
|
|
158
159
|
}
|
|
159
160
|
let items = null;
|
|
161
|
+
// Parent URL, parsed once and reused as the origin for the strategy checks below.
|
|
162
|
+
let sitemapUrl;
|
|
160
163
|
if (source.type === 'url') {
|
|
161
|
-
|
|
164
|
+
sitemapUrl = new URL(source.url);
|
|
162
165
|
visitedSitemapUrls.add(sitemapUrl.toString());
|
|
163
166
|
let retriesLeft = sitemapRetries + 1;
|
|
164
167
|
while (retriesLeft-- > 0) {
|
|
@@ -235,18 +238,39 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
235
238
|
if (items === null) {
|
|
236
239
|
continue;
|
|
237
240
|
}
|
|
241
|
+
// URL entries dropped by the enqueue strategy filter, reported in one warning per sitemap after
|
|
242
|
+
// the loop (per-entry warnings could flood the log; individual drops are logged at debug level).
|
|
243
|
+
let droppedUrlEntries = 0;
|
|
238
244
|
for await (const item of items) {
|
|
239
245
|
if (item.type === 'sitemapUrl' && !visitedSitemapUrls.has(item.url)) {
|
|
240
246
|
if (nestedSitemapFilter && !nestedSitemapFilter(item.url)) {
|
|
241
247
|
logger?.debug(`Skipping sitemap ${item.url} due to nestedSitemapFilter.`);
|
|
242
248
|
continue;
|
|
243
249
|
}
|
|
250
|
+
// Keep only nested sitemaps matching the strategy (and using http(s)) relative to the
|
|
251
|
+
// parent. Raw string sources have no parent URL, so the check is skipped.
|
|
252
|
+
if (source.type === 'url') {
|
|
253
|
+
const { allowed, reason } = filterUrl(item.url, sitemapUrl, enqueueStrategy);
|
|
254
|
+
if (!allowed) {
|
|
255
|
+
logger?.warning(`Skipping nested sitemap ${item.url} (parent ${source.url}): ${reason}.`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
244
259
|
sources.push({ type: 'url', url: item.url, depth: (source.depth ?? 0) + 1 });
|
|
245
260
|
if (emitNestedSitemaps) {
|
|
246
261
|
yield { loc: item.url, originSitemapUrl: null };
|
|
247
262
|
}
|
|
248
263
|
}
|
|
249
264
|
if (item.type === 'url') {
|
|
265
|
+
// Keep only URL entries that match the enqueue strategy relative to the parent (see above).
|
|
266
|
+
if (source.type === 'url') {
|
|
267
|
+
const { allowed, reason } = filterUrl(item.loc, sitemapUrl, enqueueStrategy);
|
|
268
|
+
if (!allowed) {
|
|
269
|
+
droppedUrlEntries++;
|
|
270
|
+
logger?.debug(`Skipping sitemap URL ${item.loc} (parent ${source.url}): ${reason}.`);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
250
274
|
yield {
|
|
251
275
|
...item,
|
|
252
276
|
originSitemapUrl: source.type === 'url'
|
|
@@ -255,6 +279,9 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
255
279
|
};
|
|
256
280
|
}
|
|
257
281
|
}
|
|
282
|
+
if (droppedUrlEntries > 0 && source.type === 'url') {
|
|
283
|
+
logger?.warning(`Skipped ${droppedUrlEntries} URL(s) from sitemap ${source.url} not matching enqueue strategy '${enqueueStrategy}' (or using a non-http(s) scheme). Enable debug logs to see each skipped URL.`);
|
|
284
|
+
}
|
|
258
285
|
}
|
|
259
286
|
}
|
|
260
287
|
/**
|
|
@@ -378,7 +405,8 @@ export async function* discoverValidSitemaps(urls, options = {}) {
|
|
|
378
405
|
httpClient,
|
|
379
406
|
logger,
|
|
380
407
|
});
|
|
381
|
-
|
|
408
|
+
// Surface all referenced sitemaps, including cross-host; scoping happens at load time.
|
|
409
|
+
for (const sitemapUrl of robotsFile.getSitemaps({ enqueueStrategy: 'all' })) {
|
|
382
410
|
if (addSitemapUrl(sitemapUrl)) {
|
|
383
411
|
yield sitemapUrl;
|
|
384
412
|
}
|
|
@@ -387,7 +415,7 @@ export async function* discoverValidSitemaps(urls, options = {}) {
|
|
|
387
415
|
catch (err) {
|
|
388
416
|
logger?.warning(`Failed to fetch robots.txt file for ${hostname}`, { error: err });
|
|
389
417
|
}
|
|
390
|
-
const sitemapUrl = domainUrls.find((url) => /sitemap
|
|
418
|
+
const sitemapUrl = domainUrls.find((url) => /sitemap(?:_index)?\.(?:xml|txt)(?:\.gz)?$/i.test(url));
|
|
391
419
|
if (sitemapUrl !== undefined) {
|
|
392
420
|
if (addSitemapUrl(sitemapUrl)) {
|
|
393
421
|
yield sitemapUrl;
|
package/internals/url.d.ts
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
1
1
|
import type { SearchParams } from '@crawlee/types';
|
|
2
|
+
/**
|
|
3
|
+
* The different enqueueing strategies available.
|
|
4
|
+
*
|
|
5
|
+
* Depending on the strategy you select, we will only check certain parts of the URLs found. Here is a diagram of each URL part and their name:
|
|
6
|
+
*
|
|
7
|
+
* ```md
|
|
8
|
+
* Protocol Domain
|
|
9
|
+
* ┌────┐ ┌─────────┐
|
|
10
|
+
* https://example.crawlee.dev/...
|
|
11
|
+
* │ └─────────────────┤
|
|
12
|
+
* │ Hostname │
|
|
13
|
+
* │ │
|
|
14
|
+
* └─────────────────────────┘
|
|
15
|
+
* Origin
|
|
16
|
+
*```
|
|
17
|
+
*
|
|
18
|
+
* - The `Protocol` is usually `http` or `https`
|
|
19
|
+
* - The `Domain` represents the path without any possible subdomains to a website. For example, `crawlee.dev` is the domain of `https://example.crawlee.dev/`
|
|
20
|
+
* - The `Hostname` is the full path to a website, including any subdomains. For example, `example.crawlee.dev` is the hostname of `https://example.crawlee.dev/`
|
|
21
|
+
* - The `Origin` is the combination of the `Protocol` and `Hostname`. For example, `https://example.crawlee.dev` is the origin of `https://example.crawlee.dev/`
|
|
22
|
+
*/
|
|
23
|
+
export declare enum EnqueueStrategy {
|
|
24
|
+
/**
|
|
25
|
+
* Matches any URLs found
|
|
26
|
+
*/
|
|
27
|
+
All = "all",
|
|
28
|
+
/**
|
|
29
|
+
* Matches any URLs that have the same hostname.
|
|
30
|
+
* For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
|
|
31
|
+
* `https://example.com/hello` will not be matched.
|
|
32
|
+
*
|
|
33
|
+
* > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
|
|
34
|
+
*/
|
|
35
|
+
SameHostname = "same-hostname",
|
|
36
|
+
/**
|
|
37
|
+
* Matches any URLs that have the same domain as the base URL.
|
|
38
|
+
* For example, `https://wow.an.example.com` and `https://example.com` will both be matched for a base url of
|
|
39
|
+
* `https://example.com`.
|
|
40
|
+
*
|
|
41
|
+
* > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
|
|
42
|
+
*/
|
|
43
|
+
SameDomain = "same-domain",
|
|
44
|
+
/**
|
|
45
|
+
* Matches any URLs that have the same hostname and protocol.
|
|
46
|
+
* For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
|
|
47
|
+
* `http://wow.example.com/hello` will not be matched.
|
|
48
|
+
*
|
|
49
|
+
* > This strategy will ensure the protocol of the base URL is the same as the protocol of the URL to be enqueued.
|
|
50
|
+
*/
|
|
51
|
+
SameOrigin = "same-origin"
|
|
52
|
+
}
|
|
53
|
+
/** Reusable suffix for log messages explaining why a non-`http(s)` URL was rejected. */
|
|
54
|
+
export declare const UNSUPPORTED_SCHEME_MESSAGE = "unsupported URL scheme (only http and https are allowed)";
|
|
55
|
+
/**
|
|
56
|
+
* Check whether `target` matches `origin` under the given enqueue `strategy`. The URL scheme is not
|
|
57
|
+
* considered here (use {@link filterUrl} for the combined scheme + strategy check).
|
|
58
|
+
*
|
|
59
|
+
* The `enqueueLinks` implementation in `@crawlee/core` matches the same strategies via glob patterns
|
|
60
|
+
* (see `packages/core/src/enqueue_links/enqueue_links.ts`) — keep the two in sync when changing either.
|
|
61
|
+
*/
|
|
62
|
+
export declare function matchesEnqueueStrategy(strategy: EnqueueStrategy | `${EnqueueStrategy}`, target: URL, origin: URL): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Check whether `target` may be enqueued under `strategy` relative to `origin`: it must use an `http(s)`
|
|
65
|
+
* scheme and match the strategy. On rejection, `reason` is a human-readable message for log output.
|
|
66
|
+
*/
|
|
67
|
+
export declare function filterUrl(target: string | URL, origin: string | URL, strategy: EnqueueStrategy | `${EnqueueStrategy}`): {
|
|
68
|
+
allowed: boolean;
|
|
69
|
+
reason?: string;
|
|
70
|
+
};
|
|
2
71
|
/**
|
|
3
72
|
* Appends search (query string) parameters to a URL, replacing the original value (if any).
|
|
4
73
|
*
|
package/internals/url.js
CHANGED
|
@@ -1,3 +1,122 @@
|
|
|
1
|
+
import { getDomain } from 'tldts';
|
|
2
|
+
/**
|
|
3
|
+
* The different enqueueing strategies available.
|
|
4
|
+
*
|
|
5
|
+
* Depending on the strategy you select, we will only check certain parts of the URLs found. Here is a diagram of each URL part and their name:
|
|
6
|
+
*
|
|
7
|
+
* ```md
|
|
8
|
+
* Protocol Domain
|
|
9
|
+
* ┌────┐ ┌─────────┐
|
|
10
|
+
* https://example.crawlee.dev/...
|
|
11
|
+
* │ └─────────────────┤
|
|
12
|
+
* │ Hostname │
|
|
13
|
+
* │ │
|
|
14
|
+
* └─────────────────────────┘
|
|
15
|
+
* Origin
|
|
16
|
+
*```
|
|
17
|
+
*
|
|
18
|
+
* - The `Protocol` is usually `http` or `https`
|
|
19
|
+
* - The `Domain` represents the path without any possible subdomains to a website. For example, `crawlee.dev` is the domain of `https://example.crawlee.dev/`
|
|
20
|
+
* - The `Hostname` is the full path to a website, including any subdomains. For example, `example.crawlee.dev` is the hostname of `https://example.crawlee.dev/`
|
|
21
|
+
* - The `Origin` is the combination of the `Protocol` and `Hostname`. For example, `https://example.crawlee.dev` is the origin of `https://example.crawlee.dev/`
|
|
22
|
+
*/
|
|
23
|
+
export var EnqueueStrategy;
|
|
24
|
+
(function (EnqueueStrategy) {
|
|
25
|
+
/**
|
|
26
|
+
* Matches any URLs found
|
|
27
|
+
*/
|
|
28
|
+
EnqueueStrategy["All"] = "all";
|
|
29
|
+
/**
|
|
30
|
+
* Matches any URLs that have the same hostname.
|
|
31
|
+
* For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
|
|
32
|
+
* `https://example.com/hello` will not be matched.
|
|
33
|
+
*
|
|
34
|
+
* > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
|
|
35
|
+
*/
|
|
36
|
+
EnqueueStrategy["SameHostname"] = "same-hostname";
|
|
37
|
+
/**
|
|
38
|
+
* Matches any URLs that have the same domain as the base URL.
|
|
39
|
+
* For example, `https://wow.an.example.com` and `https://example.com` will both be matched for a base url of
|
|
40
|
+
* `https://example.com`.
|
|
41
|
+
*
|
|
42
|
+
* > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
|
|
43
|
+
*/
|
|
44
|
+
EnqueueStrategy["SameDomain"] = "same-domain";
|
|
45
|
+
/**
|
|
46
|
+
* Matches any URLs that have the same hostname and protocol.
|
|
47
|
+
* For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
|
|
48
|
+
* `http://wow.example.com/hello` will not be matched.
|
|
49
|
+
*
|
|
50
|
+
* > This strategy will ensure the protocol of the base URL is the same as the protocol of the URL to be enqueued.
|
|
51
|
+
*/
|
|
52
|
+
EnqueueStrategy["SameOrigin"] = "same-origin";
|
|
53
|
+
})(EnqueueStrategy || (EnqueueStrategy = {}));
|
|
54
|
+
/** Reusable suffix for log messages explaining why a non-`http(s)` URL was rejected. */
|
|
55
|
+
export const UNSUPPORTED_SCHEME_MESSAGE = 'unsupported URL scheme (only http and https are allowed)';
|
|
56
|
+
const ALLOWED_SCHEMES = new Set(['http:', 'https:']);
|
|
57
|
+
function toUrl(value) {
|
|
58
|
+
if (value instanceof URL) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return new URL(value);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Strip a trailing dot so `example.com.` equals `example.com`. */
|
|
69
|
+
function normalizeHostname(hostname) {
|
|
70
|
+
return hostname.endsWith('.') ? hostname.slice(0, -1) : hostname;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check whether `target` matches `origin` under the given enqueue `strategy`. The URL scheme is not
|
|
74
|
+
* considered here (use {@link filterUrl} for the combined scheme + strategy check).
|
|
75
|
+
*
|
|
76
|
+
* The `enqueueLinks` implementation in `@crawlee/core` matches the same strategies via glob patterns
|
|
77
|
+
* (see `packages/core/src/enqueue_links/enqueue_links.ts`) — keep the two in sync when changing either.
|
|
78
|
+
*/
|
|
79
|
+
export function matchesEnqueueStrategy(strategy, target, origin) {
|
|
80
|
+
switch (strategy) {
|
|
81
|
+
case 'all':
|
|
82
|
+
return true;
|
|
83
|
+
case 'same-hostname':
|
|
84
|
+
return normalizeHostname(target.hostname) === normalizeHostname(origin.hostname);
|
|
85
|
+
case 'same-domain': {
|
|
86
|
+
const originDomain = getDomain(origin.hostname, { mixedInputs: false });
|
|
87
|
+
if (originDomain) {
|
|
88
|
+
return originDomain === getDomain(target.hostname, { mixedInputs: false });
|
|
89
|
+
}
|
|
90
|
+
// No registrable domain (e.g. an IP address), fall back to comparing origins.
|
|
91
|
+
return target.origin === origin.origin;
|
|
92
|
+
}
|
|
93
|
+
case 'same-origin':
|
|
94
|
+
// Compare scheme/host/port directly so a trailing-dot host is normalized.
|
|
95
|
+
return (target.protocol === origin.protocol &&
|
|
96
|
+
normalizeHostname(target.hostname) === normalizeHostname(origin.hostname) &&
|
|
97
|
+
target.port === origin.port);
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`Unknown enqueue strategy '${strategy}'.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check whether `target` may be enqueued under `strategy` relative to `origin`: it must use an `http(s)`
|
|
104
|
+
* scheme and match the strategy. On rejection, `reason` is a human-readable message for log output.
|
|
105
|
+
*/
|
|
106
|
+
export function filterUrl(target, origin, strategy) {
|
|
107
|
+
const targetUrl = toUrl(target);
|
|
108
|
+
if (targetUrl === null || !ALLOWED_SCHEMES.has(targetUrl.protocol)) {
|
|
109
|
+
return { allowed: false, reason: UNSUPPORTED_SCHEME_MESSAGE };
|
|
110
|
+
}
|
|
111
|
+
const originUrl = toUrl(origin);
|
|
112
|
+
if (originUrl === null) {
|
|
113
|
+
return { allowed: false, reason: 'invalid origin URL' };
|
|
114
|
+
}
|
|
115
|
+
if (!matchesEnqueueStrategy(strategy, targetUrl, originUrl)) {
|
|
116
|
+
return { allowed: false, reason: `does not match enqueue strategy '${strategy}'` };
|
|
117
|
+
}
|
|
118
|
+
return { allowed: true };
|
|
119
|
+
}
|
|
1
120
|
/**
|
|
2
121
|
* Appends search (query string) parameters to a URL, replacing the original value (if any).
|
|
3
122
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/utils",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.128",
|
|
4
4
|
"description": "A set of shared utilities that can be used by crawlers",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -43,14 +43,15 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@apify/ps-tree": "^1.2.0",
|
|
46
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
47
|
-
"@crawlee/types": "4.0.0-beta.
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.128",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.128",
|
|
48
48
|
"@types/sax": "^1.2.7",
|
|
49
49
|
"cheerio": "^1.0.0",
|
|
50
50
|
"domhandler": "^5.0.3",
|
|
51
51
|
"file-type": "^21.0.0",
|
|
52
52
|
"robots-parser": "^3.0.1",
|
|
53
53
|
"sax": "^1.4.1",
|
|
54
|
+
"tldts": "^7.0.6",
|
|
54
55
|
"tslib": "^2.8.1",
|
|
55
56
|
"whatwg-mimetype": "^4.0.0",
|
|
56
57
|
"zod": "^4.4.3"
|
|
@@ -62,5 +63,5 @@
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
},
|
|
65
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "2c017f04d564e8fa13855bf19f31f43cb65f4f44"
|
|
66
67
|
}
|