webcrawlerapi-js 2.2.6 → 2.2.7
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 +17 -0
- package/dist/@types/api.d.ts +20 -1
- package/dist/@types/model.d.ts +73 -0
- package/dist/api.js +64 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,23 @@ for (const item of job.job_items) {
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Batch scrape
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const batch = await client.scrape({
|
|
53
|
+
url: ["https://books.toscrape.com/", "https://books.toscrape.com/catalogue/page-2.html"],
|
|
54
|
+
output_formats: ["markdown"],
|
|
55
|
+
max_concurrency: 5,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
console.log(batch.status, batch.completed, batch.failed)
|
|
59
|
+
for (const item of batch.data) console.log(item.url, item.status, item.markdown_url)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Passing an array as `url` to `scrape()` creates a batch and polls until it finishes (a single string still returns a single result). Batch-only options like `max_concurrency` and `ignore_invalid_urls` go in the same request. For manual control use `scrapeBatchAsync({ urls, ... })`, `getBatch(id, { limit, cursor })`, `getBatchErrors(id)` and `cancelBatch(id)`.
|
|
63
|
+
|
|
64
|
+
Full batch scrape documentation: [docs/batch-scrape.md](docs/batch-scrape.md).
|
|
65
|
+
|
|
49
66
|
## Run an agent
|
|
50
67
|
|
|
51
68
|
```javascript
|
package/dist/@types/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CrawlRequest, Job, JobId, ScrapeRequest, ScrapeResponse, ScrapeResponseError, ScrapeId, JobMarkdownResponse, MarkdownRequest, MarkdownResponse, AgentRunRequest, AgentRun } from "./model";
|
|
1
|
+
import { CrawlRequest, Job, JobId, ScrapeRequest, ScrapeResponse, ScrapeResponseError, ScrapeId, JobMarkdownResponse, MarkdownRequest, MarkdownResponse, AgentRunRequest, AgentRun, BatchScrapeRequest, BatchScrapeId, BatchScrape, BatchScrapeGetOptions, BatchScrapeErrors, ScrapeBatchRequest } from "./model";
|
|
2
2
|
import { JobStatus, ErrorCode } from "./constants";
|
|
3
3
|
import { WebcrawlerApiError } from "./errors";
|
|
4
4
|
export { WebcrawlerApiError, JobStatus, ErrorCode };
|
|
@@ -14,7 +14,26 @@ export declare class WebcrawlerClient {
|
|
|
14
14
|
getAgentJob(jobID: string): Promise<AgentRun>;
|
|
15
15
|
runAgent(request: AgentRunRequest, maxPolls?: number): Promise<AgentRun>;
|
|
16
16
|
getScrape(scrapeId: string): Promise<ScrapeResponse | ScrapeResponseError>;
|
|
17
|
+
/**
|
|
18
|
+
* Scrapes a single URL and waits for the result.
|
|
19
|
+
*
|
|
20
|
+
* Passing `url` as an array creates a batch scrape instead, polls until it reaches a
|
|
21
|
+
* terminal status (completed, failed or cancelled) and returns the first page of items;
|
|
22
|
+
* follow `next` with getBatch(id, { cursor }) for larger batches. Batch-only options
|
|
23
|
+
* (max_concurrency, ignore_invalid_urls, ...) go in the same request object. A batch throws a
|
|
24
|
+
* `timeout` error if it does not finish within an hour or maxPolls.
|
|
25
|
+
* For manual batch control use scrapeBatchAsync() + getBatch().
|
|
26
|
+
*/
|
|
17
27
|
scrape(request: ScrapeRequest, maxPolls?: number): Promise<ScrapeResponse | ScrapeResponseError>;
|
|
28
|
+
scrape(request: ScrapeBatchRequest, maxPolls?: number): Promise<BatchScrape>;
|
|
29
|
+
private scrapeSingle;
|
|
30
|
+
scrapeBatchAsync(request: BatchScrapeRequest): Promise<BatchScrapeId>;
|
|
31
|
+
getBatch(batchId: string, options?: BatchScrapeGetOptions): Promise<BatchScrape>;
|
|
32
|
+
getBatchErrors(batchId: string): Promise<BatchScrapeErrors>;
|
|
33
|
+
cancelBatch(batchId: string): Promise<{
|
|
34
|
+
status: string;
|
|
35
|
+
}>;
|
|
36
|
+
private scrapeBatchAndWait;
|
|
18
37
|
crawl(crawlRequest: CrawlRequest, maxPolls?: number): Promise<Job>;
|
|
19
38
|
crawlAsync(crawlRequest: CrawlRequest): Promise<JobId>;
|
|
20
39
|
getJob(jobID: string): Promise<Job>;
|
package/dist/@types/model.d.ts
CHANGED
|
@@ -163,3 +163,76 @@ export interface AgentRunList {
|
|
|
163
163
|
items: AgentRun[];
|
|
164
164
|
total: number;
|
|
165
165
|
}
|
|
166
|
+
export type BatchScrapeStatus = 'scraping' | 'completed' | 'failed' | 'cancelled';
|
|
167
|
+
export interface BatchScrapeRequest {
|
|
168
|
+
/** URLs to scrape (max is your plan's job items limit). */
|
|
169
|
+
urls: string[];
|
|
170
|
+
/** Default is ["markdown"]. */
|
|
171
|
+
output_formats?: ('markdown' | 'cleaned' | 'html' | 'links' | 'images')[];
|
|
172
|
+
main_content_only?: boolean;
|
|
173
|
+
/** CSS selectors to clean from the output. */
|
|
174
|
+
clean_selectors?: string;
|
|
175
|
+
/** Max URLs scraped in parallel for this batch. 0 or omitted means no per-batch cap. */
|
|
176
|
+
max_concurrency?: number;
|
|
177
|
+
/** Skip invalid URLs and report them in invalid_urls instead of failing the request (default: true). */
|
|
178
|
+
ignore_invalid_urls?: boolean;
|
|
179
|
+
/** Public http(s) URL called once when the batch finishes. */
|
|
180
|
+
webhook_url?: string;
|
|
181
|
+
/** Cache duration in seconds (default 604800). 0 bypasses the cache. */
|
|
182
|
+
max_age?: number;
|
|
183
|
+
respect_robots_txt?: boolean;
|
|
184
|
+
}
|
|
185
|
+
/** Request for scrape() with an array `url`: runs a batch scrape and waits for it. Same options as BatchScrapeRequest. */
|
|
186
|
+
export type ScrapeBatchRequest = Omit<BatchScrapeRequest, 'urls'> & {
|
|
187
|
+
/** URLs to scrape as a batch (max is your plan's job items limit). */
|
|
188
|
+
url: string[];
|
|
189
|
+
};
|
|
190
|
+
export interface BatchScrapeInvalidUrl {
|
|
191
|
+
url: string;
|
|
192
|
+
error_code: string;
|
|
193
|
+
error_message: string;
|
|
194
|
+
}
|
|
195
|
+
export interface BatchScrapeId {
|
|
196
|
+
id: string;
|
|
197
|
+
status_url: string;
|
|
198
|
+
invalid_urls: BatchScrapeInvalidUrl[];
|
|
199
|
+
}
|
|
200
|
+
export interface BatchScrapeItem {
|
|
201
|
+
url: string;
|
|
202
|
+
/** Item status, for example "done" or "error". */
|
|
203
|
+
status: string;
|
|
204
|
+
page_status_code?: number;
|
|
205
|
+
title?: string;
|
|
206
|
+
markdown_url?: string;
|
|
207
|
+
cleaned_url?: string;
|
|
208
|
+
html_url?: string;
|
|
209
|
+
from_cache?: boolean;
|
|
210
|
+
error_code?: string;
|
|
211
|
+
error_message?: string;
|
|
212
|
+
}
|
|
213
|
+
export interface BatchScrape {
|
|
214
|
+
id: string;
|
|
215
|
+
status: BatchScrapeStatus | string;
|
|
216
|
+
total: number;
|
|
217
|
+
completed: number;
|
|
218
|
+
failed: number;
|
|
219
|
+
canceled: number;
|
|
220
|
+
data: BatchScrapeItem[];
|
|
221
|
+
/** Cursor for the next page. Empty on the last page. */
|
|
222
|
+
next?: string;
|
|
223
|
+
}
|
|
224
|
+
export interface BatchScrapeGetOptions {
|
|
225
|
+
/** Items per page, 1 to 500 (default 100). */
|
|
226
|
+
limit?: number;
|
|
227
|
+
/** Value of `next` from the previous page. */
|
|
228
|
+
cursor?: string;
|
|
229
|
+
}
|
|
230
|
+
export interface BatchScrapeError {
|
|
231
|
+
url: string;
|
|
232
|
+
error_code: string;
|
|
233
|
+
error_message: string;
|
|
234
|
+
}
|
|
235
|
+
export interface BatchScrapeErrors {
|
|
236
|
+
errors: BatchScrapeError[];
|
|
237
|
+
robots_blocked: string[];
|
|
238
|
+
}
|
package/dist/api.js
CHANGED
|
@@ -27,6 +27,9 @@ const DEFAULT_POLL_DELAY_SECONDS = 2;
|
|
|
27
27
|
const SCRAPE_MAX_WAIT_MS = 400 * 1000;
|
|
28
28
|
// Effectively unbounded by count at the 2s default; elapsed time is the real limit
|
|
29
29
|
const SCRAPE_MAX_POLLS = 1000;
|
|
30
|
+
const BATCH_VERSION = "v2";
|
|
31
|
+
const BATCH_MAX_WAIT_MS = 3600 * 1000;
|
|
32
|
+
const BatchTerminalStatuses = ["completed", "failed", "cancelled", "canceled"];
|
|
30
33
|
const AgentTerminalStatuses = ["done", "error", "canceled"];
|
|
31
34
|
__exportStar(require("./model"), exports);
|
|
32
35
|
const SCRAPE_VERSION = "v2";
|
|
@@ -192,6 +195,13 @@ class WebcrawlerClient {
|
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
async scrape(request, maxPolls = SCRAPE_MAX_POLLS) {
|
|
198
|
+
if (Array.isArray(request.url)) {
|
|
199
|
+
const { url, ...options } = request;
|
|
200
|
+
return await this.scrapeBatchAndWait({ ...options, urls: url }, maxPolls);
|
|
201
|
+
}
|
|
202
|
+
return await this.scrapeSingle(request, maxPolls);
|
|
203
|
+
}
|
|
204
|
+
async scrapeSingle(request, maxPolls) {
|
|
195
205
|
// Start the scraping job
|
|
196
206
|
const scrapeIdResponse = await this.scrapeAsync(request);
|
|
197
207
|
const scrapeId = scrapeIdResponse.id;
|
|
@@ -223,6 +233,60 @@ class WebcrawlerClient {
|
|
|
223
233
|
// Return the last known state on timeout
|
|
224
234
|
return result;
|
|
225
235
|
}
|
|
236
|
+
async scrapeBatchAsync(request) {
|
|
237
|
+
const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape`;
|
|
238
|
+
const requestOptions = {
|
|
239
|
+
'method': 'POST',
|
|
240
|
+
'headers': this.getDefaultHeaders(),
|
|
241
|
+
'body': JSON.stringify(request),
|
|
242
|
+
};
|
|
243
|
+
return await this.sendRequest(url, requestOptions);
|
|
244
|
+
}
|
|
245
|
+
async getBatch(batchId, options = {}) {
|
|
246
|
+
const params = new URLSearchParams();
|
|
247
|
+
if (options.limit !== undefined)
|
|
248
|
+
params.set('limit', String(options.limit));
|
|
249
|
+
if (options.cursor)
|
|
250
|
+
params.set('cursor', options.cursor);
|
|
251
|
+
const query = params.toString();
|
|
252
|
+
const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape/${batchId}${query ? `?${query}` : ''}`;
|
|
253
|
+
const requestOptions = {
|
|
254
|
+
'method': 'GET',
|
|
255
|
+
'headers': this.getDefaultHeaders(true),
|
|
256
|
+
};
|
|
257
|
+
return await this.sendRequest(url, requestOptions);
|
|
258
|
+
}
|
|
259
|
+
async getBatchErrors(batchId) {
|
|
260
|
+
const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape/${batchId}/errors`;
|
|
261
|
+
const requestOptions = {
|
|
262
|
+
'method': 'GET',
|
|
263
|
+
'headers': this.getDefaultHeaders(true),
|
|
264
|
+
};
|
|
265
|
+
return await this.sendRequest(url, requestOptions);
|
|
266
|
+
}
|
|
267
|
+
async cancelBatch(batchId) {
|
|
268
|
+
const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape/${batchId}`;
|
|
269
|
+
const requestOptions = {
|
|
270
|
+
'method': 'DELETE',
|
|
271
|
+
'headers': this.getDefaultHeaders(),
|
|
272
|
+
};
|
|
273
|
+
return await this.sendRequest(url, requestOptions);
|
|
274
|
+
}
|
|
275
|
+
async scrapeBatchAndWait(request, maxPolls) {
|
|
276
|
+
const created = await this.scrapeBatchAsync(request);
|
|
277
|
+
if (!created.id) {
|
|
278
|
+
throw new errors_1.WebcrawlerApiError('invalid_response', 'Failed to create batch scrape', 0);
|
|
279
|
+
}
|
|
280
|
+
const startedAt = Date.now();
|
|
281
|
+
for (let i = 0; i < maxPolls && Date.now() - startedAt < BATCH_MAX_WAIT_MS; i++) {
|
|
282
|
+
const batch = await this.getBatch(created.id);
|
|
283
|
+
if (BatchTerminalStatuses.includes(batch.status)) {
|
|
284
|
+
return batch;
|
|
285
|
+
}
|
|
286
|
+
await new Promise(resolve => setTimeout(resolve, DEFAULT_POLL_DELAY_SECONDS * 1000));
|
|
287
|
+
}
|
|
288
|
+
throw new errors_1.WebcrawlerApiError('timeout', 'Batch scrape took too long, use scrapeBatchAsync + getBatch to keep polling', 0);
|
|
289
|
+
}
|
|
226
290
|
async crawl(crawlRequest, maxPolls = MaxPullRetries) {
|
|
227
291
|
const url = `${this.basePath}/${this.apiVersion}/crawl`;
|
|
228
292
|
const requestBody = {
|