webcrawlerapi-js 1.0.9 → 1.1.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/README.md +148 -22
- package/dist/@types/api.d.ts +15 -0
- package/dist/@types/constants.d.ts +13 -0
- package/dist/@types/errors.d.ts +12 -0
- package/dist/@types/index.d.ts +4 -0
- package/dist/@types/model.d.ts +62 -0
- package/dist/api.js +167 -211
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +19 -0
- package/dist/index.js +3 -0
- package/package.json +2 -2
- package/{api.ts → src/api.ts} +26 -51
- package/src/errors.ts +25 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +13 -7
- package/dist/index.d.ts +0 -1
- package/index.ts +0 -1
- /package/{constants.ts → src/constants.ts} +0 -0
- /package/{model.ts → src/model.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,64 +1,190 @@
|
|
|
1
1
|
# JS client for WebcrawlerAPI
|
|
2
2
|
|
|
3
|
-
Official client for [WebcrawlerAPI](https://webcrawlerapi.com/).
|
|
3
|
+
Official client for [WebcrawlerAPI](https://webcrawlerapi.com/). This SDK provides a robust and type-safe way to interact with WebcrawlerAPI's powerful web scraping capabilities, making it easy to extract structured data from any website.
|
|
4
4
|
|
|
5
|
-
WebcrawlerAPI allows you to extract data from any website with just a simple API call.
|
|
5
|
+
WebcrawlerAPI allows you to extract data from any website with just a simple API call. The service handles all the complexities of web scraping, including JavaScript rendering, proxy rotation, and rate limiting, so you can focus on using the data.
|
|
6
6
|
|
|
7
7
|
## Preparation
|
|
8
8
|
1. Register to [dashboard|https://dash.webcrawlerapi.com/].
|
|
9
9
|
2. Get an [Access Key](https://dash.webcrawlerapi.com/access).
|
|
10
10
|
|
|
11
|
+
Our dashboard provides comprehensive analytics and monitoring tools to help you track your API usage and optimize your scraping operations.
|
|
12
|
+
|
|
11
13
|
## Installation
|
|
12
14
|
Install WebcrawlerAPI js package:
|
|
13
15
|
|
|
14
16
|
`npm i webcrawlerapi-js`
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
The package is regularly updated with new features and improvements to ensure the best possible scraping experience.
|
|
19
|
+
|
|
20
|
+
## JavaScript Examples
|
|
21
|
+
|
|
22
|
+
### Synchronous Job
|
|
23
|
+
In this mode, the promise will be resolved when all data is ready. Synchronous jobs are perfect for quick scraping tasks where you need immediate results and don't want to handle polling logic.
|
|
17
24
|
|
|
18
25
|
```javascript
|
|
19
|
-
import
|
|
26
|
+
import webcrawlerapi from "webcrawlerapi-js"
|
|
20
27
|
|
|
21
28
|
async function main() {
|
|
22
29
|
const client = new WebcrawlerClient(
|
|
23
30
|
"YOUR API ACCESS KEY HERE",
|
|
24
31
|
)
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const syncJob = await client.crawl({
|
|
27
35
|
"items_limit": 10,
|
|
28
36
|
"url": "https://stripe.com/",
|
|
29
37
|
"scrape_type": "markdown"
|
|
30
|
-
}
|
|
31
|
-
)
|
|
32
|
-
for (const item of syncJob.job_items) {
|
|
33
|
-
item.getContent().then((content) => {
|
|
34
|
-
console.log(content.slice(0, 100));
|
|
35
38
|
})
|
|
39
|
+
|
|
40
|
+
for (const item of syncJob.job_items) {
|
|
41
|
+
const content = await item.getContent()
|
|
42
|
+
console.log(content.slice(0, 100))
|
|
43
|
+
}
|
|
44
|
+
console.log(syncJob)
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof webcrawlerapi.WebcrawlerApiError) {
|
|
48
|
+
console.error("Webcrawler error:", error.message)
|
|
49
|
+
}
|
|
50
|
+
return
|
|
36
51
|
}
|
|
37
|
-
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main().catch(console.error)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Asynchronous Job
|
|
58
|
+
In this mode, you get a job ID immediately and can poll for results. Asynchronous jobs are ideal for large-scale scraping operations where you want to process data in batches or handle long-running tasks efficiently.
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
```javascript
|
|
61
|
+
import webcrawlerapi from "webcrawlerapi-js"
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
async function main() {
|
|
64
|
+
const client = new WebcrawlerClient(
|
|
65
|
+
"YOUR API ACCESS KEY HERE",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Get job ID immediately
|
|
70
|
+
const jobWithId = await client.crawlAsync({
|
|
42
71
|
"items_limit": 10,
|
|
43
72
|
"url": "https://stripe.com/",
|
|
44
73
|
"scrape_type": "markdown"
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Poll for job completion
|
|
77
|
+
const jobId = jobWithId.id
|
|
78
|
+
let asyncJob = await client.getJob(jobId)
|
|
79
|
+
|
|
80
|
+
// Check job status
|
|
81
|
+
console.log("Job status:", asyncJob.status)
|
|
82
|
+
|
|
83
|
+
// Once complete, access results
|
|
84
|
+
if (asyncJob.status === "completed") {
|
|
85
|
+
for (const item of asyncJob.job_items) {
|
|
86
|
+
const content = await item.getContent()
|
|
87
|
+
console.log(content.slice(0, 100))
|
|
88
|
+
}
|
|
45
89
|
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof webcrawlerapi.WebcrawlerApiError) {
|
|
92
|
+
console.error("Webcrawler error:", error.message)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main().catch(console.error)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## TypeScript Usage
|
|
101
|
+
|
|
102
|
+
The SDK provides full TypeScript support with type safety and error handling. Our TypeScript integration ensures you catch potential issues at compile-time and provides excellent IDE support with autocompletion and inline documentation.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { WebcrawlerClient, WebcrawlerApiError } from "webcrawlerapi-js"
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
const client = new WebcrawlerClient(
|
|
109
|
+
"YOUR_API_KEY",
|
|
110
|
+
// Optional base URL, defaults to production API
|
|
111
|
+
"http://localhost:8080"
|
|
46
112
|
)
|
|
47
|
-
// wait for job to complete
|
|
48
|
-
const jobId = jobWithId.id;
|
|
49
|
-
let asyncJob = await client.getJob(jobId);
|
|
50
113
|
|
|
51
|
-
|
|
114
|
+
try {
|
|
115
|
+
// Basic crawl with markdown
|
|
116
|
+
const syncJob = await client.crawl({
|
|
117
|
+
"items_limit": 2,
|
|
118
|
+
"url": "https://books.toscrape.com/",
|
|
119
|
+
"scrape_type": "markdown"
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Type-safe status check
|
|
123
|
+
if (syncJob.status === "done") {
|
|
124
|
+
for (const item of syncJob.job_items) {
|
|
125
|
+
if (item.markdown_content_url) {
|
|
126
|
+
// Access typed properties
|
|
127
|
+
console.log("Status code:", item.page_status_code)
|
|
128
|
+
console.log("Content URL:", item.markdown_content_url)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Using specialized scraper
|
|
134
|
+
const scrapeJob = await client.scrape({
|
|
135
|
+
"crawler_id": "webcrawler/url-to-md",
|
|
136
|
+
"input": {
|
|
137
|
+
"url": "https://books.toscrape.com/"
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (scrapeJob.status === "done") {
|
|
142
|
+
const content = scrapeJob.getContent()
|
|
143
|
+
console.log("Scraped content:", content)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Type-safe error handling
|
|
148
|
+
if (error instanceof WebcrawlerApiError) {
|
|
149
|
+
console.error("API Error:", error.errorCode, error.message)
|
|
150
|
+
// errorCode is typed and can be used for specific error handling
|
|
151
|
+
if (error.errorCode === "invalid_request") {
|
|
152
|
+
// Handle invalid request
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
52
156
|
}
|
|
53
157
|
|
|
54
|
-
main().catch(console.error)
|
|
158
|
+
main().catch(console.error)
|
|
55
159
|
```
|
|
56
160
|
|
|
57
|
-
|
|
58
|
-
|
|
161
|
+
### Error Handling
|
|
162
|
+
|
|
163
|
+
The SDK provides typed error handling through the `WebcrawlerApiError` class. Our error handling system is designed to provide detailed, actionable feedback that helps you quickly identify and resolve issues in your scraping pipeline.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
try {
|
|
167
|
+
const syncJob = await client.crawl({
|
|
168
|
+
"items_limit": 2,
|
|
169
|
+
"url": "https://books.toscrape.com/",
|
|
170
|
+
// This will cause an error due to missing scrape_type
|
|
171
|
+
})
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof WebcrawlerApiError) {
|
|
174
|
+
// Typed error properties
|
|
175
|
+
console.error("Error code:", error.errorCode)
|
|
176
|
+
console.error("Error message:", error.message)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Response Structure
|
|
182
|
+
|
|
183
|
+
Example of a job response. The response structure is carefully designed to provide comprehensive information about your scraping job, including detailed status updates, timing information, and resource usage metrics.
|
|
184
|
+
|
|
185
|
+
```json
|
|
59
186
|
{
|
|
60
187
|
id: '49c4942b-b7d9-4d62-94b5-b54a3016ac51',
|
|
61
|
-
org_id: 'clxsnorta00075wuuqxgzzvxm',
|
|
62
188
|
url: 'https://stripe.com/',
|
|
63
189
|
scrape_type: 'markdown',
|
|
64
190
|
whitelist_regexp: '',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CrawlRequest, Job, JobId, ScrapeRequest, ScrapeResponse } from "./model";
|
|
2
|
+
export declare class WebcrawlerClient {
|
|
3
|
+
protected apiVersion: string;
|
|
4
|
+
protected apiKey: string;
|
|
5
|
+
protected basePath: string;
|
|
6
|
+
constructor(apiKey: string, basePath?: string, apiVersion?: string);
|
|
7
|
+
scrapeAsync(scrapeRequest: ScrapeRequest): Promise<JobId>;
|
|
8
|
+
scrapeWithMeta(scrapeRequest: ScrapeRequest, maxPollingRetries?: number): Promise<ScrapeResponse>;
|
|
9
|
+
scrape(scrapeRequest: ScrapeRequest, maxPollingRetries?: number): Promise<any>;
|
|
10
|
+
getScrapeResult(scrapeID: string): Promise<ScrapeResponse>;
|
|
11
|
+
crawl(crawlRequest: CrawlRequest): Promise<Job>;
|
|
12
|
+
crawlAsync(crawlRequest: CrawlRequest): Promise<JobId>;
|
|
13
|
+
getJob(jobID: string): Promise<Job>;
|
|
14
|
+
private sendRequest;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const JobStatus: {
|
|
2
|
+
readonly IN_PROGRESS: "in_progress";
|
|
3
|
+
readonly NEW: "new";
|
|
4
|
+
readonly DONE: "done";
|
|
5
|
+
};
|
|
6
|
+
export declare const HttpStatus: {
|
|
7
|
+
readonly OK: 200;
|
|
8
|
+
readonly BAD_REQUEST: 400;
|
|
9
|
+
readonly UNAUTHORIZED: 401;
|
|
10
|
+
readonly FORBIDDEN: 403;
|
|
11
|
+
readonly NOT_FOUND: 404;
|
|
12
|
+
readonly INTERNAL_SERVER_ERROR: 500;
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class WebcrawlerApiError extends Error {
|
|
2
|
+
errorCode: string;
|
|
3
|
+
errorMessage: string;
|
|
4
|
+
statusCode?: number;
|
|
5
|
+
constructor(errorCode: string, errorMessage: string, statusCode?: number);
|
|
6
|
+
}
|
|
7
|
+
export interface ErrorResponse {
|
|
8
|
+
error_code?: string;
|
|
9
|
+
error_message?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createErrorFromResponse(response: Response, data: ErrorResponse): WebcrawlerApiError;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface ScrapeRequest {
|
|
2
|
+
input: any;
|
|
3
|
+
crawler_id: string;
|
|
4
|
+
max_retries?: number;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
omit_cache?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface JobId {
|
|
9
|
+
id: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ScrapeResponse {
|
|
12
|
+
id: string;
|
|
13
|
+
url: string;
|
|
14
|
+
status: any;
|
|
15
|
+
page_status_code: number;
|
|
16
|
+
created_at: string;
|
|
17
|
+
structured_data: any;
|
|
18
|
+
recommended_pull_delay_ms: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CrawlRequest {
|
|
21
|
+
url: string;
|
|
22
|
+
items_limit?: number;
|
|
23
|
+
scrape_type?: string;
|
|
24
|
+
whitelist_regexp?: string;
|
|
25
|
+
blacklist_regexp?: string;
|
|
26
|
+
allow_subdomains?: boolean;
|
|
27
|
+
webhook_url?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Job {
|
|
30
|
+
id: string;
|
|
31
|
+
org_id: string;
|
|
32
|
+
url: string;
|
|
33
|
+
status: string;
|
|
34
|
+
scrape_type: string;
|
|
35
|
+
whitelist_regexp: string;
|
|
36
|
+
blacklist_regexp: string;
|
|
37
|
+
allow_subdomains: boolean;
|
|
38
|
+
items_limit: number;
|
|
39
|
+
created_at: string;
|
|
40
|
+
finished_at: string;
|
|
41
|
+
updated_at: string;
|
|
42
|
+
webhook_url: string;
|
|
43
|
+
recommended_pull_delay_ms: number;
|
|
44
|
+
job_items: JobItem[];
|
|
45
|
+
}
|
|
46
|
+
export interface JobItem {
|
|
47
|
+
id: string;
|
|
48
|
+
job_id: string;
|
|
49
|
+
original_url: string;
|
|
50
|
+
page_status_code: number;
|
|
51
|
+
markdown_content_url: string;
|
|
52
|
+
status: string;
|
|
53
|
+
title: string;
|
|
54
|
+
last_error: string;
|
|
55
|
+
created_at: string;
|
|
56
|
+
updated_at: string;
|
|
57
|
+
cost: number;
|
|
58
|
+
referred_url: string;
|
|
59
|
+
raw_content_url?: string;
|
|
60
|
+
cleaned_content_url?: string;
|
|
61
|
+
getContent(): Promise<string | null>;
|
|
62
|
+
}
|
package/dist/api.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
3
|
exports.WebcrawlerClient = void 0;
|
|
13
4
|
const constants_1 = require("./constants");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
14
6
|
const BASE_PATH = "https://api.webcrawlerapi.com";
|
|
15
7
|
const initialPullDelayMs = 2000;
|
|
16
8
|
const MaxPullRetries = 100;
|
|
@@ -21,219 +13,183 @@ class WebcrawlerClient {
|
|
|
21
13
|
this.basePath = basePath;
|
|
22
14
|
this.apiVersion = apiVersion;
|
|
23
15
|
}
|
|
24
|
-
scrapeAsync(scrapeRequest) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const response = yield fetch(url, requestOptions);
|
|
37
|
-
if (response.ok) {
|
|
38
|
-
return response.json();
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const data = yield response.json();
|
|
42
|
-
throw new Error(`failed to scrape, response returned ${response.status} ${response.statusText}: ${data === null || data === void 0 ? void 0 : data.error}`);
|
|
43
|
-
}
|
|
44
|
-
catch (e) {
|
|
45
|
-
throw e;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
16
|
+
async scrapeAsync(scrapeRequest) {
|
|
17
|
+
const url = `${this.basePath}/${this.apiVersion}/scrape`;
|
|
18
|
+
const requestOptions = {
|
|
19
|
+
'method': 'POST',
|
|
20
|
+
'headers': {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
23
|
+
"User-Agent": "WebcrawlerAPI-NodeJS-Client"
|
|
24
|
+
},
|
|
25
|
+
'body': JSON.stringify(scrapeRequest),
|
|
26
|
+
};
|
|
27
|
+
return await this.sendRequest(url, requestOptions);
|
|
48
28
|
}
|
|
49
|
-
scrapeWithMeta(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
throw new Error("Scraping took too long, please retry or increase the number of polling retries");
|
|
79
|
-
});
|
|
29
|
+
async scrapeWithMeta(scrapeRequest, maxPollingRetries = MaxPullRetries) {
|
|
30
|
+
const url = `${this.basePath}/${this.apiVersion}/scrape`;
|
|
31
|
+
const requestOptions = {
|
|
32
|
+
'method': 'POST',
|
|
33
|
+
'headers': {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
36
|
+
},
|
|
37
|
+
'body': JSON.stringify(scrapeRequest),
|
|
38
|
+
};
|
|
39
|
+
const jobIdResponse = await this.sendRequest(url, requestOptions);
|
|
40
|
+
if (jobIdResponse.id === '') {
|
|
41
|
+
throw new errors_1.WebcrawlerApiError('invalid_response', 'Failed to fetch job status', 0);
|
|
42
|
+
}
|
|
43
|
+
let delayIntervalMs = initialPullDelayMs;
|
|
44
|
+
for (let i = 0; i < maxPollingRetries; i++) {
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, delayIntervalMs));
|
|
46
|
+
const scrapeResult = await this.getScrapeResult(jobIdResponse.id);
|
|
47
|
+
if (scrapeRequest.debug) {
|
|
48
|
+
console.log(`Scrape result: ${JSON.stringify(scrapeResult)}`);
|
|
49
|
+
}
|
|
50
|
+
if (scrapeResult.status !== constants_1.JobStatus.IN_PROGRESS && scrapeResult.status !== constants_1.JobStatus.NEW) {
|
|
51
|
+
return scrapeResult;
|
|
52
|
+
}
|
|
53
|
+
if (scrapeResult.recommended_pull_delay_ms > 0) {
|
|
54
|
+
delayIntervalMs = scrapeResult.recommended_pull_delay_ms;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new errors_1.WebcrawlerApiError('timeout', 'Scraping took too long, please retry or increase the number of polling retries', 0);
|
|
80
58
|
}
|
|
81
|
-
scrape(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return scrapeResult.structured_data;
|
|
85
|
-
});
|
|
59
|
+
async scrape(scrapeRequest, maxPollingRetries = MaxPullRetries) {
|
|
60
|
+
const scrapeResult = await this.scrapeWithMeta(scrapeRequest, maxPollingRetries);
|
|
61
|
+
return scrapeResult.structured_data;
|
|
86
62
|
}
|
|
87
|
-
getScrapeResult(scrapeID) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const response = yield fetch(url, requestOptions);
|
|
102
|
-
if (response.ok) {
|
|
103
|
-
return response.json();
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
const data = yield response.json();
|
|
107
|
-
throw new Error(`failed to fetch job status ${response.status} ${response.statusText}: ${JSON.stringify(data)}`);
|
|
108
|
-
}
|
|
109
|
-
catch (e) {
|
|
110
|
-
throw new Error(`failed to fetch job status ${response.status} ${response.statusText}`);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
63
|
+
async getScrapeResult(scrapeID) {
|
|
64
|
+
const url = `${this.basePath}/${this.apiVersion}/scrape/${scrapeID}`;
|
|
65
|
+
const requestOptions = {
|
|
66
|
+
'method': 'GET',
|
|
67
|
+
'headers': {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
70
|
+
"User-Agent": "WebcrawlerAPI-NodeJS-Client",
|
|
71
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
72
|
+
'Pragma': 'no-cache',
|
|
73
|
+
'Expires': '0'
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return await this.sendRequest(url, requestOptions);
|
|
113
77
|
}
|
|
114
|
-
crawl(crawlRequest) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
});
|
|
78
|
+
async crawl(crawlRequest) {
|
|
79
|
+
const url = `${this.basePath}/${this.apiVersion}/crawl`;
|
|
80
|
+
const requestOptions = {
|
|
81
|
+
'method': 'POST',
|
|
82
|
+
'headers': {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
85
|
+
"User-Agent": "WebcrawlerAPI-NodeJS-Client",
|
|
86
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
87
|
+
'Pragma': 'no-cache',
|
|
88
|
+
'Expires': '0'
|
|
89
|
+
},
|
|
90
|
+
'body': JSON.stringify(crawlRequest),
|
|
91
|
+
};
|
|
92
|
+
const jobIdResponse = await this.sendRequest(url, requestOptions);
|
|
93
|
+
if (jobIdResponse.id === '') {
|
|
94
|
+
throw new errors_1.WebcrawlerApiError('invalid_response', 'Failed to fetch job status', 0);
|
|
95
|
+
}
|
|
96
|
+
let delayIntervalMs = initialPullDelayMs;
|
|
97
|
+
for (let i = 0; i < MaxPullRetries; i++) {
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, delayIntervalMs));
|
|
99
|
+
const timestamp = new Date().getTime();
|
|
100
|
+
const job = await this.getJob(`${jobIdResponse.id}?t=${timestamp}`);
|
|
101
|
+
if (job.status !== constants_1.JobStatus.IN_PROGRESS && job.status !== constants_1.JobStatus.NEW) {
|
|
102
|
+
// Transform each job item to include getContent method
|
|
103
|
+
job.job_items = job.job_items.map(item => ({
|
|
104
|
+
...item,
|
|
105
|
+
getContent: async function () {
|
|
106
|
+
if (job.status !== constants_1.JobStatus.DONE || this.status !== constants_1.JobStatus.DONE) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
let contentUrl;
|
|
110
|
+
switch (job.scrape_type) {
|
|
111
|
+
case 'html':
|
|
112
|
+
contentUrl = this.raw_content_url;
|
|
113
|
+
break;
|
|
114
|
+
case 'cleaned':
|
|
115
|
+
contentUrl = this.cleaned_content_url;
|
|
116
|
+
break;
|
|
117
|
+
case 'markdown':
|
|
118
|
+
contentUrl = this.markdown_content_url;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (!contentUrl) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const response = await fetch(contentUrl, {
|
|
125
|
+
headers: {
|
|
126
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
127
|
+
'Accept': '*/*'
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`Failed to fetch content: ${response.statusText}`);
|
|
132
|
+
}
|
|
133
|
+
return await response.text();
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
return job;
|
|
137
|
+
}
|
|
138
|
+
if (job.recommended_pull_delay_ms > 0) {
|
|
139
|
+
delayIntervalMs = job.recommended_pull_delay_ms;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw new errors_1.WebcrawlerApiError('timeout', 'Crawling took too long, please retry or increase the number of polling retries', 0);
|
|
180
143
|
}
|
|
181
|
-
crawlAsync(crawlRequest) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
'
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return yield this.sendRequest(url, requestOptions);
|
|
193
|
-
});
|
|
144
|
+
async crawlAsync(crawlRequest) {
|
|
145
|
+
const url = `${this.basePath}/${this.apiVersion}/crawl`;
|
|
146
|
+
const requestOptions = {
|
|
147
|
+
'method': 'POST',
|
|
148
|
+
'headers': {
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
151
|
+
},
|
|
152
|
+
'body': JSON.stringify(crawlRequest),
|
|
153
|
+
};
|
|
154
|
+
return await this.sendRequest(url, requestOptions);
|
|
194
155
|
}
|
|
195
|
-
getJob(jobID) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
'
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const response = yield fetch(url, requestOptions);
|
|
210
|
-
if (response.ok) {
|
|
211
|
-
return response.json();
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
const data = yield response.json();
|
|
215
|
-
throw new Error(`failed to fetch job status ${response.status} ${response.statusText}: ${JSON.stringify(data)}`);
|
|
216
|
-
}
|
|
217
|
-
catch (e) {
|
|
218
|
-
throw new Error(`failed to fetch job status ${response.status} ${response.statusText}`);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
156
|
+
async getJob(jobID) {
|
|
157
|
+
const url = `${this.basePath}/${this.apiVersion}/job/${jobID}`;
|
|
158
|
+
const requestOptions = {
|
|
159
|
+
'method': 'GET',
|
|
160
|
+
'headers': {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
163
|
+
"User-Agent": "WebcrawlerAPI-NodeJS-Client",
|
|
164
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
165
|
+
'Pragma': 'no-cache',
|
|
166
|
+
'Expires': '0'
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return await this.sendRequest(url, requestOptions);
|
|
221
170
|
}
|
|
222
|
-
sendRequest(url, requestOptions) {
|
|
223
|
-
|
|
224
|
-
|
|
171
|
+
async sendRequest(url, requestOptions) {
|
|
172
|
+
let response;
|
|
173
|
+
try {
|
|
174
|
+
response = await fetch(url, requestOptions);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
throw new errors_1.WebcrawlerApiError('network_error', `Failed to send request: ${e}`, 0);
|
|
178
|
+
}
|
|
179
|
+
if (!response.ok) {
|
|
225
180
|
try {
|
|
226
|
-
|
|
181
|
+
const errorData = await response.json();
|
|
182
|
+
throw (0, errors_1.createErrorFromResponse)(response, errorData);
|
|
227
183
|
}
|
|
228
184
|
catch (e) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
throw new
|
|
185
|
+
if (e instanceof errors_1.WebcrawlerApiError) {
|
|
186
|
+
throw e;
|
|
187
|
+
}
|
|
188
|
+
// If we can't parse the error response, create a generic error
|
|
189
|
+
throw new errors_1.WebcrawlerApiError('unknown_error', `Request failed with status ${response.status} ${response.statusText}`, response.status);
|
|
234
190
|
}
|
|
235
|
-
|
|
236
|
-
|
|
191
|
+
}
|
|
192
|
+
return response.json();
|
|
237
193
|
}
|
|
238
194
|
}
|
|
239
195
|
exports.WebcrawlerClient = WebcrawlerClient;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class WebcrawlerApiError extends Error {
|
|
2
|
+
errorCode: string;
|
|
3
|
+
errorMessage: string;
|
|
4
|
+
statusCode?: number;
|
|
5
|
+
constructor(errorCode: string, errorMessage: string, statusCode?: number);
|
|
6
|
+
}
|
|
7
|
+
export interface ErrorResponse {
|
|
8
|
+
error_code?: string;
|
|
9
|
+
error_message?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createErrorFromResponse(response: Response, data: ErrorResponse): WebcrawlerApiError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebcrawlerApiError = void 0;
|
|
4
|
+
exports.createErrorFromResponse = createErrorFromResponse;
|
|
5
|
+
class WebcrawlerApiError extends Error {
|
|
6
|
+
constructor(errorCode, errorMessage, statusCode) {
|
|
7
|
+
super(errorMessage);
|
|
8
|
+
this.name = 'WebcrawlerApiError';
|
|
9
|
+
this.errorCode = errorCode;
|
|
10
|
+
this.errorMessage = errorMessage;
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.WebcrawlerApiError = WebcrawlerApiError;
|
|
15
|
+
function createErrorFromResponse(response, data) {
|
|
16
|
+
const errorCode = data.error_code || 'unknown_error';
|
|
17
|
+
const errorMessage = data.error_message || data.error || 'Unknown error';
|
|
18
|
+
return new WebcrawlerApiError(errorCode, errorMessage, response.status);
|
|
19
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,3 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./api"), exports);
|
|
18
|
+
__exportStar(require("./model"), exports);
|
|
19
|
+
__exportStar(require("./constants"), exports);
|
|
20
|
+
__exportStar(require("./errors"), exports);
|
package/package.json
CHANGED
package/{api.ts → src/api.ts}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {CrawlRequest, Job, JobId, ScrapeRequest, ScrapeResponse} from "./model";
|
|
2
2
|
import { JobStatus } from "./constants";
|
|
3
|
+
import { WebcrawlerApiError, createErrorFromResponse, ErrorResponse } from "./errors";
|
|
3
4
|
|
|
4
5
|
const BASE_PATH = "https://api.webcrawlerapi.com"
|
|
5
6
|
const initialPullDelayMs = 2000
|
|
@@ -29,18 +30,7 @@ export class WebcrawlerClient {
|
|
|
29
30
|
'body': JSON.stringify(scrapeRequest),
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
if (response.ok) {
|
|
34
|
-
return response.json();
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const data = await response.json();
|
|
38
|
-
throw new Error(
|
|
39
|
-
`failed to scrape, response returned ${response.status} ${response.statusText}: ${data?.error}`
|
|
40
|
-
);
|
|
41
|
-
} catch (e) {
|
|
42
|
-
throw e;
|
|
43
|
-
}
|
|
33
|
+
return await this.sendRequest(url, requestOptions);
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
public async scrapeWithMeta(scrapeRequest: ScrapeRequest, maxPollingRetries: number = MaxPullRetries): Promise<ScrapeResponse> {
|
|
@@ -58,7 +48,7 @@ export class WebcrawlerClient {
|
|
|
58
48
|
const jobIdResponse: JobId = await this.sendRequest(url, requestOptions);
|
|
59
49
|
|
|
60
50
|
if (jobIdResponse.id === '') {
|
|
61
|
-
throw new
|
|
51
|
+
throw new WebcrawlerApiError('invalid_response', 'Failed to fetch job status', 0);
|
|
62
52
|
}
|
|
63
53
|
|
|
64
54
|
let delayIntervalMs = initialPullDelayMs;
|
|
@@ -75,7 +65,7 @@ export class WebcrawlerClient {
|
|
|
75
65
|
delayIntervalMs = scrapeResult.recommended_pull_delay_ms;
|
|
76
66
|
}
|
|
77
67
|
}
|
|
78
|
-
throw new
|
|
68
|
+
throw new WebcrawlerApiError('timeout', 'Scraping took too long, please retry or increase the number of polling retries', 0);
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
public async scrape(scrapeRequest: ScrapeRequest, maxPollingRetries: number = MaxPullRetries): Promise<any> {
|
|
@@ -96,21 +86,7 @@ export class WebcrawlerClient {
|
|
|
96
86
|
'Expires': '0'
|
|
97
87
|
},
|
|
98
88
|
};
|
|
99
|
-
|
|
100
|
-
if (response.ok) {
|
|
101
|
-
return response.json();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const data = await response.json();
|
|
106
|
-
throw new Error(
|
|
107
|
-
`failed to fetch job status ${response.status} ${response.statusText}: ${JSON.stringify(data)}`
|
|
108
|
-
);
|
|
109
|
-
} catch (e) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`failed to fetch job status ${response.status} ${response.statusText}`
|
|
112
|
-
);
|
|
113
|
-
}
|
|
89
|
+
return await this.sendRequest(url, requestOptions);
|
|
114
90
|
}
|
|
115
91
|
|
|
116
92
|
public async crawl(crawlRequest: CrawlRequest): Promise<Job> {
|
|
@@ -132,7 +108,7 @@ export class WebcrawlerClient {
|
|
|
132
108
|
const jobIdResponse: JobId = await this.sendRequest(url, requestOptions);
|
|
133
109
|
|
|
134
110
|
if (jobIdResponse.id === '') {
|
|
135
|
-
throw new
|
|
111
|
+
throw new WebcrawlerApiError('invalid_response', 'Failed to fetch job status', 0);
|
|
136
112
|
}
|
|
137
113
|
|
|
138
114
|
let delayIntervalMs = initialPullDelayMs;
|
|
@@ -186,7 +162,7 @@ export class WebcrawlerClient {
|
|
|
186
162
|
delayIntervalMs = job.recommended_pull_delay_ms;
|
|
187
163
|
}
|
|
188
164
|
}
|
|
189
|
-
throw new
|
|
165
|
+
throw new WebcrawlerApiError('timeout', 'Crawling took too long, please retry or increase the number of polling retries', 0);
|
|
190
166
|
}
|
|
191
167
|
|
|
192
168
|
public async crawlAsync(crawlRequest: CrawlRequest): Promise<JobId> {
|
|
@@ -216,36 +192,35 @@ export class WebcrawlerClient {
|
|
|
216
192
|
'Pragma': 'no-cache',
|
|
217
193
|
'Expires': '0'
|
|
218
194
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (response.ok) {
|
|
222
|
-
return response.json();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
const data = await response.json();
|
|
227
|
-
throw new Error(
|
|
228
|
-
`failed to fetch job status ${response.status} ${response.statusText}: ${JSON.stringify(data)}`
|
|
229
|
-
);
|
|
230
|
-
} catch (e) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`failed to fetch job status ${response.status} ${response.statusText}`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
195
|
+
};
|
|
196
|
+
return await this.sendRequest(url, requestOptions);
|
|
235
197
|
}
|
|
236
198
|
|
|
237
|
-
|
|
238
199
|
private async sendRequest(url: string, requestOptions: any): Promise<any> {
|
|
239
200
|
let response: Response;
|
|
240
201
|
try {
|
|
241
202
|
response = await fetch(url, requestOptions);
|
|
242
203
|
} catch (e) {
|
|
243
|
-
throw new
|
|
204
|
+
throw new WebcrawlerApiError('network_error', `Failed to send request: ${e}`, 0);
|
|
244
205
|
}
|
|
206
|
+
|
|
245
207
|
if (!response.ok) {
|
|
246
|
-
|
|
247
|
-
|
|
208
|
+
try {
|
|
209
|
+
const errorData = await response.json() as ErrorResponse;
|
|
210
|
+
throw createErrorFromResponse(response, errorData);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (e instanceof WebcrawlerApiError) {
|
|
213
|
+
throw e;
|
|
214
|
+
}
|
|
215
|
+
// If we can't parse the error response, create a generic error
|
|
216
|
+
throw new WebcrawlerApiError(
|
|
217
|
+
'unknown_error',
|
|
218
|
+
`Request failed with status ${response.status} ${response.statusText}`,
|
|
219
|
+
response.status
|
|
220
|
+
);
|
|
221
|
+
}
|
|
248
222
|
}
|
|
223
|
+
|
|
249
224
|
return response.json();
|
|
250
225
|
}
|
|
251
226
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class WebcrawlerApiError extends Error {
|
|
2
|
+
public errorCode: string;
|
|
3
|
+
public errorMessage: string;
|
|
4
|
+
public statusCode?: number;
|
|
5
|
+
|
|
6
|
+
constructor(errorCode: string, errorMessage: string, statusCode?: number) {
|
|
7
|
+
super(errorMessage);
|
|
8
|
+
this.name = 'WebcrawlerApiError';
|
|
9
|
+
this.errorCode = errorCode;
|
|
10
|
+
this.errorMessage = errorMessage;
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ErrorResponse {
|
|
16
|
+
error_code?: string;
|
|
17
|
+
error_message?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createErrorFromResponse(response: Response, data: ErrorResponse): WebcrawlerApiError {
|
|
22
|
+
const errorCode = data.error_code || 'unknown_error';
|
|
23
|
+
const errorMessage = data.error_message || data.error || 'Unknown error';
|
|
24
|
+
return new WebcrawlerApiError(errorCode, errorMessage, response.status);
|
|
25
|
+
}
|
package/src/index.ts
ADDED
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"
|
|
4
|
-
|
|
3
|
+
"lib": [
|
|
4
|
+
"es2021"
|
|
5
|
+
],
|
|
5
6
|
"module": "commonjs",
|
|
6
|
-
"
|
|
7
|
+
"target": "es2021",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
7
12
|
"outDir": "dist",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"node_modules/@types"
|
|
11
|
-
]
|
|
13
|
+
"declarationDir": "dist/@types",
|
|
14
|
+
"declaration": true
|
|
12
15
|
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src/**/*"
|
|
18
|
+
],
|
|
13
19
|
"exclude": [
|
|
14
20
|
"dist",
|
|
15
21
|
"node_modules"
|
package/dist/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./api";
|
package/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./api";
|
|
File without changes
|
|
File without changes
|