@crawlee/utils 3.17.1-beta.57 → 3.17.1-beta.59
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.mjs +4 -0
- package/internals/robots.d.ts +19 -5
- package/internals/robots.js +35 -12
- package/internals/sitemap.d.ts +8 -0
- package/internals/sitemap.js +31 -3
- package/internals/url.d.ts +69 -0
- package/internals/url.js +122 -0
- package/package.json +4 -3
package/index.mjs
CHANGED
|
@@ -2,11 +2,13 @@ import mod from "./index.js";
|
|
|
2
2
|
|
|
3
3
|
export default mod;
|
|
4
4
|
export const CLOUDFLARE_RETRY_CSS_SELECTORS = mod.CLOUDFLARE_RETRY_CSS_SELECTORS;
|
|
5
|
+
export const EnqueueStrategy = mod.EnqueueStrategy;
|
|
5
6
|
export const RETRY_CSS_SELECTORS = mod.RETRY_CSS_SELECTORS;
|
|
6
7
|
export const ROTATE_PROXY_ERRORS = mod.ROTATE_PROXY_ERRORS;
|
|
7
8
|
export const RobotsFile = mod.RobotsFile;
|
|
8
9
|
export const RobotsTxtFile = mod.RobotsTxtFile;
|
|
9
10
|
export const Sitemap = mod.Sitemap;
|
|
11
|
+
export const UNSUPPORTED_SCHEME_MESSAGE = mod.UNSUPPORTED_SCHEME_MESSAGE;
|
|
10
12
|
export const URL_NO_COMMAS_REGEX = mod.URL_NO_COMMAS_REGEX;
|
|
11
13
|
export const URL_WITH_COMMAS_REGEX = mod.URL_WITH_COMMAS_REGEX;
|
|
12
14
|
export const applySearchParams = mod.applySearchParams;
|
|
@@ -20,6 +22,7 @@ export const entries = mod.entries;
|
|
|
20
22
|
export const expandShadowRoots = mod.expandShadowRoots;
|
|
21
23
|
export const extractUrls = mod.extractUrls;
|
|
22
24
|
export const extractUrlsFromCheerio = mod.extractUrlsFromCheerio;
|
|
25
|
+
export const filterUrl = mod.filterUrl;
|
|
23
26
|
export const getCgroupsVersion = mod.getCgroupsVersion;
|
|
24
27
|
export const getCurrentCpuTicksV2 = mod.getCurrentCpuTicksV2;
|
|
25
28
|
export const getMemoryInfo = mod.getMemoryInfo;
|
|
@@ -33,6 +36,7 @@ export const isDocker = mod.isDocker;
|
|
|
33
36
|
export const isIterable = mod.isIterable;
|
|
34
37
|
export const isLambda = mod.isLambda;
|
|
35
38
|
export const keys = mod.keys;
|
|
39
|
+
export const matchesEnqueueStrategy = mod.matchesEnqueueStrategy;
|
|
36
40
|
export const mergeAsyncIterables = mod.mergeAsyncIterables;
|
|
37
41
|
export const parseOpenGraph = mod.parseOpenGraph;
|
|
38
42
|
export const parseSitemap = mod.parseSitemap;
|
package/internals/robots.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { Sitemap } from './sitemap';
|
|
2
|
+
import { type EnqueueStrategy } from './url';
|
|
3
|
+
export interface RobotsTxtFileSitemapsOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Keep only sitemap URLs matching this strategy relative to the robots.txt host; non-`http(s)` schemes
|
|
6
|
+
* are always dropped. Pass `'all'` to disable host filtering.
|
|
7
|
+
* @default 'same-hostname'
|
|
8
|
+
*/
|
|
9
|
+
enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`;
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
12
|
* Loads and queries information from a [robots.txt file](https://en.wikipedia.org/wiki/Robots.txt).
|
|
4
13
|
*
|
|
@@ -18,6 +27,7 @@ import { Sitemap } from './sitemap';
|
|
|
18
27
|
* ```
|
|
19
28
|
*/
|
|
20
29
|
export declare class RobotsTxtFile {
|
|
30
|
+
private url;
|
|
21
31
|
private robots;
|
|
22
32
|
private proxyUrl?;
|
|
23
33
|
private constructor();
|
|
@@ -51,16 +61,20 @@ export declare class RobotsTxtFile {
|
|
|
51
61
|
*/
|
|
52
62
|
isAllowed(url: string, userAgent?: string): boolean;
|
|
53
63
|
/**
|
|
54
|
-
* Get URLs of sitemaps referenced in the robots file.
|
|
64
|
+
* Get URLs of sitemaps referenced in the robots file, filtered by `options.enqueueStrategy` relative to
|
|
65
|
+
* the robots.txt host (default `'same-hostname'`; pass `'all'` to disable). Non-`http(s)` schemes are
|
|
66
|
+
* always dropped.
|
|
55
67
|
*/
|
|
56
|
-
getSitemaps(): string[];
|
|
68
|
+
getSitemaps(options?: RobotsTxtFileSitemapsOptions): string[];
|
|
57
69
|
/**
|
|
58
|
-
* Parse all the sitemaps referenced in the robots file.
|
|
70
|
+
* Parse all the sitemaps referenced in the robots file. `options` are forwarded to `getSitemaps`
|
|
71
|
+
* and the sitemap parser.
|
|
59
72
|
*/
|
|
60
|
-
parseSitemaps(): Promise<Sitemap>;
|
|
73
|
+
parseSitemaps(options?: RobotsTxtFileSitemapsOptions): Promise<Sitemap>;
|
|
61
74
|
/**
|
|
62
75
|
* Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
|
|
76
|
+
* `options` are forwarded to `parseSitemaps`.
|
|
63
77
|
*/
|
|
64
|
-
parseUrlsFromSitemaps(): Promise<string[]>;
|
|
78
|
+
parseUrlsFromSitemaps(options?: RobotsTxtFileSitemapsOptions): Promise<string[]>;
|
|
65
79
|
}
|
|
66
80
|
export { RobotsTxtFile as RobotsFile };
|
package/internals/robots.js
CHANGED
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RobotsFile = exports.RobotsTxtFile = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const robots_parser_1 = tslib_1.__importDefault(require("robots-parser"));
|
|
6
|
+
const log_1 = tslib_1.__importDefault(require("@apify/log"));
|
|
6
7
|
const gotScraping_1 = require("./gotScraping");
|
|
7
8
|
const sitemap_1 = require("./sitemap");
|
|
9
|
+
const url_1 = require("./url");
|
|
8
10
|
let HTTPError;
|
|
9
11
|
/**
|
|
10
12
|
* Loads and queries information from a [robots.txt file](https://en.wikipedia.org/wiki/Robots.txt).
|
|
@@ -25,7 +27,13 @@ let HTTPError;
|
|
|
25
27
|
* ```
|
|
26
28
|
*/
|
|
27
29
|
class RobotsTxtFile {
|
|
28
|
-
constructor(robots, proxyUrl) {
|
|
30
|
+
constructor(url, robots, proxyUrl) {
|
|
31
|
+
Object.defineProperty(this, "url", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: url
|
|
36
|
+
});
|
|
29
37
|
Object.defineProperty(this, "robots", {
|
|
30
38
|
enumerable: true,
|
|
31
39
|
configurable: true,
|
|
@@ -60,7 +68,7 @@ class RobotsTxtFile {
|
|
|
60
68
|
* @param [proxyUrl] a proxy to be used for fetching the robots.txt file
|
|
61
69
|
*/
|
|
62
70
|
static from(url, content, proxyUrl) {
|
|
63
|
-
return new RobotsTxtFile((0, robots_parser_1.default)(url, content), proxyUrl);
|
|
71
|
+
return new RobotsTxtFile(url, (0, robots_parser_1.default)(url, content), proxyUrl);
|
|
64
72
|
}
|
|
65
73
|
static async load(url, proxyUrl, options) {
|
|
66
74
|
if (!HTTPError) {
|
|
@@ -75,11 +83,11 @@ class RobotsTxtFile {
|
|
|
75
83
|
signal: options?.signal,
|
|
76
84
|
...(options?.timeoutMillis ? { timeout: { request: options.timeoutMillis } } : {}),
|
|
77
85
|
});
|
|
78
|
-
return new RobotsTxtFile((0, robots_parser_1.default)(url.toString(), response.body), proxyUrl);
|
|
86
|
+
return new RobotsTxtFile(url, (0, robots_parser_1.default)(url.toString(), response.body), proxyUrl);
|
|
79
87
|
}
|
|
80
88
|
catch (e) {
|
|
81
89
|
if (e instanceof HTTPError && e.response.statusCode === 404) {
|
|
82
|
-
return new RobotsTxtFile({
|
|
90
|
+
return new RobotsTxtFile(url, {
|
|
83
91
|
isAllowed() {
|
|
84
92
|
return true;
|
|
85
93
|
},
|
|
@@ -100,22 +108,37 @@ class RobotsTxtFile {
|
|
|
100
108
|
return this.robots.isAllowed(url, userAgent) ?? true; // `undefined` means that there is no explicit rule for the requested URL - assume it's allowed
|
|
101
109
|
}
|
|
102
110
|
/**
|
|
103
|
-
* Get URLs of sitemaps referenced in the robots file.
|
|
111
|
+
* Get URLs of sitemaps referenced in the robots file, filtered by `options.enqueueStrategy` relative to
|
|
112
|
+
* the robots.txt host (default `'same-hostname'`; pass `'all'` to disable). Non-`http(s)` schemes are
|
|
113
|
+
* always dropped.
|
|
104
114
|
*/
|
|
105
|
-
getSitemaps() {
|
|
106
|
-
|
|
115
|
+
getSitemaps(options = {}) {
|
|
116
|
+
const { enqueueStrategy = 'same-hostname' } = options;
|
|
117
|
+
const sitemaps = [];
|
|
118
|
+
for (const sitemapUrl of this.robots.getSitemaps()) {
|
|
119
|
+
// `filterUrl` tolerates an unparseable origin (returns not-allowed) rather than throwing.
|
|
120
|
+
const { allowed, reason } = (0, url_1.filterUrl)(sitemapUrl, this.url, enqueueStrategy);
|
|
121
|
+
if (!allowed) {
|
|
122
|
+
log_1.default.warning(`Skipping sitemap ${sitemapUrl} listed in robots.txt at ${this.url}: ${reason}.`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
sitemaps.push(sitemapUrl);
|
|
126
|
+
}
|
|
127
|
+
return sitemaps;
|
|
107
128
|
}
|
|
108
129
|
/**
|
|
109
|
-
* Parse all the sitemaps referenced in the robots file.
|
|
130
|
+
* Parse all the sitemaps referenced in the robots file. `options` are forwarded to `getSitemaps`
|
|
131
|
+
* and the sitemap parser.
|
|
110
132
|
*/
|
|
111
|
-
async parseSitemaps() {
|
|
112
|
-
return sitemap_1.Sitemap.load(this.
|
|
133
|
+
async parseSitemaps(options = {}) {
|
|
134
|
+
return sitemap_1.Sitemap.load(this.getSitemaps(options), this.proxyUrl, options);
|
|
113
135
|
}
|
|
114
136
|
/**
|
|
115
137
|
* Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
|
|
138
|
+
* `options` are forwarded to `parseSitemaps`.
|
|
116
139
|
*/
|
|
117
|
-
async parseUrlsFromSitemaps() {
|
|
118
|
-
return (await this.parseSitemaps()).urls;
|
|
140
|
+
async parseUrlsFromSitemaps(options = {}) {
|
|
141
|
+
return (await this.parseSitemaps(options)).urls;
|
|
119
142
|
}
|
|
120
143
|
}
|
|
121
144
|
exports.RobotsTxtFile = RobotsTxtFile;
|
package/internals/sitemap.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
2
2
|
import type { Delays } from 'got-scraping';
|
|
3
|
+
import { type EnqueueStrategy } from './url';
|
|
3
4
|
interface SitemapUrlData {
|
|
4
5
|
loc: string;
|
|
5
6
|
lastmod?: Date;
|
|
@@ -51,6 +52,13 @@ export interface ParseSitemapOptions {
|
|
|
51
52
|
* If not provided, all nested sitemaps are followed.
|
|
52
53
|
*/
|
|
53
54
|
nestedSitemapFilter?: (sitemapUrl: string) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Keep only sitemap-derived URLs (nested `<sitemap>` and `<url>` entries) matching this strategy
|
|
57
|
+
* relative to the parent sitemap URL; non-`http(s)` schemes are always dropped. Skipped for raw string
|
|
58
|
+
* sources (no parent URL). Pass `'all'` to disable host filtering.
|
|
59
|
+
* @default 'same-hostname'
|
|
60
|
+
*/
|
|
61
|
+
enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`;
|
|
54
62
|
}
|
|
55
63
|
export declare function parseSitemap<T extends ParseSitemapOptions>(initialSources: SitemapSource[], proxyUrl?: string, options?: T): AsyncIterable<T['emitNestedSitemaps'] extends true ? SitemapUrl | NestedSitemap : SitemapUrl>;
|
|
56
64
|
/**
|
package/internals/sitemap.js
CHANGED
|
@@ -13,6 +13,7 @@ const whatwg_mimetype_1 = tslib_1.__importDefault(require("whatwg-mimetype"));
|
|
|
13
13
|
const log_1 = tslib_1.__importDefault(require("@apify/log"));
|
|
14
14
|
const iterables_1 = require("./iterables");
|
|
15
15
|
const robots_1 = require("./robots");
|
|
16
|
+
const url_1 = require("./url");
|
|
16
17
|
class SitemapTxtParser extends node_stream_1.Transform {
|
|
17
18
|
constructor() {
|
|
18
19
|
super({
|
|
@@ -171,7 +172,7 @@ class SitemapXmlParser extends node_stream_1.Transform {
|
|
|
171
172
|
async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
172
173
|
const { gotScraping } = await import('got-scraping');
|
|
173
174
|
const { fileTypeStream } = await import('file-type');
|
|
174
|
-
const { emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, networkTimeouts, reportNetworkErrors = true, nestedSitemapFilter, } = options ?? {};
|
|
175
|
+
const { emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, networkTimeouts, reportNetworkErrors = true, nestedSitemapFilter, enqueueStrategy = 'same-hostname', } = options ?? {};
|
|
175
176
|
const sources = [...initialSources];
|
|
176
177
|
const visitedSitemapUrls = new Set();
|
|
177
178
|
const createParser = (contentType = '', url) => {
|
|
@@ -197,8 +198,10 @@ async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
197
198
|
continue;
|
|
198
199
|
}
|
|
199
200
|
let items = null;
|
|
201
|
+
// Parent URL, parsed once and reused as the origin for the strategy checks below.
|
|
202
|
+
let sitemapUrl;
|
|
200
203
|
if (source.type === 'url') {
|
|
201
|
-
|
|
204
|
+
sitemapUrl = new URL(source.url);
|
|
202
205
|
visitedSitemapUrls.add(sitemapUrl.toString());
|
|
203
206
|
let retriesLeft = sitemapRetries + 1;
|
|
204
207
|
while (retriesLeft-- > 0) {
|
|
@@ -269,18 +272,39 @@ async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
269
272
|
if (items === null) {
|
|
270
273
|
continue;
|
|
271
274
|
}
|
|
275
|
+
// URL entries dropped by the enqueue strategy filter, reported in one warning per sitemap after
|
|
276
|
+
// the loop (per-entry warnings could flood the log; individual drops are logged at debug level).
|
|
277
|
+
let droppedUrlEntries = 0;
|
|
272
278
|
for await (const item of items) {
|
|
273
279
|
if (item.type === 'sitemapUrl' && !visitedSitemapUrls.has(item.url)) {
|
|
274
280
|
if (nestedSitemapFilter && !nestedSitemapFilter(item.url)) {
|
|
275
281
|
log_1.default.debug(`Skipping sitemap ${item.url} due to nestedSitemapFilter.`);
|
|
276
282
|
continue;
|
|
277
283
|
}
|
|
284
|
+
// Keep only nested sitemaps matching the strategy (and using http(s)) relative to the
|
|
285
|
+
// parent. Raw string sources have no parent URL, so the check is skipped.
|
|
286
|
+
if (source.type === 'url') {
|
|
287
|
+
const { allowed, reason } = (0, url_1.filterUrl)(item.url, sitemapUrl, enqueueStrategy);
|
|
288
|
+
if (!allowed) {
|
|
289
|
+
log_1.default.warning(`Skipping nested sitemap ${item.url} (parent ${source.url}): ${reason}.`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
278
293
|
sources.push({ type: 'url', url: item.url, depth: (source.depth ?? 0) + 1 });
|
|
279
294
|
if (emitNestedSitemaps) {
|
|
280
295
|
yield { loc: item.url, originSitemapUrl: null };
|
|
281
296
|
}
|
|
282
297
|
}
|
|
283
298
|
if (item.type === 'url') {
|
|
299
|
+
// Keep only URL entries that match the enqueue strategy relative to the parent (see above).
|
|
300
|
+
if (source.type === 'url') {
|
|
301
|
+
const { allowed, reason } = (0, url_1.filterUrl)(item.loc, sitemapUrl, enqueueStrategy);
|
|
302
|
+
if (!allowed) {
|
|
303
|
+
droppedUrlEntries++;
|
|
304
|
+
log_1.default.debug(`Skipping sitemap URL ${item.loc} (parent ${source.url}): ${reason}.`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
284
308
|
yield {
|
|
285
309
|
...item,
|
|
286
310
|
originSitemapUrl: source.type === 'url'
|
|
@@ -289,6 +313,9 @@ async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
289
313
|
};
|
|
290
314
|
}
|
|
291
315
|
}
|
|
316
|
+
if (droppedUrlEntries > 0 && source.type === 'url') {
|
|
317
|
+
log_1.default.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.`);
|
|
318
|
+
}
|
|
292
319
|
}
|
|
293
320
|
}
|
|
294
321
|
/**
|
|
@@ -411,7 +438,8 @@ async function* discoverValidSitemaps(urls, options = {}) {
|
|
|
411
438
|
timeoutMillis: requestTimeoutMillis,
|
|
412
439
|
signal,
|
|
413
440
|
});
|
|
414
|
-
|
|
441
|
+
// Surface all referenced sitemaps, including cross-host; scoping happens at load time.
|
|
442
|
+
for (const sitemapUrl of robotsFile.getSitemaps({ enqueueStrategy: 'all' })) {
|
|
415
443
|
if (addSitemapUrl(sitemapUrl)) {
|
|
416
444
|
yield sitemapUrl;
|
|
417
445
|
}
|
package/internals/url.d.ts
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
1
1
|
export type SearchParams = string | URLSearchParams | Record<string, string | number | boolean | null | undefined>;
|
|
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,6 +1,128 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UNSUPPORTED_SCHEME_MESSAGE = exports.EnqueueStrategy = void 0;
|
|
4
|
+
exports.matchesEnqueueStrategy = matchesEnqueueStrategy;
|
|
5
|
+
exports.filterUrl = filterUrl;
|
|
3
6
|
exports.applySearchParams = applySearchParams;
|
|
7
|
+
const tldts_1 = require("tldts");
|
|
8
|
+
/**
|
|
9
|
+
* The different enqueueing strategies available.
|
|
10
|
+
*
|
|
11
|
+
* 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:
|
|
12
|
+
*
|
|
13
|
+
* ```md
|
|
14
|
+
* Protocol Domain
|
|
15
|
+
* ┌────┐ ┌─────────┐
|
|
16
|
+
* https://example.crawlee.dev/...
|
|
17
|
+
* │ └─────────────────┤
|
|
18
|
+
* │ Hostname │
|
|
19
|
+
* │ │
|
|
20
|
+
* └─────────────────────────┘
|
|
21
|
+
* Origin
|
|
22
|
+
*```
|
|
23
|
+
*
|
|
24
|
+
* - The `Protocol` is usually `http` or `https`
|
|
25
|
+
* - The `Domain` represents the path without any possible subdomains to a website. For example, `crawlee.dev` is the domain of `https://example.crawlee.dev/`
|
|
26
|
+
* - 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/`
|
|
27
|
+
* - The `Origin` is the combination of the `Protocol` and `Hostname`. For example, `https://example.crawlee.dev` is the origin of `https://example.crawlee.dev/`
|
|
28
|
+
*/
|
|
29
|
+
var EnqueueStrategy;
|
|
30
|
+
(function (EnqueueStrategy) {
|
|
31
|
+
/**
|
|
32
|
+
* Matches any URLs found
|
|
33
|
+
*/
|
|
34
|
+
EnqueueStrategy["All"] = "all";
|
|
35
|
+
/**
|
|
36
|
+
* Matches any URLs that have the same hostname.
|
|
37
|
+
* For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
|
|
38
|
+
* `https://example.com/hello` will not be matched.
|
|
39
|
+
*
|
|
40
|
+
* > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
|
|
41
|
+
*/
|
|
42
|
+
EnqueueStrategy["SameHostname"] = "same-hostname";
|
|
43
|
+
/**
|
|
44
|
+
* Matches any URLs that have the same domain as the base URL.
|
|
45
|
+
* For example, `https://wow.an.example.com` and `https://example.com` will both be matched for a base url of
|
|
46
|
+
* `https://example.com`.
|
|
47
|
+
*
|
|
48
|
+
* > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
|
|
49
|
+
*/
|
|
50
|
+
EnqueueStrategy["SameDomain"] = "same-domain";
|
|
51
|
+
/**
|
|
52
|
+
* Matches any URLs that have the same hostname and protocol.
|
|
53
|
+
* For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
|
|
54
|
+
* `http://wow.example.com/hello` will not be matched.
|
|
55
|
+
*
|
|
56
|
+
* > This strategy will ensure the protocol of the base URL is the same as the protocol of the URL to be enqueued.
|
|
57
|
+
*/
|
|
58
|
+
EnqueueStrategy["SameOrigin"] = "same-origin";
|
|
59
|
+
})(EnqueueStrategy || (exports.EnqueueStrategy = EnqueueStrategy = {}));
|
|
60
|
+
/** Reusable suffix for log messages explaining why a non-`http(s)` URL was rejected. */
|
|
61
|
+
exports.UNSUPPORTED_SCHEME_MESSAGE = 'unsupported URL scheme (only http and https are allowed)';
|
|
62
|
+
const ALLOWED_SCHEMES = new Set(['http:', 'https:']);
|
|
63
|
+
function toUrl(value) {
|
|
64
|
+
if (value instanceof URL) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return new URL(value);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Strip a trailing dot so `example.com.` equals `example.com`. */
|
|
75
|
+
function normalizeHostname(hostname) {
|
|
76
|
+
return hostname.endsWith('.') ? hostname.slice(0, -1) : hostname;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check whether `target` matches `origin` under the given enqueue `strategy`. The URL scheme is not
|
|
80
|
+
* considered here (use {@link filterUrl} for the combined scheme + strategy check).
|
|
81
|
+
*
|
|
82
|
+
* The `enqueueLinks` implementation in `@crawlee/core` matches the same strategies via glob patterns
|
|
83
|
+
* (see `packages/core/src/enqueue_links/enqueue_links.ts`) — keep the two in sync when changing either.
|
|
84
|
+
*/
|
|
85
|
+
function matchesEnqueueStrategy(strategy, target, origin) {
|
|
86
|
+
switch (strategy) {
|
|
87
|
+
case 'all':
|
|
88
|
+
return true;
|
|
89
|
+
case 'same-hostname':
|
|
90
|
+
return normalizeHostname(target.hostname) === normalizeHostname(origin.hostname);
|
|
91
|
+
case 'same-domain': {
|
|
92
|
+
const originDomain = (0, tldts_1.getDomain)(origin.hostname, { mixedInputs: false });
|
|
93
|
+
if (originDomain) {
|
|
94
|
+
return originDomain === (0, tldts_1.getDomain)(target.hostname, { mixedInputs: false });
|
|
95
|
+
}
|
|
96
|
+
// No registrable domain (e.g. an IP address), fall back to comparing origins.
|
|
97
|
+
return target.origin === origin.origin;
|
|
98
|
+
}
|
|
99
|
+
case 'same-origin':
|
|
100
|
+
// Compare scheme/host/port directly so a trailing-dot host is normalized.
|
|
101
|
+
return (target.protocol === origin.protocol &&
|
|
102
|
+
normalizeHostname(target.hostname) === normalizeHostname(origin.hostname) &&
|
|
103
|
+
target.port === origin.port);
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Unknown enqueue strategy '${strategy}'.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check whether `target` may be enqueued under `strategy` relative to `origin`: it must use an `http(s)`
|
|
110
|
+
* scheme and match the strategy. On rejection, `reason` is a human-readable message for log output.
|
|
111
|
+
*/
|
|
112
|
+
function filterUrl(target, origin, strategy) {
|
|
113
|
+
const targetUrl = toUrl(target);
|
|
114
|
+
if (targetUrl === null || !ALLOWED_SCHEMES.has(targetUrl.protocol)) {
|
|
115
|
+
return { allowed: false, reason: exports.UNSUPPORTED_SCHEME_MESSAGE };
|
|
116
|
+
}
|
|
117
|
+
const originUrl = toUrl(origin);
|
|
118
|
+
if (originUrl === null) {
|
|
119
|
+
return { allowed: false, reason: 'invalid origin URL' };
|
|
120
|
+
}
|
|
121
|
+
if (!matchesEnqueueStrategy(strategy, targetUrl, originUrl)) {
|
|
122
|
+
return { allowed: false, reason: `does not match enqueue strategy '${strategy}'` };
|
|
123
|
+
}
|
|
124
|
+
return { allowed: true };
|
|
125
|
+
}
|
|
4
126
|
/**
|
|
5
127
|
* Appends search (query string) parameters to a URL, replacing the original value (if any).
|
|
6
128
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/utils",
|
|
3
|
-
"version": "3.17.1-beta.
|
|
3
|
+
"version": "3.17.1-beta.59",
|
|
4
4
|
"description": "A set of shared utilities that can be used by crawlers",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=16.0.0"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@apify/log": "^2.4.0",
|
|
51
51
|
"@apify/ps-tree": "^1.2.0",
|
|
52
|
-
"@crawlee/types": "3.17.1-beta.
|
|
52
|
+
"@crawlee/types": "3.17.1-beta.59",
|
|
53
53
|
"@types/sax": "^1.2.7",
|
|
54
54
|
"cheerio": "1.0.0-rc.12",
|
|
55
55
|
"file-type": "^21.3.1",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"ow": "^0.28.1",
|
|
58
58
|
"robots-parser": "^3.0.1",
|
|
59
59
|
"sax": "^1.4.1",
|
|
60
|
+
"tldts": "^7.0.0",
|
|
60
61
|
"tslib": "^2.4.0",
|
|
61
62
|
"whatwg-mimetype": "^4.0.0"
|
|
62
63
|
},
|
|
@@ -70,5 +71,5 @@
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
},
|
|
73
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "8980103b700213806843d4fe1355b75519474ea4"
|
|
74
75
|
}
|