@crawlee/basic 4.0.0-beta.134 → 4.0.0-beta.136
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.d.ts +0 -1
- package/internals/basic-crawler.d.ts +3 -139
- package/internals/basic-crawler.js +51 -49
- package/package.json +7 -7
package/index.d.ts
CHANGED
|
@@ -367,93 +367,8 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
367
367
|
*/
|
|
368
368
|
additionalHttpErrorStatusCodes?: number[];
|
|
369
369
|
}
|
|
370
|
-
/**
|
|
371
|
-
* Provides a simple framework for parallel crawling of web pages.
|
|
372
|
-
* The URLs to crawl are fed either from a static list of URLs
|
|
373
|
-
* or from a dynamic queue of URLs enabling recursive crawling of websites.
|
|
374
|
-
*
|
|
375
|
-
* `BasicCrawler` is a low-level tool that requires the user to implement the page
|
|
376
|
-
* download and data extraction functionality themselves.
|
|
377
|
-
* If we want a crawler that already facilitates this functionality,
|
|
378
|
-
* we should consider using {@link CheerioCrawler}, {@link PuppeteerCrawler} or {@link PlaywrightCrawler}.
|
|
379
|
-
*
|
|
380
|
-
* `BasicCrawler` invokes the user-provided {@link BasicCrawlerOptions.requestHandler|`requestHandler`}
|
|
381
|
-
* for each {@link Request} object, which represents a single URL to crawl.
|
|
382
|
-
* The {@link Request} objects are fed from the {@link IRequestManager|request manager} provided via the
|
|
383
|
-
* {@link BasicCrawlerOptions.requestManager|`requestManager`} constructor option (a {@link RequestQueue} is
|
|
384
|
-
* itself a request manager). If no `requestManager` is provided, the crawler opens the default {@link RequestQueue}
|
|
385
|
-
* either when the {@link BasicCrawler.addRequests|`crawler.addRequests()`} function is called, or if the `requests`
|
|
386
|
-
* parameter (representing the initial requests) of the {@link BasicCrawler.run|`crawler.run()`} function is provided.
|
|
387
|
-
*
|
|
388
|
-
* To read requests from a read-only source such as a {@link RequestList} or {@link SitemapRequestLoader} while
|
|
389
|
-
* still being able to enqueue new ones, combine the loader with a queue into a {@link RequestManagerTandem} using
|
|
390
|
-
* {@link IRequestLoader.toTandem|`requestLoader.toTandem()`} and pass the result as `requestManager`. The tandem
|
|
391
|
-
* first processes URLs from the loader and automatically enqueues them into the queue, ensuring a single URL is not
|
|
392
|
-
* crawled multiple times.
|
|
393
|
-
*
|
|
394
|
-
* > The legacy {@link BasicCrawlerOptions.requestList|`requestList`} and
|
|
395
|
-
* > {@link BasicCrawlerOptions.requestQueue|`requestQueue`} options are deprecated. They are still accepted and
|
|
396
|
-
* > folded into a single `requestManager` (combined into a tandem when both are given), but new code should use
|
|
397
|
-
* > `requestManager` directly.
|
|
398
|
-
*
|
|
399
|
-
* The crawler finishes if there are no more {@link Request} objects to crawl.
|
|
400
|
-
*
|
|
401
|
-
* New requests are only dispatched when there is enough free CPU and memory available, as judged by the crawler's
|
|
402
|
-
* {@link ConcurrencySystem}.
|
|
403
|
-
* Concurrency is tuned via the {@link BasicCrawlerOptions.minConcurrency|`minConcurrency`},
|
|
404
|
-
* {@link BasicCrawlerOptions.maxConcurrency|`maxConcurrency`} and
|
|
405
|
-
* {@link BasicCrawlerOptions.maxRequestsPerMinute|`maxRequestsPerMinute`} shortcuts, or, for finer control, by
|
|
406
|
-
* injecting a pre-configured {@link BasicCrawlerOptions.concurrencySystem|`concurrencySystem`}.
|
|
407
|
-
*
|
|
408
|
-
* **Example usage:**
|
|
409
|
-
*
|
|
410
|
-
* ```javascript
|
|
411
|
-
* import { BasicCrawler, Dataset } from 'crawlee';
|
|
412
|
-
*
|
|
413
|
-
* // Create a crawler instance
|
|
414
|
-
* const crawler = new BasicCrawler({
|
|
415
|
-
* async requestHandler({ request, sendRequest }) {
|
|
416
|
-
* // 'request' contains an instance of the Request class
|
|
417
|
-
* // Here we simply fetch the HTML of the page and store it to a dataset
|
|
418
|
-
* const { body } = await sendRequest({
|
|
419
|
-
* url: request.url,
|
|
420
|
-
* method: request.method,
|
|
421
|
-
* body: request.payload,
|
|
422
|
-
* headers: request.headers,
|
|
423
|
-
* });
|
|
424
|
-
*
|
|
425
|
-
* await Dataset.pushData({
|
|
426
|
-
* url: request.url,
|
|
427
|
-
* html: body,
|
|
428
|
-
* })
|
|
429
|
-
* },
|
|
430
|
-
* });
|
|
431
|
-
*
|
|
432
|
-
* // Enqueue the initial requests and run the crawler
|
|
433
|
-
* await crawler.run([
|
|
434
|
-
* 'http://www.example.com/page-1',
|
|
435
|
-
* 'http://www.example.com/page-2',
|
|
436
|
-
* ]);
|
|
437
|
-
* ```
|
|
438
|
-
* @category Crawlers
|
|
439
|
-
*/
|
|
440
|
-
/**
|
|
441
|
-
* Identifies a crawler instance for storage aliasing, `useState()` and status-message events.
|
|
442
|
-
*/
|
|
443
|
-
interface CrawlerIdentity {
|
|
444
|
-
/**
|
|
445
|
-
* 0-based instantiation order across all crawlers in the process.
|
|
446
|
-
* Note that the value can be subject to race conditions between different script invocations.
|
|
447
|
-
*/
|
|
448
|
-
readonly instanceIndex: number;
|
|
449
|
-
/** The user-supplied `id` option, or a fallback derived from `instanceIndex`. */
|
|
450
|
-
readonly id: string;
|
|
451
|
-
/** Whether `id` came from the user (as opposed to being derived from `instanceIndex`). */
|
|
452
|
-
readonly hasExplicitId: boolean;
|
|
453
|
-
}
|
|
454
370
|
export declare class BasicCrawler<Context extends CrawlingContext = CrawlingContext, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, StatisticStateExtension extends object = {}> {
|
|
455
371
|
#private;
|
|
456
|
-
protected static readonly CRAWLEE_STATE_KEY = "CRAWLEE_STATE";
|
|
457
372
|
/**
|
|
458
373
|
* Tracks the number of crawler instances created. The first crawler uses the default
|
|
459
374
|
* request queue; subsequent ones get their own queue via a unique alias so they don't
|
|
@@ -515,13 +430,8 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
515
430
|
hasFinishedBefore: boolean;
|
|
516
431
|
get log(): CrawleeLogger;
|
|
517
432
|
protected readonly requestHandler: RequestHandler<ExtendedContext>;
|
|
518
|
-
protected readonly errorHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
|
|
519
|
-
protected readonly failedRequestHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
|
|
520
433
|
private requestHandlerTimeoutMillis;
|
|
521
434
|
protected readonly internalTimeoutMillis: number;
|
|
522
|
-
protected readonly maxRequestRetries: number;
|
|
523
|
-
protected readonly maxCrawlDepth?: number;
|
|
524
|
-
protected readonly maxRequestsPerCrawl?: number;
|
|
525
435
|
private get handledRequestsCount();
|
|
526
436
|
protected blockedStatusCodes: Set<number>;
|
|
527
437
|
protected readonly additionalHttpErrorStatusCodes: Set<number>;
|
|
@@ -534,8 +444,9 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
534
444
|
private taskLoopOptions;
|
|
535
445
|
protected readonly httpClient: BaseHttpClient;
|
|
536
446
|
protected readonly retryOnBlocked: boolean;
|
|
537
|
-
|
|
538
|
-
|
|
447
|
+
/**
|
|
448
|
+
* @internal
|
|
449
|
+
*/
|
|
539
450
|
protected static optionsShape: {
|
|
540
451
|
contextPipelineBuilder: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
541
452
|
extendContext: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
@@ -580,50 +491,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
580
491
|
statistics: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
581
492
|
id: z.ZodOptional<z.ZodString>;
|
|
582
493
|
};
|
|
583
|
-
protected static optionsSchema: z.ZodObject<{
|
|
584
|
-
contextPipelineBuilder: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
585
|
-
extendContext: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
586
|
-
requestList: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
587
|
-
requestQueue: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
588
|
-
requestManager: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
589
|
-
requestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
590
|
-
requestHandlerTimeoutSecs: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
591
|
-
errorHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
592
|
-
failedRequestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
593
|
-
maxRequestRetries: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
594
|
-
sameDomainDelaySecs: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
595
|
-
maxRequestsPerCrawl: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
596
|
-
maxCrawlDepth: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
597
|
-
taskLoopOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
598
|
-
concurrencySystem: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
599
|
-
sessionPool: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
600
|
-
proxyConfiguration: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
601
|
-
statusMessageLoggingInterval: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
602
|
-
statusMessageCallback: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
603
|
-
additionalHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
604
|
-
ignoreHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
605
|
-
blockedStatusCodes: z.ZodOptional<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
606
|
-
retryOnBlocked: z.ZodDefault<z.ZodBoolean>;
|
|
607
|
-
respectRobotsTxtFile: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCustom<Dictionary, Dictionary>]>>;
|
|
608
|
-
transactionalStorage: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
609
|
-
requestQueue: z.ZodOptional<z.ZodEnum<{
|
|
610
|
-
deferred: "deferred";
|
|
611
|
-
writeThrough: "writeThrough";
|
|
612
|
-
}>>;
|
|
613
|
-
}, z.core.$strict>]>>;
|
|
614
|
-
onSkippedRequest: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
615
|
-
httpClient: z.ZodOptional<z.ZodCustom<BaseHttpClient, BaseHttpClient>>;
|
|
616
|
-
configuration: z.ZodOptional<z.ZodCustom<Configuration, Configuration>>;
|
|
617
|
-
storageBackend: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
618
|
-
eventManager: z.ZodOptional<z.ZodCustom<EventManager, EventManager>>;
|
|
619
|
-
logger: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
620
|
-
minConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
621
|
-
maxConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
622
|
-
maxRequestsPerMinute: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
623
|
-
keepAlive: z.ZodOptional<z.ZodBoolean>;
|
|
624
|
-
statistics: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
625
|
-
id: z.ZodOptional<z.ZodString>;
|
|
626
|
-
}, z.core.$strict>;
|
|
627
494
|
/**
|
|
628
495
|
* All `BasicCrawler` parameters are passed via an options object.
|
|
629
496
|
*/
|
|
@@ -745,9 +612,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
745
612
|
*/
|
|
746
613
|
private validateRequestUserData;
|
|
747
614
|
useState<State extends Dictionary = Dictionary>(defaultValue?: State): Promise<State>;
|
|
748
|
-
protected getPendingRequestCountApproximation(): Promise<number>;
|
|
749
|
-
protected calculateEnqueuedRequestLimit(explicitLimit?: number): Promise<number | undefined>;
|
|
750
|
-
protected handleSkippedRequest(options: Parameters<SkippedRequestCallback>[0]): Promise<void>;
|
|
751
615
|
private logOncePerRun;
|
|
752
616
|
/**
|
|
753
617
|
* Adds requests to the queue in batches. By default, it will resolve after the initial batch is added, and continue
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { applyRequestTransform, AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, buildEnqueueStrategyPatterns, ConcurrencySystem, Configuration, constructUrlPatternObjects, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createRequestOptions, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, filterRequestOptionsByPatterns, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected,
|
|
3
|
+
import { applyRequestTransform, AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, buildEnqueueStrategyPatterns, ConcurrencySystem, Configuration, constructUrlPatternObjects, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createRequestOptions, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, filterRequestOptionsByPatterns, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, supportsDomainThrottling, Router, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, ThrottlingRequestManager, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
|
|
4
4
|
import { BaseHttpClient, FetchHttpClient } from '@crawlee/http-client';
|
|
5
|
-
import { isAsyncIterable, isIterable, ROTATE_PROXY_ERRORS } from '@crawlee/utils/internal';
|
|
5
|
+
import { isAsyncIterable, isIterable, parseArgument, ROTATE_PROXY_ERRORS, schemas } from '@crawlee/utils/internal';
|
|
6
6
|
import { RobotsTxtFile } from '@crawlee/utils';
|
|
7
7
|
import { getDomain } from 'tldts';
|
|
8
8
|
import { z } from 'zod';
|
|
@@ -71,7 +71,7 @@ const addRequestsOptionsSchema = z.looseObject({
|
|
|
71
71
|
onSkippedRequest: schemas.anyFunction.optional(),
|
|
72
72
|
});
|
|
73
73
|
export class BasicCrawler {
|
|
74
|
-
static CRAWLEE_STATE_KEY = 'CRAWLEE_STATE';
|
|
74
|
+
static #CRAWLEE_STATE_KEY = 'CRAWLEE_STATE';
|
|
75
75
|
/**
|
|
76
76
|
* Tracks the number of crawler instances created. The first crawler uses the default
|
|
77
77
|
* request queue; subsequent ones get their own queue via a unique alias so they don't
|
|
@@ -191,15 +191,15 @@ export class BasicCrawler {
|
|
|
191
191
|
return this.#log;
|
|
192
192
|
}
|
|
193
193
|
requestHandler;
|
|
194
|
-
errorHandler;
|
|
195
|
-
failedRequestHandler;
|
|
194
|
+
#errorHandler;
|
|
195
|
+
#failedRequestHandler;
|
|
196
196
|
// kept as TS-private: tests read it at runtime
|
|
197
197
|
requestHandlerTimeoutMillis;
|
|
198
198
|
internalTimeoutMillis;
|
|
199
|
-
maxRequestRetries;
|
|
200
|
-
maxCrawlDepth;
|
|
199
|
+
#maxRequestRetries;
|
|
200
|
+
#maxCrawlDepth;
|
|
201
201
|
#sameDomainDelaySecs;
|
|
202
|
-
maxRequestsPerCrawl;
|
|
202
|
+
#maxRequestsPerCrawl;
|
|
203
203
|
get handledRequestsCount() {
|
|
204
204
|
return this.statistics.state.requestsFinished + this.statistics.state.requestsFailed;
|
|
205
205
|
}
|
|
@@ -223,12 +223,15 @@ export class BasicCrawler {
|
|
|
223
223
|
#transactionalStorageEnabled;
|
|
224
224
|
/** The resolved per-storage-type write policy overrides forwarded to each request's transaction. */
|
|
225
225
|
#storageWritePolicy;
|
|
226
|
-
onSkippedRequest;
|
|
226
|
+
#onSkippedRequest;
|
|
227
227
|
#closeEvents;
|
|
228
228
|
#loggedPerRun = new Set();
|
|
229
229
|
#robotsTxtFileCache;
|
|
230
|
-
identity;
|
|
230
|
+
#identity;
|
|
231
231
|
#contextPipelineOptions;
|
|
232
|
+
/**
|
|
233
|
+
* @internal
|
|
234
|
+
*/
|
|
232
235
|
static optionsShape = {
|
|
233
236
|
contextPipelineBuilder: schemas.anyObject.optional(),
|
|
234
237
|
extendContext: schemas.anyFunction.optional(),
|
|
@@ -278,12 +281,12 @@ export class BasicCrawler {
|
|
|
278
281
|
statistics: schemas.anyObject.optional(),
|
|
279
282
|
id: z.string().optional(),
|
|
280
283
|
};
|
|
281
|
-
static optionsSchema = z.strictObject(BasicCrawler.optionsShape);
|
|
284
|
+
static #optionsSchema = z.strictObject(BasicCrawler.optionsShape);
|
|
282
285
|
/**
|
|
283
286
|
* All `BasicCrawler` parameters are passed via an options object.
|
|
284
287
|
*/
|
|
285
288
|
constructor(options = {}) {
|
|
286
|
-
const parsedOptions = parseArgument(options, BasicCrawler
|
|
289
|
+
const parsedOptions = parseArgument(options, BasicCrawler.#optionsSchema, 'BasicCrawlerOptions');
|
|
287
290
|
const {
|
|
288
291
|
// oxlint-disable-next-line typescript/no-deprecated -- still accepted and folded into `requestManager` for back-compat
|
|
289
292
|
requestList,
|
|
@@ -329,7 +332,7 @@ export class BasicCrawler {
|
|
|
329
332
|
// Initialize the Configuration instance to avoid lazy loading in the components
|
|
330
333
|
serviceLocator.getConfiguration();
|
|
331
334
|
const instanceIndex = BasicCrawler.instanceCount++;
|
|
332
|
-
this
|
|
335
|
+
this.#identity = { instanceIndex, hasExplicitId: id !== undefined, id: id ?? String(instanceIndex) };
|
|
333
336
|
if (requestManager !== undefined) {
|
|
334
337
|
if (requestList !== undefined || requestQueue !== undefined) {
|
|
335
338
|
throw new Error('The `requestManager` option cannot be used in conjunction with `requestList` and/or `requestQueue`');
|
|
@@ -361,12 +364,11 @@ export class BasicCrawler {
|
|
|
361
364
|
this.#statusMessageLoggingInterval = statusMessageLoggingInterval;
|
|
362
365
|
this.#statusMessageCallback = statusMessageCallback;
|
|
363
366
|
this.#robotsTxtFileCache = new LruCache({ maxLength: 1000 });
|
|
364
|
-
this.handleSkippedRequest = this.handleSkippedRequest.bind(this);
|
|
365
367
|
this.additionalHttpErrorStatusCodes = new Set([...additionalHttpErrorStatusCodes]);
|
|
366
368
|
this.#ignoreHttpErrorStatusCodes = new Set([...ignoreHttpErrorStatusCodes]);
|
|
367
369
|
this.requestHandler = requestHandler ?? this.router;
|
|
368
|
-
this
|
|
369
|
-
this
|
|
370
|
+
this.#failedRequestHandler = failedRequestHandler;
|
|
371
|
+
this.#errorHandler = errorHandler;
|
|
370
372
|
if (requestHandlerTimeoutSecs) {
|
|
371
373
|
this.requestHandlerTimeoutMillis = requestHandlerTimeoutSecs * 1000;
|
|
372
374
|
}
|
|
@@ -379,13 +381,13 @@ export class BasicCrawler {
|
|
|
379
381
|
const transactionalStorageOption = transactionalStorage;
|
|
380
382
|
this.#transactionalStorageEnabled = transactionalStorageOption !== false;
|
|
381
383
|
this.#storageWritePolicy = typeof transactionalStorageOption === 'object' ? transactionalStorageOption : {};
|
|
382
|
-
this
|
|
384
|
+
this.#onSkippedRequest = onSkippedRequest;
|
|
383
385
|
// allow at least 5min for internal timeouts
|
|
384
386
|
this.internalTimeoutMillis =
|
|
385
387
|
serviceLocator.getConfiguration().internalTimeoutMillis ??
|
|
386
388
|
Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
|
|
387
|
-
this
|
|
388
|
-
this
|
|
389
|
+
this.#maxRequestRetries = maxRequestRetries;
|
|
390
|
+
this.#maxCrawlDepth = maxCrawlDepth;
|
|
389
391
|
this.#sameDomainDelaySecs = sameDomainDelaySecs;
|
|
390
392
|
this.#statisticsDep = OwnedOrInjected.resolve(statistics,
|
|
391
393
|
// A crawler-built default tracks the built-in fields only. A non-empty `StatisticStateExtension` can
|
|
@@ -394,7 +396,7 @@ export class BasicCrawler {
|
|
|
394
396
|
() => new Statistics({
|
|
395
397
|
logMessage: `${this.constructor.name} request statistics:`,
|
|
396
398
|
log: this.log,
|
|
397
|
-
id: this
|
|
399
|
+
id: this.#identity.id,
|
|
398
400
|
}));
|
|
399
401
|
if (sessionPool && proxyConfiguration) {
|
|
400
402
|
this.log.warning('Both `sessionPool` and `proxyConfiguration` were provided to the crawler. ' +
|
|
@@ -416,8 +418,8 @@ export class BasicCrawler {
|
|
|
416
418
|
this.requestHandlerTimeoutMillis = maxSignedInteger;
|
|
417
419
|
}
|
|
418
420
|
this.internalTimeoutMillis = Math.min(this.internalTimeoutMillis, maxSignedInteger);
|
|
419
|
-
this
|
|
420
|
-
const isMaxPagesExceeded = () => this
|
|
421
|
+
this.#maxRequestsPerCrawl = maxRequestsPerCrawl;
|
|
422
|
+
const isMaxPagesExceeded = () => this.#maxRequestsPerCrawl && this.#maxRequestsPerCrawl <= this.handledRequestsCount;
|
|
421
423
|
// eslint-disable-next-line prefer-const
|
|
422
424
|
let { isFinishedFunction, isTaskReadyFunction } = taskLoopOptions;
|
|
423
425
|
// override even if `isFinishedFunction` provided by user - `keepAlive` has higher priority
|
|
@@ -484,7 +486,7 @@ export class BasicCrawler {
|
|
|
484
486
|
isTaskReadyFunction: async () => {
|
|
485
487
|
if (isMaxPagesExceeded()) {
|
|
486
488
|
this.logOncePerRun('shuttingDown', 'Crawler reached the maxRequestsPerCrawl limit of ' +
|
|
487
|
-
`${this
|
|
489
|
+
`${this.#maxRequestsPerCrawl} requests and will shut down soon. Requests that are in progress will be allowed to finish.`);
|
|
488
490
|
return false;
|
|
489
491
|
}
|
|
490
492
|
if (this.#unexpectedStop) {
|
|
@@ -496,7 +498,7 @@ export class BasicCrawler {
|
|
|
496
498
|
},
|
|
497
499
|
isFinishedFunction: async () => {
|
|
498
500
|
if (isMaxPagesExceeded()) {
|
|
499
|
-
this.log.info(`Earlier, the crawler reached the maxRequestsPerCrawl limit of ${this
|
|
501
|
+
this.log.info(`Earlier, the crawler reached the maxRequestsPerCrawl limit of ${this.#maxRequestsPerCrawl} requests ` +
|
|
500
502
|
'and all requests that were in progress at that time have now finished. ' +
|
|
501
503
|
`In total, the crawler processed ${this.handledRequestsCount} requests and will shut down.`);
|
|
502
504
|
return true;
|
|
@@ -573,7 +575,7 @@ export class BasicCrawler {
|
|
|
573
575
|
this.log.warning(`Skipping request ${request.url} (${request.id}) because it is disallowed based on robots.txt`);
|
|
574
576
|
request.state = RequestState.SKIPPED;
|
|
575
577
|
request.noRetry = true;
|
|
576
|
-
await this
|
|
578
|
+
await this.#handleSkippedRequest({
|
|
577
579
|
url: request.url,
|
|
578
580
|
reason: 'robotsTxt',
|
|
579
581
|
});
|
|
@@ -673,7 +675,7 @@ export class BasicCrawler {
|
|
|
673
675
|
this.log.debug(message);
|
|
674
676
|
request.noRetry = true;
|
|
675
677
|
request.state = RequestState.SKIPPED;
|
|
676
|
-
await this
|
|
678
|
+
await this.#handleSkippedRequest({ url: request.url, reason: 'redirect' });
|
|
677
679
|
throw new ContextPipelineInterruptedError(message);
|
|
678
680
|
}
|
|
679
681
|
return context;
|
|
@@ -707,7 +709,7 @@ export class BasicCrawler {
|
|
|
707
709
|
// Setting the status message is not a storage concern, so we intentionally don't route it
|
|
708
710
|
// through the storage client anymore.
|
|
709
711
|
serviceLocator.getEventManager().emit(EventType.STATUS_MESSAGE, {
|
|
710
|
-
crawlerId: this
|
|
712
|
+
crawlerId: this.#identity.id,
|
|
711
713
|
message,
|
|
712
714
|
isStatusMessageTerminal: options.isStatusMessageTerminal,
|
|
713
715
|
level: options.level,
|
|
@@ -930,7 +932,7 @@ export class BasicCrawler {
|
|
|
930
932
|
minCrawlDelaySecs: this.#sameDomainDelaySecs,
|
|
931
933
|
// What `sameDomainDelaySecs` has always meant: one clock for a site, subdomains included.
|
|
932
934
|
throttleBy: 'registrableDomain',
|
|
933
|
-
persistStateKey: `CRAWLEE_THROTTLED_DOMAINS_${this
|
|
935
|
+
persistStateKey: `CRAWLEE_THROTTLED_DOMAINS_${this.#identity.id}`,
|
|
934
936
|
});
|
|
935
937
|
}
|
|
936
938
|
// Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
|
|
@@ -957,7 +959,7 @@ export class BasicCrawler {
|
|
|
957
959
|
async openOwnedRequestQueue() {
|
|
958
960
|
// The first crawler instance uses the default queue (null identifier);
|
|
959
961
|
// subsequent instances get their own queue via a unique alias so they don't collide.
|
|
960
|
-
const identifier = this
|
|
962
|
+
const identifier = this.#identity.instanceIndex === 0 ? null : { alias: `__default_${this.#identity.id}__` };
|
|
961
963
|
const requestQueue = await RequestQueue.open(identifier, { configuration: serviceLocator.getConfiguration() });
|
|
962
964
|
return this.#ownedRequestQueue.set(requestQueue);
|
|
963
965
|
}
|
|
@@ -1004,11 +1006,11 @@ export class BasicCrawler {
|
|
|
1004
1006
|
}
|
|
1005
1007
|
async useState(defaultValue = {}) {
|
|
1006
1008
|
const kvs = await KeyValueStore.open(null, { configuration: serviceLocator.getConfiguration() });
|
|
1007
|
-
if (this
|
|
1008
|
-
const stateKey = `${BasicCrawler
|
|
1009
|
+
if (this.#identity.hasExplicitId) {
|
|
1010
|
+
const stateKey = `${BasicCrawler.#CRAWLEE_STATE_KEY}_${this.#identity.id}`;
|
|
1009
1011
|
return kvs.getAutoSavedValue(stateKey, defaultValue);
|
|
1010
1012
|
}
|
|
1011
|
-
BasicCrawler.#useStateAnonymousIndices.add(this
|
|
1013
|
+
BasicCrawler.#useStateAnonymousIndices.add(this.#identity.instanceIndex);
|
|
1012
1014
|
if (BasicCrawler.#useStateAnonymousIndices.size > 1) {
|
|
1013
1015
|
serviceLocator
|
|
1014
1016
|
.getLogger()
|
|
@@ -1017,30 +1019,30 @@ export class BasicCrawler {
|
|
|
1017
1019
|
'To fix this, provide a unique `id` option to each crawler instance. \n' +
|
|
1018
1020
|
'Example: new BasicCrawler({ id: "my-crawler-1", ... })');
|
|
1019
1021
|
}
|
|
1020
|
-
return kvs.getAutoSavedValue(BasicCrawler
|
|
1022
|
+
return kvs.getAutoSavedValue(BasicCrawler.#CRAWLEE_STATE_KEY, defaultValue);
|
|
1021
1023
|
}
|
|
1022
|
-
async getPendingRequestCountApproximation() {
|
|
1024
|
+
async #getPendingRequestCountApproximation() {
|
|
1023
1025
|
return (await this.requestManager?.getPendingCount()) ?? 0;
|
|
1024
1026
|
}
|
|
1025
|
-
async calculateEnqueuedRequestLimit(explicitLimit) {
|
|
1026
|
-
if (this
|
|
1027
|
+
async #calculateEnqueuedRequestLimit(explicitLimit) {
|
|
1028
|
+
if (this.#maxRequestsPerCrawl === undefined) {
|
|
1027
1029
|
return explicitLimit;
|
|
1028
1030
|
}
|
|
1029
|
-
const limit = Math.max(0, this
|
|
1031
|
+
const limit = Math.max(0, this.#maxRequestsPerCrawl - this.handledRequestsCount - (await this.#getPendingRequestCountApproximation()));
|
|
1030
1032
|
return Math.min(limit, explicitLimit ?? Infinity);
|
|
1031
1033
|
}
|
|
1032
|
-
async handleSkippedRequest(options) {
|
|
1034
|
+
async #handleSkippedRequest(options) {
|
|
1033
1035
|
// A skipped request is a *successful* outcome, but the interrupt still unwinds through the
|
|
1034
1036
|
// transaction scope, which rolls back - so the skip bookkeeping must write directly.
|
|
1035
1037
|
await withDirectStorageAccess(async () => {
|
|
1036
1038
|
if (options.reason === 'limit') {
|
|
1037
1039
|
this.logOncePerRun('maxRequestsPerCrawl', 'The number of requests enqueued by the crawler reached the maxRequestsPerCrawl limit of ' +
|
|
1038
|
-
`${this
|
|
1040
|
+
`${this.#maxRequestsPerCrawl} requests and no further requests will be added.`);
|
|
1039
1041
|
}
|
|
1040
1042
|
if (options.reason === 'depth') {
|
|
1041
|
-
this.logOncePerRun('maxCrawlDepth', `The crawler reached the maxCrawlDepth limit of ${this
|
|
1043
|
+
this.logOncePerRun('maxCrawlDepth', `The crawler reached the maxCrawlDepth limit of ${this.#maxCrawlDepth} and no further requests will be enqueued.`);
|
|
1042
1044
|
}
|
|
1043
|
-
await this
|
|
1045
|
+
await this.#onSkippedRequest?.(options);
|
|
1044
1046
|
});
|
|
1045
1047
|
}
|
|
1046
1048
|
logOncePerRun(key, message, level = 'info') {
|
|
@@ -1079,7 +1081,7 @@ export class BasicCrawler {
|
|
|
1079
1081
|
if (options.label !== undefined || options.userData !== undefined) {
|
|
1080
1082
|
await this.validateRequestUserData({ label: options.label, userData: options.userData });
|
|
1081
1083
|
}
|
|
1082
|
-
const requestLimit = await this
|
|
1084
|
+
const requestLimit = await this.#calculateEnqueuedRequestLimit(options.limit);
|
|
1083
1085
|
const strategy = options.strategy ?? EnqueueStrategy.All;
|
|
1084
1086
|
const urlExcludePatternObjects = options.exclude?.length
|
|
1085
1087
|
? constructUrlPatternObjects(options.exclude)
|
|
@@ -1093,7 +1095,7 @@ export class BasicCrawler {
|
|
|
1093
1095
|
? buildEnqueueStrategyPatterns(options.baseUrl, strategy)
|
|
1094
1096
|
: [];
|
|
1095
1097
|
const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
|
|
1096
|
-
const maxCrawlDepth = this
|
|
1098
|
+
const maxCrawlDepth = this.#maxCrawlDepth;
|
|
1097
1099
|
const validateRequestUserData = this.validateRequestUserData.bind(this);
|
|
1098
1100
|
const allSkipped = [];
|
|
1099
1101
|
async function* filteredRequests() {
|
|
@@ -1155,7 +1157,7 @@ export class BasicCrawler {
|
|
|
1155
1157
|
: `Skipping requests in this call due to the remaining maxRequestsPerCrawl budget of ${requestLimit}, which is lower than the enqueueLinks limit of ${options.limit}.`);
|
|
1156
1158
|
}
|
|
1157
1159
|
await Promise.all(allSkipped.map(async ({ url, reason }) => {
|
|
1158
|
-
await this
|
|
1160
|
+
await this.#handleSkippedRequest({ url, reason });
|
|
1159
1161
|
await options.onSkippedRequest?.({ url, reason });
|
|
1160
1162
|
}));
|
|
1161
1163
|
}
|
|
@@ -1256,7 +1258,7 @@ export class BasicCrawler {
|
|
|
1256
1258
|
this.#autoscaledPool = new AutoscaledPool({
|
|
1257
1259
|
...this.taskLoopOptions,
|
|
1258
1260
|
concurrencySystem: this.#concurrencySystemDep.value,
|
|
1259
|
-
consumer: this
|
|
1261
|
+
consumer: this.#identity,
|
|
1260
1262
|
});
|
|
1261
1263
|
await this.getRequestManager();
|
|
1262
1264
|
}
|
|
@@ -1611,7 +1613,7 @@ export class BasicCrawler {
|
|
|
1611
1613
|
const shouldRetryRequest = this.canRequestBeRetried(request, error);
|
|
1612
1614
|
if (shouldRetryRequest) {
|
|
1613
1615
|
await this.statistics.errorTrackerRetry.addAsync(error, crawlingContext);
|
|
1614
|
-
await this
|
|
1616
|
+
await this.#errorHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
|
|
1615
1617
|
error);
|
|
1616
1618
|
if (error instanceof SessionError) {
|
|
1617
1619
|
crawlingContext.session?.retire();
|
|
@@ -1656,8 +1658,8 @@ export class BasicCrawler {
|
|
|
1656
1658
|
const { id, url, method, uniqueKey } = crawlingContext.request;
|
|
1657
1659
|
const message = this.getMessageFromError(error, true);
|
|
1658
1660
|
this.log.error(`Request failed and reached maximum retries. ${message}`, { id, url, method, uniqueKey });
|
|
1659
|
-
if (this
|
|
1660
|
-
await this
|
|
1661
|
+
if (this.#failedRequestHandler) {
|
|
1662
|
+
await this.#failedRequestHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
|
|
1661
1663
|
error);
|
|
1662
1664
|
}
|
|
1663
1665
|
}
|
|
@@ -1697,7 +1699,7 @@ export class BasicCrawler {
|
|
|
1697
1699
|
return true;
|
|
1698
1700
|
}
|
|
1699
1701
|
// Ensure there are more retries available for the request
|
|
1700
|
-
const maxRequestRetries = request.maxRetries ?? this
|
|
1702
|
+
const maxRequestRetries = request.maxRetries ?? this.#maxRequestRetries;
|
|
1701
1703
|
return request.retryCount < maxRequestRetries;
|
|
1702
1704
|
}
|
|
1703
1705
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/basic",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.136",
|
|
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": ">=22.0.0"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@apify/datastructures": "^2.0.0",
|
|
43
43
|
"@apify/timeout": "^0.4.4",
|
|
44
44
|
"@apify/utilities": "^2.15.5",
|
|
45
|
-
"@crawlee/core": "4.0.0-beta.
|
|
46
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
47
|
-
"@crawlee/types": "4.0.0-beta.
|
|
48
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
45
|
+
"@crawlee/core": "4.0.0-beta.136",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.136",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.136",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.136",
|
|
49
49
|
"csv-stringify": "^6.5.2",
|
|
50
50
|
"tldts": "^7.0.6",
|
|
51
51
|
"tslib": "^2.8.1",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"zod": "^4.4.3"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@crawlee/impit-client": "^4.0.0-beta.
|
|
56
|
+
"@crawlee/impit-client": "^4.0.0-beta.136"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "d425bb9841885b4b71b84ae6fb0bafbdc4a7e978"
|
|
66
66
|
}
|