webcrawlerapi-js 2.2.5 → 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 +75 -0
- package/dist/api.js +83 -7
- 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
|
@@ -29,6 +29,8 @@ export interface ScrapeResponse {
|
|
|
29
29
|
page_title?: string;
|
|
30
30
|
structured_data?: any;
|
|
31
31
|
links?: string[];
|
|
32
|
+
/** Server-recommended delay before the next status poll, if provided */
|
|
33
|
+
recommended_pull_delay_ms?: number;
|
|
32
34
|
}
|
|
33
35
|
export interface ScrapeResponseError {
|
|
34
36
|
success: boolean;
|
|
@@ -161,3 +163,76 @@ export interface AgentRunList {
|
|
|
161
163
|
items: AgentRun[];
|
|
162
164
|
total: number;
|
|
163
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
|
@@ -24,6 +24,12 @@ const BASE_PATH = "https://api.webcrawlerapi.com";
|
|
|
24
24
|
const initialPullDelayMs = 2000;
|
|
25
25
|
const MaxPullRetries = 100;
|
|
26
26
|
const DEFAULT_POLL_DELAY_SECONDS = 2;
|
|
27
|
+
const SCRAPE_MAX_WAIT_MS = 400 * 1000;
|
|
28
|
+
// Effectively unbounded by count at the 2s default; elapsed time is the real limit
|
|
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"];
|
|
27
33
|
const AgentTerminalStatuses = ["done", "error", "canceled"];
|
|
28
34
|
__exportStar(require("./model"), exports);
|
|
29
35
|
const SCRAPE_VERSION = "v2";
|
|
@@ -183,17 +189,28 @@ class WebcrawlerClient {
|
|
|
183
189
|
return {
|
|
184
190
|
success: false,
|
|
185
191
|
status: status,
|
|
186
|
-
page_status_code: 0
|
|
192
|
+
page_status_code: 0,
|
|
193
|
+
recommended_pull_delay_ms: responseData.recommended_pull_delay_ms
|
|
187
194
|
};
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
|
-
async scrape(request, maxPolls =
|
|
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) {
|
|
191
205
|
// Start the scraping job
|
|
192
206
|
const scrapeIdResponse = await this.scrapeAsync(request);
|
|
193
207
|
const scrapeId = scrapeIdResponse.id;
|
|
208
|
+
const startedAt = Date.now();
|
|
194
209
|
let polls = 0;
|
|
210
|
+
let delayMs = DEFAULT_POLL_DELAY_SECONDS * 1000;
|
|
195
211
|
let result;
|
|
196
|
-
|
|
212
|
+
// Bounded by both elapsed time (SCRAPE_MAX_WAIT_MS) and maxPolls
|
|
213
|
+
while (polls < maxPolls && Date.now() - startedAt < SCRAPE_MAX_WAIT_MS) {
|
|
197
214
|
result = await this.getScrape(scrapeId);
|
|
198
215
|
// Return immediately if scrape is done
|
|
199
216
|
if ('status' in result && result.status === "done") {
|
|
@@ -203,14 +220,73 @@ class WebcrawlerClient {
|
|
|
203
220
|
if ('error_code' in result) {
|
|
204
221
|
return result;
|
|
205
222
|
}
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
// Use server-recommended delay when present, otherwise keep the default
|
|
224
|
+
const recommended = result.recommended_pull_delay_ms;
|
|
225
|
+
delayMs = recommended && recommended > 0 ? recommended : DEFAULT_POLL_DELAY_SECONDS * 1000;
|
|
226
|
+
// Don't sleep past the overall deadline
|
|
227
|
+
const remainingMs = SCRAPE_MAX_WAIT_MS - (Date.now() - startedAt);
|
|
228
|
+
if (remainingMs <= 0)
|
|
229
|
+
break;
|
|
230
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(delayMs, remainingMs)));
|
|
209
231
|
polls++;
|
|
210
232
|
}
|
|
211
|
-
// Return the last known state
|
|
233
|
+
// Return the last known state on timeout
|
|
212
234
|
return result;
|
|
213
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
|
+
}
|
|
214
290
|
async crawl(crawlRequest, maxPolls = MaxPullRetries) {
|
|
215
291
|
const url = `${this.basePath}/${this.apiVersion}/crawl`;
|
|
216
292
|
const requestBody = {
|