@crawlee/playwright 4.0.0-beta.104 → 4.0.0-beta.106
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/adaptive-playwright-crawler.d.ts +16 -32
- package/internals/adaptive-playwright-crawler.js +97 -113
- package/internals/playwright-crawler.d.ts +3 -1
- package/internals/playwright-crawler.js +1 -1
- package/internals/utils/rendering-type-prediction.d.ts +1 -1
- package/internals/utils/rendering-type-prediction.js +4 -3
- package/package.json +9 -10
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { BrowserHook, LoadedRequest, Request, RouterHandler, RouteSchemas, RoutesFromSchemas } from '@crawlee/browser';
|
|
2
2
|
import type { BasicCrawlerOptions } from '@crawlee/basic';
|
|
3
3
|
import { BasicCrawler } from '@crawlee/basic';
|
|
4
|
-
import type { ContextPipeline, CrawlingContext, EnqueueLinksOptions, GetUserDataFromRequest, RouterRoutes, StatisticsOptions, StatisticState } from '@crawlee/core';
|
|
5
|
-
import {
|
|
4
|
+
import type { ContextPipeline, CrawlingContext, EnqueueLinksOptions, GetUserDataFromRequest, RouterRoutes, StatisticsOptions, StatisticState, StorageTransactionView } from '@crawlee/core';
|
|
5
|
+
import { Statistics } from '@crawlee/core';
|
|
6
6
|
import type { Dictionary, Awaitable } from '@crawlee/types';
|
|
7
7
|
import { type CheerioRoot } from '@crawlee/utils';
|
|
8
8
|
import { type Cheerio } from 'cheerio';
|
|
@@ -108,11 +108,12 @@ export interface AdaptivePlaywrightCrawlerOptions<ContextExtension = Dictionary<
|
|
|
108
108
|
*/
|
|
109
109
|
renderingTypeDetectionRatio?: number;
|
|
110
110
|
/**
|
|
111
|
-
* An optional callback that is called on
|
|
111
|
+
* An optional callback that is called on the storage writes recorded by the request handler in plain
|
|
112
|
+
* HTTP mode (exposed as a read-only {@link StorageTransactionView}).
|
|
112
113
|
* If it returns false, the request is retried in a browser.
|
|
113
|
-
* If no callback is specified, every
|
|
114
|
+
* If no callback is specified, every result is considered valid.
|
|
114
115
|
*/
|
|
115
|
-
resultChecker?: (result:
|
|
116
|
+
resultChecker?: (result: StorageTransactionView) => boolean;
|
|
116
117
|
/**
|
|
117
118
|
* An optional callback that decides whether an error thrown during the plain HTTP request handler
|
|
118
119
|
* should be propagated (instead of falling back to browser navigation).
|
|
@@ -135,18 +136,13 @@ export interface AdaptivePlaywrightCrawlerOptions<ContextExtension = Dictionary<
|
|
|
135
136
|
*
|
|
136
137
|
* For a stricter, ready-made comparator that also takes enqueued requests and key-value store changes into account, see {@link fullResultComparator}.
|
|
137
138
|
*/
|
|
138
|
-
resultComparator?: (resultA:
|
|
139
|
+
resultComparator?: (resultA: StorageTransactionView, resultB: StorageTransactionView) => boolean | 'equal' | 'different' | 'inconclusive';
|
|
139
140
|
/**
|
|
140
141
|
* A custom rendering type predictor. A predictor passed here is borrowed - the crawler never drives its
|
|
141
142
|
* lifecycle, so set it up yourself (the built-in {@link RenderingTypePredictor} needs `initialize()`).
|
|
142
143
|
* Omit the option and the crawler builds its own from `renderingTypeDetectionRatio` - and initializes it.
|
|
143
144
|
*/
|
|
144
145
|
renderingTypePredictor?: IRenderingTypePredictor;
|
|
145
|
-
/**
|
|
146
|
-
* Prevent direct access to storage in request handlers (only allow using context helpers).
|
|
147
|
-
* Defaults to `true`
|
|
148
|
-
*/
|
|
149
|
-
preventDirectStorageAccess?: boolean;
|
|
150
146
|
}
|
|
151
147
|
/**
|
|
152
148
|
* An extension of {@link PlaywrightCrawler} that uses a more limited request handler interface so that it is able to switch to HTTP-only crawling when it detects it may be possible.
|
|
@@ -178,20 +174,10 @@ export interface AdaptivePlaywrightCrawlerOptions<ContextExtension = Dictionary<
|
|
|
178
174
|
* @experimental
|
|
179
175
|
*/
|
|
180
176
|
export declare class AdaptivePlaywrightCrawler<ContextExtension = Dictionary<never>, ExtendedContext extends AdaptivePlaywrightCrawlerContext = AdaptivePlaywrightCrawlerContext & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<AdaptivePlaywrightCrawlerContext['request']>>> extends BasicCrawler<AdaptivePlaywrightCrawlerContext, ContextExtension, ExtendedContext, Routes> {
|
|
181
|
-
private
|
|
182
|
-
private resultChecker;
|
|
183
|
-
private shouldPropagateError;
|
|
184
|
-
private resultComparator;
|
|
185
|
-
private preventDirectStorageAccess;
|
|
186
|
-
private staticContextPipeline;
|
|
187
|
-
private browserContextPipeline;
|
|
188
|
-
private individualRequestHandlerTimeoutMillis;
|
|
177
|
+
#private;
|
|
189
178
|
get stats(): AdaptivePlaywrightCrawlerStatistics;
|
|
190
|
-
private resultObjects;
|
|
191
|
-
private inFlightRenderingTypeDetections;
|
|
192
|
-
private teardownHooks;
|
|
193
179
|
constructor(options?: AdaptivePlaywrightCrawlerOptions<ContextExtension, ExtendedContext, Routes>);
|
|
194
|
-
protected
|
|
180
|
+
protected init(): Promise<void>;
|
|
195
181
|
protected buildContextPipeline(): ContextPipeline<CrawlingContext<Dictionary>, CrawlingContext<Dictionary> & {
|
|
196
182
|
readonly request: LoadedRequest<Request<Dictionary>>;
|
|
197
183
|
readonly response: Response;
|
|
@@ -203,16 +189,14 @@ export declare class AdaptivePlaywrightCrawler<ContextExtension = Dictionary<nev
|
|
|
203
189
|
}>;
|
|
204
190
|
private adaptCheerioContext;
|
|
205
191
|
private adaptPlaywrightContext;
|
|
206
|
-
private crawlOne;
|
|
207
|
-
protected runRequestHandler(crawlingContext: CrawlingContext): Promise<void>;
|
|
208
|
-
private commitResult;
|
|
209
|
-
private allowStorageAccess;
|
|
210
192
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
193
|
+
* Runs one request handler attempt inside its own {@link StorageTransaction}, wrapping the inner
|
|
194
|
+
* (static or browser) context pipeline. The transaction is pushed to `transactions` *at creation
|
|
195
|
+
* time, before the `try`* - the `ok: false` branch of the returned {@link Result} carries no
|
|
196
|
+
* result, and failed attempts are routine here. The caller owns the outcome and disposal.
|
|
214
197
|
*/
|
|
215
|
-
|
|
198
|
+
private crawlOne;
|
|
199
|
+
protected runRequestHandler(crawlingContext: CrawlingContext): Promise<void>;
|
|
216
200
|
private enqueueLinks;
|
|
217
201
|
private createLogProxy;
|
|
218
202
|
teardown(): Promise<void>;
|
|
@@ -243,5 +227,5 @@ export declare function createAdaptivePlaywrightRouter<Context extends AdaptiveP
|
|
|
243
227
|
* });
|
|
244
228
|
* ```
|
|
245
229
|
*/
|
|
246
|
-
export declare function fullResultComparator(resultA:
|
|
230
|
+
export declare function fullResultComparator(resultA: StorageTransactionView, resultB: StorageTransactionView): boolean;
|
|
247
231
|
export {};
|
|
@@ -2,8 +2,9 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
2
2
|
import { BasicCrawler } from '@crawlee/basic';
|
|
3
3
|
import { extractUrlsFromPage } from '@crawlee/browser';
|
|
4
4
|
import { CheerioCrawler } from '@crawlee/cheerio';
|
|
5
|
-
import { OwnedOrInjected, RequestHandlerError,
|
|
5
|
+
import { createStorageTransaction, OwnedOrInjected, RequestHandlerError, resolveBaseUrlForEnqueueLinksFiltering, Router, Statistics, } from '@crawlee/core';
|
|
6
6
|
import { extractUrlsFromCheerio } from '@crawlee/utils';
|
|
7
|
+
import ow from 'ow';
|
|
7
8
|
import { addTimeoutToPromise } from '@apify/timeout';
|
|
8
9
|
import { PlaywrightCrawler } from './playwright-crawler.js';
|
|
9
10
|
import { RenderingTypePredictor, } from './utils/rendering-type-prediction.js';
|
|
@@ -82,23 +83,35 @@ const proxyLogMethods = [
|
|
|
82
83
|
* @experimental
|
|
83
84
|
*/
|
|
84
85
|
export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
85
|
-
renderingTypePredictor;
|
|
86
|
-
resultChecker;
|
|
87
|
-
shouldPropagateError;
|
|
88
|
-
resultComparator;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
individualRequestHandlerTimeoutMillis;
|
|
86
|
+
#renderingTypePredictor;
|
|
87
|
+
#resultChecker;
|
|
88
|
+
#shouldPropagateError;
|
|
89
|
+
#resultComparator;
|
|
90
|
+
#staticContextPipeline;
|
|
91
|
+
#browserContextPipeline;
|
|
92
|
+
#individualRequestHandlerTimeoutMillis;
|
|
93
93
|
// The constructor always injects an `AdaptivePlaywrightCrawlerStatistics`, so narrowing the cast is sound.
|
|
94
94
|
get stats() {
|
|
95
95
|
return super.stats;
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The write policy of the per-attempt transactions. Defaults the request queue to `deferred`:
|
|
99
|
+
* a discarded attempt's enqueues must never reach the queue.
|
|
100
|
+
*/
|
|
101
|
+
#attemptWritePolicy;
|
|
102
|
+
#teardownHooks = [];
|
|
100
103
|
constructor(options = {}) {
|
|
101
|
-
const { requestHandler, renderingTypeDetectionRatio = 0.1, renderingTypePredictor, resultChecker, shouldPropagateError, resultComparator, statistics,
|
|
104
|
+
const { requestHandler, renderingTypeDetectionRatio = 0.1, renderingTypePredictor, resultChecker, shouldPropagateError, resultComparator, statistics, requestHandlerTimeoutSecs = 60, errorHandler, failedRequestHandler, preNavigationHooks = [], postNavigationHooks = [], extendContext, contextPipelineBuilder, transactionalStorage, ...rest } = options;
|
|
105
|
+
// The user's value is replaced by `false` in the `super` call below — validate it separately.
|
|
106
|
+
ow(transactionalStorage, 'transactionalStorage', BasicCrawler.optionsShape.transactionalStorage);
|
|
107
|
+
// Per-attempt buffering is load-bearing here: the handler runs up to twice per request and the
|
|
108
|
+
// losing attempt's writes must be discardable.
|
|
109
|
+
if (transactionalStorage === false) {
|
|
110
|
+
throw new Error('AdaptivePlaywrightCrawler requires transactional storage - it runs the request handler ' +
|
|
111
|
+
'multiple times per request and must be able to discard the storage writes of losing ' +
|
|
112
|
+
'attempts. `transactionalStorage: false` is therefore not supported; a write policy ' +
|
|
113
|
+
'object is accepted and forwarded to the per-attempt transactions.');
|
|
114
|
+
}
|
|
102
115
|
if (statistics !== undefined && !(statistics instanceof AdaptivePlaywrightCrawlerStatistics)) {
|
|
103
116
|
throw new Error('AdaptivePlaywrightCrawler tracks extra fields on its own Statistics subclass and cannot use a ' +
|
|
104
117
|
'plain `statistics` instance. Omit the option to let the crawler build its own.');
|
|
@@ -115,21 +128,29 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
115
128
|
logMessage: `${AdaptivePlaywrightCrawler.name} request statistics:`,
|
|
116
129
|
}),
|
|
117
130
|
contextPipelineBuilder: contextPipelineBuilder ?? (() => this.buildContextPipeline()),
|
|
131
|
+
// The base crawler must not wrap requests in a transaction of its own - this crawler opens
|
|
132
|
+
// one per request handler attempt in `crawlOne` instead, forwarding the write policy of the
|
|
133
|
+
// user-facing option (validated above) to those.
|
|
134
|
+
transactionalStorage: false,
|
|
118
135
|
});
|
|
119
|
-
this
|
|
136
|
+
this.#individualRequestHandlerTimeoutMillis = requestHandlerTimeoutSecs * 1000;
|
|
120
137
|
// `renderingTypeDetectionRatio` only configures the default predictor - an injected one brings its own
|
|
121
138
|
// detection ratio (and its own state), so the option is ignored in that case.
|
|
122
|
-
this
|
|
123
|
-
this
|
|
124
|
-
|
|
139
|
+
this.#renderingTypePredictor = OwnedOrInjected.resolve(renderingTypePredictor, () => new RenderingTypePredictor({ detectionRatio: renderingTypeDetectionRatio }));
|
|
140
|
+
this.#attemptWritePolicy = {
|
|
141
|
+
requestQueue: 'deferred',
|
|
142
|
+
...(typeof transactionalStorage === 'object' ? transactionalStorage : {}),
|
|
143
|
+
};
|
|
144
|
+
this.#resultChecker = resultChecker ?? (() => true);
|
|
145
|
+
this.#shouldPropagateError = shouldPropagateError ?? (() => false);
|
|
125
146
|
if (resultComparator !== undefined) {
|
|
126
|
-
this
|
|
147
|
+
this.#resultComparator = resultComparator;
|
|
127
148
|
}
|
|
128
149
|
else if (resultChecker !== undefined) {
|
|
129
|
-
this
|
|
150
|
+
this.#resultComparator = (resultA, resultB) => this.#resultChecker(resultA) && this.#resultChecker(resultB);
|
|
130
151
|
}
|
|
131
152
|
else {
|
|
132
|
-
this
|
|
153
|
+
this.#resultComparator = (resultA, resultB) => {
|
|
133
154
|
return (resultA.datasetItems.length === resultB.datasetItems.length &&
|
|
134
155
|
resultA.datasetItems.every((itemA, i) => {
|
|
135
156
|
const itemB = resultB.datasetItems[i];
|
|
@@ -159,20 +180,19 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
159
180
|
postNavigationHooks: postNavigationHooks,
|
|
160
181
|
extendContext,
|
|
161
182
|
});
|
|
162
|
-
this
|
|
163
|
-
this
|
|
183
|
+
this.#teardownHooks.push(browserCrawler.teardown.bind(browserCrawler));
|
|
184
|
+
this.#staticContextPipeline = staticCrawler.contextPipeline.compose({
|
|
164
185
|
action: this.adaptCheerioContext.bind(this),
|
|
165
186
|
});
|
|
166
|
-
this
|
|
187
|
+
this.#browserContextPipeline = browserCrawler.contextPipeline.compose({
|
|
167
188
|
action: this.adaptPlaywrightContext.bind(this),
|
|
168
189
|
});
|
|
169
|
-
this.preventDirectStorageAccess = preventDirectStorageAccess;
|
|
170
190
|
}
|
|
171
|
-
async
|
|
191
|
+
async init() {
|
|
172
192
|
// Only the predictor we built ourselves is ours to initialize - an injected one is borrowed, so its
|
|
173
193
|
// lifecycle (including restoring persisted state) stays with whoever created it.
|
|
174
|
-
await this
|
|
175
|
-
return await super.
|
|
194
|
+
await this.#renderingTypePredictor.ifOwned((predictor) => predictor.initialize());
|
|
195
|
+
return await super.init();
|
|
176
196
|
}
|
|
177
197
|
buildContextPipeline() {
|
|
178
198
|
const errorMessage = (prop) => `The \`${prop}\` property is not available on the outer context pipeline of AdaptivePlaywrightCrawler - it is provided by the inner (static/browser) pipelines`;
|
|
@@ -203,11 +223,6 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
203
223
|
});
|
|
204
224
|
}
|
|
205
225
|
async adaptCheerioContext(cheerioContext) {
|
|
206
|
-
// Capture the original response to avoid infinite recursion when the getter is copied to the context
|
|
207
|
-
const result = this.resultObjects.get(cheerioContext);
|
|
208
|
-
if (result === undefined) {
|
|
209
|
-
throw new Error('Logical error - `this.resultObjects` does not contain the result object');
|
|
210
|
-
}
|
|
211
226
|
return {
|
|
212
227
|
get page() {
|
|
213
228
|
throw new Error('Page object was used in HTTP-only request handler');
|
|
@@ -221,17 +236,14 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
221
236
|
enqueueLinks: async (options = {}) => {
|
|
222
237
|
const urls = options.urls ??
|
|
223
238
|
extractUrlsFromCheerio(cheerioContext.$, options.selector, options.baseUrl ?? cheerioContext.request.loadedUrl);
|
|
224
|
-
return (await this.enqueueLinks({ ...options, urls }, cheerioContext.request
|
|
239
|
+
return (await this.enqueueLinks({ ...options, urls }, cheerioContext.request));
|
|
225
240
|
},
|
|
226
241
|
response: cheerioContext.response,
|
|
227
242
|
};
|
|
228
243
|
}
|
|
229
244
|
async adaptPlaywrightContext(playwrightContext) {
|
|
245
|
+
// Capture the original response to avoid infinite recursion when the getter is copied to the context
|
|
230
246
|
const originalResponse = playwrightContext.response;
|
|
231
|
-
const result = this.resultObjects.get(playwrightContext);
|
|
232
|
-
if (result === undefined) {
|
|
233
|
-
throw new Error('Logical error - `this.resultObjects` does not contain the result object');
|
|
234
|
-
}
|
|
235
247
|
return {
|
|
236
248
|
response: new Response(Uint8Array.from(await originalResponse.body()), {
|
|
237
249
|
headers: originalResponse.headers(),
|
|
@@ -264,48 +276,50 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
264
276
|
else {
|
|
265
277
|
urls = options.urls;
|
|
266
278
|
}
|
|
267
|
-
return (await this.enqueueLinks({ ...options, urls }, playwrightContext.request
|
|
279
|
+
return (await this.enqueueLinks({ ...options, urls }, playwrightContext.request));
|
|
268
280
|
},
|
|
269
281
|
};
|
|
270
282
|
}
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Runs one request handler attempt inside its own {@link StorageTransaction}, wrapping the inner
|
|
285
|
+
* (static or browser) context pipeline. The transaction is pushed to `transactions` *at creation
|
|
286
|
+
* time, before the `try`* - the `ok: false` branch of the returned {@link Result} carries no
|
|
287
|
+
* result, and failed attempts are routine here. The caller owns the outcome and disposal.
|
|
288
|
+
*/
|
|
289
|
+
async crawlOne(renderingType, context, useStateFunction, transactions) {
|
|
290
|
+
const transaction = createStorageTransaction({
|
|
291
|
+
policy: this.#attemptWritePolicy,
|
|
292
|
+
commitTimeoutMillis: this.internalTimeoutMillis,
|
|
293
|
+
});
|
|
294
|
+
transactions.push(transaction);
|
|
273
295
|
const logs = [];
|
|
274
296
|
const deferredCleanup = [];
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
pushData: result.pushData,
|
|
278
|
-
useState: this.allowStorageAccess(useStateFunction),
|
|
279
|
-
getKeyValueStore: this.allowStorageAccess(result.getKeyValueStore),
|
|
297
|
+
const attemptBoundContextHelpers = {
|
|
298
|
+
useState: useStateFunction,
|
|
280
299
|
log: this.createLogProxy(context.log, logs),
|
|
281
300
|
registerDeferredCleanup: (cleanup) => deferredCleanup.push(cleanup),
|
|
282
301
|
};
|
|
283
302
|
const subCrawlerContext = Object.defineProperties({}, Object.getOwnPropertyDescriptors(context));
|
|
284
|
-
// Mark
|
|
285
|
-
// (which would otherwise override them with the sub-crawler's own versions, losing the
|
|
286
|
-
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(
|
|
303
|
+
// Mark attempt-bound helpers as non-configurable so they survive the sub-crawler context pipeline
|
|
304
|
+
// (which would otherwise override them with the sub-crawler's own versions, losing the binding).
|
|
305
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(attemptBoundContextHelpers))) {
|
|
287
306
|
Object.defineProperty(subCrawlerContext, key, { ...descriptor, configurable: false });
|
|
288
307
|
}
|
|
289
|
-
this.resultObjects.set(subCrawlerContext, result);
|
|
290
308
|
try {
|
|
291
309
|
const callAdaptiveRequestHandler = async () => {
|
|
292
310
|
if (renderingType === 'static') {
|
|
293
|
-
await this
|
|
311
|
+
await this.#staticContextPipeline.call(subCrawlerContext, this.requestHandler.bind(this));
|
|
294
312
|
}
|
|
295
313
|
else if (renderingType === 'clientOnly') {
|
|
296
|
-
await this
|
|
314
|
+
await this.#browserContextPipeline.call(subCrawlerContext, this.requestHandler.bind(this));
|
|
297
315
|
}
|
|
298
316
|
};
|
|
299
317
|
// this crawler overrides `runRequestHandler` and times each rendering-type run itself, so it has
|
|
300
318
|
// to resolve any per-route override too - otherwise routes would be silently ignored here
|
|
301
319
|
const routeTimeoutSecs = this.requestHandler.getTimeoutSecs?.(context.request.label);
|
|
302
|
-
const timeoutMillis = routeTimeoutSecs === undefined ? this
|
|
303
|
-
await addTimeoutToPromise(async () =>
|
|
304
|
-
|
|
305
|
-
throw new Error('Directly accessing storage in a request handler is not allowed in AdaptivePlaywrightCrawler');
|
|
306
|
-
}
|
|
307
|
-
}, callAdaptiveRequestHandler), timeoutMillis, 'Request handler timed out');
|
|
308
|
-
return { result, ok: true, logs };
|
|
320
|
+
const timeoutMillis = routeTimeoutSecs === undefined ? this.#individualRequestHandlerTimeoutMillis : routeTimeoutSecs * 1000;
|
|
321
|
+
await addTimeoutToPromise(async () => transaction.run(callAdaptiveRequestHandler), timeoutMillis, 'Request handler timed out');
|
|
322
|
+
return { result: transaction, ok: true, logs };
|
|
309
323
|
}
|
|
310
324
|
catch (error) {
|
|
311
325
|
return { error, ok: false, logs };
|
|
@@ -315,23 +329,24 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
315
329
|
}
|
|
316
330
|
}
|
|
317
331
|
async runRequestHandler(crawlingContext) {
|
|
318
|
-
const renderingTypePrediction = this
|
|
332
|
+
const renderingTypePrediction = this.#renderingTypePredictor.value.predict(crawlingContext.request);
|
|
319
333
|
const shouldDetectRenderingType = Math.random() < renderingTypePrediction.detectionProbabilityRecommendation;
|
|
320
334
|
if (!shouldDetectRenderingType) {
|
|
321
335
|
crawlingContext.log.debug(`Predicted rendering type ${renderingTypePrediction.renderingType} for ${crawlingContext.request.url}`);
|
|
322
336
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
337
|
+
// Every transaction created for this request - up to two, since the static-then-browser
|
|
338
|
+
// fall-through and the browser-then-detection pair are mutually exclusive. Disposed in the
|
|
339
|
+
// `finally` below, not earlier: the comparators read the journals after `crawlOne` returns.
|
|
340
|
+
const transactions = [];
|
|
326
341
|
try {
|
|
327
342
|
if (renderingTypePrediction.renderingType === 'static' && !shouldDetectRenderingType) {
|
|
328
343
|
crawlingContext.log.debug(`Running HTTP-only request handler for ${crawlingContext.request.url}`);
|
|
329
344
|
this.stats.trackHttpOnlyRequestHandlerRun();
|
|
330
|
-
const plainHTTPRun = await this.crawlOne('static', crawlingContext, crawlingContext.useState);
|
|
331
|
-
if (plainHTTPRun.ok && this
|
|
345
|
+
const plainHTTPRun = await this.crawlOne('static', crawlingContext, crawlingContext.useState, transactions);
|
|
346
|
+
if (plainHTTPRun.ok && this.#resultChecker(plainHTTPRun.result)) {
|
|
332
347
|
crawlingContext.log.debug(`HTTP-only request handler succeeded for ${crawlingContext.request.url}`);
|
|
333
348
|
plainHTTPRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
|
|
334
|
-
await
|
|
349
|
+
await plainHTTPRun.result.commit();
|
|
335
350
|
return;
|
|
336
351
|
}
|
|
337
352
|
// Execution will "fall through" and try running the request handler in a browser
|
|
@@ -339,7 +354,7 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
339
354
|
const actualError = plainHTTPRun.error instanceof RequestHandlerError
|
|
340
355
|
? plainHTTPRun.error.cause
|
|
341
356
|
: plainHTTPRun.error;
|
|
342
|
-
if (await this
|
|
357
|
+
if (await this.#shouldPropagateError(actualError, crawlingContext)) {
|
|
343
358
|
throw actualError;
|
|
344
359
|
}
|
|
345
360
|
crawlingContext.log.exception(actualError, `HTTP-only request handler failed for ${crawlingContext.request.url}`);
|
|
@@ -371,20 +386,22 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
371
386
|
return this.stateCopy;
|
|
372
387
|
},
|
|
373
388
|
};
|
|
374
|
-
const browserRun = await this.crawlOne('clientOnly', crawlingContext, stateTracker.getLiveState.bind(stateTracker));
|
|
389
|
+
const browserRun = await this.crawlOne('clientOnly', crawlingContext, stateTracker.getLiveState.bind(stateTracker), transactions);
|
|
375
390
|
if (!browserRun.ok) {
|
|
376
391
|
throw browserRun.error;
|
|
377
392
|
}
|
|
378
393
|
browserRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
|
|
379
|
-
await
|
|
394
|
+
await browserRun.result.commit();
|
|
380
395
|
if (shouldDetectRenderingType) {
|
|
381
396
|
crawlingContext.log.debug(`Detecting rendering type for ${crawlingContext.request.url}`);
|
|
382
|
-
|
|
397
|
+
// The detection attempt's transaction is never committed - its writes exist only for the
|
|
398
|
+
// result comparison.
|
|
399
|
+
const plainHTTPRun = await this.crawlOne('static', crawlingContext, stateTracker.getStateCopy.bind(stateTracker), transactions);
|
|
383
400
|
const detectionResult = (() => {
|
|
384
401
|
if (!plainHTTPRun.ok) {
|
|
385
402
|
return 'clientOnly';
|
|
386
403
|
}
|
|
387
|
-
const comparisonResult = this
|
|
404
|
+
const comparisonResult = this.#resultComparator(plainHTTPRun.result, browserRun.result);
|
|
388
405
|
if (comparisonResult === true || comparisonResult === 'equal') {
|
|
389
406
|
return 'static';
|
|
390
407
|
}
|
|
@@ -395,60 +412,28 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
395
412
|
})();
|
|
396
413
|
crawlingContext.log.debug(`Detected rendering type ${detectionResult} for ${crawlingContext.request.url}`);
|
|
397
414
|
if (detectionResult !== undefined) {
|
|
398
|
-
this
|
|
415
|
+
this.#renderingTypePredictor.value.storeResult(crawlingContext.request, detectionResult);
|
|
399
416
|
}
|
|
400
417
|
}
|
|
401
418
|
}
|
|
402
419
|
finally {
|
|
403
|
-
|
|
404
|
-
|
|
420
|
+
// A still-open transaction here belongs to a discarded attempt - roll it back, then release.
|
|
421
|
+
for (const transaction of transactions) {
|
|
422
|
+
transaction.rollback();
|
|
423
|
+
transaction.dispose();
|
|
405
424
|
}
|
|
406
425
|
}
|
|
407
426
|
}
|
|
408
|
-
async
|
|
409
|
-
await Promise.all([
|
|
410
|
-
...calls.pushData.map(async (params) => crawlingContext.pushData(...params)),
|
|
411
|
-
...calls.addRequests.map(async (params) => crawlingContext.addRequests(...params)),
|
|
412
|
-
...Object.entries(keyValueStoreChanges).map(async ([storeIdOrName, changes]) => {
|
|
413
|
-
const store = await crawlingContext.getKeyValueStore({ id: storeIdOrName });
|
|
414
|
-
await Promise.all(Object.entries(changes).map(async ([key, { changedValue, options }]) => store.setValue(key, changedValue, options)));
|
|
415
|
-
}),
|
|
416
|
-
]);
|
|
417
|
-
}
|
|
418
|
-
allowStorageAccess(func) {
|
|
419
|
-
return async (...args) => withCheckedStorageAccess(() => { }, async () => func(...args));
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Reading the pending request count queries the underlying request manager, which counts as storage access.
|
|
423
|
-
* Since this is internal crawler bookkeeping used to compute the `enqueueLinks` limit (not user-initiated storage
|
|
424
|
-
* access), it must be allowed even while a request handler runs inside the storage-access guard.
|
|
425
|
-
*/
|
|
426
|
-
async getPendingRequestCountApproximation() {
|
|
427
|
-
return this.allowStorageAccess(() => super.getPendingRequestCountApproximation())();
|
|
428
|
-
}
|
|
429
|
-
async enqueueLinks(options, request, result) {
|
|
427
|
+
async enqueueLinks(options, request) {
|
|
430
428
|
const baseUrl = resolveBaseUrlForEnqueueLinksFiltering({
|
|
431
429
|
enqueueStrategy: options?.strategy,
|
|
432
430
|
finalRequestUrl: request.loadedUrl,
|
|
433
431
|
originalRequestUrl: request.url,
|
|
434
432
|
userProvidedBaseUrl: options?.baseUrl,
|
|
435
433
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
addedRequests: requests.map(({ uniqueKey, id }) => ({
|
|
440
|
-
uniqueKey,
|
|
441
|
-
requestId: id ?? '',
|
|
442
|
-
wasAlreadyPresent: false,
|
|
443
|
-
wasAlreadyHandled: false,
|
|
444
|
-
})),
|
|
445
|
-
waitForAllRequestsToBeAdded: Promise.resolve([]),
|
|
446
|
-
requestsOverLimit: [],
|
|
447
|
-
};
|
|
448
|
-
};
|
|
449
|
-
// We need to use a mock request queue implementation, in order to add the requests into our result object
|
|
450
|
-
const mockRequestQueue = { addRequestsBatched };
|
|
451
|
-
return await this.enqueueLinksWithCrawlDepth({ ...options, baseUrl }, request, mockRequestQueue);
|
|
434
|
+
// The per-attempt transaction buffers these (the queue policy defaults to `deferred` here),
|
|
435
|
+
// so a discarded attempt's enqueues never reach the queue.
|
|
436
|
+
return await this.enqueueLinksWithCrawlDepth({ ...options, baseUrl }, request, await this.getRequestManager());
|
|
452
437
|
}
|
|
453
438
|
createLogProxy(log, logs) {
|
|
454
439
|
return new Proxy(log, {
|
|
@@ -464,7 +449,7 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
464
449
|
}
|
|
465
450
|
async teardown() {
|
|
466
451
|
await super.teardown();
|
|
467
|
-
for (const hook of this
|
|
452
|
+
for (const hook of this.#teardownHooks) {
|
|
468
453
|
await hook();
|
|
469
454
|
}
|
|
470
455
|
}
|
|
@@ -498,6 +483,5 @@ export function createAdaptivePlaywrightRouter(routesOrSchemas) {
|
|
|
498
483
|
export function fullResultComparator(resultA, resultB) {
|
|
499
484
|
return (isDeepStrictEqual(resultA.datasetItems, resultB.datasetItems) &&
|
|
500
485
|
isDeepStrictEqual(resultA.enqueuedUrls, resultB.enqueuedUrls) &&
|
|
501
|
-
isDeepStrictEqual(resultA.enqueuedUrlLists, resultB.enqueuedUrlLists) &&
|
|
502
486
|
isDeepStrictEqual(resultA.keyValueStoreChanges, resultB.keyValueStoreChanges));
|
|
503
487
|
}
|
|
@@ -215,6 +215,8 @@ export declare class PlaywrightCrawler<ContextExtension = Dictionary<never>, Ext
|
|
|
215
215
|
retryOnBlocked: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
|
|
216
216
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
217
217
|
respectRobotsTxtFile: import("ow").AnyPredicate<boolean | object>;
|
|
218
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
219
|
+
transactionalStorage: import("ow").BasePredicate<boolean | Partial<import("@crawlee/browser").StorageWritePolicy> | undefined>;
|
|
218
220
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
219
221
|
onSkippedRequest: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
|
|
220
222
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
@@ -262,7 +264,7 @@ export declare class PlaywrightCrawler<ContextExtension = Dictionary<never>, Ext
|
|
|
262
264
|
closeCookieModals: () => Promise<void>;
|
|
263
265
|
handleCloudflareChallenge: (options?: HandleCloudflareChallengeOptions) => Promise<Response | undefined>;
|
|
264
266
|
}>;
|
|
265
|
-
protected
|
|
267
|
+
protected navigationHandler(crawlingContext: PlaywrightCrawlingContext, gotoOptions: DirectNavigationOptions): Promise<Response | null>;
|
|
266
268
|
private enhanceContext;
|
|
267
269
|
}
|
|
268
270
|
/**
|
|
@@ -109,7 +109,7 @@ export class PlaywrightCrawler extends BrowserCrawler {
|
|
|
109
109
|
buildContextPipeline() {
|
|
110
110
|
return super.buildContextPipeline().compose({ action: this.enhanceContext.bind(this) });
|
|
111
111
|
}
|
|
112
|
-
async
|
|
112
|
+
async navigationHandler(crawlingContext, gotoOptions) {
|
|
113
113
|
return gotoExtended(crawlingContext.page, crawlingContext.request, gotoOptions);
|
|
114
114
|
}
|
|
115
115
|
async enhanceContext(context) {
|
|
@@ -26,7 +26,7 @@ export interface IRenderingTypePredictor {
|
|
|
26
26
|
* @experimental
|
|
27
27
|
*/
|
|
28
28
|
export declare class RenderingTypePredictor implements IRenderingTypePredictor {
|
|
29
|
-
private
|
|
29
|
+
#private;
|
|
30
30
|
private state;
|
|
31
31
|
constructor({ detectionRatio, persistenceOptions }: RenderingTypePredictorOptions);
|
|
32
32
|
/**
|
|
@@ -30,10 +30,11 @@ const mean = (values) => (values.length > 0 ? sum(values) / values.length : unde
|
|
|
30
30
|
* @experimental
|
|
31
31
|
*/
|
|
32
32
|
export class RenderingTypePredictor {
|
|
33
|
-
detectionRatio;
|
|
33
|
+
#detectionRatio;
|
|
34
|
+
// kept as TS-private: tests reach for it at runtime
|
|
34
35
|
state;
|
|
35
36
|
constructor({ detectionRatio, persistenceOptions }) {
|
|
36
|
-
this
|
|
37
|
+
this.#detectionRatio = detectionRatio;
|
|
37
38
|
this.state = new RecoverableState({
|
|
38
39
|
defaultState: {
|
|
39
40
|
logreg: new LogisticRegression({ numSteps: 1000, learningRate: 0.05 }),
|
|
@@ -86,7 +87,7 @@ export class RenderingTypePredictor {
|
|
|
86
87
|
renderingType: prediction === 1 ? 'static' : 'clientOnly',
|
|
87
88
|
detectionProbabilityRecommendation: Math.abs(scores[0] - scores[1]) < 0.1
|
|
88
89
|
? 1
|
|
89
|
-
: this
|
|
90
|
+
: this.#detectionRatio * Math.max(1, 5 - this.resultCount(label)),
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
/**
|
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.106",
|
|
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,15 +49,14 @@
|
|
|
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.106",
|
|
53
|
+
"@crawlee/browser": "4.0.0-beta.106",
|
|
54
|
+
"@crawlee/browser-pool": "4.0.0-beta.106",
|
|
55
|
+
"@crawlee/cheerio": "4.0.0-beta.106",
|
|
56
|
+
"@crawlee/core": "4.0.0-beta.106",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.106",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.106",
|
|
59
59
|
"cheerio": "^1.0.0",
|
|
60
|
-
"idcac-playwright": "^0.1.3",
|
|
61
60
|
"jquery": "^3.7.1",
|
|
62
61
|
"ml-logistic-regression": "^2.0.0",
|
|
63
62
|
"ml-matrix": "^6.12.1",
|
|
@@ -85,5 +84,5 @@
|
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
},
|
|
88
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "c622f1fc65e65221ea245817c58ecc0ffb4a5cb0"
|
|
89
88
|
}
|