@crawlee/playwright 4.0.0-beta.80 → 4.0.0-beta.81

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.
@@ -1,15 +1,14 @@
1
1
  import type { BrowserHook, LoadedRequest, Request, RouterHandler, RouteSchemas, RoutesFromSchemas } from '@crawlee/browser';
2
2
  import type { BasicCrawlerOptions } from '@crawlee/basic';
3
3
  import { BasicCrawler } from '@crawlee/basic';
4
- import type { ContextPipeline, CrawlingContext, EnqueueLinksOptions, GetUserDataFromRequest, RestrictedCrawlingContext, RouterRoutes, StatisticsOptions, StatisticState } from '@crawlee/core';
4
+ import type { ContextPipeline, CrawlingContext, EnqueueLinksOptions, GetUserDataFromRequest, RouterRoutes, StatisticsOptions, StatisticState } from '@crawlee/core';
5
5
  import { RequestHandlerResult, Statistics } from '@crawlee/core';
6
- import type { BatchAddRequestsResult, Dictionary, Awaitable } from '@crawlee/types';
6
+ import type { Dictionary, Awaitable } from '@crawlee/types';
7
7
  import { type CheerioRoot } from '@crawlee/utils';
8
8
  import { type Cheerio } from 'cheerio';
9
9
  import type { AnyNode } from 'domhandler';
10
10
  // @ts-ignore optional peer dependency or compatibility with es2022
11
11
  import type { Page } from 'playwright';
12
- import type { SetRequired } from 'type-fest';
13
12
  import type { PlaywrightCrawlingContext, PlaywrightGotoOptions } from './playwright-crawler.js';
14
13
  import { RenderingTypePredictor } from './utils/rendering-type-prediction.js';
15
14
  interface AdaptivePlaywrightCrawlerStatisticState extends StatisticState {
@@ -21,7 +20,7 @@ declare class AdaptivePlaywrightCrawlerStatistics extends Statistics {
21
20
  state: AdaptivePlaywrightCrawlerStatisticState;
22
21
  constructor(options?: StatisticsOptions);
23
22
  reset(): void;
24
- protected _maybeLoadStatistics(): Promise<void>;
23
+ protected maybeLoadStatistics(): Promise<void>;
25
24
  trackHttpOnlyRequestHandlerRun(): void;
26
25
  trackBrowserRequestHandlerRun(): void;
27
26
  trackRenderingTypeMisprediction(): void;
@@ -37,10 +36,15 @@ export interface AdaptivePlaywrightCrawlerContext<UserData extends Dictionary =
37
36
  */
38
37
  page: Page;
39
38
  /**
40
- * Wait for an element matching the selector to appear and return a Cheerio object of matched elements.
39
+ * Wait for an element matching the selector to appear and return a Cheerio object of the first matched element.
41
40
  * Timeout defaults to 5s.
42
41
  */
43
42
  querySelector(selector: string, timeoutMs?: number): Promise<Cheerio<AnyNode>>;
43
+ /**
44
+ * Wait for an element matching the selector to appear and return a Cheerio object of all matched elements.
45
+ * Timeout defaults to 5s.
46
+ */
47
+ querySelectorAll(selector: string, timeoutMs?: number): Promise<Cheerio<AnyNode>>;
44
48
  /**
45
49
  * Wait for an element matching the selector to appear.
46
50
  * Timeout defaults to 5s.
@@ -130,6 +134,8 @@ export interface AdaptivePlaywrightCrawlerOptions<ExtendedContext extends Adapti
130
134
  * If it returns 'inconclusive', the detection result won't be used.
131
135
  * If no result comparator is specified, but there is a `resultChecker`, any site where the `resultChecker` returns true is considered static.
132
136
  * If neither `resultComparator` nor `resultChecker` are specified, a deep comparison of returned dataset items is used as a default.
137
+ *
138
+ * For a stricter, ready-made comparator that also takes enqueued requests and key-value store changes into account, see {@link fullResultComparator}.
133
139
  */
134
140
  resultComparator?: (resultA: RequestHandlerResult, resultB: RequestHandlerResult) => boolean | 'equal' | 'different' | 'inconclusive';
135
141
  /**
@@ -191,6 +197,7 @@ export declare class AdaptivePlaywrightCrawler<ExtendedContext extends AdaptiveP
191
197
  readonly response: Response;
192
198
  readonly page: Page;
193
199
  readonly querySelector: AdaptivePlaywrightCrawlerContext["querySelector"];
200
+ readonly querySelectorAll: AdaptivePlaywrightCrawlerContext["querySelectorAll"];
194
201
  readonly waitForSelector: AdaptivePlaywrightCrawlerContext["waitForSelector"];
195
202
  readonly parseWithCheerio: AdaptivePlaywrightCrawlerContext["parseWithCheerio"];
196
203
  }>;
@@ -198,19 +205,43 @@ export declare class AdaptivePlaywrightCrawler<ExtendedContext extends AdaptiveP
198
205
  private adaptPlaywrightContext;
199
206
  private crawlOne;
200
207
  protected runRequestHandler(crawlingContext: CrawlingContext): Promise<void>;
201
- protected commitResult(crawlingContext: CrawlingContext, { calls, keyValueStoreChanges }: RequestHandlerResult): Promise<void>;
202
- protected allowStorageAccess<R, TArgs extends any[]>(func: (...args: TArgs) => Promise<R>): (...args: TArgs) => Promise<R>;
208
+ private commitResult;
209
+ private allowStorageAccess;
203
210
  /**
204
211
  * Reading the pending request count queries the underlying request manager, which counts as storage access.
205
212
  * Since this is internal crawler bookkeeping used to compute the `enqueueLinks` limit (not user-initiated storage
206
213
  * access), it must be allowed even while a request handler runs inside the storage-access guard.
207
214
  */
208
215
  protected getPendingRequestCountApproximation(): Promise<number>;
209
- protected enqueueLinks(options: SetRequired<EnqueueLinksOptions, 'urls'>, request: RestrictedCrawlingContext['request'], result: RequestHandlerResult): Promise<BatchAddRequestsResult>;
216
+ private enqueueLinks;
210
217
  private createLogProxy;
211
218
  teardown(): Promise<void>;
212
219
  }
213
220
  export declare function createAdaptivePlaywrightRouter<Context extends AdaptivePlaywrightCrawlerContext = AdaptivePlaywrightCrawlerContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
214
221
  export declare function createAdaptivePlaywrightRouter<Context extends AdaptivePlaywrightCrawlerContext = AdaptivePlaywrightCrawlerContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
215
222
  export declare function createAdaptivePlaywrightRouter<Context extends AdaptivePlaywrightCrawlerContext = AdaptivePlaywrightCrawlerContext, const Schemas extends RouteSchemas = RouteSchemas>(schemas: Schemas): RouterHandler<Context, RoutesFromSchemas<Schemas>>;
223
+ /**
224
+ * An opt-in {@link AdaptivePlaywrightCrawlerOptions.resultComparator|`resultComparator`} that considers two
225
+ * request handler results equal only if *all* of their observable effects match - the pushed dataset items, the
226
+ * enqueued requests, and the key-value store changes. This is stricter than the default comparator, which only
227
+ * compares dataset items.
228
+ *
229
+ * **Beware:** enqueued URLs are compared exactly. The same page rendered in a browser and via plain HTTP often
230
+ * yields links that differ only in tracking query parameters, for example:
231
+ * - `https://sdk.apify.com/docs/guides/getting-started`
232
+ * - `https://sdk.apify.com/docs/guides/getting-started?__hsfp=1136113150&__hssc=7591405.1.173549427712`
233
+ *
234
+ * Such links are treated as *different*, which will make the crawler favor browser rendering for those pages.
235
+ *
236
+ * **Example usage:**
237
+ * ```ts
238
+ * const crawler = new AdaptivePlaywrightCrawler({
239
+ * resultComparator: fullResultComparator,
240
+ * async requestHandler({ pushData, enqueueLinks }) {
241
+ * // ...
242
+ * },
243
+ * });
244
+ * ```
245
+ */
246
+ export declare function fullResultComparator(resultA: RequestHandlerResult, resultB: RequestHandlerResult): boolean;
216
247
  export {};
@@ -19,8 +19,8 @@ class AdaptivePlaywrightCrawlerStatistics extends Statistics {
19
19
  this.state.browserRequestHandlerRuns = 0;
20
20
  this.state.renderingTypeMispredictions = 0;
21
21
  }
22
- async _maybeLoadStatistics() {
23
- await super._maybeLoadStatistics();
22
+ async maybeLoadStatistics() {
23
+ await super.maybeLoadStatistics();
24
24
  const savedState = await this.keyValueStore?.getValue(this.persistStateKey);
25
25
  if (!savedState) {
26
26
  return;
@@ -183,6 +183,9 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
183
183
  get querySelector() {
184
184
  throw new Error(errorMessage('querySelector'));
185
185
  },
186
+ get querySelectorAll() {
187
+ throw new Error(errorMessage('querySelectorAll'));
188
+ },
186
189
  get waitForSelector() {
187
190
  throw new Error(errorMessage('waitForSelector'));
188
191
  },
@@ -203,6 +206,9 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
203
206
  throw new Error('Page object was used in HTTP-only request handler');
204
207
  },
205
208
  async querySelector(selector) {
209
+ return cheerioContext.$(selector).first();
210
+ },
211
+ async querySelectorAll(selector) {
206
212
  return cheerioContext.$(selector);
207
213
  },
208
214
  enqueueLinks: async (options = {}) => {
@@ -226,6 +232,12 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
226
232
  statusText: originalResponse.statusText(),
227
233
  }),
228
234
  async querySelector(selector, timeoutMs = 5000) {
235
+ const locator = playwrightContext.page.locator(selector).first();
236
+ await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
237
+ const $ = await playwrightContext.parseWithCheerio();
238
+ return $(selector).first();
239
+ },
240
+ async querySelectorAll(selector, timeoutMs = 5000) {
229
241
  const locator = playwrightContext.page.locator(selector).first();
230
242
  await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
231
243
  const $ = await playwrightContext.parseWithCheerio();
@@ -449,3 +461,32 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
449
461
  export function createAdaptivePlaywrightRouter(routesOrSchemas) {
450
462
  return Router.create(routesOrSchemas);
451
463
  }
464
+ /**
465
+ * An opt-in {@link AdaptivePlaywrightCrawlerOptions.resultComparator|`resultComparator`} that considers two
466
+ * request handler results equal only if *all* of their observable effects match - the pushed dataset items, the
467
+ * enqueued requests, and the key-value store changes. This is stricter than the default comparator, which only
468
+ * compares dataset items.
469
+ *
470
+ * **Beware:** enqueued URLs are compared exactly. The same page rendered in a browser and via plain HTTP often
471
+ * yields links that differ only in tracking query parameters, for example:
472
+ * - `https://sdk.apify.com/docs/guides/getting-started`
473
+ * - `https://sdk.apify.com/docs/guides/getting-started?__hsfp=1136113150&__hssc=7591405.1.173549427712`
474
+ *
475
+ * Such links are treated as *different*, which will make the crawler favor browser rendering for those pages.
476
+ *
477
+ * **Example usage:**
478
+ * ```ts
479
+ * const crawler = new AdaptivePlaywrightCrawler({
480
+ * resultComparator: fullResultComparator,
481
+ * async requestHandler({ pushData, enqueueLinks }) {
482
+ * // ...
483
+ * },
484
+ * });
485
+ * ```
486
+ */
487
+ export function fullResultComparator(resultA, resultB) {
488
+ return (isDeepStrictEqual(resultA.datasetItems, resultB.datasetItems) &&
489
+ isDeepStrictEqual(resultA.enqueuedUrls, resultB.enqueuedUrls) &&
490
+ isDeepStrictEqual(resultA.enqueuedUrlLists, resultB.enqueuedUrlLists) &&
491
+ isDeepStrictEqual(resultA.keyValueStoreChanges, resultB.keyValueStoreChanges));
492
+ }
@@ -1,7 +1,5 @@
1
1
  import type { RecoverableStatePersistenceOptions, Request } from '@crawlee/core';
2
2
  export type RenderingType = 'clientOnly' | 'static';
3
- type URLComponents = string[];
4
- type FeatureVector = [staticResultsSimilarity: number, clientOnlyResultsSimilarity: number];
5
3
  export interface RenderingTypePredictorOptions {
6
4
  /** A number between 0 and 1 that determines the desired ratio of rendering type detections */
7
5
  detectionRatio: number;
@@ -32,7 +30,6 @@ export declare class RenderingTypePredictor {
32
30
  */
33
31
  storeResult(requests: Request | Request[], renderingType: RenderingType): void;
34
32
  private resultCount;
35
- protected calculateFeatureVector(url: URLComponents, label: string | undefined): FeatureVector;
36
- protected retrain(): void;
33
+ private calculateFeatureVector;
34
+ private retrain;
37
35
  }
38
- export {};
@@ -10,10 +10,17 @@ const calculateUrlSimilarity = (a, b) => {
10
10
  if (a[0] !== b[0]) {
11
11
  return 0;
12
12
  }
13
- for (let i = 1; i < Math.max(a.length, b.length); i++) {
13
+ const maxLength = Math.max(a.length, b.length);
14
+ // Only the hostname is present (no path components to compare) - the hosts already match.
15
+ if (maxLength <= 1) {
16
+ return 1;
17
+ }
18
+ for (let i = 1; i < maxLength; i++) {
14
19
  values.push(stringComparison.jaroWinkler.similarity(a[i] ?? '', b[i] ?? '') > 0.8 ? 1 : 0);
15
20
  }
16
- return sum(values) / Math.max(a.length, b.length);
21
+ // The first component (index 0, the hostname) is excluded from the comparison above,
22
+ // so it must also be excluded from the denominator of the weighted average.
23
+ return sum(values) / (maxLength - 1);
17
24
  };
18
25
  const sum = (values) => values.reduce((acc, value) => acc + value);
19
26
  const mean = (values) => (values.length > 0 ? sum(values) / values.length : undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/playwright",
3
- "version": "4.0.0-beta.80",
3
+ "version": "4.0.0-beta.81",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -49,13 +49,13 @@
49
49
  "dependencies": {
50
50
  "@apify/datastructures": "^2.0.3",
51
51
  "@apify/timeout": "^0.3.2",
52
- "@crawlee/basic": "4.0.0-beta.80",
53
- "@crawlee/browser": "4.0.0-beta.80",
54
- "@crawlee/browser-pool": "4.0.0-beta.80",
55
- "@crawlee/cheerio": "4.0.0-beta.80",
56
- "@crawlee/core": "4.0.0-beta.80",
57
- "@crawlee/types": "4.0.0-beta.80",
58
- "@crawlee/utils": "4.0.0-beta.80",
52
+ "@crawlee/basic": "4.0.0-beta.81",
53
+ "@crawlee/browser": "4.0.0-beta.81",
54
+ "@crawlee/browser-pool": "4.0.0-beta.81",
55
+ "@crawlee/cheerio": "4.0.0-beta.81",
56
+ "@crawlee/core": "4.0.0-beta.81",
57
+ "@crawlee/types": "4.0.0-beta.81",
58
+ "@crawlee/utils": "4.0.0-beta.81",
59
59
  "cheerio": "^1.0.0",
60
60
  "idcac-playwright": "^0.1.3",
61
61
  "jquery": "^3.7.1",
@@ -85,5 +85,5 @@
85
85
  }
86
86
  }
87
87
  },
88
- "gitHead": "96c57b4a0c999e4b2bd198792490af28db7aa42d"
88
+ "gitHead": "80dc6b4fc82237e63a51a71153809ec8dfd0cc50"
89
89
  }