@crawlee/http 4.0.0-beta.168 → 4.0.0-beta.169

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 CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from '@crawlee/basic';
2
2
  export * from './internals/http-crawler.js';
3
+ export * from './internals/dom-crawler.js';
3
4
  export * from './internals/file-download.js';
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from '@crawlee/basic';
2
2
  export * from './internals/http-crawler.js';
3
+ export * from './internals/dom-crawler.js';
3
4
  export * from './internals/file-download.js';
@@ -0,0 +1,131 @@
1
+ import type { AddRequestsBatchedResult, ContextPipeline, CrawlingContext, EnqueueLinksOptions, ExtractLinksOptions, GetUserDataFromRequest } from '@crawlee/basic';
2
+ import type { Awaitable, Dictionary } from '@crawlee/types';
3
+ import type { CheerioAPI } from 'cheerio';
4
+ import type { HttpCrawlerOptions, InternalHttpCrawlingContext } from './http-crawler.js';
5
+ import { HttpCrawler } from './http-crawler.js';
6
+ /**
7
+ * The minimum a {@link DOMParser} has to contribute to the crawling context - the serialized document, used by
8
+ * the {@link DOMCrawlingContext.parseWithCheerio|`parseWithCheerio`} helper.
9
+ */
10
+ export interface DOMParseResult {
11
+ body: string;
12
+ }
13
+ /**
14
+ * Turns a response body into a DOM representation and knows how to query it. Passing one to {@link DOMCrawler}
15
+ * is what makes the crawler jsdom-based, linkedom-based, or based on a DOM implementation of your own.
16
+ *
17
+ * **Example usage:**
18
+ * ```ts
19
+ * import { DOMCrawler } from 'crawlee';
20
+ * import type { DOMParser } from 'crawlee';
21
+ *
22
+ * const myParser: DOMParser<{ body: string }> = {
23
+ * // ...
24
+ * };
25
+ *
26
+ * const crawler = new DOMCrawler({
27
+ * parser: myParser,
28
+ * async requestHandler({ body }) {
29
+ * // ...
30
+ * },
31
+ * });
32
+ * ```
33
+ */
34
+ export interface DOMParser<Parsed extends DOMParseResult> {
35
+ /**
36
+ * The context members {@link DOMParser.parse|`parse`} contributes, mapped to `true`. Used to build the
37
+ * placeholders that report a helpful error when the members are accessed after `skipNavigation` - the `Record`
38
+ * type forces every key of `Parsed` to be listed, so the compiler catches an omission that would otherwise
39
+ * yield `undefined` (rather than throwing) after `skipNavigation`.
40
+ */
41
+ readonly placeholderMembers: Record<keyof Parsed & string, true>;
42
+ parse(context: InternalHttpCrawlingContext): Awaitable<Parsed>;
43
+ /**
44
+ * Returns the URLs the `selector` matches, resolved against `baseUrl`.
45
+ */
46
+ extractLinks(parsed: Parsed, selector: string, baseUrl: string): Awaitable<string[]>;
47
+ /**
48
+ * Returns the current matches of `selector`. Only the count is used, by
49
+ * {@link DOMCrawlingContext.waitForSelector|`waitForSelector`}.
50
+ */
51
+ select(parsed: Parsed, selector: string): Awaitable<ArrayLike<unknown>>;
52
+ /**
53
+ * Whether the parse result can change after {@link DOMParser.parse|`parse`} returned - the case when the DOM
54
+ * implementation runs the page scripts. If it can, {@link DOMCrawlingContext.waitForSelector|`waitForSelector`}
55
+ * polls until the timeout elapses; otherwise it fails as soon as the selector does not match.
56
+ */
57
+ readonly mutable?: boolean;
58
+ /**
59
+ * Returns a Cheerio handle over the parse result, for parsers that are backed by Cheerio anyway. Without it,
60
+ * {@link DOMCrawlingContext.parseWithCheerio|`parseWithCheerio`} parses
61
+ * {@link DOMParseResult.body|`body`} again.
62
+ */
63
+ toCheerio?(parsed: Parsed): Awaitable<CheerioAPI>;
64
+ /**
65
+ * Releases whatever {@link DOMParser.parse|`parse`} allocated. Called after the request handler finishes or
66
+ * fails, and skipped entirely when navigation was skipped.
67
+ */
68
+ cleanup?(parsed: Parsed): Awaitable<void>;
69
+ }
70
+ export interface DOMCrawlingHelpers {
71
+ /**
72
+ * Extracts URLs from the parsed DOM, without adding them to the request queue.
73
+ */
74
+ extractLinks(options?: ExtractLinksOptions): Promise<string[]>;
75
+ /**
76
+ * Helper function for extracting URLs from the parsed DOM and adding them to the request queue.
77
+ */
78
+ enqueueLinks(options?: EnqueueLinksOptions): Promise<AddRequestsBatchedResult>;
79
+ /**
80
+ * Wait for an element matching the selector to appear. The `timeoutMs` only has an effect when the parser is
81
+ * {@link DOMParser.mutable|`mutable`} (e.g. {@link JSDOMCrawler} with `runScripts: true`); otherwise the
82
+ * selector is checked once and the call resolves or throws immediately.
83
+ * Timeout defaults to 5s.
84
+ *
85
+ * **Example usage:**
86
+ * ```ts
87
+ * async requestHandler({ waitForSelector, parseWithCheerio }) {
88
+ * await waitForSelector('article h1');
89
+ * const $ = await parseWithCheerio();
90
+ * const title = $('title').text();
91
+ * });
92
+ * ```
93
+ */
94
+ waitForSelector(selector: string, timeoutMs?: number): Promise<void>;
95
+ /**
96
+ * Returns Cheerio handle, allowing to work with the data same way as with {@link CheerioCrawler}.
97
+ * When provided with the `selector` argument, it will throw if it's not available.
98
+ *
99
+ * **Example usage:**
100
+ * ```javascript
101
+ * async requestHandler({ parseWithCheerio }) {
102
+ * const $ = await parseWithCheerio();
103
+ * const title = $('title').text();
104
+ * });
105
+ * ```
106
+ */
107
+ parseWithCheerio(selector?: string, timeoutMs?: number): Promise<CheerioAPI>;
108
+ }
109
+ export type DOMCrawlingContext<Parsed extends DOMParseResult = DOMParseResult, UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
110
+ JSONData extends Dictionary = any> = InternalHttpCrawlingContext<UserData, JSONData> & Parsed & DOMCrawlingHelpers;
111
+ export interface DOMCrawlerOptions<Parsed extends DOMParseResult = DOMParseResult, ContextExtension = Dictionary<never>, ExtendedContext extends DOMCrawlingContext<Parsed> = DOMCrawlingContext<Parsed> & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, any>, StatisticStateExtension extends object = {}> extends HttpCrawlerOptions<DOMCrawlingContext<Parsed>, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> {
112
+ /**
113
+ * The DOM implementation to parse the response bodies with. Its parse result becomes part of the crawling
114
+ * context, so the members the request handler receives follow from the parser you pass.
115
+ */
116
+ parser: DOMParser<Parsed>;
117
+ }
118
+ /**
119
+ * An {@link HttpCrawler} that parses each response into a DOM using the {@link DOMCrawlerOptions.parser|`parser`}
120
+ * it is given, and exposes the parse result plus the {@link DOMCrawlingContext.enqueueLinks|`enqueueLinks`} and
121
+ * {@link DOMCrawlingContext.extractLinks|`extractLinks`} helpers on the crawling context.
122
+ *
123
+ * {@link JSDOMCrawler} and {@link LinkeDOMCrawler} are this crawler with a parser already chosen.
124
+ *
125
+ * @category Crawlers
126
+ */
127
+ export declare class DOMCrawler<Parsed extends DOMParseResult = DOMParseResult, ContextExtension = Dictionary<never>, ExtendedContext extends DOMCrawlingContext<Parsed> = DOMCrawlingContext<Parsed> & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<DOMCrawlingContext<Parsed>['request']>>, StatisticStateExtension extends object = {}> extends HttpCrawler<DOMCrawlingContext<Parsed>, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> {
128
+ #private;
129
+ constructor(options: DOMCrawlerOptions<Parsed, ContextExtension, ExtendedContext, Routes, StatisticStateExtension>);
130
+ protected buildContextPipeline(): ContextPipeline<CrawlingContext, DOMCrawlingContext<Parsed>>;
131
+ }
@@ -0,0 +1,97 @@
1
+ import { EnqueueStrategy, NavigationSkippedError, resolveBaseUrlForEnqueueLinksFiltering } from '@crawlee/basic';
2
+ import { sleep } from '@crawlee/utils';
3
+ import { HttpCrawler } from './http-crawler.js';
4
+ /**
5
+ * An {@link HttpCrawler} that parses each response into a DOM using the {@link DOMCrawlerOptions.parser|`parser`}
6
+ * it is given, and exposes the parse result plus the {@link DOMCrawlingContext.enqueueLinks|`enqueueLinks`} and
7
+ * {@link DOMCrawlingContext.extractLinks|`extractLinks`} helpers on the crawling context.
8
+ *
9
+ * {@link JSDOMCrawler} and {@link LinkeDOMCrawler} are this crawler with a parser already chosen.
10
+ *
11
+ * @category Crawlers
12
+ */
13
+ export class DOMCrawler extends HttpCrawler {
14
+ #parser;
15
+ constructor(options) {
16
+ const { parser, contextPipelineBuilder, ...rest } = options;
17
+ super({
18
+ ...rest,
19
+ contextPipelineBuilder: contextPipelineBuilder ?? (() => this.buildContextPipeline()),
20
+ });
21
+ this.#parser = parser;
22
+ }
23
+ buildContextPipeline() {
24
+ return super
25
+ .buildContextPipeline()
26
+ .compose({
27
+ action: async (context) => this.#parseContent(context),
28
+ cleanup: async (context) => {
29
+ // The `skipNavigation` placeholders below throw on access, so there is nothing to clean up.
30
+ if (!context.request.skipNavigation) {
31
+ await this.#parser.cleanup?.(context);
32
+ }
33
+ },
34
+ })
35
+ .compose({ action: async (context) => this.#addHelpers(context) });
36
+ }
37
+ async #parseContent(context) {
38
+ try {
39
+ return await this.#parser.parse(context);
40
+ }
41
+ catch (err) {
42
+ if (err instanceof NavigationSkippedError) {
43
+ return Object.defineProperties({}, Object.fromEntries(Object.keys(this.#parser.placeholderMembers).map((member) => [
44
+ member,
45
+ {
46
+ configurable: true,
47
+ enumerable: true,
48
+ get() {
49
+ throw new NavigationSkippedError(`The \`${member}\` property is not available - \`skipNavigation\` was used`, { cause: err });
50
+ },
51
+ },
52
+ ])));
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ async #addHelpers(context) {
58
+ const { addRequests } = context;
59
+ const parser = this.#parser;
60
+ const extractLinks = async (options) => parser.extractLinks(context, options?.selector ?? 'a', options?.baseUrl ?? context.request.loadedUrl ?? context.request.url);
61
+ const waitForSelector = async (selector, timeoutMs = 5_000) => {
62
+ let remaining = parser.mutable ? timeoutMs : 0;
63
+ while ((await parser.select(context, selector)).length === 0) {
64
+ if (remaining <= 0) {
65
+ throw new Error(`Selector '${selector}' not found.`);
66
+ }
67
+ await sleep(50);
68
+ remaining -= 50;
69
+ }
70
+ };
71
+ return {
72
+ extractLinks,
73
+ waitForSelector,
74
+ enqueueLinks: async (options = {}) => {
75
+ const baseUrl = resolveBaseUrlForEnqueueLinksFiltering({
76
+ enqueueStrategy: options.strategy,
77
+ finalRequestUrl: context.request.loadedUrl,
78
+ originalRequestUrl: context.request.url,
79
+ userProvidedBaseUrl: options.baseUrl,
80
+ });
81
+ const urls = await extractLinks(options);
82
+ return addRequests(urls, {
83
+ ...options,
84
+ baseUrl,
85
+ strategy: options.strategy ?? EnqueueStrategy.SameHostname,
86
+ });
87
+ },
88
+ async parseWithCheerio(selector, _timeoutMs = 5_000) {
89
+ const $ = (await parser.toCheerio?.(context)) ?? (await import('cheerio')).load(context.body);
90
+ if (selector && $(selector).get().length === 0) {
91
+ throw new Error(`Selector '${selector}' not found.`);
92
+ }
93
+ return $;
94
+ },
95
+ };
96
+ }
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/http",
3
- "version": "4.0.0-beta.168",
3
+ "version": "4.0.0-beta.169",
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,11 +49,11 @@
49
49
  "dependencies": {
50
50
  "@apify/timeout": "^1.0.1",
51
51
  "@apify/utilities": "^3.0.1",
52
- "@crawlee/basic": "4.0.0-beta.168",
53
- "@crawlee/core": "4.0.0-beta.168",
54
- "@crawlee/http-client": "4.0.0-beta.168",
55
- "@crawlee/types": "4.0.0-beta.168",
56
- "@crawlee/utils": "4.0.0-beta.168",
52
+ "@crawlee/basic": "4.0.0-beta.169",
53
+ "@crawlee/core": "4.0.0-beta.169",
54
+ "@crawlee/http-client": "4.0.0-beta.169",
55
+ "@crawlee/types": "4.0.0-beta.169",
56
+ "@crawlee/utils": "4.0.0-beta.169",
57
57
  "@types/content-type": "^1.1.8",
58
58
  "cheerio": "^1.0.0",
59
59
  "content-type": "^1.0.5",
@@ -70,5 +70,5 @@
70
70
  }
71
71
  }
72
72
  },
73
- "gitHead": "fc75a7073fc330331ff2d68d81a8e6815ab4edf2"
73
+ "gitHead": "a42dc93fa0ef68180d83ab12af362082690f73e2"
74
74
  }