@crawlee/basic 4.0.0-beta.122 → 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.
- package/internals/basic-crawler.d.ts +15 -20
- package/internals/basic-crawler.js +140 -119
- package/package.json +7 -7
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, ConcurrencySystemOptions, CrawleeLogger, CrawlingContext, DatasetExportOptions,
|
|
2
|
-
import { ConcurrencySystem, Configuration, ContextPipeline, Dataset, EventManager, RequestQueue } from '@crawlee/core';
|
|
1
|
+
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, ConcurrencySystemOptions, CrawleeLogger, CrawlingContext, DatasetExportOptions, EnqueueUrlsOptions, FinalStatistics, GetUserDataFromRequest, IConcurrencySystem, IProxyConfiguration, IRequestLoader, IRequestManager, IStatistics, RequestsLike, RouterHandler, RouterRoutes, SkippedRequestCallback, Source, StatisticState, StorageIdentifier, StorageWritePolicy, TaskLoopPredicates, TypedRequestsLike } from '@crawlee/core';
|
|
2
|
+
import { ConcurrencySystem, Configuration, ContextPipeline, Request, Dataset, EventManager, RequestQueue } from '@crawlee/core';
|
|
3
3
|
import { BaseHttpClient } from '@crawlee/http-client';
|
|
4
|
-
import type { Awaitable,
|
|
4
|
+
import type { Awaitable, Dictionary, ISession, ISessionPool, ProxyInfo, SetStatusMessageOptions, StorageBackend } from '@crawlee/types';
|
|
5
5
|
import { RobotsTxtFile } from '@crawlee/utils';
|
|
6
|
-
import type { ReadonlyDeep
|
|
6
|
+
import type { ReadonlyDeep } from 'type-fest';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { TimeoutError } from '@apify/timeout';
|
|
9
9
|
export interface BasicCrawlingContext<UserData extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
|
|
@@ -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;
|
|
@@ -733,6 +737,11 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
733
737
|
* the batches via `waitBetweenBatchesMillis`. If you want to wait for all batches to be added to the queue, you can use
|
|
734
738
|
* the `waitForAllRequestsToBeAdded` promise you get in the response object.
|
|
735
739
|
*
|
|
740
|
+
* Optionally, the requests can be filtered using `include`/`exclude` glob or regexp patterns and an
|
|
741
|
+
* enqueue `strategy` (both AND-ed together, same as {@link CrawlingContext.enqueueLinks|`enqueueLinks`}),
|
|
742
|
+
* relative to `baseUrl`. Unlike `enqueueLinks`, there is no implicit "current page" to anchor the strategy
|
|
743
|
+
* to, so `strategy` defaults to {@link EnqueueStrategy.All|`all`} here.
|
|
744
|
+
*
|
|
736
745
|
* This is an alias for calling `addRequestsBatched()` on the implicit `RequestQueue` for this crawler instance.
|
|
737
746
|
*
|
|
738
747
|
* @param requests The requests to add
|
|
@@ -819,22 +828,8 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
819
828
|
* Fetches the next request to process from the underlying request provider.
|
|
820
829
|
*/
|
|
821
830
|
private fetchNextRequest;
|
|
822
|
-
/**
|
|
823
|
-
* Delays processing of the request based on the `sameDomainDelaySecs` option,
|
|
824
|
-
* adding it back to the queue after the timeout passes. Returns `true` if the request
|
|
825
|
-
* should be ignored and will be reclaimed to the queue once ready.
|
|
826
|
-
*/
|
|
827
|
-
private delayRequest;
|
|
828
831
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
829
832
|
private handleRequest;
|
|
830
|
-
/**
|
|
831
|
-
* Wrapper around the crawling context's `enqueueLinks` method:
|
|
832
|
-
* - Injects `crawlDepth` to each request being added based on the crawling context request.
|
|
833
|
-
* - Provides defaults for the `enqueueLinks` options based on the crawler configuration.
|
|
834
|
-
* - These options can be overridden by the user.
|
|
835
|
-
* @internal
|
|
836
|
-
*/
|
|
837
|
-
protected enqueueLinksWithCrawlDepth(options: SetRequired<EnqueueLinksOptions, 'urls'>, request: Request<Dictionary>, requestManager: IRequestManager): Promise<BatchAddRequestsResult>;
|
|
838
833
|
/**
|
|
839
834
|
* Generator function that yields requests injected with the given crawl depth.
|
|
840
835
|
* @internal
|
|
@@ -893,7 +888,7 @@ export interface CreateContextOptions {
|
|
|
893
888
|
session: ISession;
|
|
894
889
|
proxyInfo?: ProxyInfo;
|
|
895
890
|
}
|
|
896
|
-
export interface CrawlerAddRequestsOptions extends AddRequestsBatchedOptions {
|
|
891
|
+
export interface CrawlerAddRequestsOptions extends AddRequestsBatchedOptions, EnqueueUrlsOptions {
|
|
897
892
|
}
|
|
898
893
|
export interface CrawlerAddRequestsResult extends AddRequestsBatchedResult {
|
|
899
894
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, ConcurrencySystem, Configuration, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createStorageTransaction, CriticalError, currentStorageTransaction, Dataset,
|
|
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';
|
|
@@ -43,6 +43,33 @@ const SAFE_MIGRATION_WAIT_MILLIS = 20000;
|
|
|
43
43
|
const deferredCleanupKey = Symbol('deferredCleanup');
|
|
44
44
|
// The request timeout plumbing (the window helper, the context symbols, and the race) lives in its own module.
|
|
45
45
|
export { navigationDeadlineKey, remainingNavigationWindowMillis } from './request-timeout.js';
|
|
46
|
+
const urlPatternSchema = z.union([
|
|
47
|
+
z.string(),
|
|
48
|
+
z.instanceof(RegExp),
|
|
49
|
+
schemas.objectWithKeys(['glob']),
|
|
50
|
+
schemas.objectWithKeys(['regexp']),
|
|
51
|
+
]);
|
|
52
|
+
// `looseObject` (rather than `strictObject`) lets subclasses forward their own extraction-only options
|
|
53
|
+
// (e.g. `selector`) straight through without having to strip them out first.
|
|
54
|
+
const addRequestsOptionsSchema = z.looseObject({
|
|
55
|
+
forefront: z.boolean().optional(),
|
|
56
|
+
cache: z.boolean().optional(),
|
|
57
|
+
waitForAllRequestsToBeAdded: z.boolean().optional(),
|
|
58
|
+
batchSize: schemas.anyNumber.optional(),
|
|
59
|
+
waitBetweenBatchesMillis: schemas.anyNumber.optional(),
|
|
60
|
+
maxNewRequests: schemas.anyNumber.optional(),
|
|
61
|
+
limit: schemas.anyNumber.optional(),
|
|
62
|
+
baseUrl: z.string().optional(),
|
|
63
|
+
userData: schemas.anyObject.optional(),
|
|
64
|
+
label: z.string().optional(),
|
|
65
|
+
sessionId: z.string().optional(),
|
|
66
|
+
skipNavigation: z.boolean().optional(),
|
|
67
|
+
include: schemas.arrayOf(urlPatternSchema, 'URL patterns').min(1).optional(),
|
|
68
|
+
exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
|
|
69
|
+
transformRequestFunction: schemas.anyFunction.optional(),
|
|
70
|
+
strategy: z.enum(EnqueueStrategy).optional(),
|
|
71
|
+
onSkippedRequest: schemas.anyFunction.optional(),
|
|
72
|
+
});
|
|
46
73
|
export class BasicCrawler {
|
|
47
74
|
static CRAWLEE_STATE_KEY = 'CRAWLEE_STATE';
|
|
48
75
|
/**
|
|
@@ -171,8 +198,7 @@ export class BasicCrawler {
|
|
|
171
198
|
internalTimeoutMillis;
|
|
172
199
|
maxRequestRetries;
|
|
173
200
|
maxCrawlDepth;
|
|
174
|
-
#
|
|
175
|
-
#domainAccessedTime;
|
|
201
|
+
#sameDomainDelaySecs;
|
|
176
202
|
maxRequestsPerCrawl;
|
|
177
203
|
get handledRequestsCount() {
|
|
178
204
|
return this.stats.state.requestsFinished + this.stats.state.requestsFailed;
|
|
@@ -308,6 +334,12 @@ export class BasicCrawler {
|
|
|
308
334
|
if (requestList !== undefined || requestQueue !== undefined) {
|
|
309
335
|
throw new Error('The `requestManager` option cannot be used in conjunction with `requestList` and/or `requestQueue`');
|
|
310
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
|
+
}
|
|
311
343
|
this.requestManager = requestManager;
|
|
312
344
|
}
|
|
313
345
|
else if (requestList !== undefined && requestQueue !== undefined) {
|
|
@@ -328,7 +360,6 @@ export class BasicCrawler {
|
|
|
328
360
|
this.proxyConfiguration = proxyConfiguration;
|
|
329
361
|
this.#statusMessageLoggingInterval = statusMessageLoggingInterval;
|
|
330
362
|
this.#statusMessageCallback = statusMessageCallback;
|
|
331
|
-
this.#domainAccessedTime = new Map();
|
|
332
363
|
this.#robotsTxtFileCache = new LruCache({ maxLength: 1000 });
|
|
333
364
|
this.handleSkippedRequest = this.handleSkippedRequest.bind(this);
|
|
334
365
|
this.additionalHttpErrorStatusCodes = new Set([...additionalHttpErrorStatusCodes]);
|
|
@@ -355,7 +386,7 @@ export class BasicCrawler {
|
|
|
355
386
|
Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
|
|
356
387
|
this.maxRequestRetries = maxRequestRetries;
|
|
357
388
|
this.maxCrawlDepth = maxCrawlDepth;
|
|
358
|
-
this.#
|
|
389
|
+
this.#sameDomainDelaySecs = sameDomainDelaySecs;
|
|
359
390
|
this.#statsDep = OwnedOrInjected.resolve(statistics, () => new Statistics({
|
|
360
391
|
logMessage: `${this.constructor.name} request statistics:`,
|
|
361
392
|
log: this.log,
|
|
@@ -395,7 +426,7 @@ export class BasicCrawler {
|
|
|
395
426
|
if (!source)
|
|
396
427
|
throw new Error('Request provider is not initialized!');
|
|
397
428
|
const request = await this.resolveRequest();
|
|
398
|
-
if (!request
|
|
429
|
+
if (!request) {
|
|
399
430
|
return;
|
|
400
431
|
}
|
|
401
432
|
// Started here, rather than in `handleRequest`, so that a failure during context pipeline
|
|
@@ -598,17 +629,13 @@ export class BasicCrawler {
|
|
|
598
629
|
return { session, proxyInfo: session?.proxyInfo };
|
|
599
630
|
}
|
|
600
631
|
async createContextHelpers({ request, session }) {
|
|
601
|
-
const enqueueLinksWrapper = async (options) => {
|
|
602
|
-
const requestManager = await this.getRequestManager();
|
|
603
|
-
return await this.enqueueLinksWithCrawlDepth(options, request, requestManager);
|
|
604
|
-
};
|
|
605
632
|
const addRequests = async (requests, options = {}) => {
|
|
606
633
|
const newCrawlDepth = request.crawlDepth + 1;
|
|
607
634
|
const requestsGenerator = this.addCrawlDepthRequestGenerator(requests, newCrawlDepth);
|
|
608
|
-
await this.addRequests(requestsGenerator, options);
|
|
635
|
+
return await this.addRequests(requestsGenerator, options);
|
|
609
636
|
};
|
|
610
637
|
const sendRequest = createSendRequest(this.httpClient, request, session);
|
|
611
|
-
return {
|
|
638
|
+
return { addRequests, sendRequest };
|
|
612
639
|
}
|
|
613
640
|
buildFinalContextPipeline() {
|
|
614
641
|
const subclassPipeline = (this.#contextPipelineOptions.contextPipelineBuilder?.() ??
|
|
@@ -744,8 +771,14 @@ export class BasicCrawler {
|
|
|
744
771
|
// When `purgeRequestQueue` is explicitly `false`, nothing is purged.
|
|
745
772
|
const shouldPurge = purgeRequestQueue !== false;
|
|
746
773
|
const managerToPurge = this.#ownedRequestQueue.maybeValue ?? (purgeRequestQueue === true ? this.requestManager : undefined);
|
|
747
|
-
if (
|
|
748
|
-
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
|
+
}
|
|
749
782
|
}
|
|
750
783
|
// A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
|
|
751
784
|
await this.#statsDep.ifOwned(async (stats) => {
|
|
@@ -883,6 +916,19 @@ export class BasicCrawler {
|
|
|
883
916
|
if (!this.requestManager) {
|
|
884
917
|
this.requestManager = await this.openOwnedRequestQueue();
|
|
885
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
|
+
}
|
|
886
932
|
// Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
|
|
887
933
|
// now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
|
|
888
934
|
// but guard so we do not re-issue it on every call.
|
|
@@ -1005,6 +1051,11 @@ export class BasicCrawler {
|
|
|
1005
1051
|
* the batches via `waitBetweenBatchesMillis`. If you want to wait for all batches to be added to the queue, you can use
|
|
1006
1052
|
* the `waitForAllRequestsToBeAdded` promise you get in the response object.
|
|
1007
1053
|
*
|
|
1054
|
+
* Optionally, the requests can be filtered using `include`/`exclude` glob or regexp patterns and an
|
|
1055
|
+
* enqueue `strategy` (both AND-ed together, same as {@link CrawlingContext.enqueueLinks|`enqueueLinks`}),
|
|
1056
|
+
* relative to `baseUrl`. Unlike `enqueueLinks`, there is no implicit "current page" to anchor the strategy
|
|
1057
|
+
* to, so `strategy` defaults to {@link EnqueueStrategy.All|`all`} here.
|
|
1058
|
+
*
|
|
1008
1059
|
* This is an alias for calling `addRequestsBatched()` on the implicit `RequestQueue` for this crawler instance.
|
|
1009
1060
|
*
|
|
1010
1061
|
* @param requests The requests to add
|
|
@@ -1012,55 +1063,95 @@ export class BasicCrawler {
|
|
|
1012
1063
|
*/
|
|
1013
1064
|
async addRequests(requests, options = {}) {
|
|
1014
1065
|
await this.getRequestManager();
|
|
1015
|
-
const requestLimit = await this.calculateEnqueuedRequestLimit();
|
|
1016
|
-
const skippedBecauseOfRobots = new Set();
|
|
1017
|
-
const skippedBecauseOfMaxCrawlDepth = new Set();
|
|
1018
|
-
const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
|
|
1019
|
-
const maxCrawlDepth = this.maxCrawlDepth;
|
|
1020
|
-
const validateRequestUserData = this.validateRequestUserData.bind(this);
|
|
1021
1066
|
if (!isIterable(requests) && !isAsyncIterable(requests)) {
|
|
1022
1067
|
throw new Error(`Expected an iterable or async iterable, got ${getObjectType(requests)}`);
|
|
1023
1068
|
}
|
|
1069
|
+
parseArgument(options, addRequestsOptionsSchema, 'EnqueueUrlsOptions');
|
|
1070
|
+
// `label`/`userData` apply to every request this call produces, so a single upfront validation
|
|
1071
|
+
// against the label's schema covers them all and fails the whole call fast, rather than failing
|
|
1072
|
+
// lazily once the generator below is drained. Skipped when neither is set - each item still gets
|
|
1073
|
+
// its own per-item validation below, and validating an absent label/userData here would spuriously
|
|
1074
|
+
// check them against a registered default-route schema.
|
|
1075
|
+
if (options.label !== undefined || options.userData !== undefined) {
|
|
1076
|
+
await this.validateRequestUserData({ label: options.label, userData: options.userData });
|
|
1077
|
+
}
|
|
1078
|
+
const requestLimit = await this.calculateEnqueuedRequestLimit(options.limit);
|
|
1079
|
+
const strategy = options.strategy ?? EnqueueStrategy.All;
|
|
1080
|
+
const urlExcludePatternObjects = options.exclude?.length
|
|
1081
|
+
? constructUrlPatternObjects(options.exclude)
|
|
1082
|
+
: [];
|
|
1083
|
+
const urlPatternObjects = options.include?.length
|
|
1084
|
+
? constructUrlPatternObjects(options.include)
|
|
1085
|
+
: [];
|
|
1086
|
+
// The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
|
|
1087
|
+
// (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
|
|
1088
|
+
const enqueueStrategyPatterns = options.baseUrl
|
|
1089
|
+
? buildEnqueueStrategyPatterns(options.baseUrl, strategy)
|
|
1090
|
+
: [];
|
|
1091
|
+
const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
|
|
1092
|
+
const maxCrawlDepth = this.maxCrawlDepth;
|
|
1093
|
+
const validateRequestUserData = this.validateRequestUserData.bind(this);
|
|
1094
|
+
const allSkipped = [];
|
|
1024
1095
|
async function* filteredRequests() {
|
|
1025
1096
|
for await (const request of requests) {
|
|
1026
|
-
const
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1097
|
+
const [requestOptions] = createRequestOptions([typeof request === 'string' ? request : request], { ...options, strategy });
|
|
1098
|
+
if (!requestOptions) {
|
|
1099
|
+
continue; // invalid URL, silently dropped (matches `createRequestOptions`'s own filtering)
|
|
1100
|
+
}
|
|
1101
|
+
if (maxCrawlDepth !== undefined && requestOptions.crawlDepth > maxCrawlDepth) {
|
|
1102
|
+
allSkipped.push({ url: requestOptions.url, reason: 'depth' });
|
|
1029
1103
|
continue;
|
|
1030
1104
|
}
|
|
1031
|
-
if (await isAllowedBasedOnRobotsTxtFile(url)) {
|
|
1032
|
-
|
|
1033
|
-
|
|
1105
|
+
if (!(await isAllowedBasedOnRobotsTxtFile(requestOptions.url))) {
|
|
1106
|
+
allSkipped.push({ url: requestOptions.url, reason: 'robotsTxt' });
|
|
1107
|
+
continue;
|
|
1034
1108
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1109
|
+
const onSkippedFilterUrl = (url) => allSkipped.push({ url, reason: 'filters' });
|
|
1110
|
+
// Filter by user patterns first (with exclude)...
|
|
1111
|
+
let filtered = filterRequestOptionsByPatterns([requestOptions], urlPatternObjects.length > 0 ? urlPatternObjects : undefined, urlExcludePatternObjects, strategy, onSkippedFilterUrl);
|
|
1112
|
+
// ...then filter by the enqueue strategy (making this an AND check)
|
|
1113
|
+
filtered = filterRequestOptionsByPatterns(filtered, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], strategy, onSkippedFilterUrl);
|
|
1114
|
+
if (filtered.length === 0) {
|
|
1115
|
+
continue;
|
|
1037
1116
|
}
|
|
1117
|
+
let [finalOptions] = filtered;
|
|
1118
|
+
if (options.transformRequestFunction) {
|
|
1119
|
+
const transformed = applyRequestTransform([finalOptions], options.transformRequestFunction, (r) => allSkipped.push({ url: r.url, reason: r.skippedReason ?? 'transform' }));
|
|
1120
|
+
if (transformed.length === 0) {
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
[finalOptions] = transformed;
|
|
1124
|
+
}
|
|
1125
|
+
await validateRequestUserData(finalOptions);
|
|
1126
|
+
yield new Request(finalOptions);
|
|
1038
1127
|
}
|
|
1039
1128
|
}
|
|
1040
1129
|
const result = await this.requestManager.addRequestsBatched(filteredRequests(), {
|
|
1041
|
-
|
|
1130
|
+
forefront: options.forefront,
|
|
1131
|
+
waitForAllRequestsToBeAdded: options.waitForAllRequestsToBeAdded,
|
|
1132
|
+
batchSize: options.batchSize,
|
|
1133
|
+
waitBetweenBatchesMillis: options.waitBetweenBatchesMillis,
|
|
1042
1134
|
maxNewRequests: requestLimit,
|
|
1043
1135
|
});
|
|
1044
|
-
// Report requests skipped due to the maxNewRequests budget (i.e. maxRequestsPerCrawl limit
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
})));
|
|
1136
|
+
// Report requests skipped due to the maxNewRequests budget (i.e. maxRequestsPerCrawl limit, or an
|
|
1137
|
+
// explicit `limit` option)
|
|
1138
|
+
for (const request of result.requestsOverLimit ?? []) {
|
|
1139
|
+
allSkipped.push({ url: typeof request === 'string' ? request : request.url, reason: 'limit' });
|
|
1140
|
+
}
|
|
1141
|
+
if (allSkipped.length > 0) {
|
|
1142
|
+
const skippedRobotsUrls = allSkipped.filter((s) => s.reason === 'robotsTxt').map((s) => s.url);
|
|
1143
|
+
if (skippedRobotsUrls.length > 0) {
|
|
1144
|
+
this.log.warning(`Some requests were skipped because they were disallowed based on the robots.txt file`, { skipped: skippedRobotsUrls });
|
|
1145
|
+
}
|
|
1146
|
+
// Only log the limit message when an explicit `limit` was passed (not the internal
|
|
1147
|
+
// `maxRequestsPerCrawl`-derived one), and only once per call.
|
|
1148
|
+
if (options.limit !== undefined && allSkipped.some((s) => s.reason === 'limit')) {
|
|
1149
|
+
this.log.info(`Skipping requests in this call due to the enqueueLinks limit of ${options.limit}.`);
|
|
1150
|
+
}
|
|
1151
|
+
await Promise.all(allSkipped.map(async ({ url, reason }) => {
|
|
1152
|
+
await this.handleSkippedRequest({ url, reason });
|
|
1153
|
+
await options.onSkippedRequest?.({ url, reason });
|
|
1154
|
+
}));
|
|
1064
1155
|
}
|
|
1065
1156
|
return result;
|
|
1066
1157
|
}
|
|
@@ -1363,30 +1454,6 @@ export class BasicCrawler {
|
|
|
1363
1454
|
}
|
|
1364
1455
|
return this.requestManager.fetchNextRequest();
|
|
1365
1456
|
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Delays processing of the request based on the `sameDomainDelaySecs` option,
|
|
1368
|
-
* adding it back to the queue after the timeout passes. Returns `true` if the request
|
|
1369
|
-
* should be ignored and will be reclaimed to the queue once ready.
|
|
1370
|
-
*/
|
|
1371
|
-
delayRequest(request, source) {
|
|
1372
|
-
const domain = getDomain(request.url);
|
|
1373
|
-
if (!domain || !request) {
|
|
1374
|
-
return false;
|
|
1375
|
-
}
|
|
1376
|
-
const now = Date.now();
|
|
1377
|
-
const lastAccessTime = this.#domainAccessedTime.get(domain);
|
|
1378
|
-
if (!lastAccessTime || now - lastAccessTime >= this.#sameDomainDelayMillis) {
|
|
1379
|
-
this.#domainAccessedTime.set(domain, now);
|
|
1380
|
-
return false;
|
|
1381
|
-
}
|
|
1382
|
-
const delay = lastAccessTime + this.#sameDomainDelayMillis - now;
|
|
1383
|
-
this.log.debug(`Request ${request.url} (${request.id}) will be reclaimed after ${delay} milliseconds due to same domain delay`);
|
|
1384
|
-
setTimeout(async () => {
|
|
1385
|
-
this.log.debug(`Adding request ${request.url} (${request.id}) back to the queue`);
|
|
1386
|
-
await source.reclaimRequest(request, { forefront: request.userData?.__crawlee?.forefront });
|
|
1387
|
-
}, delay);
|
|
1388
|
-
return true;
|
|
1389
|
-
}
|
|
1390
1457
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
1391
1458
|
async handleRequest(crawlingContext, requestSource, request) {
|
|
1392
1459
|
// An earlier phase we cannot cancel (e.g. a slow `extendContext`) may have run past the internal timeout,
|
|
@@ -1459,52 +1526,6 @@ export class BasicCrawler {
|
|
|
1459
1526
|
}
|
|
1460
1527
|
}
|
|
1461
1528
|
}
|
|
1462
|
-
/**
|
|
1463
|
-
* Wrapper around the crawling context's `enqueueLinks` method:
|
|
1464
|
-
* - Injects `crawlDepth` to each request being added based on the crawling context request.
|
|
1465
|
-
* - Provides defaults for the `enqueueLinks` options based on the crawler configuration.
|
|
1466
|
-
* - These options can be overridden by the user.
|
|
1467
|
-
* @internal
|
|
1468
|
-
*/
|
|
1469
|
-
async enqueueLinksWithCrawlDepth(options, request, requestManager) {
|
|
1470
|
-
const transformRequestFunctionWrapper = (requestOptions) => {
|
|
1471
|
-
requestOptions.crawlDepth = request.crawlDepth + 1;
|
|
1472
|
-
if (this.maxCrawlDepth !== undefined && requestOptions.crawlDepth > this.maxCrawlDepth) {
|
|
1473
|
-
// Setting `skippedReason` before returning `false` ensures that `reportSkippedRequests`
|
|
1474
|
-
// reports `'depth'` as the reason (via `request.skippedReason ?? reason` fallback),
|
|
1475
|
-
// rather than the generic `'transform'` reason.
|
|
1476
|
-
requestOptions.skippedReason = 'depth';
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
// After injecting the crawlDepth, we call the user-provided transform function, if there is one.
|
|
1480
|
-
return options.transformRequestFunction?.(requestOptions) ?? requestOptions;
|
|
1481
|
-
};
|
|
1482
|
-
// Create a request-scoped callback that logs enqueueLimit once per request handler call
|
|
1483
|
-
// Only log if an explicit limit was passed to enqueueLinks (not the internal maxRequestsPerCrawl-derived limit)
|
|
1484
|
-
let loggedEnqueueLimitForThisRequest = false;
|
|
1485
|
-
const onSkippedRequest = async (skippedOptions) => {
|
|
1486
|
-
if (skippedOptions.reason === 'enqueueLimit') {
|
|
1487
|
-
if (!loggedEnqueueLimitForThisRequest && options.limit !== undefined) {
|
|
1488
|
-
this.log.info(`Skipping URLs in the handler for ${request.url} due to the enqueueLinks limit of ${options.limit}.`);
|
|
1489
|
-
loggedEnqueueLimitForThisRequest = true;
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
await this.handleSkippedRequest(skippedOptions);
|
|
1493
|
-
};
|
|
1494
|
-
// `enqueueLinks` applies `options.label`/`options.userData` to every newly enqueued request, so a single
|
|
1495
|
-
// validation against the label's schema covers them all (a no-op unless the router declares a schema).
|
|
1496
|
-
await this.validateRequestUserData({ label: options.label, userData: options.userData });
|
|
1497
|
-
return await enqueueLinks({
|
|
1498
|
-
requestManager,
|
|
1499
|
-
robotsTxtFile: await this.getRobotsTxtFileForUrl(request.url),
|
|
1500
|
-
respectRobotsTxtFile: this.#respectRobotsTxtFile,
|
|
1501
|
-
onSkippedRequest,
|
|
1502
|
-
limit: await this.calculateEnqueuedRequestLimit(options.limit),
|
|
1503
|
-
// Allow user options to override defaults set above ⤴
|
|
1504
|
-
...options,
|
|
1505
|
-
transformRequestFunction: transformRequestFunctionWrapper,
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
1529
|
/**
|
|
1509
1530
|
* Generator function that yields requests injected with the given crawl depth.
|
|
1510
1531
|
* @internal
|
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
|
}
|