@zenrows/cli 1.8.0 → 1.10.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 +36 -7
- package/agent-plugin/README.md +1 -1
- package/agent-plugin/skills/cost-control/SKILL.md +2 -2
- package/agent-plugin/skills/extract/SKILL.md +49 -9
- package/agent-plugin/skills/interact-browser/SKILL.md +4 -2
- package/agent-plugin/skills/protected-fetch/SKILL.md +1 -1
- package/agent-plugin/skills/zenrows/SKILL.md +7 -3
- package/dist/src/adapters/extract.js +18 -19
- package/dist/src/cli/commands/extract-prepare.js +168 -0
- package/dist/src/cli/commands/extract.js +21 -13
- package/dist/src/cli/commands/signup.js +42 -7
- package/dist/src/core/agent-account.js +46 -6
- package/dist/src/core/config.js +1 -1
- package/dist/src/core/ensure-key.js +11 -5
- package/dist/src/core/errors.js +3 -0
- package/dist/src/core/http.js +78 -20
- package/dist/src/core/prepared-domains-api.js +289 -0
- package/dist/src/core/signup-challenge.js +114 -0
- package/dist/src/core/workspace.js +1 -0
- package/evals/extract-smoke/README.md +1 -1
- package/package.json +1 -1
- package/registry/capabilities.json +3 -3
- package/registry/skills.json +2 -2
- package/registry/templates.json +1 -1
- package/skills/extract/SKILL.md +50 -6
- package/skills/zenrows/SKILL.md +13 -3
- package/templates/extract-pricing-pages/README.md +3 -5
- package/workflows/competitor-intelligence/WORKFLOW.md +2 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signup challenge: before it creates an account, the signup endpoint may
|
|
3
|
+
* answer 428 with one short natural-language question for the agent running
|
|
4
|
+
* the CLI to answer itself. The CLI never tries to solve it.
|
|
5
|
+
*
|
|
6
|
+
* Each CLI run is its own process, so the pending question is kept in
|
|
7
|
+
* `.zenrows/signup-challenge.json` (no secret: the id, the question, when it
|
|
8
|
+
* expires, and the signup URL that issued it, which is where the answer goes).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { ToolkitError } from "./errors.js";
|
|
13
|
+
import { findWorkspace, readJson, workspacePaths, writeJson } from "./workspace.js";
|
|
14
|
+
/** The command an agent runs to answer the pending question. */
|
|
15
|
+
export const ANSWER_COMMAND = 'zenrows signup --answer "<your answer>"';
|
|
16
|
+
export function challengePath(projectRoot) {
|
|
17
|
+
const ws = projectRoot ? workspacePaths(projectRoot) : (findWorkspace() ?? workspacePaths());
|
|
18
|
+
return join(ws.dir, "signup-challenge.json");
|
|
19
|
+
}
|
|
20
|
+
export function readChallenge(projectRoot) {
|
|
21
|
+
try {
|
|
22
|
+
const c = readJson(challengePath(projectRoot));
|
|
23
|
+
if (!c || typeof c.id !== "string" || typeof c.prompt !== "string" || typeof c.signupUrl !== "string")
|
|
24
|
+
return null;
|
|
25
|
+
return c;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeChallenge(c, projectRoot) {
|
|
32
|
+
writeJson(challengePath(projectRoot), c);
|
|
33
|
+
}
|
|
34
|
+
export function clearChallenge(projectRoot) {
|
|
35
|
+
const file = challengePath(projectRoot);
|
|
36
|
+
if (!existsSync(file))
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(file);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// best effort
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** True when the stored question can no longer be answered. */
|
|
46
|
+
export function challengeExpired(c, now = Date.now()) {
|
|
47
|
+
const t = Date.parse(c.expiresAt);
|
|
48
|
+
return Number.isFinite(t) && t <= now;
|
|
49
|
+
}
|
|
50
|
+
function parseBody(body) {
|
|
51
|
+
try {
|
|
52
|
+
const json = JSON.parse(body);
|
|
53
|
+
return json && typeof json === "object" ? json : null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse a 428 `challenge_required` body into a pending challenge, or null when
|
|
61
|
+
* the body is not one (then the caller reports a generic failure).
|
|
62
|
+
*/
|
|
63
|
+
export function parseChallenge(body, signupUrl, now = Date.now()) {
|
|
64
|
+
const json = parseBody(body);
|
|
65
|
+
if (!json || json.error !== "challenge_required")
|
|
66
|
+
return null;
|
|
67
|
+
const ch = json.challenge;
|
|
68
|
+
if (!ch || typeof ch.id !== "string" || !ch.id || typeof ch.prompt !== "string" || !ch.prompt.trim())
|
|
69
|
+
return null;
|
|
70
|
+
const ttl = typeof ch.expires_in === "number" && ch.expires_in > 0 ? ch.expires_in : 600;
|
|
71
|
+
return { id: ch.id, prompt: ch.prompt.trim(), expiresAt: new Date(now + ttl * 1000).toISOString(), signupUrl };
|
|
72
|
+
}
|
|
73
|
+
/** True when a 428 says the previous answer was wrong or came too late. */
|
|
74
|
+
export function wasWrongAnswer(body) {
|
|
75
|
+
const msg = parseBody(body)?.message;
|
|
76
|
+
return typeof msg === "string" && /^wrong or expired/i.test(msg.trim());
|
|
77
|
+
}
|
|
78
|
+
/** True when a 429 is the per-network lock after too many questions or wrong answers. */
|
|
79
|
+
export function isChallengeLocked(body) {
|
|
80
|
+
return parseBody(body)?.error === "challenge_locked";
|
|
81
|
+
}
|
|
82
|
+
/** The error an agent sees while a question is waiting for its answer. */
|
|
83
|
+
export function challengeError(c, opts = {}) {
|
|
84
|
+
const lead = opts.wrong ? "That answer was wrong or came too late, so here is a new question. " : "";
|
|
85
|
+
return new ToolkitError({
|
|
86
|
+
code: "SIGNUP_CHALLENGE",
|
|
87
|
+
message: `${lead}Zenrows needs one quick answer before it creates an account: ${c.prompt}`,
|
|
88
|
+
likely_cause: "No API key is configured, and new accounts created by agents answer one short question first.",
|
|
89
|
+
next_action: `Answer the question yourself, then run: ${ANSWER_COMMAND}. When it succeeds, re-run your original command.`,
|
|
90
|
+
suggested_commands: [ANSWER_COMMAND, "zenrows login --api-key <your-key>"],
|
|
91
|
+
challenge: { id: c.id, question: c.prompt, expires_at: c.expiresAt },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function challengeLockedError(retryAfter) {
|
|
95
|
+
return new ToolkitError({
|
|
96
|
+
code: "SIGNUP_CHALLENGE_LOCKED",
|
|
97
|
+
message: "Too many signup questions or wrong answers from this network, so Zenrows paused account creation here.",
|
|
98
|
+
likely_cause: "Too many signup questions asked, or wrong answers given, from this network within an hour.",
|
|
99
|
+
next_action: "Try again in an hour by re-running your original command, which asks a new question. If you already have a Zenrows API key, use it now: zenrows login --api-key <key> (or set ZENROWS_API_KEY).",
|
|
100
|
+
suggested_commands: ["zenrows login --api-key <your-key>"],
|
|
101
|
+
...(retryAfter !== undefined ? { retry_after: retryAfter } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function noChallengeError(expired) {
|
|
105
|
+
return new ToolkitError({
|
|
106
|
+
code: "SIGNUP_CHALLENGE_MISSING",
|
|
107
|
+
message: expired ? "The signup question expired before it was answered." : "There is no signup question waiting for an answer.",
|
|
108
|
+
likely_cause: expired
|
|
109
|
+
? "Questions are valid for a few minutes."
|
|
110
|
+
: "No signup was attempted from this directory, or the question was already answered.",
|
|
111
|
+
next_action: "Re-run your original command (for example zenrows fetch <url>) to get a new question, then answer it with: " + ANSWER_COMMAND,
|
|
112
|
+
suggested_commands: ["zenrows fetch <url>"],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -70,6 +70,7 @@ export function ensureGitignore(paths) {
|
|
|
70
70
|
`${WORKSPACE_DIRNAME}/secrets.json`,
|
|
71
71
|
`${WORKSPACE_DIRNAME}/config.json`,
|
|
72
72
|
`${WORKSPACE_DIRNAME}/account.json`,
|
|
73
|
+
`${WORKSPACE_DIRNAME}/signup-challenge.json`,
|
|
73
74
|
`${WORKSPACE_DIRNAME}/runs/`,
|
|
74
75
|
`${WORKSPACE_DIRNAME}/traces/`,
|
|
75
76
|
`${WORKSPACE_DIRNAME}/logs/`,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Reproducible smoke test for Extract (`extract=auto`).
|
|
4
4
|
|
|
5
|
-
- **Target:** `https://www.owler.com/company/meltwater` (
|
|
5
|
+
- **Target:** `https://www.owler.com/company/meltwater` (prepared domain, real company page)
|
|
6
6
|
- **Config:** `extract=auto`
|
|
7
7
|
- **Success criteria:** HTTP 2xx and a non-empty body
|
|
8
8
|
- **Cost:** ~1× basic request
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenrows/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Open-source CLI, MCP, skills, templates, workflows, recipes, and evals layer for giving AI agents reliable access to protected web data through Zenrows cloud infrastructure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
},
|
|
13
13
|
"extract": {
|
|
14
14
|
"key": "extract",
|
|
15
|
-
"label": "Extract (extract=auto /
|
|
15
|
+
"label": "Extract (extract=auto / CSS / Markdown)",
|
|
16
16
|
"status": "available",
|
|
17
17
|
"command": "zenrows extract",
|
|
18
|
-
"backend": "Extract — GET https://api.zenrows.com/v1/ (extract=auto,
|
|
18
|
+
"backend": "Extract — GET https://api.zenrows.com/v1/ (extract=auto, css_extractor, outputs, response_type; autoparse deprecated)",
|
|
19
19
|
"requiresAuth": true,
|
|
20
|
-
"notes": "Default is extract=auto, which works on any prepared domain with any key
|
|
20
|
+
"notes": "Default is extract=auto, which works on any prepared domain with any key. A REQS007 (legacy AUTH010) is reported, never retried, with the next step for its reason: on a domain not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED), prepare it with `zenrows extract prepare <domain> --url <page> [--wait]` (POST /v1/extract/prepared-domains: 1 to 5 example URLs, a few minutes, no credits) and check it with `zenrows extract prepare status <domain>`; on a page Extract did not recognise on a prepared domain (EXTRACT_PAGE_NOT_RECOGNISED), try a product, article or listing page, retry it once in a few minutes, or use --css, because preparing again does not change it. CSS / outputs / markdown work on any domain; autoparse is deprecated and runs only with --autoparse."
|
|
21
21
|
},
|
|
22
22
|
"batch": {
|
|
23
23
|
"key": "batch",
|
package/registry/skills.json
CHANGED
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
{
|
|
26
26
|
"name": "extract",
|
|
27
27
|
"type": "skill",
|
|
28
|
-
"description": "Turn protected pages into structured data with
|
|
28
|
+
"description": "Turn protected pages into structured data with Extract / CSS / Markdown (Autoparse is deprecated).",
|
|
29
29
|
"status": "available",
|
|
30
30
|
"requires_backend_capabilities": ["extract"],
|
|
31
31
|
"requires_auth": true,
|
|
32
32
|
"version": "0.1.0",
|
|
33
33
|
"path": "skills/extract",
|
|
34
|
-
"tags": ["extract", "
|
|
34
|
+
"tags": ["extract", "structured-data"]
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"name": "batch-jobs",
|
package/registry/templates.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"name": "extract-pricing-pages",
|
|
16
16
|
"type": "template",
|
|
17
|
-
"description": "Extract structured pricing fields from product pages (
|
|
17
|
+
"description": "Extract structured pricing fields from product pages (extract=auto / CSS).",
|
|
18
18
|
"status": "available",
|
|
19
19
|
"requires_backend_capabilities": ["extract"],
|
|
20
20
|
"requires_auth": true,
|
package/skills/extract/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: extract
|
|
3
|
-
description: Turn protected pages into structured data with Extract /
|
|
3
|
+
description: Turn protected pages into structured data with Extract / CSS / Markdown (Autoparse is deprecated).
|
|
4
4
|
version: 0.1.0
|
|
5
5
|
requires_backend_capabilities: [extract]
|
|
6
6
|
---
|
|
@@ -12,8 +12,8 @@ access + extraction**, not generic LLM parsing.
|
|
|
12
12
|
|
|
13
13
|
## Methods
|
|
14
14
|
```
|
|
15
|
-
zenrows extract <url> # extract=auto (default)
|
|
16
|
-
zenrows extract <url> --autoparse # general-purpose Autoparse (any domain)
|
|
15
|
+
zenrows extract <url> # extract=auto (default), any prepared domain
|
|
16
|
+
zenrows extract <url> --autoparse # deprecated general-purpose Autoparse (any domain)
|
|
17
17
|
zenrows extract <url> --css '{"title":"h1","price":".price"}' # selector map
|
|
18
18
|
zenrows extract <url> --outputs emails,links # built-in output filters → JSON
|
|
19
19
|
zenrows extract <url> --output markdown # Markdown conversion
|
|
@@ -21,14 +21,58 @@ zenrows extract <url> --validate # fail if not valid JSON
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## When to use which
|
|
24
|
-
- **Extract** (`extract=auto`, default): richest fields; works on any
|
|
25
|
-
|
|
24
|
+
- **Extract** (`extract=auto`, default): richest fields; works on any prepared
|
|
25
|
+
domain (you can prepare one yourself, see below).
|
|
26
|
+
- **Autoparse** (deprecated): only with `--autoparse`. Never automatic; do not
|
|
27
|
+
build new work on it.
|
|
26
28
|
- **CSS**: you know the exact fields/selectors and want determinism.
|
|
27
29
|
- **Markdown/plaintext**: feed clean content to an LLM yourself.
|
|
28
30
|
|
|
31
|
+
## REQS007: prepare the domain, then extract again
|
|
32
|
+
|
|
33
|
+
`extract=auto` works on any prepared domain, and you can prepare one yourself.
|
|
34
|
+
When Extract refuses a page with `403 REQS007`, `zenrows extract` says which
|
|
35
|
+
of three cases it is and prints the next step. It never retries with Autoparse.
|
|
36
|
+
|
|
37
|
+
| Error code | Meaning | Next step |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `EXTRACT_DOMAIN_NOT_PREPARED` | The domain is not prepared yet (also the legacy `402 AUTH010`). | Prepare it (below), then extract again. |
|
|
40
|
+
| `EXTRACT_PAGE_NOT_RECOGNISED` | The domain is prepared, but Extract did not recognise this page as one it can extract (a page with nothing to extract, such as a home page, or a transient failure). | Try a product, article or listing page on the same site instead. If this page should work, retry it once in a few minutes; do not retry in a loop. Otherwise use `--css`. Preparing the domain again does not change this. |
|
|
41
|
+
| `EXTRACT_DOMAIN_NOT_ENABLED` | Extract is not enabled for the site. | Use `--css`, or contact Zenrows support to have the site enabled. Preparing does not change this. |
|
|
42
|
+
|
|
43
|
+
Preparing a domain:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
zenrows extract https://example.com/p/1 # 1. fails: EXTRACT_DOMAIN_NOT_PREPARED
|
|
47
|
+
zenrows extract prepare example.com \
|
|
48
|
+
--url https://example.com/p/1 --url https://example.com/list --wait
|
|
49
|
+
# 2. prepare it and wait until ready (a few minutes)
|
|
50
|
+
zenrows extract https://example.com/p/1 # 3. extract again
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `--url`: 1 to 5 real pages you want to extract, one per page kind (for
|
|
54
|
+
example a product page and a listing page).
|
|
55
|
+
- Without `--wait` the command returns at once; then check about every 30
|
|
56
|
+
seconds with `zenrows extract prepare status example.com` until it says
|
|
57
|
+
`ready`. `--wait` polls at the same pace.
|
|
58
|
+
- The domain is the host: `shop.example.com` and `example.com` are prepared
|
|
59
|
+
separately, and a leading `www.` is dropped.
|
|
60
|
+
- Preparing takes a few minutes and costs no credits. Each new domain uses one
|
|
61
|
+
slot of the account's monthly allowance; `prepare status` shows `used` /
|
|
62
|
+
`limit` / `remaining`.
|
|
63
|
+
- `EXTRACT_PREPARE_OUT_OF_ALLOWANCE`: no slots left this month; use `--css`.
|
|
64
|
+
`EXTRACT_PREPARE_UNAVAILABLE`: retry in a few minutes; do not loop.
|
|
65
|
+
A `failed` status prints the next step for its failure code and says whether
|
|
66
|
+
the attempt used an allowance slot.
|
|
67
|
+
- A domain that is already prepared answers `ready` at once
|
|
68
|
+
(`already_prepared`) and uses no slot: run the extract. If it then fails with
|
|
69
|
+
`EXTRACT_PAGE_NOT_RECOGNISED`, follow that row of the table, not `prepare`.
|
|
70
|
+
|
|
29
71
|
## Rules
|
|
30
72
|
- Validate on a single page before scaling across many URLs.
|
|
31
|
-
-
|
|
73
|
+
- Check `empty` in `--json` output: true means every field came back null or
|
|
74
|
+
empty, so the result is not data.
|
|
75
|
+
- If a field is missing, switch to `--css` with explicit selectors. Auto
|
|
32
76
|
mode already renders JS when the target needs it, so reach for
|
|
33
77
|
`--manual --js-render` only after auto mode has returned an incomplete page:
|
|
34
78
|
it costs several times a basic call ([[cost-control]]) and it makes the
|
package/skills/zenrows/SKILL.md
CHANGED
|
@@ -20,6 +20,13 @@ the toolkit automatically creates a **free, unclaimed Zenrows Free plan account*
|
|
|
20
20
|
key locally — no human step required. To opt out, pass `--no-signup` or set
|
|
21
21
|
`auto_signup: false` in `.zenrows/policy.json`, then use `zenrows login --api-key <key>`.
|
|
22
22
|
|
|
23
|
+
Before it creates the account, Zenrows may ask you one short question (error code
|
|
24
|
+
`SIGNUP_CHALLENGE`, with the question in the message and in `error.challenge.question`).
|
|
25
|
+
Answer it yourself, do not ask the human, then run `zenrows signup --answer "<your answer>"`
|
|
26
|
+
and re-run your original command. A wrong answer returns a new question the same way.
|
|
27
|
+
After too many questions or wrong answers from one network (`SIGNUP_CHALLENGE_LOCKED`), wait an hour or use
|
|
28
|
+
`zenrows login --api-key <key>`.
|
|
29
|
+
|
|
23
30
|
When the Free plan nears its limit (or the user asks), tell the human they can **claim** the
|
|
24
31
|
account to keep their usage and history: run `zenrows account claim` (or open the claim URL
|
|
25
32
|
shown on quota errors). It prints a valid link, requesting a fresh one when the stored link is
|
|
@@ -34,7 +41,7 @@ If the user has a known URL and wants page content:
|
|
|
34
41
|
→ Use Protected Fetch. (zenrows fetch <url>)
|
|
35
42
|
|
|
36
43
|
If the user has a known URL and wants structured data:
|
|
37
|
-
→ Use Extract. (zenrows extract <url> | --
|
|
44
|
+
→ Use Extract. (zenrows extract <url> | --css)
|
|
38
45
|
|
|
39
46
|
If the user has a start page (listing, index, section) but not the item URLs:
|
|
40
47
|
→ Use Crawl. (zenrows crawl create <url> --depth 1 --follow,
|
|
@@ -75,13 +82,16 @@ Run `zenrows status` for the live capability matrix. As of this toolkit:
|
|
|
75
82
|
| Primitive | Command | Status |
|
|
76
83
|
| --- | --- | --- |
|
|
77
84
|
| Protected Fetch | `zenrows fetch` | **available**: `GET https://api.zenrows.com/v1/` |
|
|
78
|
-
| Extract (`extract=auto`,
|
|
85
|
+
| Extract (`extract=auto`, CSS, Markdown; Autoparse deprecated) | `zenrows extract` | **available**: `GET https://api.zenrows.com/v1/?extract=auto` |
|
|
79
86
|
| Batch | `zenrows batch` | **available**: `POST https://async.api.zenrows.com/v1/jobs` |
|
|
80
87
|
| Crawl (new) | `zenrows crawl` | **available**: `POST https://api.zenrows.com/v1/crawls` |
|
|
81
88
|
| Browser | `zenrows browser` | **available**: `POST https://mcp.zenrows.com/browser/sessions` |
|
|
82
89
|
| MCP | `zenrows mcp` | **available**: `https://mcp.zenrows.com/mcp` |
|
|
83
90
|
|
|
84
|
-
- Extract: `extract=auto`
|
|
91
|
+
- Extract: `extract=auto` works on any prepared domain. On one not prepared yet
|
|
92
|
+
(`EXTRACT_DOMAIN_NOT_PREPARED`), run `zenrows extract prepare <domain> --url
|
|
93
|
+
<page> --wait`, then extract again. `EXTRACT_PAGE_NOT_RECOGNISED` means the
|
|
94
|
+
domain is prepared but not this page: do not prepare again; see [[extract]].
|
|
85
95
|
- Batch: the reliable path at scale, at Fetch's prices (create, list, status, wait, results, content, pause, resume, rerun, delete). Estimate specs locally with no key.
|
|
86
96
|
- Crawl: collects the URLs behind one start page, optionally with each page's HTML.
|
|
87
97
|
- Browser: escalation-only, for pages Fetch can't handle; same backend as the MCP `browser_*` tools.
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
Extract structured pricing fields from product pages with Zenrows Extract.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
# Default: extract=auto on
|
|
6
|
+
# Default: extract=auto on a prepared domain (site-tailored JSON).
|
|
7
|
+
# Not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED)? zenrows extract prepare <domain> --url <product-url> --wait
|
|
7
8
|
zenrows extract https://www.owler.com/company/meltwater
|
|
8
9
|
|
|
9
|
-
#
|
|
10
|
-
zenrows extract https://www.scrapingcourse.com/ecommerce/ --autoparse
|
|
11
|
-
|
|
12
|
-
# Or a precise CSS selector map
|
|
10
|
+
# A precise CSS selector map, on any domain
|
|
13
11
|
zenrows extract <product-url> --css '{"title":"h1","price":".price"}' --validate
|
|
14
12
|
```
|
|
15
13
|
|
|
@@ -9,7 +9,7 @@ status: available
|
|
|
9
9
|
|
|
10
10
|
A higher-level process built on Zenrows **Fetch and Extract**: retrieve
|
|
11
11
|
protected pages with `zenrows fetch`, then turn them into structured data with
|
|
12
|
-
`zenrows extract` (the same API's
|
|
12
|
+
`zenrows extract` (the same API's `extract=auto` / CSS / Markdown — not a separate
|
|
13
13
|
product). Scheduling and scaling use the tools you already have (cron/CI, a
|
|
14
14
|
simple per-URL loop).
|
|
15
15
|
|
|
@@ -20,7 +20,7 @@ simple per-URL loop).
|
|
|
20
20
|
```
|
|
21
21
|
zenrows fetch https://competitor.example/products
|
|
22
22
|
```
|
|
23
|
-
2. **Validate extraction** on one page (
|
|
23
|
+
2. **Validate extraction** on one page (on a domain not prepared yet, `EXTRACT_DOMAIN_NOT_PREPARED`, run `zenrows extract prepare <domain> --url <page> --wait` first).
|
|
24
24
|
```
|
|
25
25
|
zenrows extract https://competitor.example/p/123 --validate
|
|
26
26
|
```
|