@crawlee/playwright 4.0.0-beta.163 → 4.0.0-beta.164
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.
|
@@ -198,6 +198,24 @@ export declare class AdaptivePlaywrightCrawler<ContextExtension = Dictionary<nev
|
|
|
198
198
|
protected runRequestHandler(crawlingContext: CrawlingContext): Promise<void>;
|
|
199
199
|
private enqueueLinks;
|
|
200
200
|
private createLogProxy;
|
|
201
|
+
/**
|
|
202
|
+
* Number of rendering type detections that have not settled yet, including results the predictor is
|
|
203
|
+
* still persisting.
|
|
204
|
+
*/
|
|
205
|
+
get inFlightRenderingTypeDetectionCount(): number;
|
|
206
|
+
/**
|
|
207
|
+
* Waits for in-flight rendering type detections to settle, bounded by `timeoutMillis` (defaults to the
|
|
208
|
+
* internal timeout).
|
|
209
|
+
*/
|
|
210
|
+
drainRenderingDetections({ timeoutMillis }?: {
|
|
211
|
+
timeoutMillis?: number;
|
|
212
|
+
}): Promise<void>;
|
|
213
|
+
/**
|
|
214
|
+
* Stops the crawler immediately, but not before rendering type detections already under way (and results
|
|
215
|
+
* the predictor is still persisting) have settled - see
|
|
216
|
+
* {@link AdaptivePlaywrightCrawler.drainRenderingDetections|`drainRenderingDetections()`}. Requests
|
|
217
|
+
* that are still running are not waited for, unlike {@link BasicCrawler.stop|`stop()`}.
|
|
218
|
+
*/
|
|
201
219
|
teardown(): Promise<void>;
|
|
202
220
|
}
|
|
203
221
|
export declare function createAdaptivePlaywrightRouter<Context extends AdaptivePlaywrightCrawlerContext = AdaptivePlaywrightCrawlerContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
1
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
3
|
import { BasicCrawler } from '@crawlee/basic';
|
|
3
4
|
import { extractUrlsFromPage } from '@crawlee/browser';
|
|
@@ -76,6 +77,14 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
76
77
|
* a discarded attempt's enqueues must never reach the queue.
|
|
77
78
|
*/
|
|
78
79
|
#attemptWritePolicy;
|
|
80
|
+
/**
|
|
81
|
+
* In-flight rendering type detections, plus the pending results of an asynchronous `storeResult`.
|
|
82
|
+
*/
|
|
83
|
+
#activeDetections = new Set();
|
|
84
|
+
/**
|
|
85
|
+
* Set once `teardown()` starts, so that requests still in the pool stop opening new detections.
|
|
86
|
+
*/
|
|
87
|
+
#shutDown = false;
|
|
79
88
|
#teardownHooks = [];
|
|
80
89
|
constructor(options = {}) {
|
|
81
90
|
const { requestHandler, renderingTypeDetectionRatio = 0.1, renderingTypePredictor, resultChecker, shouldPropagateError, resultComparator, statistics, requestHandlerTimeoutSecs = 60, errorHandler, failedRequestHandler, preNavigationHooks = [], postNavigationHooks = [], extendContext, contextPipelineBuilder, transactionalStorage, launchContext, headless, browserPool, remoteBrowser, ...rest } = options;
|
|
@@ -180,6 +189,8 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
180
189
|
});
|
|
181
190
|
}
|
|
182
191
|
async init() {
|
|
192
|
+
// A crawler can be run again after a teardown.
|
|
193
|
+
this.#shutDown = false;
|
|
183
194
|
// Only the predictor we built ourselves is ours to initialize - an injected one is borrowed, so its
|
|
184
195
|
// lifecycle (including restoring persisted state) stays with whoever created it.
|
|
185
196
|
await this.#renderingTypePredictor.ifOwned((predictor) => predictor.initialize());
|
|
@@ -314,8 +325,8 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
314
325
|
}
|
|
315
326
|
}
|
|
316
327
|
async runRequestHandler(crawlingContext) {
|
|
317
|
-
const renderingTypePrediction = this.#renderingTypePredictor.value.predict(crawlingContext.request);
|
|
318
|
-
const shouldDetectRenderingType = Math.random() < renderingTypePrediction.detectionProbabilityRecommendation;
|
|
328
|
+
const renderingTypePrediction = await this.#renderingTypePredictor.value.predict(crawlingContext.request);
|
|
329
|
+
const shouldDetectRenderingType = !this.#shutDown && Math.random() < renderingTypePrediction.detectionProbabilityRecommendation;
|
|
319
330
|
if (!shouldDetectRenderingType) {
|
|
320
331
|
crawlingContext.log.debug(`Predicted rendering type ${renderingTypePrediction.renderingType} for ${crawlingContext.request.url}`);
|
|
321
332
|
}
|
|
@@ -378,27 +389,36 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
378
389
|
browserRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
|
|
379
390
|
await browserRun.result.commit();
|
|
380
391
|
if (shouldDetectRenderingType) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
392
|
+
const detectionPromise = (async () => {
|
|
393
|
+
crawlingContext.log.debug(`Detecting rendering type for ${crawlingContext.request.url}`);
|
|
394
|
+
// The detection attempt's transaction is never committed - its writes exist only for the
|
|
395
|
+
// result comparison.
|
|
396
|
+
const plainHTTPRun = await this.crawlOne('static', crawlingContext, stateTracker.getStateCopy.bind(stateTracker), transactions);
|
|
397
|
+
const detectionResult = (() => {
|
|
398
|
+
if (!plainHTTPRun.ok) {
|
|
399
|
+
return 'clientOnly';
|
|
400
|
+
}
|
|
401
|
+
const comparisonResult = this.#resultComparator(plainHTTPRun.result, browserRun.result);
|
|
402
|
+
if (comparisonResult === true || comparisonResult === 'equal') {
|
|
403
|
+
return 'static';
|
|
404
|
+
}
|
|
405
|
+
if (comparisonResult === false || comparisonResult === 'different') {
|
|
406
|
+
return 'clientOnly';
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
})();
|
|
410
|
+
crawlingContext.log.debug(`Detected rendering type ${detectionResult} for ${crawlingContext.request.url}`);
|
|
411
|
+
if (detectionResult !== undefined) {
|
|
412
|
+
// Deliberately not awaited: a predictor that persists asynchronously gets to keep
|
|
413
|
+
// batching its writes, and the drain below catches whatever is still pending.
|
|
414
|
+
const stored = this.#renderingTypePredictor.value.storeResult(crawlingContext.request, detectionResult);
|
|
415
|
+
if (stored !== undefined) {
|
|
416
|
+
// Nothing downstream awaits this, so a failed write would otherwise be silent.
|
|
417
|
+
void this.#trackDetection(Promise.resolve(stored).catch((error) => this.log.exception(error, `Failed to store the rendering type detection result for ${crawlingContext.request.url}`)));
|
|
418
|
+
}
|
|
395
419
|
}
|
|
396
|
-
return undefined;
|
|
397
420
|
})();
|
|
398
|
-
|
|
399
|
-
if (detectionResult !== undefined) {
|
|
400
|
-
this.#renderingTypePredictor.value.storeResult(crawlingContext.request, detectionResult);
|
|
401
|
-
}
|
|
421
|
+
await this.#trackDetection(detectionPromise);
|
|
402
422
|
}
|
|
403
423
|
}
|
|
404
424
|
finally {
|
|
@@ -443,7 +463,66 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
443
463
|
},
|
|
444
464
|
});
|
|
445
465
|
}
|
|
466
|
+
#trackDetection(promise) {
|
|
467
|
+
this.#activeDetections.add(promise);
|
|
468
|
+
// Not `finally()`: the promise it derives would reject on its own and go unhandled. A rejection here
|
|
469
|
+
// belongs to whoever awaits the original, or to `allSettled` in the drain.
|
|
470
|
+
void promise.catch(() => { }).then(() => this.#activeDetections.delete(promise));
|
|
471
|
+
return promise;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Number of rendering type detections that have not settled yet, including results the predictor is
|
|
475
|
+
* still persisting.
|
|
476
|
+
*/
|
|
477
|
+
get inFlightRenderingTypeDetectionCount() {
|
|
478
|
+
return this.#activeDetections.size;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Waits for in-flight rendering type detections to settle, bounded by `timeoutMillis` (defaults to the
|
|
482
|
+
* internal timeout).
|
|
483
|
+
*/
|
|
484
|
+
async drainRenderingDetections({ timeoutMillis } = {}) {
|
|
485
|
+
if (this.#activeDetections.size === 0) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const drained = (async () => {
|
|
489
|
+
while (this.#activeDetections.size > 0) {
|
|
490
|
+
await Promise.allSettled(Array.from(this.#activeDetections));
|
|
491
|
+
}
|
|
492
|
+
})();
|
|
493
|
+
const millis = timeoutMillis ?? this.internalTimeoutMillis;
|
|
494
|
+
// A caller opting out of the bound would otherwise get an immediate spurious timeout - `setTimeout`
|
|
495
|
+
// clamps a non-finite delay to 1ms.
|
|
496
|
+
if (!Number.isFinite(millis)) {
|
|
497
|
+
await drained;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const abortTimer = new AbortController();
|
|
501
|
+
try {
|
|
502
|
+
const outcome = await Promise.race([
|
|
503
|
+
drained.then(() => 'drained'),
|
|
504
|
+
delay(millis, 'timedOut', { signal: abortTimer.signal }).catch(() => 'aborted'),
|
|
505
|
+
]);
|
|
506
|
+
if (outcome === 'timedOut') {
|
|
507
|
+
this.log.warning(`Timed out after ${millis / 1e3} seconds waiting for ${this.#activeDetections.size} rendering type detection(s) to settle - their results may be lost.`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
abortTimer.abort();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Stops the crawler immediately, but not before rendering type detections already under way (and results
|
|
516
|
+
* the predictor is still persisting) have settled - see
|
|
517
|
+
* {@link AdaptivePlaywrightCrawler.drainRenderingDetections|`drainRenderingDetections()`}. Requests
|
|
518
|
+
* that are still running are not waited for, unlike {@link BasicCrawler.stop|`stop()`}.
|
|
519
|
+
*/
|
|
446
520
|
async teardown() {
|
|
521
|
+
// Called from outside `run()` - under `keepAlive`, say - the pool keeps dispatching until
|
|
522
|
+
// `super.teardown()` aborts it, and a request starting during the drain would open a detection the
|
|
523
|
+
// drain has already passed. Closing that first makes the drain a fence.
|
|
524
|
+
this.#shutDown = true;
|
|
525
|
+
await this.drainRenderingDetections();
|
|
447
526
|
await super.teardown();
|
|
448
527
|
// Mirrors the owned-only `initialize()` in `init()` - without this, the predictor we built keeps its
|
|
449
528
|
// PERSIST_STATE listener registered after the crawl and never gets a final write.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RecoverableStatePersistenceOptions, Request } from '@crawlee/core';
|
|
2
|
+
import type { Awaitable } from '@crawlee/types';
|
|
2
3
|
export type RenderingType = 'clientOnly' | 'static';
|
|
3
4
|
export interface RenderingTypePredictorOptions {
|
|
4
5
|
/** A number between 0 and 1 that determines the desired ratio of rendering type detections */
|
|
@@ -12,13 +13,24 @@ export interface RenderingTypePredictorOptions {
|
|
|
12
13
|
* @experimental
|
|
13
14
|
*/
|
|
14
15
|
export interface IRenderingTypePredictor {
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Predict the rendering type for a request, and how likely the crawler should be to verify it.
|
|
18
|
+
*
|
|
19
|
+
* Called once per request, before navigation - the crawler awaits it, so prefer loading whatever the
|
|
20
|
+
* prediction needs up front over per-request I/O.
|
|
21
|
+
*/
|
|
22
|
+
predict(request: Request): Awaitable<{
|
|
17
23
|
renderingType: RenderingType;
|
|
18
24
|
detectionProbabilityRecommendation: number;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Report a detected rendering type, so that future predictions can take it into account.
|
|
28
|
+
*
|
|
29
|
+
* The crawler does not await this per detection - it collects the returned promises and drains them in
|
|
30
|
+
* `teardown()`, so an implementation that persists results asynchronously can keep batching its writes
|
|
31
|
+
* and still be sure they land before the crawl ends.
|
|
32
|
+
*/
|
|
33
|
+
storeResult(requests: Request | Request[], renderingType: RenderingType): Awaitable<void>;
|
|
22
34
|
}
|
|
23
35
|
/**
|
|
24
36
|
* Stores rendering type information for previously crawled URLs and predicts the rendering type for URLs that have yet to be crawled and recommends when rendering type detection should be performed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/playwright",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.164",
|
|
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"
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@apify/datastructures": "^2.0.3",
|
|
51
51
|
"@apify/timeout": "^0.4.4",
|
|
52
|
-
"@crawlee/basic": "4.0.0-beta.
|
|
53
|
-
"@crawlee/browser": "4.0.0-beta.
|
|
54
|
-
"@crawlee/browser-pool": "4.0.0-beta.
|
|
55
|
-
"@crawlee/cheerio": "4.0.0-beta.
|
|
56
|
-
"@crawlee/core": "4.0.0-beta.
|
|
57
|
-
"@crawlee/types": "4.0.0-beta.
|
|
58
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
52
|
+
"@crawlee/basic": "4.0.0-beta.164",
|
|
53
|
+
"@crawlee/browser": "4.0.0-beta.164",
|
|
54
|
+
"@crawlee/browser-pool": "4.0.0-beta.164",
|
|
55
|
+
"@crawlee/cheerio": "4.0.0-beta.164",
|
|
56
|
+
"@crawlee/core": "4.0.0-beta.164",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.164",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.164",
|
|
59
59
|
"cheerio": "^1.0.0",
|
|
60
60
|
"jquery": "^3.7.1",
|
|
61
61
|
"ml-logistic-regression": "^2.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "a2e4e2a3aa3a364c8a44fd2e49ca57f500d2c01f"
|
|
84
84
|
}
|