webcrawlerapi-js 2.0.10 → 2.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 +28 -155
- package/dist/@types/api.d.ts +0 -1
- package/dist/@types/model.d.ts +25 -3
- package/dist/api.js +51 -39
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,179 +1,52 @@
|
|
|
1
1
|
# JS client for WebcrawlerAPI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Webcrawler API for your AI agent. Get markdown of any website or page with a single call.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Preparation
|
|
8
|
-
1. Register to [dashboard|https://dash.webcrawlerapi.com/].
|
|
9
|
-
2. Get an [Access Key](https://dash.webcrawlerapi.com/access).
|
|
10
|
-
|
|
11
|
-
Our dashboard provides comprehensive analytics and monitoring tools to help you track your API usage and optimize your scraping operations.
|
|
5
|
+
Official JavaScript/TypeScript client for [WebcrawlerAPI](https://webcrawlerapi.com/) — web scraping and crawling with JS rendering, proxy rotation, and AI extraction.
|
|
12
6
|
|
|
13
7
|
## Installation
|
|
14
|
-
Install WebcrawlerAPI js package:
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
```
|
|
10
|
+
npm i webcrawlerapi-js
|
|
11
|
+
```
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
Use `crawlRawMarkdown` when you want a crawl to finish and immediately return the combined `/job/{id}/markdown` content.
|
|
13
|
+
Get your API key at [dash.webcrawlerapi.com/access](https://dash.webcrawlerapi.com/access).
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
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. However, for long running jobs we recommend to use async crawling or the webhook.
|
|
15
|
+
## Scrape a single page
|
|
25
16
|
|
|
26
17
|
```javascript
|
|
27
18
|
import webcrawlerapi from "webcrawlerapi-js"
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"url": "https://books.toscrape.com/",
|
|
36
|
-
"scrape_type": "markdown"
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// Get job details
|
|
40
|
-
const job = await client.getJob(crawlJob.id);
|
|
41
|
-
console.log("Job status:", job.status);
|
|
42
|
-
|
|
43
|
-
// Output item content
|
|
44
|
-
for (const item of job.job_items) {
|
|
45
|
-
const content = await item.getContent()
|
|
46
|
-
console.log("---- Title: " + item.title + "----")
|
|
47
|
-
console.log(content.slice(0, 100))
|
|
48
|
-
}
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if (error instanceof webcrawlerapi.WebcrawlerApiError) {
|
|
51
|
-
console.error("Webcrawler error:", error.message)
|
|
52
|
-
}
|
|
53
|
-
console.error(error)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
20
|
+
const client = new webcrawlerapi.WebcrawlerClient("YOUR_API_KEY")
|
|
21
|
+
|
|
22
|
+
const result = await client.scrape({
|
|
23
|
+
url: "https://books.toscrape.com/",
|
|
24
|
+
output_formats: ["markdown"],
|
|
25
|
+
})
|
|
56
26
|
|
|
57
|
-
|
|
27
|
+
console.log(result.markdown)
|
|
58
28
|
```
|
|
59
29
|
|
|
60
|
-
|
|
61
|
-
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.
|
|
30
|
+
## Crawl multiple pages
|
|
62
31
|
|
|
63
32
|
```javascript
|
|
64
33
|
import webcrawlerapi from "webcrawlerapi-js"
|
|
65
34
|
|
|
66
|
-
|
|
67
|
-
const client = new webcrawlerapi.WebcrawlerClient("YOUR API ACCESS KEY HERE")
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
try {
|
|
71
|
-
const jobWithId = await client.crawlAsync({
|
|
72
|
-
"items_limit": 10,
|
|
73
|
-
"url": "https://books.toscrape.com/",
|
|
74
|
-
"scrape_type": "markdown"
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// Poll for job completion
|
|
78
|
-
const jobId = jobWithId.id
|
|
79
|
-
let asyncJob = await client.getJob(jobId)
|
|
80
|
-
|
|
81
|
-
// Check job status
|
|
82
|
-
console.log("Job id:", asyncJob.id)
|
|
83
|
-
console.log("Dashboard link:", `https://dash.webcrawlerapi.com/jobs/job/${asyncJob.id}`)
|
|
84
|
-
|
|
85
|
-
//wait until job is done
|
|
86
|
-
while (asyncJob.status !== "done") {
|
|
87
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
88
|
-
asyncJob = await client.getJob(jobId)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Once complete, access results
|
|
92
|
-
if (asyncJob.status === "done") {
|
|
93
|
-
for (const item of asyncJob.job_items) {
|
|
94
|
-
const content = await item.getContent()
|
|
95
|
-
console.log(content.slice(0, 100))
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if (error instanceof webcrawlerapi.WebcrawlerApiError) {
|
|
100
|
-
console.error("Webcrawler error:", error.message)
|
|
101
|
-
}
|
|
102
|
-
console.error(error)
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
}
|
|
35
|
+
const client = new webcrawlerapi.WebcrawlerClient("YOUR_API_KEY")
|
|
106
36
|
|
|
107
|
-
|
|
108
|
-
|
|
37
|
+
const job = await client.crawl({
|
|
38
|
+
url: "https://books.toscrape.com/",
|
|
39
|
+
items_limit: 10,
|
|
40
|
+
output_formats: ["markdown"],
|
|
41
|
+
})
|
|
109
42
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import {WebcrawlerClient, WebcrawlerApiError} from "webcrawlerapi-js";
|
|
116
|
-
|
|
117
|
-
async function main() {
|
|
118
|
-
const client = new webcrawlerapi.WebcrawlerClient("YOUR_API_KEY")
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
// Basic crawl with markdown
|
|
122
|
-
const syncJob = await client.crawl({
|
|
123
|
-
"items_limit": 10,
|
|
124
|
-
"url": "https://books.toscrape.com/",
|
|
125
|
-
"scrape_type": "markdown",
|
|
126
|
-
}
|
|
127
|
-
)
|
|
128
|
-
// Type-safe status check
|
|
129
|
-
for (const item of syncJob.job_items) {
|
|
130
|
-
const content = await item.getContent()
|
|
131
|
-
console.log(content.slice(0,100))
|
|
132
|
-
}
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (error instanceof WebcrawlerApiError) {
|
|
135
|
-
console.error("API Error:", error.errorCode, error.message)
|
|
136
|
-
}
|
|
137
|
-
console.log(error)
|
|
138
|
-
}
|
|
43
|
+
for (const item of job.job_items) {
|
|
44
|
+
const content = await item.getContent()
|
|
45
|
+
console.log(item.title, content?.slice(0, 100))
|
|
139
46
|
}
|
|
140
|
-
|
|
141
|
-
main().catch(console.error)
|
|
142
47
|
```
|
|
143
48
|
|
|
144
|
-
##
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```js
|
|
149
|
-
{
|
|
150
|
-
id: '49c4942b-b7d9-4d62-94b5-b54a3016ac51',
|
|
151
|
-
url: 'https://stripe.com/',
|
|
152
|
-
scrape_type: 'markdown',
|
|
153
|
-
whitelist_regexp: '',
|
|
154
|
-
blacklist_regexp: '',
|
|
155
|
-
items_limit: 10,
|
|
156
|
-
created_at: '2024-12-28T21:36:04.417Z',
|
|
157
|
-
finished_at: null,
|
|
158
|
-
updated_at: '2024-12-28T21:36:04.383Z',
|
|
159
|
-
webhook_url: '',
|
|
160
|
-
status: 'in_progress',
|
|
161
|
-
job_items: [
|
|
162
|
-
{
|
|
163
|
-
id: 'f26cefe1-09d1-4d4c-8b74-b65e075e230d',
|
|
164
|
-
job_id: '49c4942a-b7d9-4d62-94b5-b54a3016ac51',
|
|
165
|
-
original_url: 'https://stripe.com/',
|
|
166
|
-
page_status_code: 0,
|
|
167
|
-
status: 'new',
|
|
168
|
-
title: '',
|
|
169
|
-
last_error: '',
|
|
170
|
-
error_code: '',
|
|
171
|
-
created_at: '2024-12-28T21:36:04.468Z',
|
|
172
|
-
updated_at: '2024-12-28T21:36:04.435Z',
|
|
173
|
-
cost: 0,
|
|
174
|
-
referred_url: ''
|
|
175
|
-
}
|
|
176
|
-
],
|
|
177
|
-
recommended_pull_delay_ms: 5000
|
|
178
|
-
}
|
|
179
|
-
```
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
- [Scraping](docs/scraping.md) — single page, async mode, AI extraction, parameters, response fields
|
|
52
|
+
- [Crawling](docs/crawling.md) — multi-page, async mode, combined markdown, parameters, job items
|
package/dist/@types/api.d.ts
CHANGED
|
@@ -11,7 +11,6 @@ export declare class WebcrawlerClient {
|
|
|
11
11
|
scrapeAsync(request: ScrapeRequest): Promise<ScrapeId>;
|
|
12
12
|
getScrape(scrapeId: string): Promise<ScrapeResponse | ScrapeResponseError>;
|
|
13
13
|
scrape(request: ScrapeRequest, maxPolls?: number): Promise<ScrapeResponse | ScrapeResponseError>;
|
|
14
|
-
crawlRawMarkdown(crawlRequest: CrawlRequest, actions?: Action | Action[]): Promise<string>;
|
|
15
14
|
crawl(crawlRequest: CrawlRequest, actions?: Action | Action[]): Promise<Job>;
|
|
16
15
|
crawlAsync(crawlRequest: CrawlRequest, actions?: Action | Action[]): Promise<JobId>;
|
|
17
16
|
getJob(jobID: string): Promise<Job>;
|
package/dist/@types/model.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
export interface ScrapeRequest {
|
|
2
2
|
url: string;
|
|
3
|
+
/** @deprecated Use output_formats instead */
|
|
3
4
|
output_format?: string;
|
|
5
|
+
output_formats?: ('markdown' | 'cleaned' | 'html' | 'links')[];
|
|
4
6
|
webhook_url?: string;
|
|
7
|
+
/** CSS selectors to clean from the output. */
|
|
5
8
|
clean_selectors?: string;
|
|
6
9
|
actions?: Action[];
|
|
10
|
+
/** A prompt to run on the scraped content, used to extract specific information or format the output (extra cost will be charged). */
|
|
7
11
|
prompt?: string;
|
|
12
|
+
/** JSON schema to enforce structured output when using a prompt. Follows the OpenAI Structured Outputs format. */
|
|
8
13
|
response_schema?: Record<string, any>;
|
|
9
|
-
|
|
14
|
+
/** Extract only the main content of the page. Works best for articles, blog posts, news. */
|
|
10
15
|
main_content_only?: boolean;
|
|
16
|
+
/** Maximum age of cached content in seconds. Set to 0 to always fetch fresh content. */
|
|
11
17
|
max_age?: number;
|
|
12
18
|
}
|
|
13
19
|
export interface ScrapeId {
|
|
@@ -34,17 +40,27 @@ export interface JobId {
|
|
|
34
40
|
id: string;
|
|
35
41
|
}
|
|
36
42
|
export interface CrawlRequest {
|
|
43
|
+
/** The seed URL where the crawler starts. Can be any valid URL. */
|
|
37
44
|
url?: string;
|
|
38
|
-
|
|
45
|
+
/** The crawler will stop when it reaches this limit of pages for this job. */
|
|
39
46
|
items_limit?: number;
|
|
47
|
+
/** @deprecated Use output_formats instead */
|
|
40
48
|
scrape_type?: string;
|
|
49
|
+
output_formats?: ('links' | 'markdown' | 'cleaned' | 'html')[];
|
|
50
|
+
/** Regular expression to whitelist URLs. Only URLs that match the pattern will be crawled. */
|
|
41
51
|
whitelist_regexp?: string;
|
|
52
|
+
/** Regular expression to blacklist URLs. URLs that match the pattern will be skipped. */
|
|
42
53
|
blacklist_regexp?: string;
|
|
54
|
+
/** URL where the server will send a POST request once the job is completed. */
|
|
43
55
|
webhook_url?: string;
|
|
44
56
|
actions?: Action[];
|
|
57
|
+
/** If true, the crawler will respect the website's robots.txt and skip disallowed pages. Default is false. */
|
|
45
58
|
respect_robots_txt?: boolean;
|
|
59
|
+
/** Extract only the main content of the page. Works best for articles, blog posts, news. */
|
|
46
60
|
main_content_only?: boolean;
|
|
61
|
+
/** Maximum crawl depth from the starting URL. 0 = starting page only, 1 = starting page + directly linked pages, etc. No limit by default. */
|
|
47
62
|
max_depth?: number;
|
|
63
|
+
/** Maximum age of cached content in seconds. Set to 0 to always fetch fresh content. */
|
|
48
64
|
max_age?: number;
|
|
49
65
|
}
|
|
50
66
|
export interface Job {
|
|
@@ -52,7 +68,9 @@ export interface Job {
|
|
|
52
68
|
org_id: string;
|
|
53
69
|
url: string;
|
|
54
70
|
status: string;
|
|
55
|
-
|
|
71
|
+
/** @deprecated Use output_formats instead */
|
|
72
|
+
scrape_type?: string;
|
|
73
|
+
output_formats?: string[];
|
|
56
74
|
whitelist_regexp: string;
|
|
57
75
|
blacklist_regexp: string;
|
|
58
76
|
items_limit: number;
|
|
@@ -77,11 +95,15 @@ export interface JobItem {
|
|
|
77
95
|
updated_at: string;
|
|
78
96
|
cost: number;
|
|
79
97
|
referred_url: string;
|
|
98
|
+
link?: string;
|
|
80
99
|
depth?: number;
|
|
81
100
|
raw_content_url?: string;
|
|
82
101
|
cleaned_content_url?: string;
|
|
83
102
|
error_code?: string;
|
|
84
103
|
getContent(): Promise<string | null>;
|
|
104
|
+
getMarkdown(): Promise<string | null>;
|
|
105
|
+
getCleaned(): Promise<string | null>;
|
|
106
|
+
getHTML(): Promise<string | null>;
|
|
85
107
|
}
|
|
86
108
|
export interface Action {
|
|
87
109
|
type: string;
|
package/dist/api.js
CHANGED
|
@@ -26,6 +26,44 @@ const MaxPullRetries = 100;
|
|
|
26
26
|
const DEFAULT_POLL_DELAY_SECONDS = 2;
|
|
27
27
|
__exportStar(require("./model"), exports);
|
|
28
28
|
const SCRAPE_VERSION = "v2";
|
|
29
|
+
function resolveContentUrl(job, item) {
|
|
30
|
+
// Prefer output_formats if present and non-empty, using priority: markdown > cleaned > html
|
|
31
|
+
if (Array.isArray(job.output_formats) && job.output_formats.length > 0) {
|
|
32
|
+
const priority = ['markdown', 'cleaned', 'html'];
|
|
33
|
+
for (const fmt of priority) {
|
|
34
|
+
if (job.output_formats.includes(fmt)) {
|
|
35
|
+
switch (fmt) {
|
|
36
|
+
case 'markdown': return item.markdown_content_url;
|
|
37
|
+
case 'cleaned': return item.cleaned_content_url;
|
|
38
|
+
case 'html': return item.raw_content_url;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
// Fall back to scrape_type for backward compatibility
|
|
45
|
+
switch (job.scrape_type) {
|
|
46
|
+
case 'html': return item.raw_content_url;
|
|
47
|
+
case 'cleaned': return item.cleaned_content_url;
|
|
48
|
+
case 'markdown': return item.markdown_content_url;
|
|
49
|
+
default: return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function fetchContentUrl(url) {
|
|
53
|
+
if (!url) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
headers: {
|
|
58
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
59
|
+
'Accept': '*/*'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to fetch content: ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
return response.text();
|
|
66
|
+
}
|
|
29
67
|
function addGetContentMethod(job) {
|
|
30
68
|
// Transform each job item to include getContent method
|
|
31
69
|
job.job_items = job.job_items.map(item => ({
|
|
@@ -34,31 +72,17 @@ function addGetContentMethod(job) {
|
|
|
34
72
|
if (job.status !== constants_1.JobStatus.DONE || this.status !== constants_1.JobStatus.DONE) {
|
|
35
73
|
return null;
|
|
36
74
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
if (!contentUrl) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
const response = await fetch(contentUrl, {
|
|
53
|
-
headers: {
|
|
54
|
-
'Accept-Encoding': 'gzip, deflate, br',
|
|
55
|
-
'Accept': '*/*'
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
throw new Error(`Failed to fetch content: ${response.statusText}`);
|
|
60
|
-
}
|
|
61
|
-
return await response.text();
|
|
75
|
+
const contentUrl = resolveContentUrl(job, this);
|
|
76
|
+
return fetchContentUrl(contentUrl);
|
|
77
|
+
},
|
|
78
|
+
getMarkdown: async function () {
|
|
79
|
+
return fetchContentUrl(this.markdown_content_url);
|
|
80
|
+
},
|
|
81
|
+
getCleaned: async function () {
|
|
82
|
+
return fetchContentUrl(this.cleaned_content_url);
|
|
83
|
+
},
|
|
84
|
+
getHTML: async function () {
|
|
85
|
+
return fetchContentUrl(this.raw_content_url);
|
|
62
86
|
}
|
|
63
87
|
}));
|
|
64
88
|
return job;
|
|
@@ -138,23 +162,10 @@ class WebcrawlerClient {
|
|
|
138
162
|
// Return the last known state if maxPolls is reached
|
|
139
163
|
return result;
|
|
140
164
|
}
|
|
141
|
-
async crawlRawMarkdown(crawlRequest, actions) {
|
|
142
|
-
const markdownRequest = {
|
|
143
|
-
...crawlRequest,
|
|
144
|
-
scrape_type: crawlRequest.scrape_type ?? 'markdown'
|
|
145
|
-
};
|
|
146
|
-
const job = await this.crawl(markdownRequest, actions);
|
|
147
|
-
if (job.status !== constants_1.JobStatus.DONE) {
|
|
148
|
-
throw new errors_1.WebcrawlerApiError('job_not_done', `Job finished with status ${job.status}`, 0);
|
|
149
|
-
}
|
|
150
|
-
if (job.scrape_type !== 'markdown') {
|
|
151
|
-
throw new errors_1.WebcrawlerApiError('invalid_scrape_type', 'CrawlRawMarkdown requires scrape_type to be markdown', 0);
|
|
152
|
-
}
|
|
153
|
-
return this.getJobMarkdownContent(job.id);
|
|
154
|
-
}
|
|
155
165
|
async crawl(crawlRequest, actions) {
|
|
156
166
|
const url = `${this.basePath}/${this.apiVersion}/crawl`;
|
|
157
167
|
const requestBody = {
|
|
168
|
+
output_formats: ['markdown'],
|
|
158
169
|
...crawlRequest,
|
|
159
170
|
actions: actions ? (Array.isArray(actions) ? actions : [actions]) : undefined
|
|
160
171
|
};
|
|
@@ -191,6 +202,7 @@ class WebcrawlerClient {
|
|
|
191
202
|
async crawlAsync(crawlRequest, actions) {
|
|
192
203
|
const url = `${this.basePath}/${this.apiVersion}/crawl`;
|
|
193
204
|
const requestBody = {
|
|
205
|
+
output_formats: ['markdown'],
|
|
194
206
|
...crawlRequest,
|
|
195
207
|
actions: actions ? (Array.isArray(actions) ? actions : [actions]) : undefined
|
|
196
208
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcrawlerapi-js",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "JS client for WebcrawlerAPI",
|
|
5
5
|
"main": "./dist/api.js",
|
|
6
6
|
"types": "./dist/@types/api.d.ts",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"README.md",
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc --outDir dist/"
|
|
14
|
+
},
|
|
12
15
|
"keywords": [
|
|
13
16
|
"scraping",
|
|
14
17
|
"scraper",
|
|
@@ -26,9 +29,6 @@
|
|
|
26
29
|
"license": "MIT",
|
|
27
30
|
"repository": {
|
|
28
31
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/WebCrawlerAPI/webcrawlerapi-js-sdk"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "tsc --outDir dist/"
|
|
32
|
+
"url": "git+https://github.com/WebCrawlerAPI/webcrawlerapi-js-sdk.git"
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|