@crawlee/basic 4.0.0-beta.123 → 4.0.0-beta.124
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.
|
@@ -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;
|
|
@@ -824,12 +828,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
824
828
|
* Fetches the next request to process from the underlying request provider.
|
|
825
829
|
*/
|
|
826
830
|
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
831
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
834
832
|
private handleRequest;
|
|
835
833
|
/**
|
|
@@ -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,7 +386,7 @@ export class BasicCrawler {
|
|
|
382
386
|
Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
|
|
383
387
|
this.maxRequestRetries = maxRequestRetries;
|
|
384
388
|
this.maxCrawlDepth = maxCrawlDepth;
|
|
385
|
-
this.#
|
|
389
|
+
this.#sameDomainDelaySecs = sameDomainDelaySecs;
|
|
386
390
|
this.#statsDep = OwnedOrInjected.resolve(statistics, () => new Statistics({
|
|
387
391
|
logMessage: `${this.constructor.name} request statistics:`,
|
|
388
392
|
log: this.log,
|
|
@@ -422,7 +426,7 @@ export class BasicCrawler {
|
|
|
422
426
|
if (!source)
|
|
423
427
|
throw new Error('Request provider is not initialized!');
|
|
424
428
|
const request = await this.resolveRequest();
|
|
425
|
-
if (!request
|
|
429
|
+
if (!request) {
|
|
426
430
|
return;
|
|
427
431
|
}
|
|
428
432
|
// Started here, rather than in `handleRequest`, so that a failure during context pipeline
|
|
@@ -767,8 +771,14 @@ export class BasicCrawler {
|
|
|
767
771
|
// When `purgeRequestQueue` is explicitly `false`, nothing is purged.
|
|
768
772
|
const shouldPurge = purgeRequestQueue !== false;
|
|
769
773
|
const managerToPurge = this.#ownedRequestQueue.maybeValue ?? (purgeRequestQueue === true ? this.requestManager : undefined);
|
|
770
|
-
if (
|
|
771
|
-
await managerToPurge
|
|
774
|
+
if (shouldPurge) {
|
|
775
|
+
await managerToPurge?.purge?.();
|
|
776
|
+
// The per-domain queues a `sameDomainDelaySecs` wrapper created are the crawler's own, whatever
|
|
777
|
+
// sits underneath them - so they are emptied even when the manager they wrap is spared. Purging
|
|
778
|
+
// the wrapper itself has already covered them.
|
|
779
|
+
if (this.requestManager instanceof ThrottlingRequestManager && managerToPurge !== this.requestManager) {
|
|
780
|
+
await this.requestManager.purgeDomainQueues();
|
|
781
|
+
}
|
|
772
782
|
}
|
|
773
783
|
// A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
|
|
774
784
|
await this.#statsDep.ifOwned(async (stats) => {
|
|
@@ -906,6 +916,19 @@ export class BasicCrawler {
|
|
|
906
916
|
if (!this.requestManager) {
|
|
907
917
|
this.requestManager = await this.openOwnedRequestQueue();
|
|
908
918
|
}
|
|
919
|
+
// Wrapped here rather than in the constructor, because the manager being wrapped may only be opened at
|
|
920
|
+
// this point - and because everything that enqueues goes through here first, so nothing slips past the
|
|
921
|
+
// wrapper into the queue it hides.
|
|
922
|
+
if (this.#sameDomainDelaySecs > 0 && !supportsDomainThrottling(this.requestManager)) {
|
|
923
|
+
this.requestManager = new ThrottlingRequestManager({
|
|
924
|
+
inner: this.requestManager,
|
|
925
|
+
domains: 'all',
|
|
926
|
+
minCrawlDelaySecs: this.#sameDomainDelaySecs,
|
|
927
|
+
// What `sameDomainDelaySecs` has always meant: one clock for a site, subdomains included.
|
|
928
|
+
throttleBy: 'registrableDomain',
|
|
929
|
+
persistStateKey: `CRAWLEE_THROTTLED_DOMAINS_${this.identity.id}`,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
909
932
|
// Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
|
|
910
933
|
// now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
|
|
911
934
|
// but guard so we do not re-issue it on every call.
|
|
@@ -1431,30 +1454,6 @@ export class BasicCrawler {
|
|
|
1431
1454
|
}
|
|
1432
1455
|
return this.requestManager.fetchNextRequest();
|
|
1433
1456
|
}
|
|
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
1457
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
1459
1458
|
async handleRequest(crawlingContext, requestSource, request) {
|
|
1460
1459
|
// 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.124",
|
|
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.124",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.124",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.124",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.124",
|
|
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.124"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "0694ee1b94c755b98141671baa93cc363f2bf8e3"
|
|
66
66
|
}
|