@crawlee/playwright 4.0.0-beta.99 → 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 -6
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BrowserCrawler, RequestState, Router, serviceLocator } from '@crawlee/browser';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { assertBrowserPoolNotConfigured, BrowserCrawler, parseArgument, RequestState, Router, schemas, serviceLocator, } from '@crawlee/browser';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { playwrightBrowserPool, remotePlaywrightBrowserPool } from './playwright-browser-pool.js';
|
|
4
4
|
import { gotoExtended, playwrightUtils } from './utils/playwright-utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Provides a simple framework for parallel crawling of web pages
|
|
@@ -70,46 +70,41 @@ import { gotoExtended, playwrightUtils } from './utils/playwright-utils.js';
|
|
|
70
70
|
export class PlaywrightCrawler extends BrowserCrawler {
|
|
71
71
|
static optionsShape = {
|
|
72
72
|
...BrowserCrawler.optionsShape,
|
|
73
|
-
|
|
74
|
-
launcher:
|
|
75
|
-
ignoreIframes: ow.optional.boolean,
|
|
76
|
-
ignoreShadowRoots: ow.optional.boolean,
|
|
73
|
+
headless: z.boolean().optional(),
|
|
74
|
+
launcher: schemas.anyObject.optional(),
|
|
77
75
|
};
|
|
76
|
+
static optionsSchema = z.strictObject(PlaywrightCrawler.optionsShape);
|
|
78
77
|
/**
|
|
79
78
|
* All `PlaywrightCrawler` parameters are passed via an options object.
|
|
80
79
|
*/
|
|
81
80
|
constructor(options = {}) {
|
|
82
|
-
|
|
83
|
-
const { launchContext
|
|
84
|
-
const browserPoolOptions = {
|
|
85
|
-
...options.browserPoolOptions,
|
|
86
|
-
};
|
|
81
|
+
const parsedOptions = parseArgument(options, PlaywrightCrawler.optionsSchema, 'PlaywrightCrawlerOptions');
|
|
82
|
+
const { launchContext, headless, configuration, contextPipelineBuilder, ...browserCrawlerOptions } = parsedOptions;
|
|
87
83
|
if (launchContext.proxyUrl) {
|
|
88
84
|
throw new Error('PlaywrightCrawlerOptions.launchContext.proxyUrl is not allowed in PlaywrightCrawler.' +
|
|
89
85
|
'Use PlaywrightCrawlerOptions.proxyConfiguration');
|
|
90
86
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
launchContext.launchOptions ??= {};
|
|
98
|
-
launchContext.launchOptions.headless = headless;
|
|
87
|
+
if (options.browserPool) {
|
|
88
|
+
// The raw options, not the parsed ones: `launchContext` has a default, so by now it is always set.
|
|
89
|
+
assertBrowserPoolNotConfigured(new.target.name, {
|
|
90
|
+
launchContext: options.launchContext,
|
|
91
|
+
headless: options.headless,
|
|
92
|
+
});
|
|
99
93
|
}
|
|
100
|
-
const playwrightLauncher = new PlaywrightLauncher(launchContext, options.configuration);
|
|
101
|
-
browserPoolOptions.browserPlugins = [playwrightLauncher.createBrowserPlugin()];
|
|
102
94
|
super({
|
|
103
95
|
...browserCrawlerOptions,
|
|
104
96
|
launchContext,
|
|
105
|
-
|
|
97
|
+
configuration,
|
|
98
|
+
browserPoolBuilder: (remoteBrowser) => remoteBrowser
|
|
99
|
+
? remotePlaywrightBrowserPool({ ...remoteBrowser, launchContext, headless, configuration })
|
|
100
|
+
: playwrightBrowserPool({ launchContext, headless, configuration }),
|
|
106
101
|
contextPipelineBuilder: contextPipelineBuilder ?? (() => this.buildContextPipeline()),
|
|
107
102
|
});
|
|
108
103
|
}
|
|
109
104
|
buildContextPipeline() {
|
|
110
105
|
return super.buildContextPipeline().compose({ action: this.enhanceContext.bind(this) });
|
|
111
106
|
}
|
|
112
|
-
async
|
|
107
|
+
async navigationHandler(crawlingContext, gotoOptions) {
|
|
113
108
|
return gotoExtended(crawlingContext.page, crawlingContext.request, gotoOptions);
|
|
114
109
|
}
|
|
115
110
|
async enhanceContext(context) {
|
|
@@ -3,6 +3,7 @@ import { BrowserLauncher, Configuration } from '@crawlee/browser';
|
|
|
3
3
|
import { PlaywrightPlugin } from '@crawlee/browser-pool';
|
|
4
4
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
5
5
|
import type { Browser, BrowserType, LaunchOptions } from 'playwright';
|
|
6
|
+
import { z } from 'zod';
|
|
6
7
|
/**
|
|
7
8
|
* Apify extends the launch options of Playwright.
|
|
8
9
|
* You can use any of the Playwright compatible
|
|
@@ -73,26 +74,34 @@ export declare class PlaywrightLauncher extends BrowserLauncher<PlaywrightPlugin
|
|
|
73
74
|
readonly configuration: Configuration;
|
|
74
75
|
protected static optionsShape: {
|
|
75
76
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
76
|
-
launcher: import("
|
|
77
|
+
launcher: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
|
|
77
78
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
78
|
-
launchContextOptions: import("
|
|
79
|
+
launchContextOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
|
|
80
|
+
proxyUrl: z.ZodOptional<z.ZodURL>;
|
|
81
|
+
useChrome: z.ZodOptional<z.ZodBoolean>;
|
|
82
|
+
useIncognitoPages: z.ZodOptional<z.ZodBoolean>;
|
|
83
|
+
browserPerProxy: z.ZodOptional<z.ZodBoolean>;
|
|
84
|
+
ignoreProxyCertificate: z.ZodOptional<z.ZodBoolean>;
|
|
85
|
+
userDataDir: z.ZodOptional<z.ZodString>;
|
|
79
86
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
useIncognitoPages: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
|
|
85
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
86
|
-
browserPerProxy: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
|
|
87
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
88
|
-
ignoreProxyCertificate: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
|
|
87
|
+
launchOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
|
|
88
|
+
userAgent: z.ZodOptional<z.ZodString>;
|
|
89
|
+
};
|
|
90
|
+
protected static optionsSchema: z.ZodObject<{
|
|
89
91
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
90
|
-
|
|
92
|
+
launcher: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
|
|
91
93
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
92
|
-
|
|
94
|
+
launchContextOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
|
|
95
|
+
proxyUrl: z.ZodOptional<z.ZodURL>;
|
|
96
|
+
useChrome: z.ZodOptional<z.ZodBoolean>;
|
|
97
|
+
useIncognitoPages: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
+
browserPerProxy: z.ZodOptional<z.ZodBoolean>;
|
|
99
|
+
ignoreProxyCertificate: z.ZodOptional<z.ZodBoolean>;
|
|
100
|
+
userDataDir: z.ZodOptional<z.ZodString>;
|
|
93
101
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
launchOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
|
|
103
|
+
userAgent: z.ZodOptional<z.ZodString>;
|
|
104
|
+
}, z.core.$strict>;
|
|
96
105
|
/**
|
|
97
106
|
* All `PlaywrightLauncher` parameters are passed via this launchContext object.
|
|
98
107
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BrowserLauncher, Configuration } from '@crawlee/browser';
|
|
1
|
+
import { BrowserLauncher, Configuration, parseArgument, schemas } from '@crawlee/browser';
|
|
2
2
|
import { PlaywrightPlugin } from '@crawlee/browser-pool';
|
|
3
|
-
import
|
|
3
|
+
import { z } from 'zod';
|
|
4
4
|
/**
|
|
5
5
|
* `PlaywrightLauncher` is based on the `BrowserLauncher`. It launches `playwright` browser instance.
|
|
6
6
|
* @ignore
|
|
@@ -9,21 +9,23 @@ export class PlaywrightLauncher extends BrowserLauncher {
|
|
|
9
9
|
configuration;
|
|
10
10
|
static optionsShape = {
|
|
11
11
|
...BrowserLauncher.optionsShape,
|
|
12
|
-
launcher
|
|
13
|
-
|
|
12
|
+
// Passthrough schemas — the launcher module object must keep its prototype through parsing.
|
|
13
|
+
launcher: schemas.anyObject.optional(),
|
|
14
|
+
launchContextOptions: schemas.anyObject.optional(),
|
|
14
15
|
};
|
|
16
|
+
static optionsSchema = z.strictObject(PlaywrightLauncher.optionsShape);
|
|
15
17
|
/**
|
|
16
18
|
* All `PlaywrightLauncher` parameters are passed via this launchContext object.
|
|
17
19
|
*/
|
|
18
20
|
constructor(launchContext = {}, configuration = Configuration.getGlobalConfiguration()) {
|
|
19
|
-
|
|
20
|
-
const { launcher = BrowserLauncher.requireLauncherOrThrow('playwright', 'apify/actor-node-playwright-*').chromium, } =
|
|
21
|
-
const { launchOptions = {}, ...rest } =
|
|
21
|
+
const parsedContext = parseArgument(launchContext, PlaywrightLauncher.optionsSchema, 'PlaywrightLaunchContext');
|
|
22
|
+
const { launcher = BrowserLauncher.requireLauncherOrThrow('playwright', 'apify/actor-node-playwright-*').chromium, } = parsedContext;
|
|
23
|
+
const { launchOptions = {}, ...rest } = parsedContext;
|
|
22
24
|
super({
|
|
23
25
|
...rest,
|
|
24
26
|
launchOptions: {
|
|
25
27
|
...launchOptions,
|
|
26
|
-
executablePath: getDefaultExecutablePath(
|
|
28
|
+
executablePath: getDefaultExecutablePath(parsedContext, configuration),
|
|
27
29
|
},
|
|
28
30
|
launcher,
|
|
29
31
|
}, configuration);
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { Configuration, type Request } from '@crawlee/browser';
|
|
21
21
|
import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
|
|
22
|
-
import { type CheerioRoot } from '@crawlee/utils';
|
|
22
|
+
import { type CheerioRoot } from '@crawlee/utils/internal';
|
|
23
23
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
24
24
|
import type { Download, Page, Response } from 'playwright';
|
|
25
25
|
import type { EnqueueLinksByClickingElementsOptions } from '../enqueue-links/click-elements.js';
|
|
@@ -454,8 +454,7 @@ export interface PlaywrightContextUtils {
|
|
|
454
454
|
* in `href` elements, but rather navigations are triggered in click handlers.
|
|
455
455
|
* If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
|
|
456
456
|
*
|
|
457
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of
|
|
458
|
-
* and override settings of the enqueued {@link Request} objects.
|
|
457
|
+
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
459
458
|
*
|
|
460
459
|
* **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
|
|
461
460
|
* such as changing the Z-index of elements being clicked and their visibility. Therefore,
|
|
@@ -476,9 +475,9 @@ export interface PlaywrightContextUtils {
|
|
|
476
475
|
* async requestHandler({ enqueueLinksByClickingElements }) {
|
|
477
476
|
* await enqueueLinksByClickingElements({
|
|
478
477
|
* selector: 'a.product-detail',
|
|
479
|
-
*
|
|
480
|
-
* 'https://www.example.com/handbags/**'
|
|
481
|
-
* 'https://www.example.com/purses/**'
|
|
478
|
+
* include: [
|
|
479
|
+
* 'https://www.example.com/handbags/**',
|
|
480
|
+
* 'https://www.example.com/purses/**',
|
|
482
481
|
* ],
|
|
483
482
|
* });
|
|
484
483
|
* });
|
|
@@ -20,10 +20,9 @@
|
|
|
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, serviceLocator, SessionError, validators } from '@crawlee/browser';
|
|
23
|
+
import { Configuration, KeyValueStore, parseArgument, schemas, serviceLocator, SessionError, validators, } from '@crawlee/browser';
|
|
24
24
|
import { expandShadowRoots, sleep } from '@crawlee/utils';
|
|
25
|
-
import
|
|
26
|
-
import ow from 'ow';
|
|
25
|
+
import { z } from 'zod';
|
|
27
26
|
import { LruCache } from '@apify/datastructures';
|
|
28
27
|
import { enqueueLinksByClickingElements } from '../enqueue-links/click-elements.js';
|
|
29
28
|
import { RenderingTypePredictor } from './rendering-type-prediction.js';
|
|
@@ -32,6 +31,36 @@ const require = createRequire(import.meta.url);
|
|
|
32
31
|
const jqueryPath = require.resolve('jquery');
|
|
33
32
|
const MAX_INJECT_FILE_CACHE_SIZE = 10;
|
|
34
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
|
+
});
|
|
35
64
|
/**
|
|
36
65
|
* Cache contents of previously injected files to limit file system access.
|
|
37
66
|
*/
|
|
@@ -48,18 +77,16 @@ const injectedFilesCache = new LruCache({ maxLength: MAX_INJECT_FILE_CACHE_SIZE
|
|
|
48
77
|
* @param [options]
|
|
49
78
|
*/
|
|
50
79
|
export async function injectFile(page, filePath, options = {}) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
surviveNavigations: ow.optional.boolean,
|
|
55
|
-
}));
|
|
80
|
+
parseArgument(page, validators.browserPage);
|
|
81
|
+
parseArgument(filePath, filePathSchema);
|
|
82
|
+
const { surviveNavigations } = parseArgument(options, injectFileOptionsSchema);
|
|
56
83
|
let contents = injectedFilesCache.get(filePath);
|
|
57
84
|
if (!contents) {
|
|
58
85
|
contents = await readFile(filePath, 'utf8');
|
|
59
86
|
injectedFilesCache.add(filePath, contents);
|
|
60
87
|
}
|
|
61
88
|
const evalP = page.evaluate(contents);
|
|
62
|
-
if (
|
|
89
|
+
if (surviveNavigations) {
|
|
63
90
|
page.on('framenavigated', async () => page
|
|
64
91
|
.evaluate(contents)
|
|
65
92
|
.catch((error) => getLog().warning('An error occurred during the script injection!', { error })));
|
|
@@ -93,7 +120,7 @@ export async function injectFile(page, filePath, options = {}) {
|
|
|
93
120
|
* @param [options.surviveNavigations] Opt-out option to disable the JQuery reinjection after navigation.
|
|
94
121
|
*/
|
|
95
122
|
export async function injectJQuery(page, options) {
|
|
96
|
-
|
|
123
|
+
parseArgument(page, validators.browserPage);
|
|
97
124
|
return injectFile(page, jqueryPath, { surviveNavigations: options?.surviveNavigations ?? true });
|
|
98
125
|
}
|
|
99
126
|
/**
|
|
@@ -109,14 +136,9 @@ export async function injectJQuery(page, options) {
|
|
|
109
136
|
* @param [gotoOptions] Custom options for `page.goto()`.
|
|
110
137
|
*/
|
|
111
138
|
export async function gotoExtended(page, request, gotoOptions = {}) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
method: ow.optional.string,
|
|
116
|
-
headers: ow.optional.object,
|
|
117
|
-
payload: ow.optional.any(ow.string, ow.uint8Array),
|
|
118
|
-
}));
|
|
119
|
-
ow(gotoOptions, ow.object);
|
|
139
|
+
parseArgument(page, validators.browserPage);
|
|
140
|
+
parseArgument(request, gotoExtendedRequestSchema);
|
|
141
|
+
parseArgument(gotoOptions, schemas.anyObject);
|
|
120
142
|
const { url, method, headers, payload } = request;
|
|
121
143
|
const isEmpty = (o) => !o || Object.keys(o).length === 0;
|
|
122
144
|
if (method !== 'GET' || payload) {
|
|
@@ -201,12 +223,8 @@ export async function gotoExtended(page, request, gotoOptions = {}) {
|
|
|
201
223
|
* @param [options]
|
|
202
224
|
*/
|
|
203
225
|
export async function blockRequests(page, options = {}) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
urlPatterns: ow.optional.array.ofType(ow.string),
|
|
207
|
-
extraUrlPatterns: ow.optional.array.ofType(ow.string),
|
|
208
|
-
}));
|
|
209
|
-
const { urlPatterns = DEFAULT_BLOCK_REQUEST_URL_PATTERNS, extraUrlPatterns = [] } = options;
|
|
226
|
+
parseArgument(page, validators.browserPage);
|
|
227
|
+
const { urlPatterns, extraUrlPatterns } = parseArgument(options, blockRequestsOptionsSchema);
|
|
210
228
|
const patternsToBlock = [...urlPatterns, ...extraUrlPatterns];
|
|
211
229
|
try {
|
|
212
230
|
const client = await page.context().newCDPSession(page);
|
|
@@ -264,16 +282,8 @@ export function compileScript(scriptString, context = Object.create(null)) {
|
|
|
264
282
|
* @param [options]
|
|
265
283
|
*/
|
|
266
284
|
export async function infiniteScroll(page, options = {}) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
timeoutSecs: ow.optional.number,
|
|
270
|
-
maxScrollHeight: ow.optional.number,
|
|
271
|
-
waitForSecs: ow.optional.number,
|
|
272
|
-
scrollDownAndUp: ow.optional.boolean,
|
|
273
|
-
buttonSelector: ow.optional.string,
|
|
274
|
-
stopScrollCallback: ow.optional.function,
|
|
275
|
-
}));
|
|
276
|
-
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);
|
|
277
287
|
let finished;
|
|
278
288
|
const startTime = Date.now();
|
|
279
289
|
const CHECK_INTERVAL_MILLIS = 1000;
|
|
@@ -350,16 +360,8 @@ export async function infiniteScroll(page, options = {}) {
|
|
|
350
360
|
* @param [options]
|
|
351
361
|
*/
|
|
352
362
|
export async function saveSnapshot(page, options = {}) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
key: ow.optional.string.nonEmpty,
|
|
356
|
-
screenshotQuality: ow.optional.number,
|
|
357
|
-
saveScreenshot: ow.optional.boolean,
|
|
358
|
-
saveHtml: ow.optional.boolean,
|
|
359
|
-
keyValueStoreName: ow.optional.string,
|
|
360
|
-
configuration: ow.optional.object,
|
|
361
|
-
}));
|
|
362
|
-
const { key = 'SNAPSHOT', screenshotQuality = 50, saveScreenshot = true, saveHtml = true, keyValueStoreName, configuration, } = options;
|
|
363
|
+
parseArgument(page, validators.browserPage);
|
|
364
|
+
const { key, screenshotQuality, saveScreenshot, saveHtml, keyValueStoreName, configuration } = parseArgument(options, saveSnapshotOptionsSchema);
|
|
363
365
|
try {
|
|
364
366
|
const store = await KeyValueStore.open(keyValueStoreName ? { name: keyValueStoreName } : null, {
|
|
365
367
|
configuration: configuration ?? Configuration.getGlobalConfiguration(),
|
|
@@ -397,12 +399,13 @@ export async function saveSnapshot(page, options = {}) {
|
|
|
397
399
|
* @param ignoreShadowRoots
|
|
398
400
|
*/
|
|
399
401
|
export async function parseWithCheerio(page, ignoreShadowRoots = false, ignoreIframes = false) {
|
|
400
|
-
|
|
402
|
+
parseArgument(page, validators.browserPage);
|
|
401
403
|
const html = ignoreShadowRoots
|
|
402
404
|
? null
|
|
403
405
|
: (await page.evaluate(`(${expandShadowRoots.toString()})(document)`));
|
|
404
406
|
const pageContent = html || (await page.content());
|
|
405
|
-
const
|
|
407
|
+
const { load } = await import('cheerio');
|
|
408
|
+
const $ = load(pageContent);
|
|
406
409
|
if (page.frames().length > 1 && !ignoreIframes) {
|
|
407
410
|
const frames = await page.$$('iframe');
|
|
408
411
|
const cheerioIframes = $('iframe').toArray();
|
|
@@ -457,7 +460,7 @@ ${error.message}
|
|
|
457
460
|
return idcacPlaywright;
|
|
458
461
|
}
|
|
459
462
|
export async function closeCookieModals(page) {
|
|
460
|
-
|
|
463
|
+
parseArgument(page, validators.browserPage);
|
|
461
464
|
const idcac = await getIdcacPlaywright();
|
|
462
465
|
if (idcac?.getInjectableScript()) {
|
|
463
466
|
await page.evaluate(idcac.getInjectableScript());
|
|
@@ -496,9 +499,9 @@ async function handleCloudflareChallenge(page, url, options = {}) {
|
|
|
496
499
|
};
|
|
497
500
|
options.isChallengeCallback ??= async () => {
|
|
498
501
|
return await page.evaluate(async () => {
|
|
499
|
-
// Cloudflare
|
|
500
|
-
//
|
|
501
|
-
return !!document.querySelector('.footer .footer-inner .
|
|
502
|
+
// Cloudflare keeps reshuffling the wrapper elements between `.footer-inner` and `.ray-id`,
|
|
503
|
+
// so only the stable outer classes are matched.
|
|
504
|
+
return !!document.querySelector('.footer .footer-inner .ray-id');
|
|
502
505
|
});
|
|
503
506
|
};
|
|
504
507
|
const retryBlocked = async () => {
|
|
@@ -26,7 +26,7 @@ export interface IRenderingTypePredictor {
|
|
|
26
26
|
* @experimental
|
|
27
27
|
*/
|
|
28
28
|
export declare class RenderingTypePredictor implements IRenderingTypePredictor {
|
|
29
|
-
private
|
|
29
|
+
#private;
|
|
30
30
|
private state;
|
|
31
31
|
constructor({ detectionRatio, persistenceOptions }: RenderingTypePredictorOptions);
|
|
32
32
|
/**
|
|
@@ -2,6 +2,7 @@ import { RecoverableState } from '@crawlee/core';
|
|
|
2
2
|
import LogisticRegression from 'ml-logistic-regression';
|
|
3
3
|
import { Matrix } from 'ml-matrix';
|
|
4
4
|
import stringComparison from 'string-comparison';
|
|
5
|
+
import { z } from 'zod';
|
|
5
6
|
const urlComponents = (url) => {
|
|
6
7
|
return [url.hostname, ...url.pathname.split('/')];
|
|
7
8
|
};
|
|
@@ -24,41 +25,57 @@ const calculateUrlSimilarity = (a, b) => {
|
|
|
24
25
|
};
|
|
25
26
|
const sum = (values) => values.reduce((acc, value) => acc + value);
|
|
26
27
|
const mean = (values) => (values.length > 0 ? sum(values) / values.length : undefined);
|
|
28
|
+
const renderingType = z.enum(['clientOnly', 'static']);
|
|
29
|
+
const predictorState = z.object({
|
|
30
|
+
logreg: z.instanceof(LogisticRegression),
|
|
31
|
+
detectionResults: z.map(renderingType, z.map(z.string().optional(), z.array(z.array(z.string())))),
|
|
32
|
+
});
|
|
33
|
+
const persistedState = z.object({
|
|
34
|
+
logreg: z
|
|
35
|
+
.record(z.string(), z.unknown())
|
|
36
|
+
.prefault(() => new LogisticRegression({ numSteps: 1000, learningRate: 0.05 }).toJSON()),
|
|
37
|
+
detectionResults: z
|
|
38
|
+
.array(z.object({
|
|
39
|
+
renderingType,
|
|
40
|
+
urlPartsByLabel: z.array(z.object({
|
|
41
|
+
label: z.string().optional(),
|
|
42
|
+
urlParts: z.array(z.array(z.string())),
|
|
43
|
+
})),
|
|
44
|
+
}))
|
|
45
|
+
.prefault([]),
|
|
46
|
+
});
|
|
47
|
+
const stateCodec = z.codec(persistedState, predictorState, {
|
|
48
|
+
decode: ({ logreg, detectionResults }) => ({
|
|
49
|
+
logreg: LogisticRegression.load(logreg),
|
|
50
|
+
detectionResults: new Map(detectionResults.map(({ renderingType, urlPartsByLabel }) => [
|
|
51
|
+
renderingType,
|
|
52
|
+
new Map(urlPartsByLabel.map(({ label, urlParts }) => [label, urlParts])),
|
|
53
|
+
])),
|
|
54
|
+
}),
|
|
55
|
+
encode: ({ logreg, detectionResults }) => ({
|
|
56
|
+
logreg: logreg.toJSON(),
|
|
57
|
+
detectionResults: Array.from(detectionResults.entries()).map(([renderingType, urlPartsByLabel]) => ({
|
|
58
|
+
renderingType,
|
|
59
|
+
urlPartsByLabel: Array.from(urlPartsByLabel.entries()).map(([label, urlParts]) => ({ label, urlParts })),
|
|
60
|
+
})),
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
27
63
|
/**
|
|
28
64
|
* 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.
|
|
29
65
|
*
|
|
30
66
|
* @experimental
|
|
31
67
|
*/
|
|
32
68
|
export class RenderingTypePredictor {
|
|
33
|
-
detectionRatio;
|
|
69
|
+
#detectionRatio;
|
|
70
|
+
// kept as TS-private: tests reach for it at runtime
|
|
34
71
|
state;
|
|
35
72
|
constructor({ detectionRatio, persistenceOptions }) {
|
|
36
|
-
this
|
|
73
|
+
this.#detectionRatio = detectionRatio;
|
|
37
74
|
this.state = new RecoverableState({
|
|
38
|
-
defaultState: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
serialize: (state) => JSON.stringify({
|
|
43
|
-
logreg: state.logreg.toJSON(),
|
|
44
|
-
detectionResults: Array.from(state.detectionResults.entries()).map(([renderingType, urlPartsByLabel]) => ({
|
|
45
|
-
renderingType,
|
|
46
|
-
urlPartsByLabel: Array.from(urlPartsByLabel.entries()).map(([label, urlParts]) => ({
|
|
47
|
-
label,
|
|
48
|
-
urlParts,
|
|
49
|
-
})),
|
|
50
|
-
})),
|
|
51
|
-
}),
|
|
52
|
-
deserialize: (serializedState) => {
|
|
53
|
-
const { logreg, detectionResults = [] } = JSON.parse(serializedState);
|
|
54
|
-
return {
|
|
55
|
-
logreg: LogisticRegression.load(logreg),
|
|
56
|
-
detectionResults: new Map(detectionResults.map((serializedItem) => [
|
|
57
|
-
serializedItem.renderingType,
|
|
58
|
-
new Map(serializedItem.urlPartsByLabel.map((item) => [item.label, item.urlParts])),
|
|
59
|
-
])),
|
|
60
|
-
};
|
|
61
|
-
},
|
|
75
|
+
defaultState: () => stateCodec.decode({}),
|
|
76
|
+
// The codec validates in the decode direction, so it is a Standard Schema as-is; encoding needs a call.
|
|
77
|
+
deserialize: stateCodec,
|
|
78
|
+
serialize: (state) => stateCodec.encode(state),
|
|
62
79
|
persistStateKey: 'rendering-type-predictor-state',
|
|
63
80
|
persistenceEnabled: true,
|
|
64
81
|
...persistenceOptions,
|
|
@@ -86,7 +103,7 @@ export class RenderingTypePredictor {
|
|
|
86
103
|
renderingType: prediction === 1 ? 'static' : 'clientOnly',
|
|
87
104
|
detectionProbabilityRecommendation: Math.abs(scores[0] - scores[1]) < 0.1
|
|
88
105
|
? 1
|
|
89
|
-
: this
|
|
106
|
+
: this.#detectionRatio * Math.max(1, 5 - this.resultCount(label)),
|
|
90
107
|
};
|
|
91
108
|
}
|
|
92
109
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/playwright",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-rc.0",
|
|
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,22 +49,21 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@apify/datastructures": "^2.0.3",
|
|
51
51
|
"@apify/timeout": "^0.4.4",
|
|
52
|
-
"@crawlee/basic": "4.0.0-
|
|
53
|
-
"@crawlee/browser": "4.0.0-
|
|
54
|
-
"@crawlee/browser-pool": "4.0.0-
|
|
55
|
-
"@crawlee/cheerio": "4.0.0-
|
|
56
|
-
"@crawlee/core": "4.0.0-
|
|
57
|
-
"@crawlee/types": "4.0.0-
|
|
58
|
-
"@crawlee/utils": "4.0.0-
|
|
52
|
+
"@crawlee/basic": "4.0.0-rc.0",
|
|
53
|
+
"@crawlee/browser": "4.0.0-rc.0",
|
|
54
|
+
"@crawlee/browser-pool": "4.0.0-rc.0",
|
|
55
|
+
"@crawlee/cheerio": "4.0.0-rc.0",
|
|
56
|
+
"@crawlee/core": "4.0.0-rc.0",
|
|
57
|
+
"@crawlee/types": "4.0.0-rc.0",
|
|
58
|
+
"@crawlee/utils": "4.0.0-rc.0",
|
|
59
59
|
"cheerio": "^1.0.0",
|
|
60
|
-
"idcac-playwright": "^0.1.3",
|
|
61
60
|
"jquery": "^3.7.1",
|
|
62
61
|
"ml-logistic-regression": "^2.0.0",
|
|
63
62
|
"ml-matrix": "^6.12.1",
|
|
64
|
-
"ow": "^2.0.0",
|
|
65
63
|
"string-comparison": "^1.3.0",
|
|
66
64
|
"tslib": "^2.8.1",
|
|
67
|
-
"type-fest": "^4.0.0"
|
|
65
|
+
"type-fest": "^4.0.0",
|
|
66
|
+
"zod": "^4.4.3"
|
|
68
67
|
},
|
|
69
68
|
"peerDependencies": {
|
|
70
69
|
"idcac-playwright": "^0.2.0",
|
|
@@ -85,5 +84,5 @@
|
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
},
|
|
88
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "79ab33dacdacb83e0197e6516d145f3aceef80c7"
|
|
89
88
|
}
|