@crawlee/basic 4.0.0-beta.155 → 4.0.0-beta.157
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.js +108 -104
- package/package.json +7 -7
|
@@ -7,7 +7,7 @@ import { RobotsTxtFile } from '@crawlee/utils';
|
|
|
7
7
|
import { getDomain } from 'tldts';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { LruCache } from '@apify/datastructures';
|
|
10
|
-
import { addTimeoutToPromise, extendTimeout, TimeoutError, tryCancel } from '@apify/timeout';
|
|
10
|
+
import { addTimeoutToPromise, extendTimeout, storage as timeoutStorage, TimeoutError, tryCancel } from '@apify/timeout';
|
|
11
11
|
import { cryptoRandomObjectId } from '@apify/utilities';
|
|
12
12
|
import { extendTimeoutKey, navigationDeadlineKey, raceWithTimeout, timeoutExpiredKey, } from './request-timeout.js';
|
|
13
13
|
import { createSendRequest } from './send-request.js';
|
|
@@ -797,115 +797,119 @@ export class BasicCrawler {
|
|
|
797
797
|
* @param [options] Options for the request queue.
|
|
798
798
|
*/
|
|
799
799
|
async run(requests, options) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (this.hasFinishedBefore) {
|
|
805
|
-
// When executing the run method for the second time explicitly,
|
|
806
|
-
// we need to purge the RQ to allow processing the same requests again — this is important so users can
|
|
807
|
-
// pass in failed requests back to the `crawler.run()`, otherwise they would be considered as handled and
|
|
808
|
-
// ignored — as a failed request is still handled.
|
|
809
|
-
// `purgeRequestQueue` unset purges only storage the crawler opened itself (see `#purgeableExtent`);
|
|
810
|
-
// `true` also purges a caller-supplied manager, `false` purges nothing.
|
|
811
|
-
if (purgeRequestQueue === undefined && this.#purgeableExtent === 'ambiguous') {
|
|
812
|
-
throw new Error('Cannot decide what to purge before running again: `sameDomainDelaySecs` paces the request ' +
|
|
813
|
-
'manager you supplied, so the per-domain queues that have to be emptied are the ' +
|
|
814
|
-
"crawler's while the manager underneath them is yours. Say which you want: " +
|
|
815
|
-
'`run(requests, { purgeRequestQueue: true })` empties both, `false` empties neither.');
|
|
800
|
+
// A crawl is the top level of its own transaction and timeout scope, not a participant in the caller's.
|
|
801
|
+
return withDirectStorageAccess(async () => timeoutStorage.exit(async () => {
|
|
802
|
+
if (this.running) {
|
|
803
|
+
throw new Error('This crawler instance is already running, you can add more requests to it via `crawler.addRequests()`.');
|
|
816
804
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
//
|
|
820
|
-
|
|
805
|
+
const { purgeRequestQueue, ...addRequestsOptions } = options ?? {};
|
|
806
|
+
if (this.hasFinishedBefore) {
|
|
807
|
+
// When executing the run method for the second time explicitly,
|
|
808
|
+
// we need to purge the RQ to allow processing the same requests again — this is important so users can
|
|
809
|
+
// pass in failed requests back to the `crawler.run()`, otherwise they would be considered as handled and
|
|
810
|
+
// ignored — as a failed request is still handled.
|
|
811
|
+
// `purgeRequestQueue` unset purges only storage the crawler opened itself (see `#purgeableExtent`);
|
|
812
|
+
// `true` also purges a caller-supplied manager, `false` purges nothing.
|
|
813
|
+
if (purgeRequestQueue === undefined && this.#purgeableExtent === 'ambiguous') {
|
|
814
|
+
throw new Error('Cannot decide what to purge before running again: `sameDomainDelaySecs` paces the request ' +
|
|
815
|
+
'manager you supplied, so the per-domain queues that have to be emptied are the ' +
|
|
816
|
+
"crawler's while the manager underneath them is yours. Say which you want: " +
|
|
817
|
+
'`run(requests, { purgeRequestQueue: true })` empties both, `false` empties neither.');
|
|
818
|
+
}
|
|
819
|
+
if (purgeRequestQueue !== false &&
|
|
820
|
+
(this.#purgeableExtent === 'all' || purgeRequestQueue === true)) {
|
|
821
|
+
// One call from the outside in reaches everything the manager wraps, a pacer's per-domain queues
|
|
822
|
+
// included.
|
|
823
|
+
await this.requestManager?.purge?.();
|
|
824
|
+
}
|
|
825
|
+
// A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
|
|
826
|
+
await this.#statisticsDep.ifOwned(async (stats) => {
|
|
827
|
+
stats.reset();
|
|
828
|
+
await stats.resetStore();
|
|
829
|
+
});
|
|
830
|
+
await this.#sessionPoolDep.ifOwned((pool) => pool.resetStore());
|
|
821
831
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
this.#unexpectedStop = false;
|
|
830
|
-
this.running = true;
|
|
831
|
-
this.#loggedPerRun.clear();
|
|
832
|
-
await purgeDefaultStorages({
|
|
833
|
-
onlyPurgeOnce: true,
|
|
834
|
-
storageBackend: serviceLocator.getStorageBackend(),
|
|
835
|
-
configuration: serviceLocator.getConfiguration(),
|
|
836
|
-
});
|
|
837
|
-
if (requests) {
|
|
838
|
-
await this.addRequests(requests, addRequestsOptions);
|
|
839
|
-
}
|
|
840
|
-
try {
|
|
841
|
-
await this.init();
|
|
842
|
-
await this.statistics.startCapturing();
|
|
843
|
-
}
|
|
844
|
-
catch (error) {
|
|
845
|
-
// Clean up here before propagating, otherwise a failed startup would leave the process hanging.
|
|
846
|
-
await this.teardown().catch((teardownError) => {
|
|
847
|
-
this.log.exception(teardownError, 'Cleaning up after a failed crawler startup failed.');
|
|
832
|
+
this.#unexpectedStop = false;
|
|
833
|
+
this.running = true;
|
|
834
|
+
this.#loggedPerRun.clear();
|
|
835
|
+
await purgeDefaultStorages({
|
|
836
|
+
onlyPurgeOnce: true,
|
|
837
|
+
storageBackend: serviceLocator.getStorageBackend(),
|
|
838
|
+
configuration: serviceLocator.getConfiguration(),
|
|
848
839
|
});
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
// Attach a listener to handle migration and aborting events gracefully.
|
|
861
|
-
const boundPauseOnMigration = this.pauseOnMigration.bind(this);
|
|
862
|
-
process.once('SIGINT', sigintHandler);
|
|
863
|
-
const eventManager = serviceLocator.getEventManager();
|
|
864
|
-
eventManager.on(EventType.MIGRATING, boundPauseOnMigration);
|
|
865
|
-
eventManager.on(EventType.ABORTING, boundPauseOnMigration);
|
|
866
|
-
let stats = {};
|
|
867
|
-
try {
|
|
868
|
-
await this.#autoscaledPool.run();
|
|
869
|
-
}
|
|
870
|
-
finally {
|
|
871
|
-
await this.statistics.stopCapturing();
|
|
872
|
-
await this.teardown();
|
|
873
|
-
process.off('SIGINT', sigintHandler);
|
|
874
|
-
eventManager.off(EventType.MIGRATING, boundPauseOnMigration);
|
|
875
|
-
eventManager.off(EventType.ABORTING, boundPauseOnMigration);
|
|
876
|
-
const finalStats = this.statistics.calculate();
|
|
877
|
-
stats = {
|
|
878
|
-
requestsFinished: this.statistics.state.requestsFinished,
|
|
879
|
-
requestsFailed: this.statistics.state.requestsFailed,
|
|
880
|
-
retryHistogram: this.statistics.requestRetryHistogram,
|
|
881
|
-
...finalStats,
|
|
882
|
-
};
|
|
883
|
-
this.log.info('Final request statistics:', stats);
|
|
884
|
-
if (this.statistics.errorTracker.total !== 0) {
|
|
885
|
-
const prettify = ([count, info]) => `${count}x: ${info.at(-1).trim()} (${info[0]})`;
|
|
886
|
-
this.log.info(`Error analysis:`, {
|
|
887
|
-
totalErrors: this.statistics.errorTracker.total,
|
|
888
|
-
uniqueErrors: this.statistics.errorTracker.getUniqueErrorCount(),
|
|
889
|
-
mostCommonErrors: this.statistics.errorTracker.getMostPopularErrors(3).map(prettify),
|
|
840
|
+
if (requests) {
|
|
841
|
+
await this.addRequests(requests, addRequestsOptions);
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
await this.init();
|
|
845
|
+
await this.statistics.startCapturing();
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
// Clean up here before propagating, otherwise a failed startup would leave the process hanging.
|
|
849
|
+
await this.teardown().catch((teardownError) => {
|
|
850
|
+
this.log.exception(teardownError, 'Cleaning up after a failed crawler startup failed.');
|
|
890
851
|
});
|
|
852
|
+
// The run never began, so let the instance be run again instead of leaving it wedged as `running`.
|
|
853
|
+
this.running = false;
|
|
854
|
+
throw error;
|
|
891
855
|
}
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
856
|
+
const periodicLogger = this.getPeriodicLogger();
|
|
857
|
+
this.setStatusMessage('Starting the crawler.', { level: 'INFO' });
|
|
858
|
+
const sigintHandler = async () => {
|
|
859
|
+
this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
|
|
860
|
+
await this.pauseOnMigration();
|
|
861
|
+
await this.#autoscaledPool.abort();
|
|
862
|
+
};
|
|
863
|
+
// Attach a listener to handle migration and aborting events gracefully.
|
|
864
|
+
const boundPauseOnMigration = this.pauseOnMigration.bind(this);
|
|
865
|
+
process.once('SIGINT', sigintHandler);
|
|
866
|
+
const eventManager = serviceLocator.getEventManager();
|
|
867
|
+
eventManager.on(EventType.MIGRATING, boundPauseOnMigration);
|
|
868
|
+
eventManager.on(EventType.ABORTING, boundPauseOnMigration);
|
|
869
|
+
let stats = {};
|
|
870
|
+
try {
|
|
871
|
+
await this.#autoscaledPool.run();
|
|
902
872
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
873
|
+
finally {
|
|
874
|
+
await this.statistics.stopCapturing();
|
|
875
|
+
await this.teardown();
|
|
876
|
+
process.off('SIGINT', sigintHandler);
|
|
877
|
+
eventManager.off(EventType.MIGRATING, boundPauseOnMigration);
|
|
878
|
+
eventManager.off(EventType.ABORTING, boundPauseOnMigration);
|
|
879
|
+
const finalStats = this.statistics.calculate();
|
|
880
|
+
stats = {
|
|
881
|
+
requestsFinished: this.statistics.state.requestsFinished,
|
|
882
|
+
requestsFailed: this.statistics.state.requestsFailed,
|
|
883
|
+
retryHistogram: this.statistics.requestRetryHistogram,
|
|
884
|
+
...finalStats,
|
|
885
|
+
};
|
|
886
|
+
this.log.info('Final request statistics:', stats);
|
|
887
|
+
if (this.statistics.errorTracker.total !== 0) {
|
|
888
|
+
const prettify = ([count, info]) => `${count}x: ${info.at(-1).trim()} (${info[0]})`;
|
|
889
|
+
this.log.info(`Error analysis:`, {
|
|
890
|
+
totalErrors: this.statistics.errorTracker.total,
|
|
891
|
+
uniqueErrors: this.statistics.errorTracker.getUniqueErrorCount(),
|
|
892
|
+
mostCommonErrors: this.statistics.errorTracker.getMostPopularErrors(3).map(prettify),
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
const client = serviceLocator.getStorageBackend();
|
|
896
|
+
if (client.teardown) {
|
|
897
|
+
let finished = false;
|
|
898
|
+
setTimeout(() => {
|
|
899
|
+
if (!finished) {
|
|
900
|
+
this.log.info('Waiting for the storage to write its state to file system.');
|
|
901
|
+
}
|
|
902
|
+
}, 1000);
|
|
903
|
+
await client.teardown();
|
|
904
|
+
finished = true;
|
|
905
|
+
}
|
|
906
|
+
periodicLogger.stop();
|
|
907
|
+
this.setStatusMessage(`Finished! Total ${this.statistics.state.requestsFinished + this.statistics.state.requestsFailed} requests: ${this.statistics.state.requestsFinished} succeeded, ${this.statistics.state.requestsFailed} failed.`, { isStatusMessageTerminal: true, level: 'INFO' });
|
|
908
|
+
this.running = false;
|
|
909
|
+
this.hasFinishedBefore = true;
|
|
910
|
+
}
|
|
911
|
+
return stats;
|
|
912
|
+
}));
|
|
909
913
|
}
|
|
910
914
|
/**
|
|
911
915
|
* Gracefully stops the current run of the crawler.
|
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.157",
|
|
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.157",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.157",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.157",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.157",
|
|
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.157"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "a1b3d4f72a2759a7179af6648083f57d022125a9"
|
|
66
66
|
}
|