@crawlee/utils 3.17.1-beta.9 → 3.18.1-beta.0
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/general.js +2 -2
- package/internals/open_graph_parser.js +6 -6
- package/internals/robots.d.ts +19 -5
- package/internals/robots.js +35 -12
- package/internals/sitemap.d.ts +8 -0
- package/internals/sitemap.js +40 -7
- package/internals/social.js +4 -1
- package/internals/systemInfoV2/cpu-info.js +1 -0
- 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/general.js
CHANGED
|
@@ -16,12 +16,12 @@ const promises_2 = require("node:timers/promises");
|
|
|
16
16
|
* Default regular expression to match URLs in a string that may be plain text, JSON, CSV or other. It supports common URL characters
|
|
17
17
|
* and does not support URLs containing commas or spaces. The URLs also may contain Unicode letters (not symbols).
|
|
18
18
|
*/
|
|
19
|
-
exports.URL_NO_COMMAS_REGEX = /https?:\/\/(www\.)?([\p{L}0-9]|[\p{L}0-9][-\p{L}0-9@:%._+~#=]{0,254}[\p{L}0-9])\.[a-z]{2,63}(:\d{1,5})?(\/[-\p{L}0-9@:%_+.~#?&/=()]*)?/giu;
|
|
19
|
+
exports.URL_NO_COMMAS_REGEX = /https?:\/\/(www\.)?([\p{L}0-9]|[\p{L}0-9][-\p{L}0-9@:%._+~#=]{0,254}[\p{L}0-9])\.[a-z]{2,63}(:\d{1,5})?(\/[-\p{L}0-9@:%_+.~#?&/=()'*]*)?/giu;
|
|
20
20
|
/**
|
|
21
21
|
* Regular expression that, in addition to the default regular expression `URL_NO_COMMAS_REGEX`, supports matching commas in URL path and query.
|
|
22
22
|
* Note, however, that this may prevent parsing URLs from comma delimited lists, or the URLs may become malformed.
|
|
23
23
|
*/
|
|
24
|
-
exports.URL_WITH_COMMAS_REGEX = /https?:\/\/(www\.)?([\p{L}0-9]|[\p{L}0-9][-\p{L}0-9@:%._+~#=]{0,254}[\p{L}0-9])\.[a-z]{2,63}(:\d{1,5})?(\/[-\p{L}0-9@:%_+,.~#?&/=()]*)?/giu;
|
|
24
|
+
exports.URL_WITH_COMMAS_REGEX = /https?:\/\/(www\.)?([\p{L}0-9]|[\p{L}0-9][-\p{L}0-9@:%._+~#=]{0,254}[\p{L}0-9])\.[a-z]{2,63}(:\d{1,5})?(\/[-\p{L}0-9@:%_+,.~#?&/=()'*]*)?/giu;
|
|
25
25
|
let isDockerPromiseCache;
|
|
26
26
|
async function createIsDockerPromise() {
|
|
27
27
|
const promise1 = promises_1.default
|
|
@@ -258,32 +258,32 @@ const OPEN_GRAPH_PROPERTIES = [
|
|
|
258
258
|
outputName: 'articleInfo',
|
|
259
259
|
children: [
|
|
260
260
|
{
|
|
261
|
-
name: '
|
|
261
|
+
name: 'article:published_time',
|
|
262
262
|
outputName: 'publishedTime',
|
|
263
263
|
children: [],
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
|
-
name: '
|
|
266
|
+
name: 'article:modified_time',
|
|
267
267
|
outputName: 'modifiedTime',
|
|
268
268
|
children: [],
|
|
269
269
|
},
|
|
270
270
|
{
|
|
271
|
-
name: '
|
|
271
|
+
name: 'article:expiration_time',
|
|
272
272
|
outputName: 'expirationTime',
|
|
273
273
|
children: [],
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
|
-
name: '
|
|
276
|
+
name: 'article:author',
|
|
277
277
|
outputName: 'author',
|
|
278
278
|
children: [],
|
|
279
279
|
},
|
|
280
280
|
{
|
|
281
|
-
name: '
|
|
281
|
+
name: 'article:section',
|
|
282
282
|
outputName: 'section',
|
|
283
283
|
children: [],
|
|
284
284
|
},
|
|
285
285
|
{
|
|
286
|
-
name: '
|
|
286
|
+
name: 'article:tag',
|
|
287
287
|
outputName: 'tag',
|
|
288
288
|
children: [],
|
|
289
289
|
},
|
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({
|
|
@@ -134,8 +135,10 @@ class SitemapXmlParser extends node_stream_1.Transform {
|
|
|
134
135
|
if (name === 'loc' || name === 'lastmod' || name === 'priority' || name === 'changefreq') {
|
|
135
136
|
this.currentTag = undefined;
|
|
136
137
|
}
|
|
137
|
-
if (name === 'url'
|
|
138
|
-
|
|
138
|
+
if (name === 'url') {
|
|
139
|
+
if (this.url.loc !== undefined) {
|
|
140
|
+
this.push({ type: 'url', ...this.url, loc: this.url.loc });
|
|
141
|
+
}
|
|
139
142
|
this.url = {};
|
|
140
143
|
}
|
|
141
144
|
}
|
|
@@ -151,7 +154,10 @@ class SitemapXmlParser extends node_stream_1.Transform {
|
|
|
151
154
|
}
|
|
152
155
|
text = text.trim();
|
|
153
156
|
if (this.currentTag === 'lastmod') {
|
|
154
|
-
|
|
157
|
+
const lastmod = new Date(text);
|
|
158
|
+
if (!Number.isNaN(lastmod.getTime())) {
|
|
159
|
+
this.url.lastmod = lastmod;
|
|
160
|
+
}
|
|
155
161
|
}
|
|
156
162
|
if (this.currentTag === 'priority') {
|
|
157
163
|
this.url.priority = Number(text);
|
|
@@ -166,7 +172,7 @@ class SitemapXmlParser extends node_stream_1.Transform {
|
|
|
166
172
|
async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
167
173
|
const { gotScraping } = await import('got-scraping');
|
|
168
174
|
const { fileTypeStream } = await import('file-type');
|
|
169
|
-
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 ?? {};
|
|
170
176
|
const sources = [...initialSources];
|
|
171
177
|
const visitedSitemapUrls = new Set();
|
|
172
178
|
const createParser = (contentType = '', url) => {
|
|
@@ -192,8 +198,10 @@ async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
192
198
|
continue;
|
|
193
199
|
}
|
|
194
200
|
let items = null;
|
|
201
|
+
// Parent URL, parsed once and reused as the origin for the strategy checks below.
|
|
202
|
+
let sitemapUrl;
|
|
195
203
|
if (source.type === 'url') {
|
|
196
|
-
|
|
204
|
+
sitemapUrl = new URL(source.url);
|
|
197
205
|
visitedSitemapUrls.add(sitemapUrl.toString());
|
|
198
206
|
let retriesLeft = sitemapRetries + 1;
|
|
199
207
|
while (retriesLeft-- > 0) {
|
|
@@ -264,18 +272,39 @@ async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
264
272
|
if (items === null) {
|
|
265
273
|
continue;
|
|
266
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;
|
|
267
278
|
for await (const item of items) {
|
|
268
279
|
if (item.type === 'sitemapUrl' && !visitedSitemapUrls.has(item.url)) {
|
|
269
280
|
if (nestedSitemapFilter && !nestedSitemapFilter(item.url)) {
|
|
270
281
|
log_1.default.debug(`Skipping sitemap ${item.url} due to nestedSitemapFilter.`);
|
|
271
282
|
continue;
|
|
272
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
|
+
}
|
|
273
293
|
sources.push({ type: 'url', url: item.url, depth: (source.depth ?? 0) + 1 });
|
|
274
294
|
if (emitNestedSitemaps) {
|
|
275
295
|
yield { loc: item.url, originSitemapUrl: null };
|
|
276
296
|
}
|
|
277
297
|
}
|
|
278
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
|
+
}
|
|
279
308
|
yield {
|
|
280
309
|
...item,
|
|
281
310
|
originSitemapUrl: source.type === 'url'
|
|
@@ -284,6 +313,9 @@ async function* parseSitemap(initialSources, proxyUrl, options) {
|
|
|
284
313
|
};
|
|
285
314
|
}
|
|
286
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
|
+
}
|
|
287
319
|
}
|
|
288
320
|
}
|
|
289
321
|
/**
|
|
@@ -406,7 +438,8 @@ async function* discoverValidSitemaps(urls, options = {}) {
|
|
|
406
438
|
timeoutMillis: requestTimeoutMillis,
|
|
407
439
|
signal,
|
|
408
440
|
});
|
|
409
|
-
|
|
441
|
+
// Surface all referenced sitemaps, including cross-host; scoping happens at load time.
|
|
442
|
+
for (const sitemapUrl of robotsFile.getSitemaps({ enqueueStrategy: 'all' })) {
|
|
410
443
|
if (addSitemapUrl(sitemapUrl)) {
|
|
411
444
|
yield sitemapUrl;
|
|
412
445
|
}
|
|
@@ -415,7 +448,7 @@ async function* discoverValidSitemaps(urls, options = {}) {
|
|
|
415
448
|
catch (err) {
|
|
416
449
|
log_1.default.warning(`Failed to fetch robots.txt file for ${hostname}`, { error: err });
|
|
417
450
|
}
|
|
418
|
-
const sitemapUrl = domainUrls.find((url) => /sitemap
|
|
451
|
+
const sitemapUrl = domainUrls.find((url) => /sitemap(?:_index)?\.(?:xml|txt)(?:\.gz)?$/i.test(url));
|
|
419
452
|
if (sitemapUrl !== undefined) {
|
|
420
453
|
if (addSitemapUrl(sitemapUrl)) {
|
|
421
454
|
yield sitemapUrl;
|
package/internals/social.js
CHANGED
|
@@ -10,7 +10,10 @@ const tslib_1 = require("tslib");
|
|
|
10
10
|
const cheerio = tslib_1.__importStar(require("cheerio"));
|
|
11
11
|
const cheerio_1 = require("./cheerio");
|
|
12
12
|
// Regex inspired by https://zapier.com/blog/extract-links-email-phone-regex/
|
|
13
|
-
|
|
13
|
+
// The dot-atom local part and domain labels use RFC 5321 length bounds ({1,64}, {0,62})
|
|
14
|
+
// instead of unbounded quantifiers to avoid quadratic backtracking (ReDoS) on long
|
|
15
|
+
// dotted or hyphenated inputs, e.g. text scraped by parseHandlesFromHtml().
|
|
16
|
+
const EMAIL_REGEX_STRING = '(?:[a-z0-9!#$%&\'*+/=?^_`{|}~-]{1,64}(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]{1,64}){0,32}|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\\])';
|
|
14
17
|
/**
|
|
15
18
|
* Regular expression to exactly match a single email address.
|
|
16
19
|
* It has the following form: `/^...$/i`.
|
|
@@ -51,6 +51,7 @@ function getCurrentCpuTicks() {
|
|
|
51
51
|
}, { idle: 0, total: 0 });
|
|
52
52
|
const idleTicksDelta = ticks.idle - previousTicks.idle;
|
|
53
53
|
const totalTicksDelta = ticks.total - previousTicks.total;
|
|
54
|
+
Object.assign(previousTicks, ticks);
|
|
54
55
|
return totalTicksDelta ? 1 - idleTicksDelta / totalTicksDelta : 0;
|
|
55
56
|
}
|
|
56
57
|
/**
|
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.
|
|
3
|
+
"version": "3.18.1-beta.0",
|
|
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.
|
|
52
|
+
"@crawlee/types": "3.18.1-beta.0",
|
|
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": "fe3605157b4d0262d09898aa18626ab5a3e03702"
|
|
74
75
|
}
|