@crawlee/playwright 4.0.0-beta.2 → 4.0.0-beta.200
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/README.md +17 -13
- package/index.d.ts +4 -3
- package/index.js +2 -2
- package/internals/adaptive-playwright-crawler.d.ts +146 -91
- package/internals/adaptive-playwright-crawler.js +485 -268
- package/internals/enqueue-links/click-elements.d.ts +37 -55
- package/internals/enqueue-links/click-elements.js +65 -55
- package/internals/playwright-browser-pool.d.ts +71 -0
- package/internals/playwright-browser-pool.js +61 -0
- package/internals/playwright-crawler.d.ts +177 -172
- package/internals/playwright-crawler.js +103 -61
- package/internals/playwright-launcher.d.ts +30 -20
- package/internals/playwright-launcher.js +22 -17
- package/internals/utils/playwright-utils.d.ts +55 -50
- package/internals/utils/playwright-utils.js +121 -148
- package/internals/utils/rendering-type-prediction.d.ts +44 -13
- package/internals/utils/rendering-type-prediction.js +95 -29
- package/package.json +15 -15
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/internals/adaptive-playwright-crawler.d.ts.map +0 -1
- package/internals/adaptive-playwright-crawler.js.map +0 -1
- package/internals/enqueue-links/click-elements.d.ts.map +0 -1
- package/internals/enqueue-links/click-elements.js.map +0 -1
- package/internals/playwright-crawler.d.ts.map +0 -1
- package/internals/playwright-crawler.js.map +0 -1
- package/internals/playwright-launcher.d.ts.map +0 -1
- package/internals/playwright-launcher.js.map +0 -1
- package/internals/utils/playwright-utils.d.ts.map +0 -1
- package/internals/utils/playwright-utils.js.map +0 -1
- package/internals/utils/rendering-type-prediction.d.ts.map +0 -1
- package/internals/utils/rendering-type-prediction.js.map +0 -1
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -20,20 +20,47 @@
|
|
|
20
20
|
import { readFile } from 'node:fs/promises';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
22
|
import vm from 'node:vm';
|
|
23
|
-
import { Configuration, KeyValueStore,
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
import ow from 'ow';
|
|
23
|
+
import { Configuration, KeyValueStore, serviceLocator, SessionError, validators } from '@crawlee/browser';
|
|
24
|
+
import { sleep } from '@crawlee/utils';
|
|
25
|
+
import { expandShadowRoots, parseArgument, schemas } from '@crawlee/utils/internal';
|
|
26
|
+
import { z } from 'zod';
|
|
28
27
|
import { LruCache } from '@apify/datastructures';
|
|
29
|
-
import log_ from '@apify/log';
|
|
30
28
|
import { enqueueLinksByClickingElements } from '../enqueue-links/click-elements.js';
|
|
31
|
-
|
|
32
|
-
const log = log_.child({ prefix: 'Playwright Utils' });
|
|
29
|
+
const getLog = () => serviceLocator.getChildLog('Playwright Utils');
|
|
33
30
|
const require = createRequire(import.meta.url);
|
|
34
31
|
const jqueryPath = require.resolve('jquery');
|
|
35
32
|
const MAX_INJECT_FILE_CACHE_SIZE = 10;
|
|
36
33
|
const DEFAULT_BLOCK_REQUEST_URL_PATTERNS = ['.css', '.jpg', '.jpeg', '.png', '.svg', '.gif', '.woff', '.pdf', '.zip'];
|
|
34
|
+
const filePathSchema = z.string();
|
|
35
|
+
const injectFileOptionsSchema = z.strictObject({
|
|
36
|
+
surviveNavigations: z.boolean().optional(),
|
|
37
|
+
});
|
|
38
|
+
const gotoExtendedRequestSchema = z.looseObject({
|
|
39
|
+
url: z.url(),
|
|
40
|
+
method: z.string().optional(),
|
|
41
|
+
headers: schemas.anyObject.optional(),
|
|
42
|
+
payload: z.union([z.string(), z.instanceof(Uint8Array)]).optional(),
|
|
43
|
+
});
|
|
44
|
+
const blockRequestsOptionsSchema = z.strictObject({
|
|
45
|
+
urlPatterns: schemas.arrayOf(z.string(), 'strings').default(DEFAULT_BLOCK_REQUEST_URL_PATTERNS),
|
|
46
|
+
extraUrlPatterns: schemas.arrayOf(z.string(), 'strings').default(() => []),
|
|
47
|
+
});
|
|
48
|
+
const infiniteScrollOptionsSchema = z.strictObject({
|
|
49
|
+
timeoutSecs: schemas.anyNumber.default(0),
|
|
50
|
+
maxScrollHeight: schemas.anyNumber.default(0),
|
|
51
|
+
waitForSecs: schemas.anyNumber.default(4),
|
|
52
|
+
scrollDownAndUp: z.boolean().default(false),
|
|
53
|
+
buttonSelector: z.string().optional(),
|
|
54
|
+
stopScrollCallback: schemas.anyFunction.optional(),
|
|
55
|
+
});
|
|
56
|
+
const saveSnapshotOptionsSchema = z.strictObject({
|
|
57
|
+
key: z.string().min(1).default('SNAPSHOT'),
|
|
58
|
+
screenshotQuality: schemas.anyNumber.default(50),
|
|
59
|
+
saveScreenshot: z.boolean().default(true),
|
|
60
|
+
saveHtml: z.boolean().default(true),
|
|
61
|
+
keyValueStoreName: z.string().optional(),
|
|
62
|
+
configuration: schemas.anyObject.optional(),
|
|
63
|
+
});
|
|
37
64
|
/**
|
|
38
65
|
* Cache contents of previously injected files to limit file system access.
|
|
39
66
|
*/
|
|
@@ -50,21 +77,19 @@ const injectedFilesCache = new LruCache({ maxLength: MAX_INJECT_FILE_CACHE_SIZE
|
|
|
50
77
|
* @param [options]
|
|
51
78
|
*/
|
|
52
79
|
export async function injectFile(page, filePath, options = {}) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
surviveNavigations: ow.optional.boolean,
|
|
57
|
-
}));
|
|
80
|
+
parseArgument(page, validators.browserPage);
|
|
81
|
+
parseArgument(filePath, filePathSchema);
|
|
82
|
+
const { surviveNavigations } = parseArgument(options, injectFileOptionsSchema);
|
|
58
83
|
let contents = injectedFilesCache.get(filePath);
|
|
59
84
|
if (!contents) {
|
|
60
85
|
contents = await readFile(filePath, 'utf8');
|
|
61
86
|
injectedFilesCache.add(filePath, contents);
|
|
62
87
|
}
|
|
63
88
|
const evalP = page.evaluate(contents);
|
|
64
|
-
if (
|
|
89
|
+
if (surviveNavigations) {
|
|
65
90
|
page.on('framenavigated', async () => page
|
|
66
91
|
.evaluate(contents)
|
|
67
|
-
.catch((error) =>
|
|
92
|
+
.catch((error) => getLog().warning('An error occurred during the script injection!', { error })));
|
|
68
93
|
}
|
|
69
94
|
return evalP;
|
|
70
95
|
}
|
|
@@ -95,7 +120,7 @@ export async function injectFile(page, filePath, options = {}) {
|
|
|
95
120
|
* @param [options.surviveNavigations] Opt-out option to disable the JQuery reinjection after navigation.
|
|
96
121
|
*/
|
|
97
122
|
export async function injectJQuery(page, options) {
|
|
98
|
-
|
|
123
|
+
parseArgument(page, validators.browserPage);
|
|
99
124
|
return injectFile(page, jqueryPath, { surviveNavigations: options?.surviveNavigations ?? true });
|
|
100
125
|
}
|
|
101
126
|
/**
|
|
@@ -111,19 +136,14 @@ export async function injectJQuery(page, options) {
|
|
|
111
136
|
* @param [gotoOptions] Custom options for `page.goto()`.
|
|
112
137
|
*/
|
|
113
138
|
export async function gotoExtended(page, request, gotoOptions = {}) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
method: ow.optional.string,
|
|
118
|
-
headers: ow.optional.object,
|
|
119
|
-
payload: ow.optional.any(ow.string, ow.uint8Array),
|
|
120
|
-
}));
|
|
121
|
-
ow(gotoOptions, ow.object);
|
|
139
|
+
parseArgument(page, validators.browserPage);
|
|
140
|
+
parseArgument(request, gotoExtendedRequestSchema);
|
|
141
|
+
parseArgument(gotoOptions, schemas.anyObject);
|
|
122
142
|
const { url, method, headers, payload } = request;
|
|
123
143
|
const isEmpty = (o) => !o || Object.keys(o).length === 0;
|
|
124
|
-
if (method !== 'GET' || payload
|
|
144
|
+
if (method !== 'GET' || payload) {
|
|
125
145
|
// This is not deprecated, we use it to log only once.
|
|
126
|
-
|
|
146
|
+
getLog().deprecated('Using other request methods than GET, rewriting headers and adding payloads has a high impact on performance ' +
|
|
127
147
|
'in recent versions of Playwright. Use only when necessary.');
|
|
128
148
|
let wasCalled = false;
|
|
129
149
|
const interceptRequestHandler = async (route) => {
|
|
@@ -144,12 +164,15 @@ export async function gotoExtended(page, request, gotoOptions = {}) {
|
|
|
144
164
|
await route.continue(overrides);
|
|
145
165
|
}
|
|
146
166
|
catch (error) {
|
|
147
|
-
|
|
167
|
+
getLog().debug('Error inside request interceptor', { error });
|
|
148
168
|
}
|
|
149
169
|
return undefined;
|
|
150
170
|
};
|
|
151
171
|
await page.route('**/*', interceptRequestHandler);
|
|
152
172
|
}
|
|
173
|
+
else if (!isEmpty(headers)) {
|
|
174
|
+
await page.setExtraHTTPHeaders(headers);
|
|
175
|
+
}
|
|
153
176
|
return page.goto(url, gotoOptions);
|
|
154
177
|
}
|
|
155
178
|
/**
|
|
@@ -200,12 +223,8 @@ export async function gotoExtended(page, request, gotoOptions = {}) {
|
|
|
200
223
|
* @param [options]
|
|
201
224
|
*/
|
|
202
225
|
export async function blockRequests(page, options = {}) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
urlPatterns: ow.optional.array.ofType(ow.string),
|
|
206
|
-
extraUrlPatterns: ow.optional.array.ofType(ow.string),
|
|
207
|
-
}));
|
|
208
|
-
const { urlPatterns = DEFAULT_BLOCK_REQUEST_URL_PATTERNS, extraUrlPatterns = [] } = options;
|
|
226
|
+
parseArgument(page, validators.browserPage);
|
|
227
|
+
const { urlPatterns, extraUrlPatterns } = parseArgument(options, blockRequestsOptionsSchema);
|
|
209
228
|
const patternsToBlock = [...urlPatterns, ...extraUrlPatterns];
|
|
210
229
|
try {
|
|
211
230
|
const client = await page.context().newCDPSession(page);
|
|
@@ -213,7 +232,7 @@ export async function blockRequests(page, options = {}) {
|
|
|
213
232
|
await client.send('Network.setBlockedURLs', { urls: patternsToBlock });
|
|
214
233
|
}
|
|
215
234
|
catch {
|
|
216
|
-
|
|
235
|
+
getLog().warning('blockRequests() helper is incompatible with non-Chromium browsers.');
|
|
217
236
|
}
|
|
218
237
|
}
|
|
219
238
|
/**
|
|
@@ -249,7 +268,7 @@ export function compileScript(scriptString, context = Object.create(null)) {
|
|
|
249
268
|
func = vm.runInNewContext(funcString, context); // "Secure" the context by removing prototypes, unless custom context is provided.
|
|
250
269
|
}
|
|
251
270
|
catch (err) {
|
|
252
|
-
|
|
271
|
+
getLog().exception(err, 'Cannot compile script!');
|
|
253
272
|
throw err;
|
|
254
273
|
}
|
|
255
274
|
if (typeof func !== 'function')
|
|
@@ -263,16 +282,8 @@ export function compileScript(scriptString, context = Object.create(null)) {
|
|
|
263
282
|
* @param [options]
|
|
264
283
|
*/
|
|
265
284
|
export async function infiniteScroll(page, options = {}) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
timeoutSecs: ow.optional.number,
|
|
269
|
-
maxScrollHeight: ow.optional.number,
|
|
270
|
-
waitForSecs: ow.optional.number,
|
|
271
|
-
scrollDownAndUp: ow.optional.boolean,
|
|
272
|
-
buttonSelector: ow.optional.string,
|
|
273
|
-
stopScrollCallback: ow.optional.function,
|
|
274
|
-
}));
|
|
275
|
-
const { timeoutSecs = 0, maxScrollHeight = 0, waitForSecs = 4, scrollDownAndUp = false, buttonSelector, stopScrollCallback, } = options;
|
|
285
|
+
parseArgument(page, validators.browserPage);
|
|
286
|
+
const { timeoutSecs, maxScrollHeight, waitForSecs, scrollDownAndUp, buttonSelector, stopScrollCallback } = parseArgument(options, infiniteScrollOptionsSchema);
|
|
276
287
|
let finished;
|
|
277
288
|
const startTime = Date.now();
|
|
278
289
|
const CHECK_INTERVAL_MILLIS = 1000;
|
|
@@ -349,19 +360,11 @@ export async function infiniteScroll(page, options = {}) {
|
|
|
349
360
|
* @param [options]
|
|
350
361
|
*/
|
|
351
362
|
export async function saveSnapshot(page, options = {}) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
key: ow.optional.string.nonEmpty,
|
|
355
|
-
screenshotQuality: ow.optional.number,
|
|
356
|
-
saveScreenshot: ow.optional.boolean,
|
|
357
|
-
saveHtml: ow.optional.boolean,
|
|
358
|
-
keyValueStoreName: ow.optional.string,
|
|
359
|
-
config: ow.optional.object,
|
|
360
|
-
}));
|
|
361
|
-
const { key = 'SNAPSHOT', screenshotQuality = 50, saveScreenshot = true, saveHtml = true, keyValueStoreName, config, } = options;
|
|
363
|
+
parseArgument(page, validators.browserPage);
|
|
364
|
+
const { key, screenshotQuality, saveScreenshot, saveHtml, keyValueStoreName, configuration } = parseArgument(options, saveSnapshotOptionsSchema);
|
|
362
365
|
try {
|
|
363
|
-
const store = await KeyValueStore.open(keyValueStoreName, {
|
|
364
|
-
|
|
366
|
+
const store = await KeyValueStore.open(keyValueStoreName ? { name: keyValueStoreName } : null, {
|
|
367
|
+
configuration: configuration ?? Configuration.getGlobalConfiguration(),
|
|
365
368
|
});
|
|
366
369
|
if (saveScreenshot) {
|
|
367
370
|
const screenshotName = `${key}.jpg`;
|
|
@@ -396,36 +399,44 @@ export async function saveSnapshot(page, options = {}) {
|
|
|
396
399
|
* @param ignoreShadowRoots
|
|
397
400
|
*/
|
|
398
401
|
export async function parseWithCheerio(page, ignoreShadowRoots = false, ignoreIframes = false) {
|
|
399
|
-
|
|
402
|
+
parseArgument(page, validators.browserPage);
|
|
403
|
+
const html = ignoreShadowRoots
|
|
404
|
+
? null
|
|
405
|
+
: (await page.evaluate(`(${expandShadowRoots.toString()})(document)`));
|
|
406
|
+
const pageContent = html || (await page.content());
|
|
407
|
+
// Full cheerio (parse5) import for browser compliant parser
|
|
408
|
+
const { load } = await import('cheerio');
|
|
409
|
+
const $ = load(pageContent);
|
|
400
410
|
if (page.frames().length > 1 && !ignoreIframes) {
|
|
401
411
|
const frames = await page.$$('iframe');
|
|
402
|
-
|
|
412
|
+
const cheerioIframes = $('iframe').toArray();
|
|
413
|
+
if (frames.length !== cheerioIframes.length) {
|
|
414
|
+
serviceLocator
|
|
415
|
+
.getLogger()
|
|
416
|
+
.warning(`parseWithCheerio: iframe count mismatch between live DOM (${frames.length}) and page snapshot (${cheerioIframes.length}). Some iframes may not be expanded.`);
|
|
417
|
+
}
|
|
418
|
+
await Promise.all(frames.map(async (frame, index) => {
|
|
403
419
|
try {
|
|
404
420
|
const iframe = await frame.contentFrame();
|
|
405
|
-
if (iframe) {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
421
|
+
if (iframe && cheerioIframes[index]) {
|
|
422
|
+
const getIframeHTML = async () => {
|
|
423
|
+
try {
|
|
424
|
+
return iframe.locator('body').first().innerHTML();
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return iframe.content();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const contents = await getIframeHTML();
|
|
431
|
+
$(cheerioIframes[index]).replaceWith(`<div class="crawlee-iframe-replacement">${contents}</div>`);
|
|
413
432
|
}
|
|
414
433
|
}
|
|
415
434
|
catch (error) {
|
|
416
|
-
|
|
435
|
+
getLog().warning(`Failed to extract iframe content: ${error}`);
|
|
417
436
|
}
|
|
418
437
|
}));
|
|
419
438
|
}
|
|
420
|
-
|
|
421
|
-
? null
|
|
422
|
-
: (await page.evaluate(`(${expandShadowRoots.toString()})(document)`));
|
|
423
|
-
const pageContent = html || (await page.content());
|
|
424
|
-
return cheerio.load(pageContent);
|
|
425
|
-
}
|
|
426
|
-
export async function closeCookieModals(page) {
|
|
427
|
-
ow(page, ow.object.validate(validators.browserPage));
|
|
428
|
-
await page.evaluate(getCookieClosingScript());
|
|
439
|
+
return $;
|
|
429
440
|
}
|
|
430
441
|
/**
|
|
431
442
|
* This helper tries to solve the Cloudflare challenge automatically by clicking on the checkbox.
|
|
@@ -434,30 +445,24 @@ export async function closeCookieModals(page) {
|
|
|
434
445
|
* result in a SessionError which will be automatically retried, so only successful requests will get
|
|
435
446
|
* into the `requestHandler`.
|
|
436
447
|
*
|
|
448
|
+
* On a successfully solved challenge the page is reloaded and the new {@link Response} is returned, so
|
|
449
|
+
* it can be propagated back to the crawling context via a hook return value (see
|
|
450
|
+
* {@link handleCloudflareChallengeHook}).
|
|
451
|
+
*
|
|
437
452
|
* Works best with camoufox.
|
|
438
453
|
*
|
|
439
454
|
* **Example usage**
|
|
440
455
|
* ```ts
|
|
441
456
|
* postNavigationHooks: [
|
|
442
|
-
* async ({ handleCloudflareChallenge })
|
|
443
|
-
* await handleCloudflareChallenge();
|
|
444
|
-
* },
|
|
457
|
+
* async (context) => ({ response: await context.handleCloudflareChallenge() }),
|
|
445
458
|
* ],
|
|
446
459
|
* ```
|
|
447
460
|
*
|
|
448
461
|
* @param page Playwright [`Page`](https://playwright.dev/docs/api/class-page) object
|
|
449
462
|
* @param url current URL for request identification, only used for logging
|
|
450
|
-
* @param [session] current session object
|
|
451
463
|
* @param [options]
|
|
452
464
|
*/
|
|
453
|
-
async function handleCloudflareChallenge(page, url,
|
|
454
|
-
// eslint-disable-next-line dot-notation
|
|
455
|
-
const blockedStatusCodes = session?.['sessionPool']['blockedStatusCodes'];
|
|
456
|
-
// Cloudflare pages are 403, which are blocked by default
|
|
457
|
-
if (blockedStatusCodes?.includes(403)) {
|
|
458
|
-
const idx = blockedStatusCodes.indexOf(403);
|
|
459
|
-
blockedStatusCodes.splice(idx, 1);
|
|
460
|
-
}
|
|
465
|
+
export async function handleCloudflareChallenge(page, url, options = {}) {
|
|
461
466
|
options.isBlockedCallback ??= async () => {
|
|
462
467
|
const isBlocked = await page.evaluate(() => {
|
|
463
468
|
return document.querySelector('h1')?.textContent?.trim().includes('Sorry, you have been blocked');
|
|
@@ -466,7 +471,9 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
|
|
|
466
471
|
};
|
|
467
472
|
options.isChallengeCallback ??= async () => {
|
|
468
473
|
return await page.evaluate(async () => {
|
|
469
|
-
|
|
474
|
+
// Cloudflare keeps reshuffling the wrapper elements between `.footer-inner` and `.ray-id`,
|
|
475
|
+
// so only the stable outer classes are matched.
|
|
476
|
+
return !!document.querySelector('.footer .footer-inner .ray-id');
|
|
470
477
|
});
|
|
471
478
|
};
|
|
472
479
|
const retryBlocked = async () => {
|
|
@@ -481,31 +488,41 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
|
|
|
481
488
|
};
|
|
482
489
|
if (!(await isChallenge())) {
|
|
483
490
|
await retryBlocked();
|
|
484
|
-
return;
|
|
491
|
+
return undefined;
|
|
485
492
|
}
|
|
486
493
|
const logLevel = options.verbose ? 'info' : 'debug';
|
|
487
|
-
|
|
494
|
+
getLog()[logLevel](`Detected Cloudflare challenge at ${url}, trying to solve it. This can take up to ${10 + (options.sleepSecs ?? 10)} seconds.`);
|
|
488
495
|
const bb = await page
|
|
489
496
|
.evaluate(() => {
|
|
490
|
-
|
|
497
|
+
// Prefer the actual challenge widget (the box holding the Turnstile checkbox input);
|
|
498
|
+
// fall back to the first content div for older challenge layouts.
|
|
499
|
+
const div = document.querySelector('.main-content div:has(input[id^="cf-chl-widget-"])') ??
|
|
500
|
+
document.querySelector('.main-content div');
|
|
491
501
|
return div?.getBoundingClientRect();
|
|
492
502
|
})
|
|
493
503
|
.catch(() => undefined);
|
|
494
504
|
if (!bb) {
|
|
495
|
-
return;
|
|
505
|
+
return undefined;
|
|
496
506
|
}
|
|
497
507
|
const randomOffset = (range) => {
|
|
498
508
|
return Math.round(100 * range * Math.random()) / 100;
|
|
499
509
|
};
|
|
500
|
-
|
|
501
|
-
|
|
510
|
+
let x = bb.x + 30;
|
|
511
|
+
let y = bb.y + 25;
|
|
502
512
|
// try to click the checkbox every second
|
|
503
513
|
for (let i = 0; i < 10; i++) {
|
|
504
|
-
await sleep(1000);
|
|
514
|
+
await sleep((options.preChallengeSleepSecs ?? 1) * 1000);
|
|
505
515
|
// break early if we are no longer on the CF challenge page
|
|
506
516
|
if (!(await isChallenge())) {
|
|
507
517
|
break;
|
|
508
518
|
}
|
|
519
|
+
if (options.clickPositionCallback) {
|
|
520
|
+
const pos = await options.clickPositionCallback(page);
|
|
521
|
+
if (pos) {
|
|
522
|
+
x = pos.x;
|
|
523
|
+
y = pos.y;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
509
526
|
if (options.clickCallback) {
|
|
510
527
|
await options.clickCallback(page, { x, y });
|
|
511
528
|
continue;
|
|
@@ -513,7 +530,10 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
|
|
|
513
530
|
// we can click on the text too, so X can be a bit larger
|
|
514
531
|
const xRandomized = x + randomOffset(10);
|
|
515
532
|
const yRandomized = y + randomOffset(10);
|
|
516
|
-
|
|
533
|
+
getLog()[logLevel](`Trying to click on the Cloudflare checkbox at ${url}`, {
|
|
534
|
+
x: xRandomized,
|
|
535
|
+
y: yRandomized,
|
|
536
|
+
});
|
|
517
537
|
await page.mouse.click(xRandomized, yRandomized);
|
|
518
538
|
// sometimes the checkbox is lower (could be caused by a lag when rendering the logo)
|
|
519
539
|
await page.mouse.click(xRandomized, yRandomized + 35);
|
|
@@ -523,55 +543,8 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
|
|
|
523
543
|
throw new SessionError(`Blocked by Cloudflare when processing ${url}`);
|
|
524
544
|
}
|
|
525
545
|
await retryBlocked();
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
context.injectJQuery = async () => {
|
|
530
|
-
if (context.request.state === RequestState.BEFORE_NAV) {
|
|
531
|
-
log.warning('Using injectJQuery() in preNavigationHooks leads to unstable results. Use it in a postNavigationHook or a requestHandler instead.');
|
|
532
|
-
await injectJQuery(context.page);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
await injectJQuery(context.page, { surviveNavigations: false });
|
|
536
|
-
};
|
|
537
|
-
context.blockRequests = async (options) => blockRequests(context.page, options);
|
|
538
|
-
context.waitForSelector = async (selector, timeoutMs = 5_000) => {
|
|
539
|
-
const locator = context.page.locator(selector).first();
|
|
540
|
-
await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
|
|
541
|
-
};
|
|
542
|
-
context.parseWithCheerio = async (selector, timeoutMs = 5_000) => {
|
|
543
|
-
if (selector) {
|
|
544
|
-
await context.waitForSelector(selector, timeoutMs);
|
|
545
|
-
}
|
|
546
|
-
return parseWithCheerio(context.page, crawlerOptions.ignoreShadowRoots, crawlerOptions.ignoreIframes);
|
|
547
|
-
};
|
|
548
|
-
context.infiniteScroll = async (options) => infiniteScroll(context.page, options);
|
|
549
|
-
context.saveSnapshot = async (options) => saveSnapshot(context.page, { ...options, config: context.crawler.config });
|
|
550
|
-
context.enqueueLinksByClickingElements = async (options) => enqueueLinksByClickingElements({
|
|
551
|
-
...options,
|
|
552
|
-
page: context.page,
|
|
553
|
-
requestQueue: context.crawler.requestQueue,
|
|
554
|
-
});
|
|
555
|
-
context.compileScript = (scriptString, ctx) => compileScript(scriptString, ctx);
|
|
556
|
-
context.closeCookieModals = async () => closeCookieModals(context.page);
|
|
557
|
-
context.handleCloudflareChallenge = async (options) => {
|
|
558
|
-
return handleCloudflareChallenge(context.page, context.request.url, context.session, options);
|
|
559
|
-
};
|
|
546
|
+
// Reload to obtain a fresh Response without the challenge interstitial, which the caller can
|
|
547
|
+
// propagate back into the crawling context so downstream status-code checks see the new value.
|
|
548
|
+
return (await page.reload()) ?? undefined;
|
|
560
549
|
}
|
|
561
550
|
export { enqueueLinksByClickingElements };
|
|
562
|
-
/** @internal */
|
|
563
|
-
export const playwrightUtils = {
|
|
564
|
-
injectFile,
|
|
565
|
-
injectJQuery,
|
|
566
|
-
gotoExtended,
|
|
567
|
-
blockRequests,
|
|
568
|
-
enqueueLinksByClickingElements,
|
|
569
|
-
parseWithCheerio,
|
|
570
|
-
infiniteScroll,
|
|
571
|
-
saveSnapshot,
|
|
572
|
-
compileScript,
|
|
573
|
-
closeCookieModals,
|
|
574
|
-
RenderingTypePredictor,
|
|
575
|
-
handleCloudflareChallenge,
|
|
576
|
-
};
|
|
577
|
-
//# sourceMappingURL=playwright-utils.js.map
|
|
@@ -1,21 +1,54 @@
|
|
|
1
|
-
import type { Request } from '@crawlee/core';
|
|
1
|
+
import type { RecoverableStatePersistenceOptions, Request } from '@crawlee/core';
|
|
2
|
+
import type { Awaitable } from '@crawlee/types';
|
|
2
3
|
export type RenderingType = 'clientOnly' | 'static';
|
|
3
|
-
type URLComponents = string[];
|
|
4
|
-
type FeatureVector = [staticResultsSimilarity: number, clientOnlyResultsSimilarity: number];
|
|
5
4
|
export interface RenderingTypePredictorOptions {
|
|
6
5
|
/** A number between 0 and 1 that determines the desired ratio of rendering type detections */
|
|
7
6
|
detectionRatio: number;
|
|
7
|
+
persistenceOptions?: Partial<RecoverableStatePersistenceOptions>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Minimal contract that any object passed to {@link AdaptivePlaywrightCrawler} as its
|
|
11
|
+
* `renderingTypePredictor` option must satisfy.
|
|
12
|
+
*
|
|
13
|
+
* @experimental
|
|
14
|
+
*/
|
|
15
|
+
export interface IRenderingTypePredictor {
|
|
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<{
|
|
23
|
+
renderingType: RenderingType;
|
|
24
|
+
detectionProbabilityRecommendation: number;
|
|
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>;
|
|
8
34
|
}
|
|
9
35
|
/**
|
|
10
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.
|
|
11
37
|
*
|
|
12
38
|
* @experimental
|
|
13
39
|
*/
|
|
14
|
-
export declare class RenderingTypePredictor {
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
export declare class RenderingTypePredictor implements IRenderingTypePredictor {
|
|
41
|
+
#private;
|
|
42
|
+
constructor({ detectionRatio, persistenceOptions }: RenderingTypePredictorOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the predictor by restoring persisted state.
|
|
45
|
+
*/
|
|
46
|
+
initialize(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Stop persisting the model, writing it out one last time. `initialize()` reopens the persistence window.
|
|
49
|
+
*/
|
|
50
|
+
teardown(): Promise<void>;
|
|
51
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
19
52
|
/**
|
|
20
53
|
* Predict the rendering type for a given URL and request label.
|
|
21
54
|
*/
|
|
@@ -26,10 +59,8 @@ export declare class RenderingTypePredictor {
|
|
|
26
59
|
/**
|
|
27
60
|
* Store the rendering type for a given URL and request label. This updates the underlying prediction model, which may be costly.
|
|
28
61
|
*/
|
|
29
|
-
storeResult(
|
|
62
|
+
storeResult(requests: Request | Request[], renderingType: RenderingType): void;
|
|
30
63
|
private resultCount;
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
private calculateFeatureVector;
|
|
65
|
+
private retrain;
|
|
33
66
|
}
|
|
34
|
-
export {};
|
|
35
|
-
//# sourceMappingURL=rendering-type-prediction.d.ts.map
|