@crawlee/basic 3.17.1-beta.8 → 3.17.1-beta.80
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 +3 -0
- package/internals/basic-crawler.d.ts +27 -4
- package/internals/basic-crawler.js +78 -43
- package/package.json +7 -6
package/index.mjs
CHANGED
|
@@ -48,6 +48,7 @@ export const RequestQueue = mod.RequestQueue;
|
|
|
48
48
|
export const RequestQueueV1 = mod.RequestQueueV1;
|
|
49
49
|
export const RequestQueueV2 = mod.RequestQueueV2;
|
|
50
50
|
export const RequestState = mod.RequestState;
|
|
51
|
+
export const RequestValidationError = mod.RequestValidationError;
|
|
51
52
|
export const RetryRequestError = mod.RetryRequestError;
|
|
52
53
|
export const Router = mod.Router;
|
|
53
54
|
export const STATE_PERSISTENCE_KEY = mod.STATE_PERSISTENCE_KEY;
|
|
@@ -76,6 +77,7 @@ export const createDeserialize = mod.createDeserialize;
|
|
|
76
77
|
export const createEventLoopLoadSignal = mod.createEventLoopLoadSignal;
|
|
77
78
|
export const createRequestOptions = mod.createRequestOptions;
|
|
78
79
|
export const createRequests = mod.createRequests;
|
|
80
|
+
export const defaultRoute = mod.defaultRoute;
|
|
79
81
|
export const deserializeArray = mod.deserializeArray;
|
|
80
82
|
export const enqueueLinks = mod.enqueueLinks;
|
|
81
83
|
export const evaluateLoadSignalSample = mod.evaluateLoadSignalSample;
|
|
@@ -96,5 +98,6 @@ export const tryAbsoluteURL = mod.tryAbsoluteURL;
|
|
|
96
98
|
export const updateEnqueueLinksPatternCache = mod.updateEnqueueLinksPatternCache;
|
|
97
99
|
export const useState = mod.useState;
|
|
98
100
|
export const validateGlobPattern = mod.validateGlobPattern;
|
|
101
|
+
export const validateUserData = mod.validateUserData;
|
|
99
102
|
export const validators = mod.validators;
|
|
100
103
|
export const withCheckedStorageAccess = mod.withCheckedStorageAccess;
|
|
@@ -266,6 +266,9 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = BasicCraw
|
|
|
266
266
|
* 2. because they don't match enqueueLinks filters,
|
|
267
267
|
* 3. because they are redirected to a URL that doesn't match the enqueueLinks strategy,
|
|
268
268
|
* 4. or because the {@link BasicCrawlerOptions.maxRequestsPerCrawl|`maxRequestsPerCrawl`} limit has been reached
|
|
269
|
+
*
|
|
270
|
+
* When `enqueueLinks` is called with its own `onSkippedRequest` callback, both are invoked — this one first,
|
|
271
|
+
* then the `enqueueLinks` one.
|
|
269
272
|
*/
|
|
270
273
|
onSkippedRequest?: SkippedRequestCallback;
|
|
271
274
|
/** @internal */
|
|
@@ -436,7 +439,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = BasicCrawlin
|
|
|
436
439
|
userAgent?: string;
|
|
437
440
|
};
|
|
438
441
|
protected onSkippedRequest?: SkippedRequestCallback;
|
|
439
|
-
private
|
|
442
|
+
private _ownsEventManager;
|
|
440
443
|
private loggedPerRun;
|
|
441
444
|
private experiments;
|
|
442
445
|
private readonly robotsTxtFileCache;
|
|
@@ -545,6 +548,24 @@ export declare class BasicCrawler<Context extends CrawlingContext = BasicCrawlin
|
|
|
545
548
|
*/
|
|
546
549
|
stop(reason?: string): void;
|
|
547
550
|
getRequestQueue(): Promise<RequestProvider>;
|
|
551
|
+
/**
|
|
552
|
+
* The request handler exactly as the user supplied it — a {@link Router} when one is in use, whether it
|
|
553
|
+
* was passed as `requestHandler` or auto-wired from {@link BasicCrawler.router|`crawler.router`}.
|
|
554
|
+
*
|
|
555
|
+
* Router-aware features read per-label metadata off this handler (currently the `userData` schema map), so
|
|
556
|
+
* it must resolve to the *unwrapped* handler. Subclasses that hand a wrapper to `BasicCrawler` instead of
|
|
557
|
+
* the user's own function — {@link BrowserCrawler} and its descendants do — have to override this, or
|
|
558
|
+
* those features silently no-op against the wrapper.
|
|
559
|
+
*/
|
|
560
|
+
protected get userRequestHandler(): RequestHandler<Context>;
|
|
561
|
+
/**
|
|
562
|
+
* Validates a request source's `userData` against the {@link RouteSchemas|Standard Schema} registered
|
|
563
|
+
* for its label on the crawler's schema-router (if any), throwing a {@link RequestValidationError} on
|
|
564
|
+
* mismatch. A no-op when the user's request handler is not a schema-router, or no schema is registered for
|
|
565
|
+
* the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
|
|
566
|
+
* `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
|
|
567
|
+
*/
|
|
568
|
+
protected validateRequestUserData(source: Source | string): Promise<void>;
|
|
548
569
|
useState<State extends Dictionary = Dictionary>(defaultValue?: State): Promise<State>;
|
|
549
570
|
protected get pendingRequestCountApproximation(): number;
|
|
550
571
|
protected calculateEnqueuedRequestLimit(explicitLimit?: number): number | undefined;
|
|
@@ -618,8 +639,9 @@ export declare class BasicCrawler<Context extends CrawlingContext = BasicCrawlin
|
|
|
618
639
|
/**
|
|
619
640
|
* Wrapper around the crawling context's `enqueueLinks` method:
|
|
620
641
|
* - Injects `crawlDepth` to each request being added based on the crawling context request.
|
|
621
|
-
* -
|
|
622
|
-
*
|
|
642
|
+
* - Combines the `enqueueLinks` options with the crawler configuration - the user options take precedence,
|
|
643
|
+
* but the crawler limits are always enforced (the `limit` is capped by the remaining `maxRequestsPerCrawl`
|
|
644
|
+
* budget and skipped requests are always reported to the crawler too).
|
|
623
645
|
* @internal
|
|
624
646
|
*/
|
|
625
647
|
protected enqueueLinksWithCrawlDepth(options: SetRequired<EnqueueLinksOptions, 'urls'>, request: Request<Dictionary>, requestQueue: RequestProvider): Promise<BatchAddRequestsResult>;
|
|
@@ -719,5 +741,6 @@ interface HandlePropertyNameChangeData<New, Old> {
|
|
|
719
741
|
* await crawler.run();
|
|
720
742
|
* ```
|
|
721
743
|
*/
|
|
722
|
-
export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext,
|
|
744
|
+
export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
|
|
745
|
+
export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
|
|
723
746
|
export {};
|
|
@@ -336,11 +336,11 @@ class BasicCrawler {
|
|
|
336
336
|
writable: true,
|
|
337
337
|
value: void 0
|
|
338
338
|
});
|
|
339
|
-
Object.defineProperty(this, "
|
|
339
|
+
Object.defineProperty(this, "_ownsEventManager", {
|
|
340
340
|
enumerable: true,
|
|
341
341
|
configurable: true,
|
|
342
342
|
writable: true,
|
|
343
|
-
value:
|
|
343
|
+
value: false
|
|
344
344
|
});
|
|
345
345
|
Object.defineProperty(this, "loggedPerRun", {
|
|
346
346
|
enumerable: true,
|
|
@@ -655,8 +655,8 @@ class BasicCrawler {
|
|
|
655
655
|
await this.autoscaledPool.run();
|
|
656
656
|
}
|
|
657
657
|
finally {
|
|
658
|
-
await this.teardown();
|
|
659
658
|
await this.stats.stopCapturing();
|
|
659
|
+
await this.teardown();
|
|
660
660
|
process.off('SIGINT', sigintHandler);
|
|
661
661
|
this.events.off("migrating" /* EventType.MIGRATING */, boundPauseOnMigration);
|
|
662
662
|
this.events.off("aborting" /* EventType.ABORTING */, boundPauseOnMigration);
|
|
@@ -688,8 +688,12 @@ class BasicCrawler {
|
|
|
688
688
|
finished = true;
|
|
689
689
|
}
|
|
690
690
|
periodicLogger.stop();
|
|
691
|
-
//
|
|
692
|
-
|
|
691
|
+
// Give the event loop a single tick to flush the HTTP
|
|
692
|
+
// 1ms is enough because we already have a keep-alive connection to the API
|
|
693
|
+
await Promise.race([
|
|
694
|
+
this.setStatusMessage(`Finished! Total ${this.stats.state.requestsFinished + this.stats.state.requestsFailed} requests: ${this.stats.state.requestsFinished} succeeded, ${this.stats.state.requestsFailed} failed.`, { isStatusMessageTerminal: true, level: 'INFO' }),
|
|
695
|
+
(0, utils_1.sleep)(1),
|
|
696
|
+
]);
|
|
693
697
|
this.running = false;
|
|
694
698
|
this.hasFinishedBefore = true;
|
|
695
699
|
}
|
|
@@ -725,6 +729,44 @@ class BasicCrawler {
|
|
|
725
729
|
}
|
|
726
730
|
return this.requestQueue;
|
|
727
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* The request handler exactly as the user supplied it — a {@link Router} when one is in use, whether it
|
|
734
|
+
* was passed as `requestHandler` or auto-wired from {@link BasicCrawler.router|`crawler.router`}.
|
|
735
|
+
*
|
|
736
|
+
* Router-aware features read per-label metadata off this handler (currently the `userData` schema map), so
|
|
737
|
+
* it must resolve to the *unwrapped* handler. Subclasses that hand a wrapper to `BasicCrawler` instead of
|
|
738
|
+
* the user's own function — {@link BrowserCrawler} and its descendants do — have to override this, or
|
|
739
|
+
* those features silently no-op against the wrapper.
|
|
740
|
+
*/
|
|
741
|
+
get userRequestHandler() {
|
|
742
|
+
return this.requestHandler;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Validates a request source's `userData` against the {@link RouteSchemas|Standard Schema} registered
|
|
746
|
+
* for its label on the crawler's schema-router (if any), throwing a {@link RequestValidationError} on
|
|
747
|
+
* mismatch. A no-op when the user's request handler is not a schema-router, or no schema is registered for
|
|
748
|
+
* the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
|
|
749
|
+
* `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
|
|
750
|
+
*/
|
|
751
|
+
async validateRequestUserData(source) {
|
|
752
|
+
if (typeof source === 'string') {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const getSchema = this.userRequestHandler.getSchema;
|
|
756
|
+
if (typeof getSchema !== 'function') {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
// Resolve the label via its public accessors only — the top-level `label` of a `RequestOptions` or the
|
|
760
|
+
// `Request.label` getter — rather than reaching into `userData`, where the request happens to store it.
|
|
761
|
+
const target = source;
|
|
762
|
+
const schema = getSchema(target.label);
|
|
763
|
+
if (!schema) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Store the parsed value rather than the raw input, so the queue holds the same coerced `userData` the
|
|
767
|
+
// handler will see. Assigning through a `Request` instance's setter keeps its internal `__crawlee` meta.
|
|
768
|
+
target.userData = await (0, core_1.validateUserData)(target.label, schema, target.userData ?? {});
|
|
769
|
+
}
|
|
728
770
|
async useState(defaultValue = {}) {
|
|
729
771
|
const kvs = await core_1.KeyValueStore.open(null, { config: this.config });
|
|
730
772
|
return kvs.getAutoSavedValue(BasicCrawler.CRAWLEE_STATE_KEY, defaultValue);
|
|
@@ -773,6 +815,7 @@ class BasicCrawler {
|
|
|
773
815
|
const skippedBecauseOfMaxCrawlDepth = new Set();
|
|
774
816
|
const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
|
|
775
817
|
const maxCrawlDepth = this.maxCrawlDepth;
|
|
818
|
+
const validateRequestUserData = this.validateRequestUserData.bind(this);
|
|
776
819
|
(0, ow_1.default)(requests, ow_1.default.object
|
|
777
820
|
.is((value) => (0, utils_1.isIterable)(value) || (0, utils_1.isAsyncIterable)(value))
|
|
778
821
|
.message((value) => `Expected an iterable or async iterable, got ${(0, utils_1.getObjectType)(value)}`));
|
|
@@ -784,6 +827,7 @@ class BasicCrawler {
|
|
|
784
827
|
continue;
|
|
785
828
|
}
|
|
786
829
|
if (await isAllowedBasedOnRobotsTxtFile(url)) {
|
|
830
|
+
await validateRequestUserData(request);
|
|
787
831
|
yield request;
|
|
788
832
|
}
|
|
789
833
|
else {
|
|
@@ -888,7 +932,7 @@ class BasicCrawler {
|
|
|
888
932
|
async _init() {
|
|
889
933
|
if (!this.events.isInitialized()) {
|
|
890
934
|
await this.events.init();
|
|
891
|
-
this.
|
|
935
|
+
this._ownsEventManager = true;
|
|
892
936
|
}
|
|
893
937
|
this.autoscaledPool = new core_1.AutoscaledPool(this.autoscaledPoolOptions, this.config);
|
|
894
938
|
if (this.useSessionPool) {
|
|
@@ -1147,8 +1191,9 @@ class BasicCrawler {
|
|
|
1147
1191
|
/**
|
|
1148
1192
|
* Wrapper around the crawling context's `enqueueLinks` method:
|
|
1149
1193
|
* - Injects `crawlDepth` to each request being added based on the crawling context request.
|
|
1150
|
-
* -
|
|
1151
|
-
*
|
|
1194
|
+
* - Combines the `enqueueLinks` options with the crawler configuration - the user options take precedence,
|
|
1195
|
+
* but the crawler limits are always enforced (the `limit` is capped by the remaining `maxRequestsPerCrawl`
|
|
1196
|
+
* budget and skipped requests are always reported to the crawler too).
|
|
1152
1197
|
* @internal
|
|
1153
1198
|
*/
|
|
1154
1199
|
async enqueueLinksWithCrawlDepth(options, request, requestQueue) {
|
|
@@ -1159,28 +1204,37 @@ class BasicCrawler {
|
|
|
1159
1204
|
return false;
|
|
1160
1205
|
}
|
|
1161
1206
|
// After injecting the crawlDepth, we call the user-provided transform function, if there is one.
|
|
1162
|
-
return
|
|
1207
|
+
// Its return value is passed through as is, so a falsy one still skips the request.
|
|
1208
|
+
return options.transformRequestFunction ? options.transformRequestFunction(newRequest) : newRequest;
|
|
1163
1209
|
};
|
|
1210
|
+
const limit = this.calculateEnqueuedRequestLimit(options.limit);
|
|
1164
1211
|
// Create a request-scoped callback that logs enqueueLimit once per request handler call
|
|
1165
1212
|
// Only log if an explicit limit was passed to enqueueLinks (not the internal maxRequestsPerCrawl-derived limit)
|
|
1166
1213
|
let loggedEnqueueLimitForThisRequest = false;
|
|
1167
1214
|
const onSkippedRequest = async (skippedOptions) => {
|
|
1168
1215
|
if (skippedOptions.reason === 'enqueueLimit') {
|
|
1169
1216
|
if (!loggedEnqueueLimitForThisRequest && options.limit !== undefined) {
|
|
1170
|
-
this.log.info(
|
|
1217
|
+
this.log.info(limit === options.limit
|
|
1218
|
+
? `Skipping URLs in the handler for ${request.url} due to the enqueueLinks limit of ${options.limit}.`
|
|
1219
|
+
: `Skipping URLs in the handler for ${request.url} due to the remaining maxRequestsPerCrawl budget of ${limit}, which is lower than the enqueueLinks limit of ${options.limit}.`);
|
|
1171
1220
|
loggedEnqueueLimitForThisRequest = true;
|
|
1172
1221
|
}
|
|
1173
1222
|
}
|
|
1174
1223
|
await this.handleSkippedRequest(skippedOptions);
|
|
1224
|
+
await options.onSkippedRequest?.(skippedOptions);
|
|
1175
1225
|
};
|
|
1226
|
+
// `enqueueLinks` applies `options.label`/`options.userData` to every newly enqueued request, so a single
|
|
1227
|
+
// validation against the label's schema covers them all (a no-op unless the router declares a schema).
|
|
1228
|
+
await this.validateRequestUserData({ label: options.label, userData: options.userData });
|
|
1176
1229
|
return (0, core_1.enqueueLinks)({
|
|
1177
|
-
requestQueue,
|
|
1178
|
-
robotsTxtFile: await this.getRobotsTxtFileForUrl(request.url),
|
|
1179
|
-
respectRobotsTxtFile: this.respectRobotsTxtFile,
|
|
1180
|
-
onSkippedRequest,
|
|
1181
|
-
limit: this.calculateEnqueuedRequestLimit(options.limit),
|
|
1182
|
-
// Allow user options to override defaults set above ⤴
|
|
1183
1230
|
...options,
|
|
1231
|
+
// The options below are merged with the user options, so an explicitly `undefined` value
|
|
1232
|
+
// (e.g. `enqueueLinks({ urls, limit: config.limit })`) cannot discard the crawler defaults ⤵
|
|
1233
|
+
requestQueue: options.requestQueue ?? requestQueue,
|
|
1234
|
+
robotsTxtFile: options.robotsTxtFile ?? (await this.getRobotsTxtFileForUrl(request.url)),
|
|
1235
|
+
respectRobotsTxtFile: options.respectRobotsTxtFile ?? this.respectRobotsTxtFile,
|
|
1236
|
+
onSkippedRequest,
|
|
1237
|
+
limit,
|
|
1184
1238
|
transformRequestFunction: transformRequestFunctionWrapper,
|
|
1185
1239
|
});
|
|
1186
1240
|
}
|
|
@@ -1359,11 +1413,16 @@ class BasicCrawler {
|
|
|
1359
1413
|
* To stop the crawler gracefully (waiting for all running requests to finish), use {@link BasicCrawler.stop|`crawler.stop()`} instead.
|
|
1360
1414
|
*/
|
|
1361
1415
|
async teardown() {
|
|
1362
|
-
this
|
|
1416
|
+
// When this crawler initialized the event manager, its close() call emits
|
|
1417
|
+
// the final persistence event after the crawler-specific state has been
|
|
1418
|
+
// saved. External event managers still need an explicit event here.
|
|
1419
|
+
if (!this._ownsEventManager) {
|
|
1420
|
+
this.events.emit("persistState" /* EventType.PERSIST_STATE */, { isMigrating: false });
|
|
1421
|
+
}
|
|
1363
1422
|
if (this.useSessionPool) {
|
|
1364
|
-
await this.sessionPool.teardown();
|
|
1423
|
+
await this.sessionPool.teardown({ persistState: this._ownsEventManager });
|
|
1365
1424
|
}
|
|
1366
|
-
if (this.
|
|
1425
|
+
if (this._ownsEventManager) {
|
|
1367
1426
|
await this.events.close();
|
|
1368
1427
|
}
|
|
1369
1428
|
await this.autoscaledPool?.abort();
|
|
@@ -1502,30 +1561,6 @@ Object.defineProperty(BasicCrawler, "optionsShape", {
|
|
|
1502
1561
|
statisticsOptions: ow_1.default.optional.object,
|
|
1503
1562
|
}
|
|
1504
1563
|
});
|
|
1505
|
-
/**
|
|
1506
|
-
* Creates new {@link Router} instance that works based on request labels.
|
|
1507
|
-
* This instance can then serve as a {@link BasicCrawlerOptions.requestHandler|`requestHandler`} of our {@link BasicCrawler}.
|
|
1508
|
-
* Defaults to the {@link BasicCrawlingContext}.
|
|
1509
|
-
*
|
|
1510
|
-
* > Serves as a shortcut for using `Router.create<BasicCrawlingContext>()`.
|
|
1511
|
-
*
|
|
1512
|
-
* ```ts
|
|
1513
|
-
* import { BasicCrawler, createBasicRouter } from 'crawlee';
|
|
1514
|
-
*
|
|
1515
|
-
* const router = createBasicRouter();
|
|
1516
|
-
* router.addHandler('label-a', async (ctx) => {
|
|
1517
|
-
* ctx.log.info('...');
|
|
1518
|
-
* });
|
|
1519
|
-
* router.addDefaultHandler(async (ctx) => {
|
|
1520
|
-
* ctx.log.info('...');
|
|
1521
|
-
* });
|
|
1522
|
-
*
|
|
1523
|
-
* const crawler = new BasicCrawler({
|
|
1524
|
-
* requestHandler: router,
|
|
1525
|
-
* });
|
|
1526
|
-
* await crawler.run();
|
|
1527
|
-
* ```
|
|
1528
|
-
*/
|
|
1529
1564
|
function createBasicRouter(routes) {
|
|
1530
1565
|
return core_1.Router.create(routes);
|
|
1531
1566
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/basic",
|
|
3
|
-
"version": "3.17.1-beta.
|
|
3
|
+
"version": "3.17.1-beta.80",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=16.0.0"
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@apify/datastructures": "^2.0.0",
|
|
48
49
|
"@apify/log": "^2.4.0",
|
|
49
|
-
"@apify/timeout": "^0.
|
|
50
|
+
"@apify/timeout": "^0.4.0",
|
|
50
51
|
"@apify/utilities": "^2.7.10",
|
|
51
|
-
"@crawlee/core": "3.17.1-beta.
|
|
52
|
-
"@crawlee/types": "3.17.1-beta.
|
|
53
|
-
"@crawlee/utils": "3.17.1-beta.
|
|
52
|
+
"@crawlee/core": "3.17.1-beta.80",
|
|
53
|
+
"@crawlee/types": "3.17.1-beta.80",
|
|
54
|
+
"@crawlee/utils": "3.17.1-beta.80",
|
|
54
55
|
"csv-stringify": "^6.2.0",
|
|
55
56
|
"fs-extra": "^11.0.0",
|
|
56
57
|
"got-scraping": "^4.2.1",
|
|
@@ -66,5 +67,5 @@
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
},
|
|
69
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "5b3c2d52f128643de6f515e87eeb531d55a187f0"
|
|
70
71
|
}
|