-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathshared.ts
More file actions
305 lines (255 loc) · 9.54 KB
/
Copy pathshared.ts
File metadata and controls
305 lines (255 loc) · 9.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import { URL } from 'node:url';
import type { Awaitable } from '@crawlee/types';
import { Minimatch } from 'minimatch';
import { purlToRegExp } from '@apify/pseudo_url';
import type { RequestOptions } from '../request';
import { Request } from '../request';
import type { EnqueueLinksOptions } from './enqueue_links';
export { tryAbsoluteURL } from '@crawlee/utils';
const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
/**
* To enable direct use of the Actor UI `globs`/`regexps`/`pseudoUrls` output while keeping high performance,
* all the regexps from the output are only constructed once and kept in a cache
* by the `enqueueLinks()` function.
* @ignore
*/
const enqueueLinksPatternCache = new Map();
export type UrlPatternObject = {
glob?: string;
regexp?: RegExp;
} & Pick<RequestOptions, 'method' | 'payload' | 'label' | 'userData' | 'headers'>;
export type PseudoUrlObject = { purl: string } & Pick<
RequestOptions,
'method' | 'payload' | 'label' | 'userData' | 'headers'
>;
export type PseudoUrlInput = string | PseudoUrlObject;
export type GlobObject = { glob: string } & Pick<
RequestOptions,
'method' | 'payload' | 'label' | 'userData' | 'headers'
>;
export type GlobInput = string | GlobObject;
export type RegExpObject = { regexp: RegExp } & Pick<
RequestOptions,
'method' | 'payload' | 'label' | 'userData' | 'headers'
>;
export type RegExpInput = RegExp | RegExpObject;
export type SkippedRequestReason = 'robotsTxt' | 'limit' | 'enqueueLimit' | 'filters' | 'redirect' | 'depth';
export type SkippedRequestCallback = (args: { url: string; reason: SkippedRequestReason }) => Awaitable<void>;
/**
* @ignore
*/
export function updateEnqueueLinksPatternCache(
item: GlobInput | RegExpInput | PseudoUrlInput,
pattern: RegExpObject | GlobObject,
): void {
enqueueLinksPatternCache.set(item, pattern);
if (enqueueLinksPatternCache.size > MAX_ENQUEUE_LINKS_CACHE_SIZE) {
const key = enqueueLinksPatternCache.keys().next().value;
enqueueLinksPatternCache.delete(key);
}
}
/**
* Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
* to construct RegExps from PseudoUrl strings.
* @ignore
*/
export function constructRegExpObjectsFromPseudoUrls(pseudoUrls: readonly PseudoUrlInput[]): RegExpObject[] {
return pseudoUrls.map((item) => {
// Get pseudoUrl object from cache.
let regexpObject = enqueueLinksPatternCache.get(item);
if (regexpObject) return regexpObject;
if (typeof item === 'string') {
regexpObject = { regexp: purlToRegExp(item) };
} else {
const { purl, ...requestOptions } = item;
regexpObject = { regexp: purlToRegExp(purl), ...requestOptions };
}
updateEnqueueLinksPatternCache(item, regexpObject);
return regexpObject;
});
}
/**
* Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
* to construct Glob objects from Glob pattern strings.
* @ignore
*/
export function constructGlobObjectsFromGlobs(globs: readonly GlobInput[]): GlobObject[] {
return globs
.filter((glob) => {
// Skip possibly nullish, empty strings
if (!glob) {
return false;
}
if (typeof glob === 'string') {
return glob.trim().length > 0;
}
if (glob.glob) {
return glob.glob.trim().length > 0;
}
return false;
})
.map((item) => {
// Get glob object from cache.
let globObject = enqueueLinksPatternCache.get(item);
if (globObject) return globObject;
if (typeof item === 'string') {
globObject = { glob: validateGlobPattern(item) };
} else {
const { glob, ...requestOptions } = item;
globObject = { glob: validateGlobPattern(glob), ...requestOptions };
}
updateEnqueueLinksPatternCache(item, globObject);
return globObject;
});
}
/**
* @internal
*/
export function validateGlobPattern(glob: string): string {
const globTrimmed = glob.trim();
if (globTrimmed.length === 0)
throw new Error(`Cannot parse Glob pattern '${globTrimmed}': it must be an non-empty string`);
return globTrimmed;
}
/**
* Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
* to check RegExps input and return valid RegExps.
* @ignore
*/
export function constructRegExpObjectsFromRegExps(regexps: readonly RegExpInput[]): RegExpObject[] {
return regexps.map((item) => {
// Get regexp object from cache.
let regexpObject = enqueueLinksPatternCache.get(item);
if (regexpObject) return regexpObject;
if (item instanceof RegExp) {
regexpObject = { regexp: item };
} else {
regexpObject = item;
}
updateEnqueueLinksPatternCache(item, regexpObject);
return regexpObject;
});
}
/**
* @ignore
*/
export function createRequests(
requestOptions: (string | RequestOptions)[],
urlPatternObjects?: UrlPatternObject[],
excludePatternObjects: UrlPatternObject[] = [],
strategy?: EnqueueLinksOptions['strategy'],
onSkippedUrl?: (url: string) => void,
): Request[] {
const excludePatternObjectMatchers = excludePatternObjects.map(createPatternObjectMatcher);
const urlPatternObjectMatchers = urlPatternObjects?.map(createPatternObjectMatcher);
return requestOptions
.map((opts) => ({ url: typeof opts === 'string' ? opts : opts.url, opts }))
.filter(({ url }) => {
const matchesExcludePatterns = excludePatternObjectMatchers.some(({ match }) => match(url));
if (matchesExcludePatterns) {
onSkippedUrl?.(url);
}
return !matchesExcludePatterns;
})
.map(({ url, opts }) => {
if (!urlPatternObjectMatchers || !urlPatternObjectMatchers.length) {
return new Request(typeof opts === 'string' ? { url: opts, enqueueStrategy: strategy } : { ...opts });
}
for (const urlPatternObject of urlPatternObjectMatchers) {
const { match, glob, regexp, ...requestRegExpOptions } = urlPatternObject;
if (match(url)) {
const request =
typeof opts === 'string'
? { url: opts, ...requestRegExpOptions, enqueueStrategy: strategy }
: { ...opts, ...requestRegExpOptions, enqueueStrategy: strategy };
return new Request(request);
}
}
// didn't match any positive pattern
onSkippedUrl?.(url);
return null;
})
.filter((request) => request) as Request[];
}
export function filterRequestsByPatterns(
requests: Request[],
patterns?: UrlPatternObject[],
onSkippedUrl?: (url: string) => void,
): Request[] {
if (!patterns?.length) {
return requests;
}
const filtered: Request[] = [];
const patternMatchers = patterns?.map(createPatternObjectMatcher);
for (const request of requests) {
const matchingPattern = patternMatchers.find(({ match }) => match(request.url));
if (matchingPattern !== undefined) {
filtered.push(request);
} else {
onSkippedUrl?.(request.url);
}
}
return filtered;
}
/**
* @ignore
*/
export function createRequestOptions(
sources: (string | Record<string, unknown>)[],
options: Pick<EnqueueLinksOptions, 'label' | 'userData' | 'baseUrl' | 'skipNavigation' | 'strategy'> = {},
): RequestOptions[] {
return sources
.map((src) =>
typeof src === 'string'
? { url: src, enqueueStrategy: options.strategy }
: ({ ...src, enqueueStrategy: options.strategy } as RequestOptions),
)
.filter(({ url }) => {
try {
return new URL(url, options.baseUrl).href;
} catch (err) {
return false;
}
})
.map((requestOptions) => {
requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
requestOptions.userData ??= options.userData ?? {};
if (typeof options.label === 'string') {
requestOptions.userData = {
...requestOptions.userData,
label: options.label,
};
}
if (options.skipNavigation) {
requestOptions.skipNavigation = true;
}
return requestOptions;
});
}
/**
* @ignore
*/
function createPatternObjectMatcher(urlPatternObject: UrlPatternObject) {
const { regexp, glob } = urlPatternObject;
let match;
if (regexp) {
match = (url: string) => regexp.test(url);
} else if (glob) {
const m = new Minimatch(glob, { nocase: true });
match = (url: string) => m.match(url);
} else {
match = () => false;
}
return { ...urlPatternObject, match };
}
/**
* Takes an Apify {@apilink RequestOptions} object and changes its attributes in a desired way. This user-function is used
* {@apilink enqueueLinks} to modify requests before enqueuing them.
*/
export interface RequestTransform {
/**
* @param original Request options to be modified.
* @returns The modified request options to enqueue, or any falsy value to skip the request.
*/
(original: RequestOptions): RequestOptions | false | undefined | null;
}