@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.
Files changed (33) hide show
  1. package/README.md +17 -13
  2. package/index.d.ts +4 -3
  3. package/index.js +2 -2
  4. package/internals/adaptive-playwright-crawler.d.ts +146 -91
  5. package/internals/adaptive-playwright-crawler.js +485 -268
  6. package/internals/enqueue-links/click-elements.d.ts +37 -55
  7. package/internals/enqueue-links/click-elements.js +65 -55
  8. package/internals/playwright-browser-pool.d.ts +71 -0
  9. package/internals/playwright-browser-pool.js +61 -0
  10. package/internals/playwright-crawler.d.ts +177 -172
  11. package/internals/playwright-crawler.js +103 -61
  12. package/internals/playwright-launcher.d.ts +30 -20
  13. package/internals/playwright-launcher.js +22 -17
  14. package/internals/utils/playwright-utils.d.ts +55 -50
  15. package/internals/utils/playwright-utils.js +121 -148
  16. package/internals/utils/rendering-type-prediction.d.ts +44 -13
  17. package/internals/utils/rendering-type-prediction.js +95 -29
  18. package/package.json +15 -15
  19. package/index.d.ts.map +0 -1
  20. package/index.js.map +0 -1
  21. package/internals/adaptive-playwright-crawler.d.ts.map +0 -1
  22. package/internals/adaptive-playwright-crawler.js.map +0 -1
  23. package/internals/enqueue-links/click-elements.d.ts.map +0 -1
  24. package/internals/enqueue-links/click-elements.js.map +0 -1
  25. package/internals/playwright-crawler.d.ts.map +0 -1
  26. package/internals/playwright-crawler.js.map +0 -1
  27. package/internals/playwright-launcher.d.ts.map +0 -1
  28. package/internals/playwright-launcher.js.map +0 -1
  29. package/internals/utils/playwright-utils.d.ts.map +0 -1
  30. package/internals/utils/playwright-utils.js.map +0 -1
  31. package/internals/utils/rendering-type-prediction.d.ts.map +0 -1
  32. package/internals/utils/rendering-type-prediction.js.map +0 -1
  33. 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, RequestState, SessionError, validators, } from '@crawlee/browser';
24
- import { expandShadowRoots, sleep } from '@crawlee/utils';
25
- import * as cheerio from 'cheerio';
26
- import { getInjectableScript as getCookieClosingScript } from 'idcac-playwright';
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
- import { RenderingTypePredictor } from './rendering-type-prediction.js';
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
- ow(page, ow.object.validate(validators.browserPage));
54
- ow(filePath, ow.string);
55
- ow(options, ow.object.exactShape({
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 (options.surviveNavigations) {
89
+ if (surviveNavigations) {
65
90
  page.on('framenavigated', async () => page
66
91
  .evaluate(contents)
67
- .catch((error) => log.warning('An error occurred during the script injection!', { 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
- ow(page, ow.object.validate(validators.browserPage));
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
- ow(page, ow.object.validate(validators.browserPage));
115
- ow(request, ow.object.partialShape({
116
- url: ow.string.url,
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 || !isEmpty(headers)) {
144
+ if (method !== 'GET' || payload) {
125
145
  // This is not deprecated, we use it to log only once.
126
- log.deprecated('Using other request methods than GET, rewriting headers and adding payloads has a high impact on performance ' +
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
- log.debug('Error inside request interceptor', { error });
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
- ow(page, ow.object.validate(validators.browserPage));
204
- ow(options, ow.object.exactShape({
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
- log.warning('blockRequests() helper is incompatible with non-Chromium browsers.');
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
- log.exception(err, 'Cannot compile script!');
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
- ow(page, ow.object.validate(validators.browserPage));
267
- ow(options, ow.object.exactShape({
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
- ow(page, ow.object.validate(validators.browserPage));
353
- ow(options, ow.object.exactShape({
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
- config: config ?? Configuration.getGlobalConfig(),
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
- ow(page, ow.object.validate(validators.browserPage));
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
- await Promise.all(frames.map(async (frame) => {
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 contents = await iframe.content();
407
- await frame.evaluate((f, c) => {
408
- const replacementNode = document.createElement('div');
409
- replacementNode.innerHTML = c;
410
- replacementNode.className = 'crawlee-iframe-replacement';
411
- f.replaceWith(replacementNode);
412
- }, contents);
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
- log.warning(`Failed to extract iframe content: ${error}`);
435
+ getLog().warning(`Failed to extract iframe content: ${error}`);
417
436
  }
418
437
  }));
419
438
  }
420
- const html = ignoreShadowRoots
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, session, options = {}) {
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
- return !!document.querySelector('.footer > .footer-inner > .diagnostic-wrapper > .ray-id');
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
- log[logLevel](`Detected Cloudflare challenge at ${url}, trying to solve it. This can take up to ${10 + (options.sleepSecs ?? 10)} seconds.`);
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
- const div = document.querySelector('.main-content div');
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
- const x = bb.x + 30;
501
- const y = bb.y + 25;
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
- log[logLevel](`Trying to click on the Cloudflare checkbox at ${url}`, { x: xRandomized, y: yRandomized });
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
- export function registerUtilsToContext(context, crawlerOptions) {
528
- context.injectFile = async (filePath, options) => injectFile(context.page, filePath, options);
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 renderingTypeDetectionResults;
16
- private detectionRatio;
17
- private logreg;
18
- constructor({ detectionRatio }: RenderingTypePredictorOptions);
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({ url, loadedUrl, label }: Request, renderingType: RenderingType): void;
62
+ storeResult(requests: Request | Request[], renderingType: RenderingType): void;
30
63
  private resultCount;
31
- protected calculateFeatureVector(url: URLComponents, label: string | undefined): FeatureVector;
32
- protected retrain(): void;
64
+ private calculateFeatureVector;
65
+ private retrain;
33
66
  }
34
- export {};
35
- //# sourceMappingURL=rendering-type-prediction.d.ts.map