@crawlee/core 4.0.0-beta.121 → 4.0.0-beta.123
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/autoscaling/autoscaled_pool.js +20 -12
- package/autoscaling/concurrency_system.d.ts +2 -2
- package/autoscaling/concurrency_system.js +31 -20
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +7 -6
- package/autoscaling/load_signal.js +2 -1
- package/autoscaling/snapshotter.d.ts +6 -6
- package/autoscaling/snapshotter.js +9 -9
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +13 -12
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +11 -11
- package/autoscaling/system_status.d.ts +8 -8
- package/autoscaling/system_status.js +2 -2
- package/configuration.d.ts +15 -15
- package/configuration.js +3 -3
- package/crawlers/crawler_commons.d.ts +8 -54
- package/crawlers/statistics.d.ts +1 -1
- package/crawlers/statistics.js +14 -14
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.d.ts +33 -61
- package/enqueue_links/enqueue_links.js +35 -152
- package/enqueue_links/shared.d.ts +17 -4
- package/enqueue_links/shared.js +28 -1
- package/memory-storage/resource-clients/dataset.js +2 -8
- package/memory-storage/resource-clients/key-value-store.js +23 -26
- package/memory-storage/resource-clients/request-queue.js +9 -22
- package/package.json +7 -8
- package/proxy_configuration.js +10 -6
- package/request.d.ts +2 -2
- package/request.js +44 -31
- package/router.d.ts +5 -5
- package/serialization.js +6 -4
- package/session_pool/session.js +22 -20
- package/session_pool/session_pool.js +20 -17
- package/storages/dataset.js +11 -9
- package/storages/key_value_store.js +30 -27
- package/storages/request_list.d.ts +2 -1
- package/storages/request_list.js +26 -21
- package/storages/request_queue.js +64 -59
- package/storages/sitemap_request_loader.d.ts +1 -1
- package/storages/sitemap_request_loader.js +22 -22
- package/storages/throttling_request_manager.js +11 -9
- package/storages/utils.d.ts +2 -1
- package/validators.d.ts +22 -25
- package/validators.js +13 -25
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Dictionary, HttpRequestOptions, ISession, ProxyInfo, SendRequestOptions } from '@crawlee/types';
|
|
2
|
-
import type { ReadonlyDeep
|
|
3
|
-
import type {
|
|
2
|
+
import type { ReadonlyDeep } from 'type-fest';
|
|
3
|
+
import type { EnqueueUrlsOptions } from '../enqueue_links/enqueue_links.js';
|
|
4
4
|
import type { CrawleeLogger } from '../log.js';
|
|
5
5
|
import type { Request, RequestOptions, Source } from '../request.js';
|
|
6
6
|
import type { StorageIdentifier } from '../storages/storage_instance_manager.js';
|
|
7
7
|
import type { Dataset } from '../storages/dataset.js';
|
|
8
8
|
import type { KeyValueStore } from '../storages/key_value_store.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { AddRequestsBatchedResult } from '../storages/request_queue.js';
|
|
10
10
|
/** @internal */
|
|
11
11
|
export type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
12
12
|
/**
|
|
@@ -37,7 +37,7 @@ export type TypedRequestsLike<Routes extends Record<keyof Routes, Dictionary>> =
|
|
|
37
37
|
* The label-aware `addRequests` method signature exposed on a request handler's context when the crawler is
|
|
38
38
|
* bound to a typed router. Mirrors {@link RestrictedCrawlingContext.addRequests} with typed sources.
|
|
39
39
|
*/
|
|
40
|
-
export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<
|
|
40
|
+
export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
|
|
41
41
|
/**
|
|
42
42
|
* An `enqueueLinks`-options object with its `label`/`userData` retyped according to a router's route map: a
|
|
43
43
|
* declared `label` requires the matching `userData` shape (unknown labels are rejected), while unlabeled
|
|
@@ -87,36 +87,15 @@ export interface RestrictedCrawlingContext<UserData extends Dictionary = Diction
|
|
|
87
87
|
*/
|
|
88
88
|
pushData(data: ReadonlyDeep<Parameters<Dataset['pushData']>[0]>, datasetIdentifier?: string | StorageIdentifier): Promise<void>;
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
* currently used by the crawler.
|
|
90
|
+
* Add requests directly to the request queue currently used by the crawler.
|
|
92
91
|
*
|
|
93
|
-
* Optionally, the function allows you to filter the target
|
|
94
|
-
*
|
|
95
|
-
* Check out the [Crawl a website with relative links](https://crawlee.dev/js/docs/examples/crawl-relative-links) example
|
|
96
|
-
* for more details regarding its usage.
|
|
97
|
-
*
|
|
98
|
-
* **Example usage**
|
|
99
|
-
*
|
|
100
|
-
* ```ts
|
|
101
|
-
* async requestHandler({ enqueueLinks }) {
|
|
102
|
-
* await enqueueLinks({
|
|
103
|
-
* include: [
|
|
104
|
-
* 'https://www.example.com/handbags/*',
|
|
105
|
-
* ],
|
|
106
|
-
* });
|
|
107
|
-
* },
|
|
108
|
-
* ```
|
|
109
|
-
*
|
|
110
|
-
* @param [options] All `enqueueLinks()` parameters are passed via an options object.
|
|
111
|
-
*/
|
|
112
|
-
enqueueLinks: (options: ReadonlyDeep<Omit<SetRequired<EnqueueLinksOptions, 'urls'>, 'requestManager' | 'robotsTxtFile'>>) => Promise<unknown>;
|
|
113
|
-
/**
|
|
114
|
-
* Add requests directly to the request queue.
|
|
92
|
+
* Optionally, the function allows you to filter the target URLs using an array of glob or regexp patterns,
|
|
93
|
+
* the same way {@link CrawlingContext.enqueueLinks|`enqueueLinks`} does for extracted links.
|
|
115
94
|
*
|
|
116
95
|
* @param requests The requests to add
|
|
117
96
|
* @param options Options for the request queue
|
|
118
97
|
*/
|
|
119
|
-
addRequests: (requestsLike: ReadonlyDeep<(string | Source)[]>, options?: ReadonlyDeep<
|
|
98
|
+
addRequests: (requestsLike: ReadonlyDeep<(string | Source)[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
|
|
120
99
|
/**
|
|
121
100
|
* Returns the state - a piece of mutable persistent data shared across all the request handler runs.
|
|
122
101
|
*/
|
|
@@ -131,31 +110,6 @@ export interface RestrictedCrawlingContext<UserData extends Dictionary = Diction
|
|
|
131
110
|
log: CrawleeLogger;
|
|
132
111
|
}
|
|
133
112
|
export interface CrawlingContext<UserData extends Dictionary = Dictionary> extends RestrictedCrawlingContext<UserData> {
|
|
134
|
-
/**
|
|
135
|
-
* This function automatically finds and enqueues links from the current page, adding them to the {@link RequestQueue}
|
|
136
|
-
* currently used by the crawler.
|
|
137
|
-
*
|
|
138
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
139
|
-
*
|
|
140
|
-
* Check out the [Crawl a website with relative links](https://crawlee.dev/js/docs/examples/crawl-relative-links) example
|
|
141
|
-
* for more details regarding its usage.
|
|
142
|
-
*
|
|
143
|
-
* **Example usage**
|
|
144
|
-
*
|
|
145
|
-
* ```ts
|
|
146
|
-
* async requestHandler({ enqueueLinks }) {
|
|
147
|
-
* await enqueueLinks({
|
|
148
|
-
* include: [
|
|
149
|
-
* 'https://www.example.com/handbags/*',
|
|
150
|
-
* ],
|
|
151
|
-
* });
|
|
152
|
-
* },
|
|
153
|
-
* ```
|
|
154
|
-
*
|
|
155
|
-
* @param [options] All `enqueueLinks()` parameters are passed via an options object.
|
|
156
|
-
* @returns Promise that resolves to {@link BatchAddRequestsResult} object.
|
|
157
|
-
*/
|
|
158
|
-
enqueueLinks(options: ReadonlyDeep<Omit<SetRequired<EnqueueLinksOptions, 'urls'>, 'requestManager' | 'robotsTxtFile'>> & Pick<EnqueueLinksOptions, 'requestManager' | 'robotsTxtFile'>): Promise<unknown>;
|
|
159
113
|
/**
|
|
160
114
|
* Fires HTTP request via the internal HTTP client, allowing to override the request options on the fly.
|
|
161
115
|
*
|
package/crawlers/statistics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CrawleeLogger } from '../log.js';
|
|
2
|
-
import
|
|
2
|
+
import { KeyValueStore } from '../storages/key_value_store.js';
|
|
3
3
|
import { ErrorTracker } from './error_tracker.js';
|
|
4
4
|
/**
|
|
5
5
|
* Persistence-related options to control how and when crawler's data gets persisted.
|
package/crawlers/statistics.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import ow from 'ow';
|
|
2
1
|
import { z } from 'zod';
|
|
3
2
|
import { RecoverableState } from '../recoverable_state.js';
|
|
4
3
|
import { serviceLocator } from '../service_locator.js';
|
|
4
|
+
import { KeyValueStore } from '../storages/key_value_store.js';
|
|
5
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
5
6
|
import { ErrorTracker } from './error_tracker.js';
|
|
6
7
|
/**
|
|
7
8
|
* @ignore
|
|
@@ -17,6 +18,16 @@ class Job {
|
|
|
17
18
|
return this.#durationMillis;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
const statisticsOptionsSchema = z.strictObject({
|
|
22
|
+
logIntervalSecs: schemas.anyNumber.default(60),
|
|
23
|
+
logMessage: z.string().default('Statistics'),
|
|
24
|
+
log: validators.logger.optional(),
|
|
25
|
+
keyValueStore: z.instanceof(KeyValueStore).optional(),
|
|
26
|
+
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain copy).
|
|
27
|
+
persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
|
|
28
|
+
saveErrorSnapshots: z.boolean().default(false),
|
|
29
|
+
id: z.union([schemas.anyNumber, z.string()]).optional(),
|
|
30
|
+
});
|
|
20
31
|
const errorTrackerConfig = {
|
|
21
32
|
showErrorCode: true,
|
|
22
33
|
showErrorName: true,
|
|
@@ -184,21 +195,10 @@ export class Statistics {
|
|
|
184
195
|
* persistence or error snapshots, share it across sequential runs, or subclass it to track extra fields.
|
|
185
196
|
*/
|
|
186
197
|
constructor(options = {}) {
|
|
187
|
-
|
|
188
|
-
logIntervalSecs: ow.optional.number,
|
|
189
|
-
logMessage: ow.optional.string,
|
|
190
|
-
log: ow.optional.object,
|
|
191
|
-
keyValueStore: ow.optional.object,
|
|
192
|
-
persistenceOptions: ow.optional.object,
|
|
193
|
-
saveErrorSnapshots: ow.optional.boolean,
|
|
194
|
-
id: ow.optional.any(ow.number, ow.string),
|
|
195
|
-
}));
|
|
196
|
-
const { logIntervalSecs = 60, logMessage = 'Statistics', keyValueStore, persistenceOptions = {
|
|
197
|
-
enable: true,
|
|
198
|
-
}, saveErrorSnapshots = false, id, } = options;
|
|
198
|
+
const { logIntervalSecs, logMessage, log, keyValueStore, persistenceOptions, saveErrorSnapshots, id } = parseArgument(options, statisticsOptionsSchema);
|
|
199
199
|
this.id = id ?? String(Statistics.id++);
|
|
200
200
|
this.persistStateKey = `CRAWLEE_CRAWLER_STATISTICS_${this.id}`;
|
|
201
|
-
this.log = (
|
|
201
|
+
this.log = (log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
|
|
202
202
|
this.errorTracker = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
|
|
203
203
|
this.errorTrackerRetry = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
|
|
204
204
|
this.#logIntervalMillis = logIntervalSecs * 1000;
|
package/debug.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
|
-
import
|
|
2
|
+
import { parseArgument, schemas } from './validators.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a standardized debug info from request and response. This info is usually added to dataset under the hidden `#debug` field.
|
|
5
5
|
*
|
|
@@ -12,9 +12,9 @@ import ow from 'ow';
|
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
14
|
export function createRequestDebugInfo(request, response = {}, additionalFields = {}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
parseArgument(request, schemas.anyObject);
|
|
16
|
+
parseArgument(response, schemas.anyObject);
|
|
17
|
+
parseArgument(additionalFields, schemas.anyObject);
|
|
18
18
|
return {
|
|
19
19
|
requestId: request.id,
|
|
20
20
|
url: request.url,
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import type { Dictionary } from '@crawlee/types';
|
|
2
|
+
import type { RequestQueueOperationOptions } from '../storages/request_queue.js';
|
|
3
|
+
import type { RequestTransform, SkippedRequestCallback, UrlPatternInput, UrlPatternObject } from './shared.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options shared by the `extractLinks()` context helper across crawler types.
|
|
6
|
+
*/
|
|
7
|
+
export interface ExtractLinksOptions {
|
|
8
|
+
/** A CSS selector matching links to be extracted. */
|
|
9
|
+
selector?: string;
|
|
10
|
+
/**
|
|
11
|
+
* A base URL that will be used to resolve relative URLs when using Cheerio. Ignored when using Puppeteer,
|
|
12
|
+
* since the relative URL resolution is done inside the browser automatically.
|
|
13
|
+
*/
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Options accepted by the `enqueueUrls()` context helper exposed by `BasicCrawler`.
|
|
18
|
+
*/
|
|
19
|
+
export interface EnqueueUrlsOptions extends RequestQueueOperationOptions {
|
|
9
20
|
/** Limit the amount of actually enqueued URLs to this number. Useful for testing across the entire crawling scope. */
|
|
10
21
|
limit?: number;
|
|
11
|
-
/** An array of URLs to enqueue. */
|
|
12
|
-
urls?: readonly string[];
|
|
13
|
-
/** A request manager to which the URLs will be enqueued. */
|
|
14
|
-
requestManager?: IRequestManager;
|
|
15
|
-
/** A CSS selector matching links to be enqueued. */
|
|
16
|
-
selector?: string;
|
|
17
22
|
/** Sets {@link Request.userData} for newly enqueued requests. */
|
|
18
23
|
userData?: Dictionary;
|
|
19
24
|
/**
|
|
@@ -30,8 +35,7 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
30
35
|
*/
|
|
31
36
|
skipNavigation?: boolean;
|
|
32
37
|
/**
|
|
33
|
-
* A base URL that will be used to resolve relative URLs
|
|
34
|
-
* since the relative URL resolution is done inside the browser automatically.
|
|
38
|
+
* A base URL that will be used to resolve relative URLs.
|
|
35
39
|
*/
|
|
36
40
|
baseUrl?: string;
|
|
37
41
|
/**
|
|
@@ -42,11 +46,11 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
42
46
|
* Glob matching is always case-insensitive.
|
|
43
47
|
* If you need case-sensitive matching, use a `RegExp`.
|
|
44
48
|
*
|
|
45
|
-
* The patterns are combined with the {@link
|
|
49
|
+
* The patterns are combined with the {@link EnqueueUrlsOptions.strategy|`strategy`} using AND logic - a URL
|
|
46
50
|
* must match at least one `include` pattern **and** satisfy the strategy to be enqueued. To match URLs across
|
|
47
51
|
* hostnames, pass an explicit {@link EnqueueStrategy.All} strategy.
|
|
48
52
|
*
|
|
49
|
-
* If `undefined`, the links are enqueued based on the {@link
|
|
53
|
+
* If `undefined`, the links are enqueued based on the {@link EnqueueUrlsOptions.strategy|`strategy`} alone.
|
|
50
54
|
* Passing an empty array is not allowed.
|
|
51
55
|
*/
|
|
52
56
|
include?: readonly UrlPatternInput[];
|
|
@@ -106,25 +110,12 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
106
110
|
*
|
|
107
111
|
* @default EnqueueStrategy.SameHostname
|
|
108
112
|
*/
|
|
109
|
-
strategy?:
|
|
113
|
+
strategy?: EnqueueStrategyOption;
|
|
110
114
|
/**
|
|
111
115
|
* By default, only the first batch (1000) of found requests will be added to the queue before resolving the call.
|
|
112
116
|
* You can use this option to wait for adding all of them.
|
|
113
117
|
*/
|
|
114
118
|
waitForAllRequestsToBeAdded?: boolean;
|
|
115
|
-
/**
|
|
116
|
-
* RobotsTxtFile instance for the current request that triggered the `enqueueLinks`.
|
|
117
|
-
* If provided, disallowed URLs will be ignored.
|
|
118
|
-
*/
|
|
119
|
-
robotsTxtFile?: Pick<RobotsTxtFile, 'isAllowed'>;
|
|
120
|
-
/**
|
|
121
|
-
* Mirrors {@link BasicCrawlerOptions.respectRobotsTxtFile}: pass `false` to disable filtering or
|
|
122
|
-
* `{ userAgent }` to evaluate rules for a specific user-agent. Defaults to `*` when
|
|
123
|
-
* {@link EnqueueLinksOptions.robotsTxtFile|`robotsTxtFile`} is provided.
|
|
124
|
-
*/
|
|
125
|
-
respectRobotsTxtFile?: boolean | {
|
|
126
|
-
userAgent?: string;
|
|
127
|
-
};
|
|
128
119
|
/**
|
|
129
120
|
* When a request is skipped for some reason, you can use this callback to act on it.
|
|
130
121
|
* This is currently fired for requests skipped
|
|
@@ -134,6 +125,8 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
134
125
|
*/
|
|
135
126
|
onSkippedRequest?: SkippedRequestCallback;
|
|
136
127
|
}
|
|
128
|
+
/** The combined options accepted by a crawler context's `enqueueLinks()` helper: `extractLinks()` + `enqueueUrls()`. */
|
|
129
|
+
export type EnqueueLinksOptions = ExtractLinksOptions & EnqueueUrlsOptions;
|
|
137
130
|
/**
|
|
138
131
|
* The different enqueueing strategies available.
|
|
139
132
|
*
|
|
@@ -185,34 +178,8 @@ export declare enum EnqueueStrategy {
|
|
|
185
178
|
*/
|
|
186
179
|
SameOrigin = "same-origin"
|
|
187
180
|
}
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
* you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
|
|
191
|
-
*
|
|
192
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
193
|
-
*
|
|
194
|
-
* **Example usage**
|
|
195
|
-
*
|
|
196
|
-
* ```javascript
|
|
197
|
-
* await enqueueLinks({
|
|
198
|
-
* urls: aListOfFoundUrls,
|
|
199
|
-
* requestManager,
|
|
200
|
-
* selector: 'a.product-detail',
|
|
201
|
-
* include: [
|
|
202
|
-
* 'https://www.example.com/handbags/*',
|
|
203
|
-
* 'https://www.example.com/purses/*'
|
|
204
|
-
* ],
|
|
205
|
-
* });
|
|
206
|
-
* ```
|
|
207
|
-
*
|
|
208
|
-
* @param options All `enqueueLinks()` parameters are passed via an options object.
|
|
209
|
-
* @returns Promise that resolves to {@link BatchAddRequestsResult} object.
|
|
210
|
-
*/
|
|
211
|
-
export declare function enqueueLinks(options: SetRequired<Omit<EnqueueLinksOptions, 'requestManager'>, 'urls'> & {
|
|
212
|
-
requestManager: {
|
|
213
|
-
addRequestsBatched: (requests: Request<Dictionary>[], options: AddRequestsBatchedOptions) => Promise<AddRequestsBatchedResult>;
|
|
214
|
-
};
|
|
215
|
-
}): Promise<BatchAddRequestsResult>;
|
|
181
|
+
/** The `strategy` option accepted by {@link ExtractLinksOptions} and {@link EnqueueUrlsOptions}. */
|
|
182
|
+
export type EnqueueStrategyOption = EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
|
|
216
183
|
/**
|
|
217
184
|
* @internal
|
|
218
185
|
* This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
|
|
@@ -227,7 +194,12 @@ export declare function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy
|
|
|
227
194
|
*/
|
|
228
195
|
export interface ResolveBaseUrl {
|
|
229
196
|
userProvidedBaseUrl?: string;
|
|
230
|
-
enqueueStrategy?:
|
|
197
|
+
enqueueStrategy?: EnqueueStrategyOption;
|
|
231
198
|
originalRequestUrl: string;
|
|
232
199
|
finalRequestUrl?: string;
|
|
233
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* @internal
|
|
203
|
+
* Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
|
|
204
|
+
*/
|
|
205
|
+
export declare function buildEnqueueStrategyPatterns(baseUrl: string, strategy: EnqueueStrategyOption): UrlPatternObject[];
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import ow from 'ow';
|
|
2
1
|
import { getDomain } from 'tldts';
|
|
3
|
-
import { Request } from '../request.js';
|
|
4
|
-
import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, filterRequestOptionsByPatterns, } from './shared.js';
|
|
5
2
|
/**
|
|
6
3
|
* The different enqueueing strategies available.
|
|
7
4
|
*
|
|
@@ -54,155 +51,6 @@ export var EnqueueStrategy;
|
|
|
54
51
|
*/
|
|
55
52
|
EnqueueStrategy["SameOrigin"] = "same-origin";
|
|
56
53
|
})(EnqueueStrategy || (EnqueueStrategy = {}));
|
|
57
|
-
/**
|
|
58
|
-
* This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
|
|
59
|
-
* you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
|
|
60
|
-
*
|
|
61
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
62
|
-
*
|
|
63
|
-
* **Example usage**
|
|
64
|
-
*
|
|
65
|
-
* ```javascript
|
|
66
|
-
* await enqueueLinks({
|
|
67
|
-
* urls: aListOfFoundUrls,
|
|
68
|
-
* requestManager,
|
|
69
|
-
* selector: 'a.product-detail',
|
|
70
|
-
* include: [
|
|
71
|
-
* 'https://www.example.com/handbags/*',
|
|
72
|
-
* 'https://www.example.com/purses/*'
|
|
73
|
-
* ],
|
|
74
|
-
* });
|
|
75
|
-
* ```
|
|
76
|
-
*
|
|
77
|
-
* @param options All `enqueueLinks()` parameters are passed via an options object.
|
|
78
|
-
* @returns Promise that resolves to {@link BatchAddRequestsResult} object.
|
|
79
|
-
*/
|
|
80
|
-
export async function enqueueLinks(options) {
|
|
81
|
-
if (!options || Object.keys(options).length === 0) {
|
|
82
|
-
throw new RangeError([
|
|
83
|
-
'enqueueLinks() was called without the required options. You can only do that when you use the `crawlingContext.enqueueLinks()` method in request handlers.',
|
|
84
|
-
'Check out our guide on how to use enqueueLinks() here: https://crawlee.dev/js/docs/examples/crawl-relative-links',
|
|
85
|
-
].join('\n'));
|
|
86
|
-
}
|
|
87
|
-
const urlPatternValidator = ow.any(ow.string, ow.regExp, ow.object.hasKeys('glob'), ow.object.hasKeys('regexp'));
|
|
88
|
-
ow(options, ow.object.exactShape({
|
|
89
|
-
urls: ow.array.ofType(ow.string),
|
|
90
|
-
requestManager: ow.object.hasKeys('addRequestsBatched'),
|
|
91
|
-
robotsTxtFile: ow.optional.object.hasKeys('isAllowed'),
|
|
92
|
-
respectRobotsTxtFile: ow.optional.any(ow.boolean, ow.object.exactShape({ userAgent: ow.optional.string })),
|
|
93
|
-
onSkippedRequest: ow.optional.function,
|
|
94
|
-
forefront: ow.optional.boolean,
|
|
95
|
-
skipNavigation: ow.optional.boolean,
|
|
96
|
-
sessionId: ow.optional.string,
|
|
97
|
-
limit: ow.optional.number,
|
|
98
|
-
selector: ow.optional.string,
|
|
99
|
-
baseUrl: ow.optional.string,
|
|
100
|
-
userData: ow.optional.object,
|
|
101
|
-
label: ow.optional.string,
|
|
102
|
-
include: ow.optional.array.minLength(1).ofType(urlPatternValidator),
|
|
103
|
-
exclude: ow.optional.array.ofType(urlPatternValidator),
|
|
104
|
-
transformRequestFunction: ow.optional.function,
|
|
105
|
-
strategy: ow.optional.string.oneOf(Object.values(EnqueueStrategy)),
|
|
106
|
-
waitForAllRequestsToBeAdded: ow.optional.boolean,
|
|
107
|
-
}));
|
|
108
|
-
const { requestManager, limit, urls, include, exclude, transformRequestFunction, forefront, waitForAllRequestsToBeAdded, robotsTxtFile, onSkippedRequest, } = options;
|
|
109
|
-
const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
|
|
110
|
-
const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
|
|
111
|
-
// The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
|
|
112
|
-
// (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
|
|
113
|
-
options.strategy ??= EnqueueStrategy.SameHostname;
|
|
114
|
-
const enqueueStrategyPatterns = [];
|
|
115
|
-
if (options.baseUrl) {
|
|
116
|
-
const url = new URL(options.baseUrl);
|
|
117
|
-
switch (options.strategy) {
|
|
118
|
-
case EnqueueStrategy.SameHostname:
|
|
119
|
-
// We need to get the origin of the passed in domain in the event someone sets baseUrl
|
|
120
|
-
// to an url like https://example.com/deep/default/path and one of the found urls is an
|
|
121
|
-
// absolute relative path (/path/to/page)
|
|
122
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
123
|
-
break;
|
|
124
|
-
case EnqueueStrategy.SameDomain: {
|
|
125
|
-
// Get the actual hostname from the base url
|
|
126
|
-
const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
|
|
127
|
-
if (baseUrlHostname) {
|
|
128
|
-
// We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
|
|
129
|
-
url.hostname = baseUrlHostname;
|
|
130
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) }, { glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
// We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
|
|
134
|
-
// as SameDomainAndSubdomain
|
|
135
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
136
|
-
}
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
case EnqueueStrategy.SameOrigin: {
|
|
140
|
-
// The same behavior as SameHostname, but respecting the protocol of the URL
|
|
141
|
-
enqueueStrategyPatterns.push({ glob: `${url.origin}/**` });
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
case EnqueueStrategy.All:
|
|
145
|
-
default:
|
|
146
|
-
enqueueStrategyPatterns.push({ glob: `http{s,}://**` });
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async function reportSkippedRequests(skippedRequests, reason) {
|
|
151
|
-
if (onSkippedRequest && skippedRequests.length > 0) {
|
|
152
|
-
await Promise.all(skippedRequests.map((request) => {
|
|
153
|
-
return onSkippedRequest({
|
|
154
|
-
url: request.url,
|
|
155
|
-
reason: request.skippedReason ?? reason,
|
|
156
|
-
});
|
|
157
|
-
}));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
let requestOptions = createRequestOptions(urls, options);
|
|
161
|
-
if (robotsTxtFile && options.respectRobotsTxtFile !== false) {
|
|
162
|
-
const robotsUserAgent = typeof options.respectRobotsTxtFile === 'object' ? (options.respectRobotsTxtFile.userAgent ?? '*') : '*';
|
|
163
|
-
const skippedRequests = [];
|
|
164
|
-
requestOptions = requestOptions.filter((request) => {
|
|
165
|
-
if (robotsTxtFile.isAllowed(request.url, robotsUserAgent)) {
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
skippedRequests.push(request);
|
|
169
|
-
return false;
|
|
170
|
-
});
|
|
171
|
-
await reportSkippedRequests(skippedRequests, 'robotsTxt');
|
|
172
|
-
}
|
|
173
|
-
async function createFilteredRequests() {
|
|
174
|
-
const skippedRequests = [];
|
|
175
|
-
// Step 1: Filter request options by exclude patterns, user include patterns, and strategy patterns.
|
|
176
|
-
let filteredOptions;
|
|
177
|
-
if (urlPatternObjects.length === 0) {
|
|
178
|
-
filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects, options.strategy, (url) => skippedRequests.push(url));
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
// Filter by user patterns first (with exclude)
|
|
182
|
-
const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects, options.strategy, (url) => skippedRequests.push(url));
|
|
183
|
-
// ...then filter by the enqueue links strategy (making this an AND check)
|
|
184
|
-
filteredOptions = filterRequestOptionsByPatterns(afterUserPatterns, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], options.strategy, (url) => skippedRequests.push(url));
|
|
185
|
-
}
|
|
186
|
-
await reportSkippedRequests(skippedRequests.map((url) => ({ url })), 'filters');
|
|
187
|
-
// Step 2: Apply transformRequestFunction on request options - it has the highest priority
|
|
188
|
-
if (transformRequestFunction) {
|
|
189
|
-
const skippedByTransform = [];
|
|
190
|
-
filteredOptions = applyRequestTransform(filteredOptions, transformRequestFunction, (r) => skippedByTransform.push(r));
|
|
191
|
-
await reportSkippedRequests(skippedByTransform, 'transform');
|
|
192
|
-
}
|
|
193
|
-
// Step 3: Create Request instances from the final request options
|
|
194
|
-
return filteredOptions.map((opts) => new Request(opts));
|
|
195
|
-
}
|
|
196
|
-
const { addedRequests, requestsOverLimit } = await requestManager.addRequestsBatched(await createFilteredRequests(), {
|
|
197
|
-
forefront,
|
|
198
|
-
waitForAllRequestsToBeAdded,
|
|
199
|
-
maxNewRequests: limit,
|
|
200
|
-
});
|
|
201
|
-
if (requestsOverLimit?.length !== undefined && requestsOverLimit.length > 0) {
|
|
202
|
-
await reportSkippedRequests(requestsOverLimit.map((r) => ({ url: typeof r === 'string' ? r : r.url })), 'enqueueLimit');
|
|
203
|
-
}
|
|
204
|
-
return { processedRequests: addedRequests, unprocessedRequests: [] };
|
|
205
|
-
}
|
|
206
54
|
/**
|
|
207
55
|
* @internal
|
|
208
56
|
* This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
|
|
@@ -237,6 +85,41 @@ export function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy, finalR
|
|
|
237
85
|
// before actually finding the urls
|
|
238
86
|
return originalUrlOrigin;
|
|
239
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* @internal
|
|
90
|
+
* Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
|
|
91
|
+
*/
|
|
92
|
+
export function buildEnqueueStrategyPatterns(baseUrl, strategy) {
|
|
93
|
+
const url = new URL(baseUrl);
|
|
94
|
+
switch (strategy) {
|
|
95
|
+
case EnqueueStrategy.SameHostname:
|
|
96
|
+
// We need to get the origin of the passed in domain in the event someone sets baseUrl
|
|
97
|
+
// to an url like https://example.com/deep/default/path and one of the found urls is an
|
|
98
|
+
// absolute relative path (/path/to/page)
|
|
99
|
+
return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
|
|
100
|
+
case EnqueueStrategy.SameDomain: {
|
|
101
|
+
// Get the actual hostname from the base url
|
|
102
|
+
const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
|
|
103
|
+
if (baseUrlHostname) {
|
|
104
|
+
// We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
|
|
105
|
+
url.hostname = baseUrlHostname;
|
|
106
|
+
return [
|
|
107
|
+
{ glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) },
|
|
108
|
+
{ glob: ignoreHttpSchema(`${url.origin}/**`) },
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
// We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
|
|
112
|
+
// as SameDomainAndSubdomain
|
|
113
|
+
return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
|
|
114
|
+
}
|
|
115
|
+
case EnqueueStrategy.SameOrigin:
|
|
116
|
+
// The same behavior as SameHostname, but respecting the protocol of the URL
|
|
117
|
+
return [{ glob: `${url.origin}/**` }];
|
|
118
|
+
case EnqueueStrategy.All:
|
|
119
|
+
default:
|
|
120
|
+
return [{ glob: `http{s,}://**` }];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
240
123
|
/**
|
|
241
124
|
* Internal function that changes the enqueue glob patterns to match both http and https
|
|
242
125
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Awaitable } from '@crawlee/types';
|
|
1
|
+
import type { Awaitable, Dictionary } from '@crawlee/types';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import type { RequestOptions } from '../request.js';
|
|
3
|
-
import type {
|
|
4
|
+
import type { EnqueueStrategyOption } from './enqueue_links.js';
|
|
4
5
|
export { tryAbsoluteURL } from '@crawlee/utils/internal';
|
|
5
6
|
export interface UrlPatternObject {
|
|
6
7
|
glob?: string;
|
|
@@ -16,6 +17,11 @@ export interface RegExpObject {
|
|
|
16
17
|
export type RegExpInput = RegExp | RegExpObject;
|
|
17
18
|
/** Unified URL pattern input — accepts glob strings, glob objects, RegExp instances, or regexp objects. */
|
|
18
19
|
export type UrlPatternInput = GlobInput | RegExpInput;
|
|
20
|
+
/**
|
|
21
|
+
* Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare const urlPatternSchema: z.ZodType<UrlPatternInput>;
|
|
19
25
|
export type SkippedRequestReason = 'robotsTxt' | 'limit' | 'enqueueLimit' | 'filters' | 'transform' | 'redirect' | 'depth';
|
|
20
26
|
export type SkippedRequestCallback = (args: {
|
|
21
27
|
url: string;
|
|
@@ -53,11 +59,18 @@ export declare function constructUrlPatternObjects(patterns: readonly UrlPattern
|
|
|
53
59
|
* When `includePatterns` is empty/undefined, all options pass through (only exclude filtering applies).
|
|
54
60
|
* @ignore
|
|
55
61
|
*/
|
|
56
|
-
export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?:
|
|
62
|
+
export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?: EnqueueStrategyOption, onSkippedUrl?: (url: string) => void): RequestOptions[];
|
|
57
63
|
/**
|
|
58
64
|
* @ignore
|
|
59
65
|
*/
|
|
60
|
-
export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?:
|
|
66
|
+
export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?: {
|
|
67
|
+
label?: string;
|
|
68
|
+
userData?: Dictionary;
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
skipNavigation?: boolean;
|
|
71
|
+
sessionId?: string;
|
|
72
|
+
strategy?: EnqueueStrategyOption;
|
|
73
|
+
}): RequestOptions[];
|
|
61
74
|
/**
|
|
62
75
|
* Takes a {@link RequestOptions} object and changes its attributes in a desired way. This user-function is used
|
|
63
76
|
* by {@link enqueueLinks} to modify request options before they are converted to {@link Request} instances.
|
package/enqueue_links/shared.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
2
|
import { Minimatch } from 'minimatch';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { schemas } from '../validators.js';
|
|
3
5
|
export { tryAbsoluteURL } from '@crawlee/utils/internal';
|
|
4
6
|
const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
|
|
5
7
|
/**
|
|
@@ -8,6 +10,16 @@ const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
|
|
|
8
10
|
* @ignore
|
|
9
11
|
*/
|
|
10
12
|
const enqueueLinksPatternCache = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export const urlPatternSchema = z.union([
|
|
18
|
+
z.string(),
|
|
19
|
+
z.instanceof(RegExp),
|
|
20
|
+
schemas.objectWithKeys(['glob']),
|
|
21
|
+
schemas.objectWithKeys(['regexp']),
|
|
22
|
+
]);
|
|
11
23
|
/**
|
|
12
24
|
* @ignore
|
|
13
25
|
*/
|
|
@@ -132,6 +144,16 @@ export function filterRequestOptionsByPatterns(requestOptions, includePatterns,
|
|
|
132
144
|
})
|
|
133
145
|
.filter((opts) => opts !== null);
|
|
134
146
|
}
|
|
147
|
+
function isAbsoluteUrl(url) {
|
|
148
|
+
try {
|
|
149
|
+
// eslint-disable-next-line no-new
|
|
150
|
+
new URL(url);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
135
157
|
/**
|
|
136
158
|
* @ignore
|
|
137
159
|
*/
|
|
@@ -149,7 +171,12 @@ export function createRequestOptions(sources, options = {}) {
|
|
|
149
171
|
}
|
|
150
172
|
})
|
|
151
173
|
.map((requestOptions) => {
|
|
152
|
-
|
|
174
|
+
// Leave already-absolute URLs untouched - re-deriving them via `new URL()` would normalize them
|
|
175
|
+
// (e.g. adding a trailing slash to a bare domain), which is surprising for URLs that didn't need
|
|
176
|
+
// resolving against `baseUrl` in the first place.
|
|
177
|
+
if (!isAbsoluteUrl(requestOptions.url)) {
|
|
178
|
+
requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
|
|
179
|
+
}
|
|
153
180
|
requestOptions.userData ??= options.userData ?? {};
|
|
154
181
|
if (typeof options.label === 'string') {
|
|
155
182
|
requestOptions.userData = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
3
3
|
import { BaseClient } from './common/base-client.js';
|
|
4
4
|
/**
|
|
5
5
|
* This is what API returns in the x-apify-pagination-limit
|
|
@@ -49,13 +49,7 @@ export class DatasetBackend extends BaseClient {
|
|
|
49
49
|
this.updateTimestamps(true);
|
|
50
50
|
}
|
|
51
51
|
getData(options = {}) {
|
|
52
|
-
const { desc, limit, offset } =
|
|
53
|
-
.object({
|
|
54
|
-
desc: s.boolean().optional(),
|
|
55
|
-
limit: s.number().int().optional(),
|
|
56
|
-
offset: s.number().int().optional(),
|
|
57
|
-
})
|
|
58
|
-
.parse(options);
|
|
52
|
+
const { desc, limit, offset } = parseArgument(options, schemas.datasetListItemsOptions);
|
|
59
53
|
return this.getDataPage({
|
|
60
54
|
desc,
|
|
61
55
|
offset: offset ?? 0,
|