@crawlee/basic 4.0.0-beta.123 → 4.0.0-beta.125
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/internals/basic-crawler.d.ts +30 -14
- package/internals/basic-crawler.js +36 -33
- package/package.json +7 -7
|
@@ -29,7 +29,7 @@ export type StatusMessageCallback<Context extends CrawlingContext = BasicCrawlin
|
|
|
29
29
|
export type RequireContextPipeline<DefaultContextType extends CrawlingContext, FinalContextType extends DefaultContextType> = DefaultContextType extends FinalContextType ? {} : {
|
|
30
30
|
contextPipelineBuilder: () => ContextPipeline<CrawlingContext, FinalContextType>;
|
|
31
31
|
};
|
|
32
|
-
export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingContext, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']
|
|
32
|
+
export interface BasicCrawlerOptions<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 = {}> {
|
|
33
33
|
/**
|
|
34
34
|
* User-provided function that performs the logic of the crawler. It is called for each URL to crawl.
|
|
35
35
|
*
|
|
@@ -137,7 +137,11 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
137
137
|
*/
|
|
138
138
|
maxRequestRetries?: number;
|
|
139
139
|
/**
|
|
140
|
-
* Indicates how much time (in seconds) to wait before crawling another same domain request.
|
|
140
|
+
* Indicates how much time (in seconds) to wait before crawling another same domain request. Subdomains are
|
|
141
|
+
* paced together with the site they belong to.
|
|
142
|
+
*
|
|
143
|
+
* Wraps the crawler's request manager in a {@link ThrottlingRequestManager}; pass one as `requestManager`
|
|
144
|
+
* yourself to configure it further.
|
|
141
145
|
* @default 0
|
|
142
146
|
*/
|
|
143
147
|
sameDomainDelaySecs?: number;
|
|
@@ -274,10 +278,28 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
274
278
|
onSkippedRequest?: SkippedRequestCallback;
|
|
275
279
|
/**
|
|
276
280
|
* A preconfigured statistics instance. When provided, the crawler records into it instead of building its own and
|
|
277
|
-
* will not `reset()` it between `run()` calls. Accepts the built-in {@link Statistics}
|
|
278
|
-
*
|
|
281
|
+
* will not `reset()` it between `run()` calls. Accepts the built-in {@link Statistics} or any object
|
|
282
|
+
* implementing {@link IStatistics}.
|
|
283
|
+
*
|
|
284
|
+
* Custom fields declared via {@link StatisticsOptions.stateExtension|`stateExtension`} are carried over to
|
|
285
|
+
* {@link BasicCrawler.stats|`crawler.stats.state`}:
|
|
286
|
+
*
|
|
287
|
+
* ```ts
|
|
288
|
+
* const statistics = new Statistics({ stateExtension: { defaultState: { productsFound: 0 } } });
|
|
289
|
+
*
|
|
290
|
+
* const crawler = new BasicCrawler({
|
|
291
|
+
* statistics,
|
|
292
|
+
* requestHandler: async () => {
|
|
293
|
+
* statistics.state.productsFound++;
|
|
294
|
+
* },
|
|
295
|
+
* });
|
|
296
|
+
*
|
|
297
|
+
* await crawler.run();
|
|
298
|
+
* // the custom fields are typed on `crawler.stats` too
|
|
299
|
+
* console.log(crawler.stats.state.productsFound);
|
|
300
|
+
* ```
|
|
279
301
|
*/
|
|
280
|
-
statistics?: IStatistics
|
|
302
|
+
statistics?: IStatistics<StatisticStateExtension>;
|
|
281
303
|
/**
|
|
282
304
|
* HTTP client implementation for the `sendRequest` context helper and for plain HTTP crawling.
|
|
283
305
|
* Defaults to {@link ImpitHttpClient} when `@crawlee/impit-client` is installed, otherwise {@link FetchHttpClient}.
|
|
@@ -429,7 +451,7 @@ interface CrawlerIdentity {
|
|
|
429
451
|
/** Whether `id` came from the user (as opposed to being derived from `instanceIndex`). */
|
|
430
452
|
readonly hasExplicitId: boolean;
|
|
431
453
|
}
|
|
432
|
-
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']
|
|
454
|
+
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 = {}> {
|
|
433
455
|
#private;
|
|
434
456
|
protected static readonly CRAWLEE_STATE_KEY = "CRAWLEE_STATE";
|
|
435
457
|
/**
|
|
@@ -442,7 +464,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
442
464
|
* The statistics instance collecting the crawler's run statistics - either the injected `statistics` option or a
|
|
443
465
|
* crawler-built default. Typed as {@link IStatistics} so custom implementations can be plugged in.
|
|
444
466
|
*/
|
|
445
|
-
get stats(): IStatistics
|
|
467
|
+
get stats(): IStatistics<StatisticStateExtension>;
|
|
446
468
|
/**
|
|
447
469
|
* The main request-handling component of the crawler. It manages the requests that the crawler processes,
|
|
448
470
|
* combining any provided request loader and/or queue. It's initialized during the crawler startup or lazily
|
|
@@ -605,7 +627,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
605
627
|
/**
|
|
606
628
|
* All `BasicCrawler` parameters are passed via an options object.
|
|
607
629
|
*/
|
|
608
|
-
constructor(options?: BasicCrawlerOptions<Context, ContextExtension, ExtendedContext, Routes> & RequireContextPipeline<CrawlingContext, Context>);
|
|
630
|
+
constructor(options?: BasicCrawlerOptions<Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> & RequireContextPipeline<CrawlingContext, Context>);
|
|
609
631
|
/**
|
|
610
632
|
* Builds the crawler-owned default {@link ConcurrencySystem} from the resolved
|
|
611
633
|
* `minConcurrency`/`maxConcurrency`/`maxRequestsPerMinute` shortcuts. Not called when a
|
|
@@ -824,12 +846,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
824
846
|
* Fetches the next request to process from the underlying request provider.
|
|
825
847
|
*/
|
|
826
848
|
private fetchNextRequest;
|
|
827
|
-
/**
|
|
828
|
-
* Delays processing of the request based on the `sameDomainDelaySecs` option,
|
|
829
|
-
* adding it back to the queue after the timeout passes. Returns `true` if the request
|
|
830
|
-
* should be ignored and will be reclaimed to the queue once ready.
|
|
831
|
-
*/
|
|
832
|
-
private delayRequest;
|
|
833
849
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
834
850
|
private handleRequest;
|
|
835
851
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
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, parseArgument, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, supportsDomainThrottling, Router, schemas, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
|
|
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, parseArgument, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, supportsDomainThrottling, Router, schemas, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, ThrottlingRequestManager, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
|
|
4
4
|
import { BaseHttpClient, FetchHttpClient } from '@crawlee/http-client';
|
|
5
5
|
import { isAsyncIterable, isIterable, ROTATE_PROXY_ERRORS } from '@crawlee/utils/internal';
|
|
6
6
|
import { RobotsTxtFile } from '@crawlee/utils';
|
|
@@ -198,8 +198,7 @@ export class BasicCrawler {
|
|
|
198
198
|
internalTimeoutMillis;
|
|
199
199
|
maxRequestRetries;
|
|
200
200
|
maxCrawlDepth;
|
|
201
|
-
#
|
|
202
|
-
#domainAccessedTime;
|
|
201
|
+
#sameDomainDelaySecs;
|
|
203
202
|
maxRequestsPerCrawl;
|
|
204
203
|
get handledRequestsCount() {
|
|
205
204
|
return this.stats.state.requestsFinished + this.stats.state.requestsFailed;
|
|
@@ -335,6 +334,12 @@ export class BasicCrawler {
|
|
|
335
334
|
if (requestList !== undefined || requestQueue !== undefined) {
|
|
336
335
|
throw new Error('The `requestManager` option cannot be used in conjunction with `requestList` and/or `requestQueue`');
|
|
337
336
|
}
|
|
337
|
+
// Both would pace the same domains, from different keys and with no idea of one another.
|
|
338
|
+
if (sameDomainDelaySecs > 0 && supportsDomainThrottling(requestManager)) {
|
|
339
|
+
throw new Error('The `sameDomainDelaySecs` option cannot be combined with a `requestManager` that throttles ' +
|
|
340
|
+
'per domain on its own. Configure the delay on the manager instead, via the ' +
|
|
341
|
+
'`minCrawlDelaySecs` option of `ThrottlingRequestManager`.');
|
|
342
|
+
}
|
|
338
343
|
this.requestManager = requestManager;
|
|
339
344
|
}
|
|
340
345
|
else if (requestList !== undefined && requestQueue !== undefined) {
|
|
@@ -355,7 +360,6 @@ export class BasicCrawler {
|
|
|
355
360
|
this.proxyConfiguration = proxyConfiguration;
|
|
356
361
|
this.#statusMessageLoggingInterval = statusMessageLoggingInterval;
|
|
357
362
|
this.#statusMessageCallback = statusMessageCallback;
|
|
358
|
-
this.#domainAccessedTime = new Map();
|
|
359
363
|
this.#robotsTxtFileCache = new LruCache({ maxLength: 1000 });
|
|
360
364
|
this.handleSkippedRequest = this.handleSkippedRequest.bind(this);
|
|
361
365
|
this.additionalHttpErrorStatusCodes = new Set([...additionalHttpErrorStatusCodes]);
|
|
@@ -382,8 +386,12 @@ export class BasicCrawler {
|
|
|
382
386
|
Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
|
|
383
387
|
this.maxRequestRetries = maxRequestRetries;
|
|
384
388
|
this.maxCrawlDepth = maxCrawlDepth;
|
|
385
|
-
this.#
|
|
386
|
-
this.#statsDep = OwnedOrInjected.resolve(statistics,
|
|
389
|
+
this.#sameDomainDelaySecs = sameDomainDelaySecs;
|
|
390
|
+
this.#statsDep = OwnedOrInjected.resolve(statistics,
|
|
391
|
+
// A crawler-built default tracks the built-in fields only. A non-empty `StatisticStateExtension` can
|
|
392
|
+
// only be satisfied by an injected instance carrying the matching `state`, so this branch does
|
|
393
|
+
// not run in that case - hence the cast.
|
|
394
|
+
() => new Statistics({
|
|
387
395
|
logMessage: `${this.constructor.name} request statistics:`,
|
|
388
396
|
log: this.log,
|
|
389
397
|
id: this.identity.id,
|
|
@@ -422,7 +430,7 @@ export class BasicCrawler {
|
|
|
422
430
|
if (!source)
|
|
423
431
|
throw new Error('Request provider is not initialized!');
|
|
424
432
|
const request = await this.resolveRequest();
|
|
425
|
-
if (!request
|
|
433
|
+
if (!request) {
|
|
426
434
|
return;
|
|
427
435
|
}
|
|
428
436
|
// Started here, rather than in `handleRequest`, so that a failure during context pipeline
|
|
@@ -767,8 +775,14 @@ export class BasicCrawler {
|
|
|
767
775
|
// When `purgeRequestQueue` is explicitly `false`, nothing is purged.
|
|
768
776
|
const shouldPurge = purgeRequestQueue !== false;
|
|
769
777
|
const managerToPurge = this.#ownedRequestQueue.maybeValue ?? (purgeRequestQueue === true ? this.requestManager : undefined);
|
|
770
|
-
if (
|
|
771
|
-
await managerToPurge
|
|
778
|
+
if (shouldPurge) {
|
|
779
|
+
await managerToPurge?.purge?.();
|
|
780
|
+
// The per-domain queues a `sameDomainDelaySecs` wrapper created are the crawler's own, whatever
|
|
781
|
+
// sits underneath them - so they are emptied even when the manager they wrap is spared. Purging
|
|
782
|
+
// the wrapper itself has already covered them.
|
|
783
|
+
if (this.requestManager instanceof ThrottlingRequestManager && managerToPurge !== this.requestManager) {
|
|
784
|
+
await this.requestManager.purgeDomainQueues();
|
|
785
|
+
}
|
|
772
786
|
}
|
|
773
787
|
// A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
|
|
774
788
|
await this.#statsDep.ifOwned(async (stats) => {
|
|
@@ -906,6 +920,19 @@ export class BasicCrawler {
|
|
|
906
920
|
if (!this.requestManager) {
|
|
907
921
|
this.requestManager = await this.openOwnedRequestQueue();
|
|
908
922
|
}
|
|
923
|
+
// Wrapped here rather than in the constructor, because the manager being wrapped may only be opened at
|
|
924
|
+
// this point - and because everything that enqueues goes through here first, so nothing slips past the
|
|
925
|
+
// wrapper into the queue it hides.
|
|
926
|
+
if (this.#sameDomainDelaySecs > 0 && !supportsDomainThrottling(this.requestManager)) {
|
|
927
|
+
this.requestManager = new ThrottlingRequestManager({
|
|
928
|
+
inner: this.requestManager,
|
|
929
|
+
domains: 'all',
|
|
930
|
+
minCrawlDelaySecs: this.#sameDomainDelaySecs,
|
|
931
|
+
// What `sameDomainDelaySecs` has always meant: one clock for a site, subdomains included.
|
|
932
|
+
throttleBy: 'registrableDomain',
|
|
933
|
+
persistStateKey: `CRAWLEE_THROTTLED_DOMAINS_${this.identity.id}`,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
909
936
|
// Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
|
|
910
937
|
// now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
|
|
911
938
|
// but guard so we do not re-issue it on every call.
|
|
@@ -1431,30 +1458,6 @@ export class BasicCrawler {
|
|
|
1431
1458
|
}
|
|
1432
1459
|
return this.requestManager.fetchNextRequest();
|
|
1433
1460
|
}
|
|
1434
|
-
/**
|
|
1435
|
-
* Delays processing of the request based on the `sameDomainDelaySecs` option,
|
|
1436
|
-
* adding it back to the queue after the timeout passes. Returns `true` if the request
|
|
1437
|
-
* should be ignored and will be reclaimed to the queue once ready.
|
|
1438
|
-
*/
|
|
1439
|
-
delayRequest(request, source) {
|
|
1440
|
-
const domain = getDomain(request.url);
|
|
1441
|
-
if (!domain || !request) {
|
|
1442
|
-
return false;
|
|
1443
|
-
}
|
|
1444
|
-
const now = Date.now();
|
|
1445
|
-
const lastAccessTime = this.#domainAccessedTime.get(domain);
|
|
1446
|
-
if (!lastAccessTime || now - lastAccessTime >= this.#sameDomainDelayMillis) {
|
|
1447
|
-
this.#domainAccessedTime.set(domain, now);
|
|
1448
|
-
return false;
|
|
1449
|
-
}
|
|
1450
|
-
const delay = lastAccessTime + this.#sameDomainDelayMillis - now;
|
|
1451
|
-
this.log.debug(`Request ${request.url} (${request.id}) will be reclaimed after ${delay} milliseconds due to same domain delay`);
|
|
1452
|
-
setTimeout(async () => {
|
|
1453
|
-
this.log.debug(`Adding request ${request.url} (${request.id}) back to the queue`);
|
|
1454
|
-
await source.reclaimRequest(request, { forefront: request.userData?.__crawlee?.forefront });
|
|
1455
|
-
}, delay);
|
|
1456
|
-
return true;
|
|
1457
|
-
}
|
|
1458
1461
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
1459
1462
|
async handleRequest(crawlingContext, requestSource, request) {
|
|
1460
1463
|
// An earlier phase we cannot cancel (e.g. a slow `extendContext`) may have run past the internal timeout,
|
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.125",
|
|
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.125",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.125",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.125",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.125",
|
|
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.125"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "04a7212dd4abe4a5f94f5fc9632acb6089819c8b"
|
|
66
66
|
}
|