@crawlee/playwright 4.0.0-beta.98 → 4.0.0-rc.0
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/index.d.ts +1 -0
- package/index.js +1 -0
- package/internals/adaptive-playwright-crawler.d.ts +45 -52
- package/internals/adaptive-playwright-crawler.js +151 -176
- package/internals/enqueue-links/click-elements.d.ts +18 -54
- package/internals/enqueue-links/click-elements.js +27 -51
- package/internals/playwright-browser-pool.d.ts +71 -0
- package/internals/playwright-browser-pool.js +61 -0
- package/internals/playwright-crawler.d.ts +130 -109
- package/internals/playwright-crawler.js +19 -24
- package/internals/playwright-launcher.d.ts +24 -15
- package/internals/playwright-launcher.js +10 -8
- package/internals/utils/playwright-utils.d.ts +5 -7
- package/internals/utils/playwright-utils.js +53 -50
- package/internals/utils/rendering-type-prediction.d.ts +1 -1
- package/internals/utils/rendering-type-prediction.js +44 -27
- package/package.json +11 -12
|
@@ -2,46 +2,28 @@ 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,
|
|
6
|
-
import { extractUrlsFromCheerio } from '@crawlee/utils';
|
|
5
|
+
import { createStorageTransaction, EnqueueStrategy, OwnedOrInjected, parseArgument, RequestHandlerError, resolveBaseUrlForEnqueueLinksFiltering, Router, Statistics, } from '@crawlee/core';
|
|
6
|
+
import { extractUrlsFromCheerio } from '@crawlee/utils/internal';
|
|
7
|
+
import { z } from 'zod';
|
|
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';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
this.state.httpOnlyRequestHandlerRuns = savedState.httpOnlyRequestHandlerRuns;
|
|
29
|
-
this.state.browserRequestHandlerRuns = savedState.browserRequestHandlerRuns;
|
|
30
|
-
this.state.renderingTypeMispredictions = savedState.renderingTypeMispredictions;
|
|
31
|
-
}
|
|
32
|
-
trackHttpOnlyRequestHandlerRun() {
|
|
33
|
-
this.state.httpOnlyRequestHandlerRuns ??= 0;
|
|
34
|
-
this.state.httpOnlyRequestHandlerRuns += 1;
|
|
35
|
-
}
|
|
36
|
-
trackBrowserRequestHandlerRun() {
|
|
37
|
-
this.state.browserRequestHandlerRuns ??= 0;
|
|
38
|
-
this.state.browserRequestHandlerRuns += 1;
|
|
39
|
-
}
|
|
40
|
-
trackRenderingTypeMisprediction() {
|
|
41
|
-
this.state.renderingTypeMispredictions ??= 0;
|
|
42
|
-
this.state.renderingTypeMispredictions += 1;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
11
|
+
const adaptiveStatisticStateSchema = z.object({
|
|
12
|
+
/** How many requests were handled by the HTTP-only request handler. */
|
|
13
|
+
httpOnlyRequestHandlerRuns: z.number().default(0),
|
|
14
|
+
/** How many requests were handled in a browser. */
|
|
15
|
+
browserRequestHandlerRuns: z.number().default(0),
|
|
16
|
+
/** How many times the HTTP-only handler produced a result the `resultChecker` rejected. */
|
|
17
|
+
renderingTypeMispredictions: z.number().default(0),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* The {@link AdaptivePlaywrightCrawlerStatisticState} fields as a {@link Statistics} state extension, defaults
|
|
21
|
+
* and all. A {@link Statistics} instance to be injected into an {@link AdaptivePlaywrightCrawler} has to carry
|
|
22
|
+
* them - `deserialize.extend()` your own fields onto this one and pass the result as `stateExtension`.
|
|
23
|
+
*/
|
|
24
|
+
export const adaptivePlaywrightCrawlerStatisticState = {
|
|
25
|
+
deserialize: adaptiveStatisticStateSchema,
|
|
26
|
+
};
|
|
45
27
|
const proxyLogMethods = [
|
|
46
28
|
'error',
|
|
47
29
|
'exception',
|
|
@@ -82,41 +64,80 @@ const proxyLogMethods = [
|
|
|
82
64
|
* @experimental
|
|
83
65
|
*/
|
|
84
66
|
export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
85
|
-
renderingTypePredictor;
|
|
86
|
-
resultChecker;
|
|
87
|
-
shouldPropagateError;
|
|
88
|
-
resultComparator;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
67
|
+
#renderingTypePredictor;
|
|
68
|
+
#resultChecker;
|
|
69
|
+
#shouldPropagateError;
|
|
70
|
+
#resultComparator;
|
|
71
|
+
#staticContextPipeline;
|
|
72
|
+
#browserContextPipeline;
|
|
73
|
+
#individualRequestHandlerTimeoutMillis;
|
|
74
|
+
/**
|
|
75
|
+
* The write policy of the per-attempt transactions. Defaults the request queue to `deferred`:
|
|
76
|
+
* a discarded attempt's enqueues must never reach the queue.
|
|
77
|
+
*/
|
|
78
|
+
#attemptWritePolicy;
|
|
79
|
+
#teardownHooks = [];
|
|
96
80
|
constructor(options = {}) {
|
|
97
|
-
const { requestHandler, renderingTypeDetectionRatio = 0.1, renderingTypePredictor, resultChecker, shouldPropagateError, resultComparator,
|
|
81
|
+
const { requestHandler, renderingTypeDetectionRatio = 0.1, renderingTypePredictor, resultChecker, shouldPropagateError, resultComparator, statistics, requestHandlerTimeoutSecs = 60, errorHandler, failedRequestHandler, preNavigationHooks = [], postNavigationHooks = [], extendContext, contextPipelineBuilder, transactionalStorage, ...rest } = options;
|
|
82
|
+
// The user's value is replaced by `false` in the `super` call below — validate it separately,
|
|
83
|
+
// wrapped in an object so the error still names the field.
|
|
84
|
+
parseArgument({ transactionalStorage }, z.object({ transactionalStorage: BasicCrawler.optionsShape.transactionalStorage }), 'AdaptivePlaywrightCrawlerOptions');
|
|
85
|
+
// The extra fields are only tracked if the injected instance was built with them - the types enforce that,
|
|
86
|
+
// but plain JS callers would otherwise silently increment `undefined` into a sticky `NaN`. Extend
|
|
87
|
+
// `adaptivePlaywrightCrawlerStatisticState` to satisfy this.
|
|
88
|
+
if (statistics !== undefined) {
|
|
89
|
+
parseArgument(statistics.state, z.object({
|
|
90
|
+
httpOnlyRequestHandlerRuns: z.number(),
|
|
91
|
+
browserRequestHandlerRuns: z.number(),
|
|
92
|
+
renderingTypeMispredictions: z.number(),
|
|
93
|
+
}), 'statistics.state');
|
|
94
|
+
}
|
|
95
|
+
// Per-attempt buffering is load-bearing here: the handler runs up to twice per request and the
|
|
96
|
+
// losing attempt's writes must be discardable.
|
|
97
|
+
if (transactionalStorage === false) {
|
|
98
|
+
throw new Error('AdaptivePlaywrightCrawler requires transactional storage - it runs the request handler ' +
|
|
99
|
+
'multiple times per request and must be able to discard the storage writes of losing ' +
|
|
100
|
+
'attempts. `transactionalStorage: false` is therefore not supported; a write policy ' +
|
|
101
|
+
'object is accepted and forwarded to the per-attempt transactions.');
|
|
102
|
+
}
|
|
98
103
|
super({
|
|
99
104
|
...rest,
|
|
100
105
|
errorHandler,
|
|
101
106
|
failedRequestHandler,
|
|
102
107
|
requestHandler,
|
|
103
108
|
requestHandlerTimeoutSecs,
|
|
109
|
+
// The base would build a `Statistics` without the adaptive fields, so provide a default that has them.
|
|
110
|
+
// The cast covers a `StatisticStateExtension` that adds further fields - those can only come from an
|
|
111
|
+
// injected instance, in which case this default is never built.
|
|
112
|
+
statistics: statistics ??
|
|
113
|
+
new Statistics({
|
|
114
|
+
logMessage: `${AdaptivePlaywrightCrawler.name} request statistics:`,
|
|
115
|
+
stateExtension: adaptivePlaywrightCrawlerStatisticState,
|
|
116
|
+
}),
|
|
104
117
|
contextPipelineBuilder: contextPipelineBuilder ?? (() => this.buildContextPipeline()),
|
|
118
|
+
// The base crawler must not wrap requests in a transaction of its own - this crawler opens
|
|
119
|
+
// one per request handler attempt in `crawlOne` instead, forwarding the write policy of the
|
|
120
|
+
// user-facing option (validated above) to those.
|
|
121
|
+
transactionalStorage: false,
|
|
105
122
|
});
|
|
106
|
-
this
|
|
123
|
+
this.#individualRequestHandlerTimeoutMillis = requestHandlerTimeoutSecs * 1000;
|
|
107
124
|
// `renderingTypeDetectionRatio` only configures the default predictor - an injected one brings its own
|
|
108
125
|
// detection ratio (and its own state), so the option is ignored in that case.
|
|
109
|
-
this
|
|
110
|
-
this
|
|
111
|
-
|
|
126
|
+
this.#renderingTypePredictor = OwnedOrInjected.resolve(renderingTypePredictor, () => new RenderingTypePredictor({ detectionRatio: renderingTypeDetectionRatio }));
|
|
127
|
+
this.#attemptWritePolicy = {
|
|
128
|
+
requestQueue: 'deferred',
|
|
129
|
+
...(typeof transactionalStorage === 'object' ? transactionalStorage : {}),
|
|
130
|
+
};
|
|
131
|
+
this.#resultChecker = resultChecker ?? (() => true);
|
|
132
|
+
this.#shouldPropagateError = shouldPropagateError ?? (() => false);
|
|
112
133
|
if (resultComparator !== undefined) {
|
|
113
|
-
this
|
|
134
|
+
this.#resultComparator = resultComparator;
|
|
114
135
|
}
|
|
115
136
|
else if (resultChecker !== undefined) {
|
|
116
|
-
this
|
|
137
|
+
this.#resultComparator = (resultA, resultB) => this.#resultChecker(resultA) && this.#resultChecker(resultB);
|
|
117
138
|
}
|
|
118
139
|
else {
|
|
119
|
-
this
|
|
140
|
+
this.#resultComparator = (resultA, resultB) => {
|
|
120
141
|
return (resultA.datasetItems.length === resultB.datasetItems.length &&
|
|
121
142
|
resultA.datasetItems.every((itemA, i) => {
|
|
122
143
|
const itemB = resultB.datasetItems[i];
|
|
@@ -134,40 +155,31 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
134
155
|
// each hook's overrides at runtime regardless of the static type.
|
|
135
156
|
const staticCrawler = new CheerioCrawler({
|
|
136
157
|
...rest,
|
|
137
|
-
|
|
138
|
-
persistenceOptions: { enable: false },
|
|
139
|
-
},
|
|
158
|
+
statistics: new Statistics({ persistenceOptions: { enable: false } }),
|
|
140
159
|
preNavigationHooks,
|
|
141
160
|
postNavigationHooks,
|
|
142
161
|
extendContext,
|
|
143
162
|
});
|
|
144
163
|
const browserCrawler = new PlaywrightCrawler({
|
|
145
164
|
...rest,
|
|
146
|
-
|
|
147
|
-
persistenceOptions: { enable: false },
|
|
148
|
-
},
|
|
165
|
+
statistics: new Statistics({ persistenceOptions: { enable: false } }),
|
|
149
166
|
preNavigationHooks: preNavigationHooks,
|
|
150
167
|
postNavigationHooks: postNavigationHooks,
|
|
151
168
|
extendContext,
|
|
152
169
|
});
|
|
153
|
-
this
|
|
154
|
-
this
|
|
170
|
+
this.#teardownHooks.push(browserCrawler.teardown.bind(browserCrawler));
|
|
171
|
+
this.#staticContextPipeline = staticCrawler.contextPipeline.compose({
|
|
155
172
|
action: this.adaptCheerioContext.bind(this),
|
|
156
173
|
});
|
|
157
|
-
this
|
|
174
|
+
this.#browserContextPipeline = browserCrawler.contextPipeline.compose({
|
|
158
175
|
action: this.adaptPlaywrightContext.bind(this),
|
|
159
176
|
});
|
|
160
|
-
this.stats = new AdaptivePlaywrightCrawlerStatistics({
|
|
161
|
-
logMessage: `${this.log.getOptions().prefix} request statistics:`,
|
|
162
|
-
...statisticsOptions,
|
|
163
|
-
});
|
|
164
|
-
this.preventDirectStorageAccess = preventDirectStorageAccess;
|
|
165
177
|
}
|
|
166
|
-
async
|
|
178
|
+
async init() {
|
|
167
179
|
// Only the predictor we built ourselves is ours to initialize - an injected one is borrowed, so its
|
|
168
180
|
// lifecycle (including restoring persisted state) stays with whoever created it.
|
|
169
|
-
await this
|
|
170
|
-
return await super.
|
|
181
|
+
await this.#renderingTypePredictor.ifOwned((predictor) => predictor.initialize());
|
|
182
|
+
return await super.init();
|
|
171
183
|
}
|
|
172
184
|
buildContextPipeline() {
|
|
173
185
|
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`;
|
|
@@ -194,15 +206,13 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
194
206
|
get parseWithCheerio() {
|
|
195
207
|
throw new Error(errorMessage('parseWithCheerio'));
|
|
196
208
|
},
|
|
209
|
+
get enqueueLinks() {
|
|
210
|
+
throw new Error(errorMessage('enqueueLinks'));
|
|
211
|
+
},
|
|
197
212
|
}),
|
|
198
213
|
});
|
|
199
214
|
}
|
|
200
215
|
async adaptCheerioContext(cheerioContext) {
|
|
201
|
-
// Capture the original response to avoid infinite recursion when the getter is copied to the context
|
|
202
|
-
const result = this.resultObjects.get(cheerioContext);
|
|
203
|
-
if (result === undefined) {
|
|
204
|
-
throw new Error('Logical error - `this.resultObjects` does not contain the result object');
|
|
205
|
-
}
|
|
206
216
|
return {
|
|
207
217
|
get page() {
|
|
208
218
|
throw new Error('Page object was used in HTTP-only request handler');
|
|
@@ -214,19 +224,15 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
214
224
|
return cheerioContext.$(selector);
|
|
215
225
|
},
|
|
216
226
|
enqueueLinks: async (options = {}) => {
|
|
217
|
-
const urls = options.
|
|
218
|
-
|
|
219
|
-
return (await this.enqueueLinks({ ...options, urls }, cheerioContext.request, result));
|
|
227
|
+
const urls = extractUrlsFromCheerio(cheerioContext.$, options.selector, options.baseUrl ?? cheerioContext.request.loadedUrl);
|
|
228
|
+
return (await this.enqueueLinks(urls, options, cheerioContext.request));
|
|
220
229
|
},
|
|
221
230
|
response: cheerioContext.response,
|
|
222
231
|
};
|
|
223
232
|
}
|
|
224
233
|
async adaptPlaywrightContext(playwrightContext) {
|
|
234
|
+
// Capture the original response to avoid infinite recursion when the getter is copied to the context
|
|
225
235
|
const originalResponse = playwrightContext.response;
|
|
226
|
-
const result = this.resultObjects.get(playwrightContext);
|
|
227
|
-
if (result === undefined) {
|
|
228
|
-
throw new Error('Logical error - `this.resultObjects` does not contain the result object');
|
|
229
|
-
}
|
|
230
236
|
return {
|
|
231
237
|
response: new Response(Uint8Array.from(await originalResponse.body()), {
|
|
232
238
|
headers: originalResponse.headers(),
|
|
@@ -247,60 +253,54 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
247
253
|
},
|
|
248
254
|
enqueueLinks: async (options = {}, timeoutMs = 5000) => {
|
|
249
255
|
// TODO consider using `context.parseWithCheerio` to make this universal and avoid code duplication
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
urls =
|
|
256
|
-
options.urls ??
|
|
257
|
-
(await extractUrlsFromPage(playwrightContext.page, selector, options.baseUrl ?? playwrightContext.request.loadedUrl));
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
urls = options.urls;
|
|
261
|
-
}
|
|
262
|
-
return (await this.enqueueLinks({ ...options, urls }, playwrightContext.request, result));
|
|
256
|
+
const selector = options.selector ?? 'a';
|
|
257
|
+
const locator = playwrightContext.page.locator(selector).first();
|
|
258
|
+
await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
|
|
259
|
+
const urls = await extractUrlsFromPage(playwrightContext.page, selector, options.baseUrl ?? playwrightContext.request.loadedUrl);
|
|
260
|
+
return (await this.enqueueLinks(urls, options, playwrightContext.request));
|
|
263
261
|
},
|
|
264
262
|
};
|
|
265
263
|
}
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Runs one request handler attempt inside its own {@link StorageTransaction}, wrapping the inner
|
|
266
|
+
* (static or browser) context pipeline. The transaction is pushed to `transactions` *at creation
|
|
267
|
+
* time, before the `try`* - the `ok: false` branch of the returned {@link Result} carries no
|
|
268
|
+
* result, and failed attempts are routine here. The caller owns the outcome and disposal.
|
|
269
|
+
*/
|
|
270
|
+
async crawlOne(renderingType, context, useStateFunction, transactions) {
|
|
271
|
+
const transaction = createStorageTransaction({
|
|
272
|
+
policy: this.#attemptWritePolicy,
|
|
273
|
+
commitTimeoutMillis: this.internalTimeoutMillis,
|
|
274
|
+
});
|
|
275
|
+
transactions.push(transaction);
|
|
268
276
|
const logs = [];
|
|
269
277
|
const deferredCleanup = [];
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
pushData: result.pushData,
|
|
273
|
-
useState: this.allowStorageAccess(useStateFunction),
|
|
274
|
-
getKeyValueStore: this.allowStorageAccess(result.getKeyValueStore),
|
|
278
|
+
const attemptBoundContextHelpers = {
|
|
279
|
+
useState: useStateFunction,
|
|
275
280
|
log: this.createLogProxy(context.log, logs),
|
|
276
281
|
registerDeferredCleanup: (cleanup) => deferredCleanup.push(cleanup),
|
|
277
282
|
};
|
|
278
283
|
const subCrawlerContext = Object.defineProperties({}, Object.getOwnPropertyDescriptors(context));
|
|
279
|
-
// Mark
|
|
280
|
-
// (which would otherwise override them with the sub-crawler's own versions, losing the
|
|
281
|
-
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(
|
|
284
|
+
// Mark attempt-bound helpers as non-configurable so they survive the sub-crawler context pipeline
|
|
285
|
+
// (which would otherwise override them with the sub-crawler's own versions, losing the binding).
|
|
286
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(attemptBoundContextHelpers))) {
|
|
282
287
|
Object.defineProperty(subCrawlerContext, key, { ...descriptor, configurable: false });
|
|
283
288
|
}
|
|
284
|
-
this.resultObjects.set(subCrawlerContext, result);
|
|
285
289
|
try {
|
|
286
290
|
const callAdaptiveRequestHandler = async () => {
|
|
287
291
|
if (renderingType === 'static') {
|
|
288
|
-
await this
|
|
292
|
+
await this.#staticContextPipeline.call(subCrawlerContext, this.requestHandler.bind(this));
|
|
289
293
|
}
|
|
290
294
|
else if (renderingType === 'clientOnly') {
|
|
291
|
-
await this
|
|
295
|
+
await this.#browserContextPipeline.call(subCrawlerContext, this.requestHandler.bind(this));
|
|
292
296
|
}
|
|
293
297
|
};
|
|
294
298
|
// this crawler overrides `runRequestHandler` and times each rendering-type run itself, so it has
|
|
295
299
|
// to resolve any per-route override too - otherwise routes would be silently ignored here
|
|
296
300
|
const routeTimeoutSecs = this.requestHandler.getTimeoutSecs?.(context.request.label);
|
|
297
|
-
const timeoutMillis = routeTimeoutSecs === undefined ? this
|
|
298
|
-
await addTimeoutToPromise(async () =>
|
|
299
|
-
|
|
300
|
-
throw new Error('Directly accessing storage in a request handler is not allowed in AdaptivePlaywrightCrawler');
|
|
301
|
-
}
|
|
302
|
-
}, callAdaptiveRequestHandler), timeoutMillis, 'Request handler timed out');
|
|
303
|
-
return { result, ok: true, logs };
|
|
301
|
+
const timeoutMillis = routeTimeoutSecs === undefined ? this.#individualRequestHandlerTimeoutMillis : routeTimeoutSecs * 1000;
|
|
302
|
+
await addTimeoutToPromise(async () => transaction.run(callAdaptiveRequestHandler), timeoutMillis, 'Request handler timed out');
|
|
303
|
+
return { result: transaction, ok: true, logs };
|
|
304
304
|
}
|
|
305
305
|
catch (error) {
|
|
306
306
|
return { error, ok: false, logs };
|
|
@@ -310,23 +310,24 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
async runRequestHandler(crawlingContext) {
|
|
313
|
-
const renderingTypePrediction = this
|
|
313
|
+
const renderingTypePrediction = this.#renderingTypePredictor.value.predict(crawlingContext.request);
|
|
314
314
|
const shouldDetectRenderingType = Math.random() < renderingTypePrediction.detectionProbabilityRecommendation;
|
|
315
315
|
if (!shouldDetectRenderingType) {
|
|
316
316
|
crawlingContext.log.debug(`Predicted rendering type ${renderingTypePrediction.renderingType} for ${crawlingContext.request.url}`);
|
|
317
317
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
// Every transaction created for this request - up to two, since the static-then-browser
|
|
319
|
+
// fall-through and the browser-then-detection pair are mutually exclusive. Disposed in the
|
|
320
|
+
// `finally` below, not earlier: the comparators read the journals after `crawlOne` returns.
|
|
321
|
+
const transactions = [];
|
|
321
322
|
try {
|
|
322
323
|
if (renderingTypePrediction.renderingType === 'static' && !shouldDetectRenderingType) {
|
|
323
324
|
crawlingContext.log.debug(`Running HTTP-only request handler for ${crawlingContext.request.url}`);
|
|
324
|
-
this.
|
|
325
|
-
const plainHTTPRun = await this.crawlOne('static', crawlingContext, crawlingContext.useState);
|
|
326
|
-
if (plainHTTPRun.ok && this
|
|
325
|
+
this.statistics.state.httpOnlyRequestHandlerRuns++;
|
|
326
|
+
const plainHTTPRun = await this.crawlOne('static', crawlingContext, crawlingContext.useState, transactions);
|
|
327
|
+
if (plainHTTPRun.ok && this.#resultChecker(plainHTTPRun.result)) {
|
|
327
328
|
crawlingContext.log.debug(`HTTP-only request handler succeeded for ${crawlingContext.request.url}`);
|
|
328
329
|
plainHTTPRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
|
|
329
|
-
await
|
|
330
|
+
await plainHTTPRun.result.commit();
|
|
330
331
|
return;
|
|
331
332
|
}
|
|
332
333
|
// Execution will "fall through" and try running the request handler in a browser
|
|
@@ -334,18 +335,18 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
334
335
|
const actualError = plainHTTPRun.error instanceof RequestHandlerError
|
|
335
336
|
? plainHTTPRun.error.cause
|
|
336
337
|
: plainHTTPRun.error;
|
|
337
|
-
if (await this
|
|
338
|
+
if (await this.#shouldPropagateError(actualError, crawlingContext)) {
|
|
338
339
|
throw actualError;
|
|
339
340
|
}
|
|
340
341
|
crawlingContext.log.exception(actualError, `HTTP-only request handler failed for ${crawlingContext.request.url}`);
|
|
341
342
|
}
|
|
342
343
|
else {
|
|
343
344
|
crawlingContext.log.warning(`HTTP-only request handler returned a suspicious result for ${crawlingContext.request.url}`);
|
|
344
|
-
this.
|
|
345
|
+
this.statistics.state.renderingTypeMispredictions++;
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
crawlingContext.log.debug(`Running browser request handler for ${crawlingContext.request.url}`);
|
|
348
|
-
this.
|
|
349
|
+
this.statistics.state.browserRequestHandlerRuns++;
|
|
349
350
|
// Run the request handler in a browser. The copy of the crawler state is kept so that we can perform
|
|
350
351
|
// a rendering type detection if necessary. Without this measure, the HTTP request handler would run
|
|
351
352
|
// under different conditions, which could change its behavior. Changes done to the crawler state by
|
|
@@ -366,20 +367,22 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
366
367
|
return this.stateCopy;
|
|
367
368
|
},
|
|
368
369
|
};
|
|
369
|
-
const browserRun = await this.crawlOne('clientOnly', crawlingContext, stateTracker.getLiveState.bind(stateTracker));
|
|
370
|
+
const browserRun = await this.crawlOne('clientOnly', crawlingContext, stateTracker.getLiveState.bind(stateTracker), transactions);
|
|
370
371
|
if (!browserRun.ok) {
|
|
371
372
|
throw browserRun.error;
|
|
372
373
|
}
|
|
373
374
|
browserRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
|
|
374
|
-
await
|
|
375
|
+
await browserRun.result.commit();
|
|
375
376
|
if (shouldDetectRenderingType) {
|
|
376
377
|
crawlingContext.log.debug(`Detecting rendering type for ${crawlingContext.request.url}`);
|
|
377
|
-
|
|
378
|
+
// The detection attempt's transaction is never committed - its writes exist only for the
|
|
379
|
+
// result comparison.
|
|
380
|
+
const plainHTTPRun = await this.crawlOne('static', crawlingContext, stateTracker.getStateCopy.bind(stateTracker), transactions);
|
|
378
381
|
const detectionResult = (() => {
|
|
379
382
|
if (!plainHTTPRun.ok) {
|
|
380
383
|
return 'clientOnly';
|
|
381
384
|
}
|
|
382
|
-
const comparisonResult = this
|
|
385
|
+
const comparisonResult = this.#resultComparator(plainHTTPRun.result, browserRun.result);
|
|
383
386
|
if (comparisonResult === true || comparisonResult === 'equal') {
|
|
384
387
|
return 'static';
|
|
385
388
|
}
|
|
@@ -390,60 +393,33 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
390
393
|
})();
|
|
391
394
|
crawlingContext.log.debug(`Detected rendering type ${detectionResult} for ${crawlingContext.request.url}`);
|
|
392
395
|
if (detectionResult !== undefined) {
|
|
393
|
-
this
|
|
396
|
+
this.#renderingTypePredictor.value.storeResult(crawlingContext.request, detectionResult);
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
399
|
}
|
|
397
400
|
finally {
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
// A still-open transaction here belongs to a discarded attempt - roll it back, then release.
|
|
402
|
+
for (const transaction of transactions) {
|
|
403
|
+
transaction.rollback();
|
|
404
|
+
transaction.dispose();
|
|
400
405
|
}
|
|
401
406
|
}
|
|
402
407
|
}
|
|
403
|
-
async
|
|
404
|
-
await Promise.all([
|
|
405
|
-
...calls.pushData.map(async (params) => crawlingContext.pushData(...params)),
|
|
406
|
-
...calls.addRequests.map(async (params) => crawlingContext.addRequests(...params)),
|
|
407
|
-
...Object.entries(keyValueStoreChanges).map(async ([storeIdOrName, changes]) => {
|
|
408
|
-
const store = await crawlingContext.getKeyValueStore({ id: storeIdOrName });
|
|
409
|
-
await Promise.all(Object.entries(changes).map(async ([key, { changedValue, options }]) => store.setValue(key, changedValue, options)));
|
|
410
|
-
}),
|
|
411
|
-
]);
|
|
412
|
-
}
|
|
413
|
-
allowStorageAccess(func) {
|
|
414
|
-
return async (...args) => withCheckedStorageAccess(() => { }, async () => func(...args));
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Reading the pending request count queries the underlying request manager, which counts as storage access.
|
|
418
|
-
* Since this is internal crawler bookkeeping used to compute the `enqueueLinks` limit (not user-initiated storage
|
|
419
|
-
* access), it must be allowed even while a request handler runs inside the storage-access guard.
|
|
420
|
-
*/
|
|
421
|
-
async getPendingRequestCountApproximation() {
|
|
422
|
-
return this.allowStorageAccess(() => super.getPendingRequestCountApproximation())();
|
|
423
|
-
}
|
|
424
|
-
async enqueueLinks(options, request, result) {
|
|
408
|
+
async enqueueLinks(urls, options, request) {
|
|
425
409
|
const baseUrl = resolveBaseUrlForEnqueueLinksFiltering({
|
|
426
410
|
enqueueStrategy: options?.strategy,
|
|
427
411
|
finalRequestUrl: request.loadedUrl,
|
|
428
412
|
originalRequestUrl: request.url,
|
|
429
413
|
userProvidedBaseUrl: options?.baseUrl,
|
|
430
414
|
});
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
})),
|
|
440
|
-
waitForAllRequestsToBeAdded: Promise.resolve([]),
|
|
441
|
-
requestsOverLimit: [],
|
|
442
|
-
};
|
|
443
|
-
};
|
|
444
|
-
// We need to use a mock request queue implementation, in order to add the requests into our result object
|
|
445
|
-
const mockRequestQueue = { addRequestsBatched };
|
|
446
|
-
return await this.enqueueLinksWithCrawlDepth({ ...options, baseUrl }, request, mockRequestQueue);
|
|
415
|
+
const requestsWithDepth = this.addCrawlDepthRequestGenerator(urls, request.crawlDepth + 1);
|
|
416
|
+
// The per-attempt transaction buffers these (the queue policy defaults to `deferred` here),
|
|
417
|
+
// so a discarded attempt's enqueues never reach the queue.
|
|
418
|
+
return await this.addRequests(requestsWithDepth, {
|
|
419
|
+
...options,
|
|
420
|
+
baseUrl,
|
|
421
|
+
strategy: options.strategy ?? EnqueueStrategy.SameHostname,
|
|
422
|
+
});
|
|
447
423
|
}
|
|
448
424
|
createLogProxy(log, logs) {
|
|
449
425
|
return new Proxy(log, {
|
|
@@ -459,7 +435,7 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
459
435
|
}
|
|
460
436
|
async teardown() {
|
|
461
437
|
await super.teardown();
|
|
462
|
-
for (const hook of this
|
|
438
|
+
for (const hook of this.#teardownHooks) {
|
|
463
439
|
await hook();
|
|
464
440
|
}
|
|
465
441
|
}
|
|
@@ -493,6 +469,5 @@ export function createAdaptivePlaywrightRouter(routesOrSchemas) {
|
|
|
493
469
|
export function fullResultComparator(resultA, resultB) {
|
|
494
470
|
return (isDeepStrictEqual(resultA.datasetItems, resultB.datasetItems) &&
|
|
495
471
|
isDeepStrictEqual(resultA.enqueuedUrls, resultB.enqueuedUrls) &&
|
|
496
|
-
isDeepStrictEqual(resultA.enqueuedUrlLists, resultB.enqueuedUrlLists) &&
|
|
497
472
|
isDeepStrictEqual(resultA.keyValueStoreChanges, resultB.keyValueStoreChanges));
|
|
498
473
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IRequestManager, RequestTransform, SkippedRequestCallback, UrlPatternInput } from '@crawlee/browser';
|
|
2
2
|
import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
|
|
3
3
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
4
4
|
import type { Page } from 'playwright';
|
|
@@ -26,64 +26,29 @@ export interface EnqueueLinksByClickingElementsOptions {
|
|
|
26
26
|
*/
|
|
27
27
|
clickOptions?: ClickOptions;
|
|
28
28
|
/**
|
|
29
|
-
* An array of
|
|
30
|
-
* containing glob pattern strings matching the URLs to be enqueued.
|
|
29
|
+
* An array of URL patterns that URLs must match to be enqueued.
|
|
31
30
|
*
|
|
32
|
-
*
|
|
33
|
-
* All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
|
|
34
|
-
*
|
|
35
|
-
* The matching is always case-insensitive.
|
|
36
|
-
* If you need case-sensitive matching, use `regexps` property directly.
|
|
37
|
-
*
|
|
38
|
-
* If `globs` is an empty array or `undefined`, then the function
|
|
39
|
-
* enqueues all the intercepted navigation requests produced by the page
|
|
40
|
-
* after clicking on elements matching the provided CSS selector.
|
|
41
|
-
*/
|
|
42
|
-
globs?: GlobInput[];
|
|
43
|
-
/**
|
|
44
|
-
* An array of glob pattern strings, regexp patterns or plain objects
|
|
45
|
-
* containing patterns matching URLs that will **never** be enqueued.
|
|
46
|
-
*
|
|
47
|
-
* The plain objects must include either the `glob` property or the `regexp` property.
|
|
31
|
+
* Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
|
|
48
32
|
*
|
|
49
33
|
* Glob matching is always case-insensitive.
|
|
50
|
-
* If you need case-sensitive matching,
|
|
51
|
-
*/
|
|
52
|
-
exclude?: readonly (GlobInput | RegExpInput)[];
|
|
53
|
-
/**
|
|
54
|
-
* An array of regular expressions or plain objects
|
|
55
|
-
* containing regular expressions matching the URLs to be enqueued.
|
|
56
|
-
*
|
|
57
|
-
* The plain objects must include at least the `regexp` property, which holds the regular expression.
|
|
58
|
-
* All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
|
|
34
|
+
* If you need case-sensitive matching, use a `RegExp`.
|
|
59
35
|
*
|
|
60
|
-
* If `
|
|
36
|
+
* If `include` is an empty array or `undefined`, then the function
|
|
61
37
|
* enqueues all the intercepted navigation requests produced by the page
|
|
62
38
|
* after clicking on elements matching the provided CSS selector.
|
|
63
39
|
*/
|
|
64
|
-
|
|
40
|
+
include?: UrlPatternInput[];
|
|
65
41
|
/**
|
|
66
|
-
*
|
|
67
|
-
* Please use `globs` or `regexps` instead.
|
|
68
|
-
*
|
|
69
|
-
* An array of {@link PseudoUrl} strings or plain objects
|
|
70
|
-
* containing {@link PseudoUrl} strings matching the URLs to be enqueued.
|
|
71
|
-
*
|
|
72
|
-
* The plain objects must include at least the `purl` property, which holds the pseudo-URL pattern string.
|
|
73
|
-
* All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
|
|
42
|
+
* An array of URL patterns. Matching URLs will **not** be enqueued.
|
|
74
43
|
*
|
|
75
|
-
*
|
|
76
|
-
* If you need case-sensitive matching, use `regexps` property directly.
|
|
44
|
+
* Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
|
|
77
45
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* after clicking on elements matching the provided CSS selector.
|
|
81
|
-
*
|
|
82
|
-
* @deprecated prefer using `globs` or `regexps` instead
|
|
46
|
+
* Glob matching is always case-insensitive.
|
|
47
|
+
* If you need case-sensitive matching, use a `RegExp`.
|
|
83
48
|
*/
|
|
84
|
-
|
|
49
|
+
exclude?: readonly UrlPatternInput[];
|
|
85
50
|
/**
|
|
86
|
-
* After
|
|
51
|
+
* After request options are filtered by `include`/`exclude` patterns,
|
|
87
52
|
* this function can be used to remove them or modify their contents such as `userData`, `payload` or, most importantly
|
|
88
53
|
* `uniqueKey`. This is useful when you need to enqueue multiple `Requests` to the queue that share the same URL,
|
|
89
54
|
* but differ in methods or payloads, or to dynamically update or create `userData`.
|
|
@@ -98,8 +63,8 @@ export interface EnqueueLinksByClickingElementsOptions {
|
|
|
98
63
|
* }
|
|
99
64
|
* ```
|
|
100
65
|
*
|
|
101
|
-
* Note that `transformRequestFunction` has the highest priority and can overwrite
|
|
102
|
-
*
|
|
66
|
+
* Note that `transformRequestFunction` has the highest priority and can overwrite
|
|
67
|
+
* the global `label` option.
|
|
103
68
|
*
|
|
104
69
|
* The function receives a {@link RequestOptions} object and can return either:
|
|
105
70
|
* - The modified {@link RequestOptions} object
|
|
@@ -160,8 +125,7 @@ export interface EnqueueLinksByClickingElementsOptions {
|
|
|
160
125
|
* in `href` elements, but rather navigations are triggered in click handlers.
|
|
161
126
|
* If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
|
|
162
127
|
*
|
|
163
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of
|
|
164
|
-
* and override settings of the enqueued {@link Request} objects.
|
|
128
|
+
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
165
129
|
*
|
|
166
130
|
* **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
|
|
167
131
|
* such as changing the Z-index of elements being clicked and their visibility. Therefore,
|
|
@@ -183,9 +147,9 @@ export interface EnqueueLinksByClickingElementsOptions {
|
|
|
183
147
|
* page,
|
|
184
148
|
* requestManager,
|
|
185
149
|
* selector: 'a.product-detail',
|
|
186
|
-
*
|
|
187
|
-
* 'https://www.example.com/handbags
|
|
188
|
-
* 'https://www.example.com/purses
|
|
150
|
+
* include: [
|
|
151
|
+
* 'https://www.example.com/handbags/*',
|
|
152
|
+
* 'https://www.example.com/purses/*',
|
|
189
153
|
* ],
|
|
190
154
|
* });
|
|
191
155
|
* ```
|