@crawlee/playwright 4.0.0-beta.231 → 4.0.0-beta.232

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.
@@ -43,16 +43,17 @@ export interface AdaptivePlaywrightCrawlerContext<UserData extends Dictionary =
43
43
  * Playwright Page object. If accessed in HTTP-only rendering, this will throw an error and make the AdaptivePlaywrightCrawlerContext retry the request in a browser.
44
44
  */
45
45
  page: Page;
46
- /**
47
- * Wait for an element matching the selector to appear and return a Cheerio object of the first matched element.
48
- * Timeout defaults to 5s.
49
- */
50
- querySelector(selector: string, timeoutMs?: number): Promise<Cheerio<AnyNode>>;
51
46
  /**
52
47
  * Wait for an element matching the selector to appear and return a Cheerio object of all matched elements.
48
+ * Throws when nothing matches - in the HTTP-only run, that makes the crawler retry the request in a browser.
53
49
  * Timeout defaults to 5s.
54
50
  */
55
51
  querySelectorAll(selector: string, timeoutMs?: number): Promise<Cheerio<AnyNode>>;
52
+ /**
53
+ * Like {@link AdaptivePlaywrightCrawlerContext.querySelectorAll|`querySelectorAll`}, but returns only the
54
+ * first match.
55
+ */
56
+ querySelectorOne(selector: string, timeoutMs?: number): Promise<Cheerio<AnyNode>>;
56
57
  /**
57
58
  * Wait for an element matching the selector to appear.
58
59
  * Timeout defaults to 5s.
@@ -91,6 +92,11 @@ type AdaptiveHook<ContextExtension = Dictionary<never>> = BrowserHook<AdaptiveHo
91
92
  type AdaptivePostNavigationHook<ContextExtension = Dictionary<never>> = BrowserHook<Omit<AdaptiveHookContext, 'request'> & {
92
93
  request: LoadedRequest<CrawlingRequest>;
93
94
  }, ContextExtension>;
95
+ /** Passed to {@link AdaptivePlaywrightCrawlerOptions.resultChecker} and `resultComparator` alongside the results. */
96
+ export interface AdaptiveResultContext {
97
+ /** The request whose handler produced the results. */
98
+ request: CrawlingRequest;
99
+ }
94
100
  export interface AdaptivePlaywrightCrawlerOptions<ContextExtension = Dictionary<never>, ExtendedContext extends AdaptivePlaywrightCrawlerContext = AdaptivePlaywrightCrawlerContext & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<AdaptivePlaywrightCrawlerContext['request']>>, StatisticStateExtension extends AdaptivePlaywrightCrawlerStatisticState = AdaptivePlaywrightCrawlerStatisticState> extends Omit<BasicCrawlerOptions<AdaptivePlaywrightCrawlerContext, ContextExtension, ExtendedContext, Routes, StatisticStateExtension>, 'preNavigationHooks' | 'postNavigationHooks' | 'contextPipelineBuilder'>, Pick<PlaywrightCrawlerOptions, 'launchContext' | 'headless' | 'browserPool' | 'remoteBrowser'> {
95
101
  /**
96
102
  * Async functions that are sequentially evaluated before the navigation. Good for setting additional cookies.
@@ -121,7 +127,7 @@ export interface AdaptivePlaywrightCrawlerOptions<ContextExtension = Dictionary<
121
127
  * If it returns false, the request is retried in a browser.
122
128
  * If no callback is specified, every result is considered valid.
123
129
  */
124
- resultChecker?: (result: StorageTransactionView) => boolean;
130
+ resultChecker?: (result: StorageTransactionView, context: AdaptiveResultContext) => boolean;
125
131
  /**
126
132
  * An optional callback that decides whether an error thrown during the plain HTTP request handler
127
133
  * should be propagated (instead of falling back to browser navigation).
@@ -144,7 +150,7 @@ export interface AdaptivePlaywrightCrawlerOptions<ContextExtension = Dictionary<
144
150
  *
145
151
  * For a stricter, ready-made comparator that also takes enqueued requests and key-value store changes into account, see {@link fullResultComparator}.
146
152
  */
147
- resultComparator?: (resultA: StorageTransactionView, resultB: StorageTransactionView) => boolean | 'equal' | 'different' | 'inconclusive';
153
+ resultComparator?: (resultA: StorageTransactionView, resultB: StorageTransactionView, context: AdaptiveResultContext) => boolean | 'equal' | 'different' | 'inconclusive';
148
154
  /**
149
155
  * A custom rendering type predictor. A predictor passed here is borrowed - the crawler never drives its
150
156
  * lifecycle, so set it up yourself (the built-in {@link RenderingTypePredictor} needs `initialize()`).
@@ -160,9 +166,9 @@ export interface AdaptivePlaywrightCrawlerOptions<ContextExtension = Dictionary<
160
166
  * ```javascript
161
167
  * const crawler = new AdaptivePlaywrightCrawler({
162
168
  * renderingTypeDetectionRatio: 0.1,
163
- * async requestHandler({ querySelector, pushData, enqueueLinks, request, log }) {
169
+ * async requestHandler({ querySelectorAll, pushData, enqueueLinks, request, log }) {
164
170
  * // This function is called to extract data from a single web page
165
- * const $prices = await querySelector('span.price')
171
+ * const $prices = await querySelectorAll('span.price')
166
172
  *
167
173
  * await pushData({
168
174
  * url: request.url,
@@ -43,9 +43,9 @@ const proxyLogMethods = [
43
43
  * ```javascript
44
44
  * const crawler = new AdaptivePlaywrightCrawler({
45
45
  * renderingTypeDetectionRatio: 0.1,
46
- * async requestHandler({ querySelector, pushData, enqueueLinks, request, log }) {
46
+ * async requestHandler({ querySelectorAll, pushData, enqueueLinks, request, log }) {
47
47
  * // This function is called to extract data from a single web page
48
- * const $prices = await querySelector('span.price')
48
+ * const $prices = await querySelectorAll('span.price')
49
49
  *
50
50
  * await pushData({
51
51
  * url: request.url,
@@ -146,7 +146,7 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
146
146
  this.#resultComparator = resultComparator;
147
147
  }
148
148
  else if (resultChecker !== undefined) {
149
- this.#resultComparator = (resultA, resultB) => this.#resultChecker(resultA) && this.#resultChecker(resultB);
149
+ this.#resultComparator = (resultA, resultB, context) => this.#resultChecker(resultA, context) && this.#resultChecker(resultB, context);
150
150
  }
151
151
  else {
152
152
  this.#resultComparator = (resultA, resultB) => {
@@ -208,12 +208,12 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
208
208
  get page() {
209
209
  throw new Error(errorMessage('page'));
210
210
  },
211
- get querySelector() {
212
- throw new Error(errorMessage('querySelector'));
213
- },
214
211
  get querySelectorAll() {
215
212
  throw new Error(errorMessage('querySelectorAll'));
216
213
  },
214
+ get querySelectorOne() {
215
+ throw new Error(errorMessage('querySelectorOne'));
216
+ },
217
217
  get waitForSelector() {
218
218
  throw new Error(errorMessage('waitForSelector'));
219
219
  },
@@ -230,12 +230,6 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
230
230
  get page() {
231
231
  throw new Error('Page object was used in HTTP-only request handler');
232
232
  },
233
- async querySelector(selector) {
234
- return cheerioContext.$(selector).first();
235
- },
236
- async querySelectorAll(selector) {
237
- return cheerioContext.$(selector);
238
- },
239
233
  enqueueLinks: async (options = {}) => {
240
234
  const urls = extractUrlsFromCheerio(cheerioContext.$, options.selector, options.baseUrl ?? cheerioContext.request.loadedUrl);
241
235
  return (await this.#enqueueLinks(urls, options, cheerioContext.request));
@@ -252,18 +246,6 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
252
246
  status: originalResponse.status(),
253
247
  statusText: originalResponse.statusText(),
254
248
  }),
255
- async querySelector(selector, timeoutMs = 5000) {
256
- const locator = playwrightContext.page.locator(selector).first();
257
- await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
258
- const $ = await playwrightContext.parseWithCheerio();
259
- return $(selector).first();
260
- },
261
- async querySelectorAll(selector, timeoutMs = 5000) {
262
- const locator = playwrightContext.page.locator(selector).first();
263
- await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
264
- const $ = await playwrightContext.parseWithCheerio();
265
- return $(selector);
266
- },
267
249
  enqueueLinks: async (options = {}, timeoutMs = 5000) => {
268
250
  // TODO consider using `context.parseWithCheerio` to make this universal and avoid code duplication
269
251
  const selector = options.selector ?? 'a';
@@ -337,11 +319,14 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
337
319
  // `finally` below, not earlier: the comparators read the journals after `crawlOne` returns.
338
320
  const transactions = [];
339
321
  try {
322
+ // A result the `resultChecker` rejected is as good as a detection saying the page needs a browser - but
323
+ // only once the browser run has succeeded. A thrown error says nothing about rendering.
324
+ let httpResultRejected = false;
340
325
  if (renderingTypePrediction.renderingType === 'static' && !shouldDetectRenderingType) {
341
326
  crawlingContext.log.debug(`Running HTTP-only request handler for ${crawlingContext.request.url}`);
342
327
  this.statistics.state.httpOnlyRequestHandlerRuns++;
343
328
  const plainHTTPRun = await this.#crawlOne('static', crawlingContext, crawlingContext.useState, transactions);
344
- if (plainHTTPRun.ok && this.#resultChecker(plainHTTPRun.result)) {
329
+ if (plainHTTPRun.ok && this.#resultChecker(plainHTTPRun.result, { request: crawlingContext.request })) {
345
330
  crawlingContext.log.debug(`HTTP-only request handler succeeded for ${crawlingContext.request.url}`);
346
331
  plainHTTPRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
347
332
  await plainHTTPRun.result.commit();
@@ -362,6 +347,7 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
362
347
  else {
363
348
  crawlingContext.log.warning(`HTTP-only request handler returned a suspicious result for ${crawlingContext.request.url}`);
364
349
  this.statistics.state.renderingTypeMispredictions++;
350
+ httpResultRejected = true;
365
351
  }
366
352
  }
367
353
  crawlingContext.log.debug(`Running browser request handler for ${crawlingContext.request.url}`);
@@ -392,6 +378,10 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
392
378
  }
393
379
  browserRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
394
380
  await browserRun.result.commit();
381
+ // Past the shutdown fence, `teardown()` has already drained - and possibly torn down the predictor.
382
+ if (httpResultRejected && !this.#shutDown) {
383
+ this.#storeRenderingType(crawlingContext.request, 'clientOnly');
384
+ }
395
385
  if (shouldDetectRenderingType) {
396
386
  const detectionPromise = (async () => {
397
387
  crawlingContext.log.debug(`Detecting rendering type for ${crawlingContext.request.url}`);
@@ -402,7 +392,9 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
402
392
  if (!plainHTTPRun.ok) {
403
393
  return 'clientOnly';
404
394
  }
405
- const comparisonResult = this.#resultComparator(plainHTTPRun.result, browserRun.result);
395
+ const comparisonResult = this.#resultComparator(plainHTTPRun.result, browserRun.result, {
396
+ request: crawlingContext.request,
397
+ });
406
398
  if (comparisonResult === true || comparisonResult === 'equal') {
407
399
  return 'static';
408
400
  }
@@ -413,13 +405,7 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
413
405
  })();
414
406
  crawlingContext.log.debug(`Detected rendering type ${detectionResult} for ${crawlingContext.request.url}`);
415
407
  if (detectionResult !== undefined) {
416
- // Deliberately not awaited: a predictor that persists asynchronously gets to keep
417
- // batching its writes, and the drain below catches whatever is still pending.
418
- const stored = this.#renderingTypePredictor.value.storeResult(crawlingContext.request, detectionResult);
419
- if (stored !== undefined) {
420
- // Nothing downstream awaits this, so a failed write would otherwise be silent.
421
- void this.#trackDetection(Promise.resolve(stored).catch((error) => this.log.exception(error, `Failed to store the rendering type detection result for ${crawlingContext.request.url}`)));
422
- }
408
+ this.#storeRenderingType(crawlingContext.request, detectionResult);
423
409
  }
424
410
  })();
425
411
  await this.#trackDetection(detectionPromise);
@@ -433,6 +419,15 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
433
419
  }
434
420
  }
435
421
  }
422
+ #storeRenderingType(request, renderingType) {
423
+ // Deliberately not awaited: a predictor that persists asynchronously gets to keep batching its writes,
424
+ // and `drainRenderingDetections()` catches whatever is still pending.
425
+ const stored = this.#renderingTypePredictor.value.storeResult(request, renderingType);
426
+ if (stored !== undefined) {
427
+ // Nothing downstream awaits this, so a failed write would otherwise be silent.
428
+ void this.#trackDetection(Promise.resolve(stored).catch((error) => this.log.exception(error, `Failed to store the rendering type detection result for ${request.url}`)));
429
+ }
430
+ }
436
431
  async #enqueueLinks(urls, options, request) {
437
432
  const baseUrl = resolveBaseUrlForEnqueueLinksFiltering({
438
433
  enqueueStrategy: options?.strategy,
@@ -114,6 +114,21 @@ export class PlaywrightCrawler extends BrowserCrawler {
114
114
  const locator = context.page.locator(selector).first();
115
115
  await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
116
116
  };
117
+ const parseWithCheerioFn = async (selector, timeoutMs = 5_000) => {
118
+ if (selector) {
119
+ await waitForSelector(selector, timeoutMs);
120
+ }
121
+ return parseWithCheerio(context.page, this.ignoreShadowRoots, this.ignoreIframes);
122
+ };
123
+ const querySelectorAll = async (selector, timeoutMs) => {
124
+ const matches = (await parseWithCheerioFn(selector, timeoutMs))(selector);
125
+ // The browser located it but the Cheerio snapshot does not have it - a Playwright-only selector,
126
+ // or an element under a shadow root/iframe the snapshot was told to ignore.
127
+ if (matches.length === 0) {
128
+ throw new Error(`Selector '${selector}' not found in the page snapshot.`);
129
+ }
130
+ return matches;
131
+ };
117
132
  const downloads = [];
118
133
  context.page.on('download', (download) => downloads.push(download));
119
134
  return {
@@ -128,12 +143,9 @@ export class PlaywrightCrawler extends BrowserCrawler {
128
143
  },
129
144
  blockRequests: async (options) => blockRequests(context.page, options),
130
145
  waitForSelector,
131
- parseWithCheerio: async (selector, timeoutMs = 5_000) => {
132
- if (selector) {
133
- await waitForSelector(selector, timeoutMs);
134
- }
135
- return parseWithCheerio(context.page, this.ignoreShadowRoots, this.ignoreIframes);
136
- },
146
+ parseWithCheerio: parseWithCheerioFn,
147
+ querySelectorAll,
148
+ querySelectorOne: async (selector, timeoutMs) => (await querySelectorAll(selector, timeoutMs)).first(),
137
149
  infiniteScroll: async (options) => infiniteScroll(context.page, options),
138
150
  listDownloads: async () => downloads,
139
151
  saveSnapshot: async (options) => saveSnapshot(context.page, {
@@ -19,7 +19,8 @@
19
19
  */
20
20
  import { Configuration, type Request } from '@crawlee/browser';
21
21
  import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
22
- import type { CheerioAPI } from 'cheerio';
22
+ import type { Cheerio, CheerioAPI } from 'cheerio';
23
+ import type { AnyNode } from 'domhandler';
23
24
  // @ts-ignore optional peer dependency or compatibility with es2022
24
25
  import type { Download, Page, Response } from 'playwright';
25
26
  import type { EnqueueLinksByClickingElementsOptions } from '../enqueue-links/click-elements.js';
@@ -420,6 +421,15 @@ export interface PlaywrightContextUtils {
420
421
  * ```
421
422
  */
422
423
  waitForSelector(selector: string, timeoutMs?: number): Promise<void>;
424
+ /**
425
+ * Like {@link PlaywrightContextUtils.waitForSelector|`waitForSelector`}, but returns the matched elements
426
+ * as a Cheerio object. Throws when nothing matches.
427
+ */
428
+ querySelectorAll(selector: string, timeoutMs?: number): Promise<Cheerio<AnyNode>>;
429
+ /**
430
+ * Like {@link PlaywrightContextUtils.querySelectorAll|`querySelectorAll`}, but returns only the first match.
431
+ */
432
+ querySelectorOne(selector: string, timeoutMs?: number): Promise<Cheerio<AnyNode>>;
423
433
  /**
424
434
  * Returns Cheerio handle for `page.content()`, allowing to work with the data same way as with {@link CheerioCrawler}.
425
435
  * When provided with the `selector` argument, it waits for it to be available first.
@@ -24,7 +24,8 @@ export interface IRenderingTypePredictor {
24
24
  detectionProbabilityRecommendation: number;
25
25
  }>;
26
26
  /**
27
- * Report a detected rendering type, so that future predictions can take it into account.
27
+ * Report a rendering type established by a detection, or `clientOnly` when the `resultChecker` rejected an
28
+ * HTTP-only result, so that future predictions can take it into account.
28
29
  *
29
30
  * The crawler does not await this per detection - it collects the returned promises and drains them in
30
31
  * `teardown()`, so an implementation that persists results asynchronously can keep batching its writes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/playwright",
3
- "version": "4.0.0-beta.231",
3
+ "version": "4.0.0-beta.232",
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.13.0"
@@ -49,14 +49,15 @@
49
49
  "dependencies": {
50
50
  "@apify/datastructures": "^3.0.1",
51
51
  "@apify/timeout": "^1.0.1",
52
- "@crawlee/basic": "4.0.0-beta.231",
53
- "@crawlee/browser": "4.0.0-beta.231",
54
- "@crawlee/browser-pool": "4.0.0-beta.231",
55
- "@crawlee/cheerio": "4.0.0-beta.231",
56
- "@crawlee/core": "4.0.0-beta.231",
57
- "@crawlee/types": "4.0.0-beta.231",
58
- "@crawlee/utils": "4.0.0-beta.231",
52
+ "@crawlee/basic": "4.0.0-beta.232",
53
+ "@crawlee/browser": "4.0.0-beta.232",
54
+ "@crawlee/browser-pool": "4.0.0-beta.232",
55
+ "@crawlee/cheerio": "4.0.0-beta.232",
56
+ "@crawlee/core": "4.0.0-beta.232",
57
+ "@crawlee/types": "4.0.0-beta.232",
58
+ "@crawlee/utils": "4.0.0-beta.232",
59
59
  "cheerio": "^1.0.0",
60
+ "domhandler": "^6.0.0",
60
61
  "jquery": "^3.7.1",
61
62
  "ml-logistic-regression": "^2.0.0",
62
63
  "ml-matrix": "^6.12.1",
@@ -79,5 +80,5 @@
79
80
  }
80
81
  }
81
82
  },
82
- "gitHead": "bc264d72aeef35d0915047359e96ca3635af4cea"
83
+ "gitHead": "5eab8f877242d46d7cae13b640d8e1adbf194911"
83
84
  }