@crawlee/cheerio 4.0.0-beta.99 → 4.0.0-rc.1
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 +1 -1
- package/index.d.ts +1 -0
- package/internals/cheerio-crawler.d.ts +8 -83
- package/internals/cheerio-crawler.js +4 -111
- package/internals/cheerio-parser.d.ts +11 -0
- package/internals/cheerio-parser.js +27 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Crawlee is available as the [`crawlee`](https://www.npmjs.com/package/crawlee) N
|
|
|
34
34
|
|
|
35
35
|
We recommend visiting the [Introduction tutorial](https://crawlee.dev/js/docs/introduction) in Crawlee documentation for more information.
|
|
36
36
|
|
|
37
|
-
> Crawlee requires **Node.js
|
|
37
|
+
> Crawlee requires **Node.js 22.13 or higher**.
|
|
38
38
|
|
|
39
39
|
### With Crawlee CLI
|
|
40
40
|
|
package/index.d.ts
CHANGED
|
@@ -1,61 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import
|
|
5
|
-
import type { CheerioAPI } from 'cheerio';
|
|
6
|
-
import * as cheerio from 'cheerio';
|
|
1
|
+
import type { CrawlingContext, DOMCrawlingContext, ErrorHandler, GetUserDataFromRequest, HttpCrawlerOptions, RequestHandler, RouterHandler, RouterRoutes, RouteSchemas, RoutesFromSchemas } from '@crawlee/http';
|
|
2
|
+
import { DOMCrawler } from '@crawlee/http';
|
|
3
|
+
import type { Dictionary } from '@crawlee/types';
|
|
4
|
+
import type { CheerioParseResult } from './cheerio-parser.js';
|
|
7
5
|
export type CheerioErrorHandler<UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
8
6
|
JSONData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
9
7
|
ContextExtension = Dictionary<never>> = ErrorHandler<CrawlingContext, CheerioCrawlingContext<UserData, JSONData> & ContextExtension>;
|
|
10
8
|
export interface CheerioCrawlerOptions<ContextExtension = Dictionary<never>, ExtendedContext extends CheerioCrawlingContext = CheerioCrawlingContext & ContextExtension, UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
11
9
|
JSONData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
12
|
-
Routes extends Record<keyof Routes, Dictionary> = Record<string, UserData
|
|
10
|
+
Routes extends Record<keyof Routes, Dictionary> = Record<string, UserData>, StatisticStateExtension extends object = {}> extends Omit<HttpCrawlerOptions<CheerioCrawlingContext<UserData, JSONData>, ContextExtension, ExtendedContext, Routes, StatisticStateExtension>, 'contextPipelineBuilder'> {
|
|
13
11
|
}
|
|
14
|
-
export type CheerioHook<UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
15
|
-
JSONData extends Dictionary = any> = InternalHttpHook<CheerioCrawlingContext<UserData, JSONData>>;
|
|
16
12
|
export interface CheerioCrawlingContext<UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
17
|
-
JSONData extends Dictionary = any> extends
|
|
18
|
-
/**
|
|
19
|
-
* The raw HTML content of the web page as a string.
|
|
20
|
-
*/
|
|
21
|
-
body: string;
|
|
22
|
-
/**
|
|
23
|
-
* The [Cheerio](https://cheerio.js.org/) object with parsed HTML.
|
|
24
|
-
* Cheerio is available only for HTML and XML content types.
|
|
25
|
-
*/
|
|
26
|
-
$: cheerio.CheerioAPI;
|
|
27
|
-
/**
|
|
28
|
-
* Wait for an element matching the selector to appear. Timeout is ignored.
|
|
29
|
-
*
|
|
30
|
-
* **Example usage:**
|
|
31
|
-
* ```ts
|
|
32
|
-
* async requestHandler({ waitForSelector, parseWithCheerio }) {
|
|
33
|
-
* await waitForSelector('article h1');
|
|
34
|
-
* const $ = await parseWithCheerio();
|
|
35
|
-
* const title = $('title').text();
|
|
36
|
-
* });
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
waitForSelector(selector: string, timeoutMs?: number): Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
* Returns Cheerio handle, this is here to unify the crawler API, so they all have this handy method.
|
|
42
|
-
* It has the same return type as the `$` context property, use it only if you are abstracting your workflow to
|
|
43
|
-
* support different context types in one handler.
|
|
44
|
-
* When provided with the `selector` argument, it will throw if it's not available.
|
|
45
|
-
*
|
|
46
|
-
* **Example usage:**
|
|
47
|
-
* ```ts
|
|
48
|
-
* async requestHandler({ parseWithCheerio }) {
|
|
49
|
-
* const $ = await parseWithCheerio();
|
|
50
|
-
* const title = $('title').text();
|
|
51
|
-
* });
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
parseWithCheerio(selector?: string, timeoutMs?: number): Promise<CheerioRoot>;
|
|
55
|
-
/**
|
|
56
|
-
* Helper function for extracting URLs from the parsed HTML and adding them to the request queue.
|
|
57
|
-
*/
|
|
58
|
-
enqueueLinks(options?: EnqueueLinksOptions): Promise<BatchAddRequestsResult>;
|
|
13
|
+
JSONData extends Dictionary = any> extends DOMCrawlingContext<CheerioParseResult, UserData, JSONData> {
|
|
59
14
|
}
|
|
60
15
|
export type CheerioRequestHandler<UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
61
16
|
JSONData extends Dictionary = any> = RequestHandler<CheerioCrawlingContext<UserData, JSONData>>;
|
|
@@ -138,41 +93,12 @@ JSONData extends Dictionary = any> = RequestHandler<CheerioCrawlingContext<UserD
|
|
|
138
93
|
* ```
|
|
139
94
|
* @category Crawlers
|
|
140
95
|
*/
|
|
141
|
-
export declare class CheerioCrawler<ContextExtension = Dictionary<never>, ExtendedContext extends CheerioCrawlingContext = CheerioCrawlingContext & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<CheerioCrawlingContext['request']
|
|
96
|
+
export declare class CheerioCrawler<ContextExtension = Dictionary<never>, ExtendedContext extends CheerioCrawlingContext = CheerioCrawlingContext & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<CheerioCrawlingContext['request']>>, StatisticStateExtension extends object = {}> extends DOMCrawler<CheerioParseResult, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> {
|
|
142
97
|
/**
|
|
143
98
|
* All `CheerioCrawler` parameters are passed via an options object.
|
|
144
99
|
*/
|
|
145
|
-
constructor(options?: CheerioCrawlerOptions<ContextExtension, ExtendedContext, any, any, Routes>);
|
|
146
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
147
|
-
protected buildContextPipeline(): import("@crawlee/http").ContextPipeline<CrawlingContext<Dictionary>, InternalHttpCrawlingContext<any, any> & {
|
|
148
|
-
readonly body: string;
|
|
149
|
-
readonly $: CheerioAPI;
|
|
150
|
-
} & {
|
|
151
|
-
enqueueLinks: (enqueueOptions?: EnqueueLinksOptions) => Promise<BatchAddRequestsResult>;
|
|
152
|
-
waitForSelector: (selector: string, _timeoutMs?: number) => Promise<void>;
|
|
153
|
-
parseWithCheerio: (selector?: string, timeoutMs?: number) => Promise<CheerioAPI>;
|
|
154
|
-
}>;
|
|
155
|
-
private parseContent;
|
|
156
|
-
private addHelpers;
|
|
157
|
-
}
|
|
158
|
-
interface EnqueueLinksInternalOptions {
|
|
159
|
-
options?: EnqueueLinksOptions;
|
|
160
|
-
$: cheerio.CheerioAPI | null;
|
|
161
|
-
requestManager: IRequestManager;
|
|
162
|
-
robotsTxtFile?: RobotsTxtFile;
|
|
163
|
-
onSkippedRequest?: SkippedRequestCallback;
|
|
164
|
-
originalRequestUrl: string;
|
|
165
|
-
finalRequestUrl?: string;
|
|
166
|
-
}
|
|
167
|
-
interface BoundEnqueueLinksInternalOptions {
|
|
168
|
-
enqueueLinks: BasicCrawlingContext['enqueueLinks'];
|
|
169
|
-
options?: EnqueueLinksOptions;
|
|
170
|
-
$: cheerio.CheerioAPI | null;
|
|
171
|
-
originalRequestUrl: string;
|
|
172
|
-
finalRequestUrl?: string;
|
|
100
|
+
constructor(options?: CheerioCrawlerOptions<ContextExtension, ExtendedContext, any, any, Routes, StatisticStateExtension>);
|
|
173
101
|
}
|
|
174
|
-
/** @internal */
|
|
175
|
-
export declare function cheerioCrawlerEnqueueLinks(options: EnqueueLinksInternalOptions | BoundEnqueueLinksInternalOptions): Promise<unknown>;
|
|
176
102
|
/**
|
|
177
103
|
* Creates new {@link Router} instance that works based on request labels.
|
|
178
104
|
* This instance can then serve as a `requestHandler` of your {@link CheerioCrawler}.
|
|
@@ -200,4 +126,3 @@ export declare function cheerioCrawlerEnqueueLinks(options: EnqueueLinksInternal
|
|
|
200
126
|
export declare function createCheerioRouter<Context extends CheerioCrawlingContext = CheerioCrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
|
|
201
127
|
export declare function createCheerioRouter<Context extends CheerioCrawlingContext = CheerioCrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
|
|
202
128
|
export declare function createCheerioRouter<Context extends CheerioCrawlingContext = CheerioCrawlingContext, const Schemas extends RouteSchemas = RouteSchemas>(schemas: Schemas): RouterHandler<Context, RoutesFromSchemas<Schemas>>;
|
|
203
|
-
export {};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import * as cheerio from 'cheerio';
|
|
4
|
-
import { parseDocument } from 'htmlparser2';
|
|
1
|
+
import { DOMCrawler, Router } from '@crawlee/http';
|
|
2
|
+
import { cheerioParser } from './cheerio-parser.js';
|
|
5
3
|
/**
|
|
6
4
|
* Provides a framework for the parallel crawling of web pages using plain HTTP requests and
|
|
7
5
|
* [cheerio](https://www.npmjs.com/package/cheerio) HTML parser.
|
|
@@ -81,118 +79,13 @@ import { parseDocument } from 'htmlparser2';
|
|
|
81
79
|
* ```
|
|
82
80
|
* @category Crawlers
|
|
83
81
|
*/
|
|
84
|
-
export class CheerioCrawler extends
|
|
82
|
+
export class CheerioCrawler extends DOMCrawler {
|
|
85
83
|
/**
|
|
86
84
|
* All `CheerioCrawler` parameters are passed via an options object.
|
|
87
85
|
*/
|
|
88
86
|
constructor(options) {
|
|
89
|
-
|
|
90
|
-
super({
|
|
91
|
-
...rest,
|
|
92
|
-
contextPipelineBuilder: contextPipelineBuilder ?? (() => this.buildContextPipeline()),
|
|
93
|
-
});
|
|
87
|
+
super({ ...options, parser: cheerioParser() });
|
|
94
88
|
}
|
|
95
|
-
buildContextPipeline() {
|
|
96
|
-
return super
|
|
97
|
-
.buildContextPipeline()
|
|
98
|
-
.compose({
|
|
99
|
-
action: async (context) => await this.parseContent(context),
|
|
100
|
-
})
|
|
101
|
-
.compose({ action: async (context) => await this.addHelpers(context) });
|
|
102
|
-
}
|
|
103
|
-
async parseContent(crawlingContext) {
|
|
104
|
-
try {
|
|
105
|
-
const isXml = crawlingContext.contentType.type.includes('xml');
|
|
106
|
-
const body = Buffer.isBuffer(crawlingContext.body)
|
|
107
|
-
? crawlingContext.body.toString(crawlingContext.contentType.encoding)
|
|
108
|
-
: crawlingContext.body;
|
|
109
|
-
const dom = parseDocument(body, { decodeEntities: true, xmlMode: isXml });
|
|
110
|
-
const $ = cheerio.load(dom, {
|
|
111
|
-
xml: { decodeEntities: true, xmlMode: isXml },
|
|
112
|
-
});
|
|
113
|
-
return {
|
|
114
|
-
$,
|
|
115
|
-
body,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
if (err instanceof NavigationSkippedError) {
|
|
120
|
-
return {
|
|
121
|
-
get body() {
|
|
122
|
-
throw new NavigationSkippedError('The `body` property is not available - `skipNavigation` was used', { cause: err });
|
|
123
|
-
},
|
|
124
|
-
get $() {
|
|
125
|
-
throw new NavigationSkippedError('The `$` property is not available - `skipNavigation` was used', { cause: err });
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
throw err;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
async addHelpers(crawlingContext) {
|
|
133
|
-
const originalEnqueueLinks = crawlingContext.enqueueLinks;
|
|
134
|
-
return {
|
|
135
|
-
enqueueLinks: async (enqueueOptions) => {
|
|
136
|
-
return (await cheerioCrawlerEnqueueLinks({
|
|
137
|
-
options: {
|
|
138
|
-
...enqueueOptions,
|
|
139
|
-
limit: await this.calculateEnqueuedRequestLimit(enqueueOptions?.limit),
|
|
140
|
-
},
|
|
141
|
-
$: crawlingContext.$,
|
|
142
|
-
requestManager: await this.getRequestManager(),
|
|
143
|
-
robotsTxtFile: await this.getRobotsTxtFileForUrl(crawlingContext.request.url),
|
|
144
|
-
onSkippedRequest: this.handleSkippedRequest,
|
|
145
|
-
originalRequestUrl: crawlingContext.request.url,
|
|
146
|
-
finalRequestUrl: crawlingContext.request.loadedUrl,
|
|
147
|
-
enqueueLinks: originalEnqueueLinks,
|
|
148
|
-
})); // TODO make this type safe
|
|
149
|
-
},
|
|
150
|
-
waitForSelector: async (selector, _timeoutMs) => {
|
|
151
|
-
if (crawlingContext.$(selector).get().length === 0) {
|
|
152
|
-
throw new Error(`Selector '${selector}' not found.`);
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
parseWithCheerio: async (selector, timeoutMs) => {
|
|
156
|
-
if (selector) {
|
|
157
|
-
await crawlingContext.waitForSelector(selector, timeoutMs);
|
|
158
|
-
}
|
|
159
|
-
return crawlingContext.$;
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/** @internal */
|
|
165
|
-
function containsEnqueueLinks(options) {
|
|
166
|
-
return !!options.enqueueLinks;
|
|
167
|
-
}
|
|
168
|
-
/** @internal */
|
|
169
|
-
export async function cheerioCrawlerEnqueueLinks(options) {
|
|
170
|
-
const { options: enqueueLinksOptions, $, originalRequestUrl, finalRequestUrl } = options;
|
|
171
|
-
if (!$) {
|
|
172
|
-
throw new Error('Cannot enqueue links because the DOM is not available.');
|
|
173
|
-
}
|
|
174
|
-
const baseUrl = resolveBaseUrlForEnqueueLinksFiltering({
|
|
175
|
-
enqueueStrategy: enqueueLinksOptions?.strategy,
|
|
176
|
-
finalRequestUrl,
|
|
177
|
-
originalRequestUrl,
|
|
178
|
-
userProvidedBaseUrl: enqueueLinksOptions?.baseUrl,
|
|
179
|
-
});
|
|
180
|
-
const urls = extractUrlsFromCheerio($, enqueueLinksOptions?.selector ?? 'a', enqueueLinksOptions?.baseUrl ?? finalRequestUrl ?? originalRequestUrl);
|
|
181
|
-
if (containsEnqueueLinks(options)) {
|
|
182
|
-
return options.enqueueLinks({
|
|
183
|
-
urls,
|
|
184
|
-
baseUrl,
|
|
185
|
-
...enqueueLinksOptions,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
return enqueueLinks({
|
|
189
|
-
requestManager: options.requestManager,
|
|
190
|
-
robotsTxtFile: options.robotsTxtFile,
|
|
191
|
-
onSkippedRequest: options.onSkippedRequest,
|
|
192
|
-
urls,
|
|
193
|
-
baseUrl,
|
|
194
|
-
...enqueueLinksOptions,
|
|
195
|
-
});
|
|
196
89
|
}
|
|
197
90
|
export function createCheerioRouter(routesOrSchemas) {
|
|
198
91
|
return Router.create(routesOrSchemas);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DOMParser } from '@crawlee/http';
|
|
2
|
+
import type { CheerioAPI } from 'cheerio';
|
|
3
|
+
export interface CheerioParseResult {
|
|
4
|
+
$: CheerioAPI;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A {@link DOMParser} backed by [cheerio](https://www.npmjs.com/package/cheerio). Pass it to a
|
|
9
|
+
* {@link DOMCrawler} to get the crawling context {@link CheerioCrawler} provides.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cheerioParser(): DOMParser<CheerioParseResult>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { extractUrlsFromCheerio } from '@crawlee/utils/internal';
|
|
2
|
+
// slim uses faster htmlparser2 parser and doesn't load extra libs like undici
|
|
3
|
+
import * as cheerio from 'cheerio/slim';
|
|
4
|
+
import { parseDocument } from 'htmlparser2';
|
|
5
|
+
/**
|
|
6
|
+
* A {@link DOMParser} backed by [cheerio](https://www.npmjs.com/package/cheerio). Pass it to a
|
|
7
|
+
* {@link DOMCrawler} to get the crawling context {@link CheerioCrawler} provides.
|
|
8
|
+
*/
|
|
9
|
+
export function cheerioParser() {
|
|
10
|
+
return {
|
|
11
|
+
placeholderMembers: { $: true, body: true },
|
|
12
|
+
parse(context) {
|
|
13
|
+
const isXml = context.contentType.type.includes('xml');
|
|
14
|
+
const body = Buffer.isBuffer(context.body)
|
|
15
|
+
? context.body.toString(context.contentType.encoding)
|
|
16
|
+
: context.body;
|
|
17
|
+
const dom = parseDocument(body, { decodeEntities: true, xmlMode: isXml });
|
|
18
|
+
const $ = cheerio.load(dom, {
|
|
19
|
+
xml: { decodeEntities: true, xmlMode: isXml },
|
|
20
|
+
});
|
|
21
|
+
return { $, body };
|
|
22
|
+
},
|
|
23
|
+
extractLinks: ({ $ }, selector, baseUrl) => extractUrlsFromCheerio($, selector, baseUrl),
|
|
24
|
+
select: ({ $ }, selector) => $(selector).get(),
|
|
25
|
+
toCheerio: ({ $ }) => $,
|
|
26
|
+
};
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/cheerio",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-rc.1",
|
|
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
|
-
"node": ">=22.
|
|
6
|
+
"node": ">=22.13.0"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"exports": {
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@crawlee/http": "4.0.0-
|
|
51
|
-
"@crawlee/types": "4.0.0-
|
|
52
|
-
"@crawlee/utils": "4.0.0-
|
|
50
|
+
"@crawlee/http": "4.0.0-rc.1",
|
|
51
|
+
"@crawlee/types": "4.0.0-rc.1",
|
|
52
|
+
"@crawlee/utils": "4.0.0-rc.1",
|
|
53
53
|
"cheerio": "^1.0.0",
|
|
54
54
|
"htmlparser2": "^10.0.0",
|
|
55
55
|
"tslib": "^2.8.1"
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
|
|
65
65
|
}
|