@opencraw/mcp 0.1.45 → 0.1.48
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 -1
- package/dist/diff-tool/diff-tool.handler.d.ts +42 -0
- package/dist/diff-tool/index.d.ts +3 -0
- package/dist/docs-tool/docs-folder.repository.d.ts +24 -0
- package/dist/docs-tool/docs-root.client.d.ts +8 -0
- package/dist/docs-tool/docs-tool.handler.d.ts +21 -0
- package/dist/docs-tool/docs-topics.algorithm.d.ts +39 -0
- package/dist/docs-tool/index.d.ts +3 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.esm.js +517 -1
- package/dist/list-tool/index.d.ts +3 -0
- package/dist/list-tool/list-recipes-tool.handler.d.ts +32 -0
- package/dist/main.d.ts +7 -0
- package/dist/probe-tool/index.d.ts +2 -0
- package/dist/probe-tool/probe-tool.handler.d.ts +27 -0
- package/dist/recipe-source/index.d.ts +3 -0
- package/dist/recipe-source/recipe-source.mapper.d.ts +21 -0
- package/dist/run-tool/index.d.ts +3 -0
- package/dist/run-tool/run-tool.handler.d.ts +51 -0
- package/dist/server/create-server.use-case.d.ts +11 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/src/docs-tool/docs-folder.repository.d.ts +24 -0
- package/dist/src/docs-tool/docs-root.client.d.ts +8 -0
- package/dist/src/docs-tool/docs-tool.handler.d.ts +21 -0
- package/dist/src/docs-tool/docs-topics.algorithm.d.ts +39 -0
- package/dist/src/docs-tool/index.d.ts +3 -0
- package/dist/src/server/create-server.use-case.d.ts +1 -1
- package/dist/src/studio-tool/index.d.ts +2 -0
- package/dist/src/studio-tool/studio-command.client.d.ts +11 -0
- package/dist/src/studio-tool/studio-instance.repository.d.ts +22 -0
- package/dist/src/studio-tool/studio-tools.handler.d.ts +68 -0
- package/dist/studio-tool/index.d.ts +2 -0
- package/dist/studio-tool/studio-command.client.d.ts +11 -0
- package/dist/studio-tool/studio-instance.repository.d.ts +22 -0
- package/dist/studio-tool/studio-tools.handler.d.ts +68 -0
- package/dist/validate-tool/index.d.ts +3 -0
- package/dist/validate-tool/validate-tool.handler.d.ts +31 -0
- package/docs/how-it-works/01-recipes.md +698 -0
- package/docs/how-it-works/02-page-steps.md +672 -0
- package/docs/how-it-works/03-data-steps.md +813 -0
- package/docs/how-it-works/04-flow-steps.md +738 -0
- package/docs/how-it-works/05-templates-and-scope.md +428 -0
- package/docs/how-it-works/06-documents.md +1413 -0
- package/docs/how-it-works/07-mapping-and-transforms.md +699 -0
- package/docs/how-it-works/08-policies.md +405 -0
- package/docs/how-it-works/09-running.md +804 -0
- package/docs/how-it-works/README.md +225 -0
- package/docs/recipes/README.md +22 -0
- package/docs/recipes/access.md +440 -0
- package/docs/recipes/authoring.md +1503 -0
- package/docs/recipes/azure-durable.md +180 -0
- package/docs/recipes/captcha.md +204 -0
- package/docs/recipes/movies-example.md +12 -0
- package/docs/recipes/programmatic-authoring.md +101 -0
- package/docs/recipes/quick-guide.md +157 -0
- package/docs/recipes/vehicles-example.md +9 -0
- package/docs/recipes/worker-mode.md +221 -0
- package/docs/studio/manual.md +314 -0
- package/docs/studio/pdf-agreements.md +150 -0
- package/package.json +29 -4
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Page steps
|
|
6
|
+
|
|
7
|
+
Part 3 of [How OpenCraw works](README.md). Next: [4. Data steps](03-data-steps.md).
|
|
8
|
+
|
|
9
|
+
A `web` recipe drives a real browser. Its page steps do what a person does: open a page, type, press a key,
|
|
10
|
+
click, pick from a list, scroll, wait. This page shows each of them on a real page, with the elements the step
|
|
11
|
+
acts on outlined, the trace lines it produces and what it binds. The fields of every step are in the reference,
|
|
12
|
+
[authoring.md §3.1](../recipes/authoring.md#31-web-steps-browser-page); this page explains what happens when
|
|
13
|
+
they run.
|
|
14
|
+
|
|
15
|
+
## What web mode is underneath
|
|
16
|
+
|
|
17
|
+
The engine runs web steps with [Playwright](https://playwright.dev). Three objects matter:
|
|
18
|
+
|
|
19
|
+
- **The browser.** One per crawler, launched the first time a recipe needs it (headless Chromium unless the
|
|
20
|
+
crawler's `browser` options say otherwise), and launched again if it crashes. `crawler.close()` closes it.
|
|
21
|
+
- **The context.** A browser context is a private profile: its own cookies, storage and cache. Each recipe run
|
|
22
|
+
gets **a new context with one page (tab)** when it starts, before its first start point. Every start point
|
|
23
|
+
of that run uses the same page, so a login made on the first start point is still there on the second. When
|
|
24
|
+
the run ends, the context is closed, and everything in it is gone.
|
|
25
|
+
- **The page.** Every step acts on that one tab. The browser moves from page to page on it, the way a person's
|
|
26
|
+
tab does.
|
|
27
|
+
|
|
28
|
+
When a context opens, and which:
|
|
29
|
+
|
|
30
|
+
| Situation | What opens |
|
|
31
|
+
|---|---|
|
|
32
|
+
| A `web` recipe run starts | A new context and page on the shared browser, with the recipe's `session` (cookies, headers, user agent, viewport) and access (proxy). |
|
|
33
|
+
| `session.bootstrap` | A throwaway context of its own, closed once the bootstrap ran; only what `keep` lists is carried into the run's context ([part 10](09-running.md#2-sessions)). |
|
|
34
|
+
| `session.browserProfile` | A persistent context in the profile's folder instead: cookies and storage survive between runs. |
|
|
35
|
+
| The access lease is a remote browser (`cdp`) | The provider's own context, over the DevTools protocol. |
|
|
36
|
+
| A block with `onBlock.rotate` | The context is closed and a new one opened with a new lease; the bootstrap runs again. |
|
|
37
|
+
| `forEach` with `limits.concurrency` above 1 | More tabs **in the same context**, one per iteration running at once: they share the cookies. |
|
|
38
|
+
|
|
39
|
+
An `api` recipe opens no browser for its steps: it has an HTTP client instead, and page steps are refused when
|
|
40
|
+
the recipes are loaded. Only its bootstrap runs in a browser.
|
|
41
|
+
|
|
42
|
+
After **every** web step, whatever it was, the engine reads the tab's URL and, if it changed, writes it to
|
|
43
|
+
`page.url`. A click or a key press that navigates therefore keeps `page.url` right. `page.number` is not
|
|
44
|
+
touched: it is the pagination counter, and only `paginate` advances it.
|
|
45
|
+
|
|
46
|
+
### Timeouts
|
|
47
|
+
|
|
48
|
+
Each step waits for something, and not every step takes its limit from the same place:
|
|
49
|
+
|
|
50
|
+
| Step | How long it waits |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `goto` | `limits.timeoutMs`, else the browser's default |
|
|
53
|
+
| `wait` (`selector`, `state`) | the step's `timeoutMs`, else `limits.timeoutMs`, else the browser's default |
|
|
54
|
+
| `select` with `values`, `multiple`, `force` or `search` | the step's `timeoutMs`, else `limits.timeoutMs`, else 30 s |
|
|
55
|
+
| `click` with `download` | `download.timeoutMs`, else `limits.timeoutMs`, else 30 s |
|
|
56
|
+
| `click` with `optional: true` | 2 s for the element to show, then the step does nothing |
|
|
57
|
+
| `click`, `fill`, `press`, a plain `select` | the browser's default only: the crawler's `browser.timeoutMs`, else Playwright's 30 s |
|
|
58
|
+
|
|
59
|
+
The last row is the one that surprises: `limits.timeoutMs` does not bound a click or a fill in a local browser.
|
|
60
|
+
|
|
61
|
+
### `extract`: the live page, or a fragment
|
|
62
|
+
|
|
63
|
+
In web mode an `extract` reads one of two things:
|
|
64
|
+
|
|
65
|
+
- **The live page**, when it has no `from`: `css` and `xpath` go through Playwright, one round trip that reads
|
|
66
|
+
every match at once; `regex` and `table` read the page's current HTML. This is the DOM as it is now, after
|
|
67
|
+
scripts ran and after every click.
|
|
68
|
+
- **A value in scope**, when it has `from` (an HTML fragment an earlier extract took, a downloaded file) or when
|
|
69
|
+
it is `jsonpath` (the JSON the last `request` fetched): the same reader api mode uses, with no browser.
|
|
70
|
+
|
|
71
|
+
Neither waits. Playwright's actions (`click`, `fill`) wait for their element; an `extract` does not. If the
|
|
72
|
+
elements are not on the page yet, a single `extract` fails and a `many` one binds `[]`. [`wait`](#wait) shows
|
|
73
|
+
it on a real page. [Part 4](03-data-steps.md#which-engine-reads-the-live-page-or-a-document) covers the two
|
|
74
|
+
readers in depth.
|
|
75
|
+
|
|
76
|
+
## `goto`
|
|
77
|
+
|
|
78
|
+
`goto` opens a URL in the tab. Before it loads the page, it resolves the URL against the scope's `page.url`,
|
|
79
|
+
so `"js"` and `"/"` are relative links. After it loads, `page.url` is **where the browser ended up**, after
|
|
80
|
+
redirects, and it emits a `page:visit` event: the `⇢` line in the trace.
|
|
81
|
+
|
|
82
|
+
The recipe [`goto-pages`](recipes/page-goto/goto-pages.input.json) opens three targets from
|
|
83
|
+
[quotes.toscrape.com](https://quotes.toscrape.com), one per `forEach` iteration, and emits where each landed:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{ "type": "set", "id": "targets", "value": ["/", "js", "does-not-exist"] },
|
|
87
|
+
{ "type": "forEach", "over": "targets", "as": "target", "emit": true, "steps": [
|
|
88
|
+
{ "type": "goto", "url": "{{target}}" },
|
|
89
|
+
{ "type": "extract", "id": "heading", "selector": "h1", "kind": "css", "take": "text" }
|
|
90
|
+
]}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
<!-- capture:page-goto records n=3 -->
|
|
94
|
+
```json
|
|
95
|
+
{"asked":"/","url":"https://quotes.toscrape.com/","number":1,"heading":"Quotes to Scrape"}
|
|
96
|
+
{"asked":"js","url":"http://quotes.toscrape.com/js/","number":1,"heading":"Quotes to Scrape"}
|
|
97
|
+
{"asked":"does-not-exist","url":"https://quotes.toscrape.com/does-not-exist","number":1,"heading":"Not Found"}
|
|
98
|
+
```
|
|
99
|
+
<!-- /capture -->
|
|
100
|
+
|
|
101
|
+
<!-- capture:page-goto trace lines=16 -->
|
|
102
|
+
```text
|
|
103
|
+
▶ goto-pages (web)
|
|
104
|
+
⇄ access direct (direct)
|
|
105
|
+
· steps.0 set targets … ms
|
|
106
|
+
⇢ page 1 https://quotes.toscrape.com/
|
|
107
|
+
· steps.1.steps.0 goto … ms
|
|
108
|
+
· steps.1.steps.1 extract heading … ms
|
|
109
|
+
✚ record ["https://quotes.toscrape.com/"]
|
|
110
|
+
⇢ page 1 http://quotes.toscrape.com/js/
|
|
111
|
+
· steps.1.steps.0 goto … ms
|
|
112
|
+
· steps.1.steps.1 extract heading … ms
|
|
113
|
+
✚ record ["http://quotes.toscrape.com/js/"]
|
|
114
|
+
⇢ page 1 https://quotes.toscrape.com/does-not-exist [404]
|
|
115
|
+
· steps.1.steps.0 goto … ms
|
|
116
|
+
· steps.1.steps.1 extract heading … ms
|
|
117
|
+
✚ record ["https://quotes.toscrape.com/does-not-exist"]
|
|
118
|
+
· steps.1 forEach … ms
|
|
119
|
+
…
|
|
120
|
+
```
|
|
121
|
+
<!-- /capture -->
|
|
122
|
+
|
|
123
|
+
What the three records show:
|
|
124
|
+
|
|
125
|
+
- **Redirects.** `https://quotes.toscrape.com/js` answers with a redirect to `http://quotes.toscrape.com/js/`.
|
|
126
|
+
`page.url` is the final URL, scheme change and trailing slash included. A later relative `goto`, a
|
|
127
|
+
`request` and the `absoluteUrl` transform all resolve against it.
|
|
128
|
+
- **Relative to the scope, not to the tab.** `does-not-exist` resolved against `https://quotes.toscrape.com/`,
|
|
129
|
+
not against the `http://…/js/` the tab was showing. Each iteration runs in its own child scope, and the
|
|
130
|
+
`goto` writes its page there; when the iteration ends, that page state goes with it, and the next iteration
|
|
131
|
+
starts again from the parent's `page.url`, the start URL.
|
|
132
|
+
- **`page.number` stays 1.** Opening a page is not turning one. Only `paginate` counts pages.
|
|
133
|
+
- **A 404 is a page.** The third `goto` succeeded: the trace marks the visit `[404]`, and the `extract` read the
|
|
134
|
+
error page's heading.
|
|
135
|
+
|
|
136
|
+
<!-- capture:page-goto screenshot alt=The_404_page_the_third_goto_opened,_with_what_the_extract_read -->
|
|
137
|
+

|
|
138
|
+
<!-- /capture -->
|
|
139
|
+
|
|
140
|
+
A `goto` fails on a response only when the recipe's block rule says it is a block (by default 403, 429 or an
|
|
141
|
+
AWS WAF challenge: [part 10](09-running.md#3-access-proxies-and-blocks)). A 408, 425, 429, 500, 502, 503 or
|
|
142
|
+
504, or a connection that fails in passing, is loaded again first, after a pause (the `↺` lines of a trace).
|
|
143
|
+
When the tries run out, the last answer is the page: a 503 error page loads like a 404 one, and a 429 counts
|
|
144
|
+
as a block. That is what a browser does, and a site's "not found" page is sometimes the data you want. When it
|
|
145
|
+
is not, check for an element the real page has.
|
|
146
|
+
|
|
147
|
+
A `request` is different: it is a data fetch, and a 4xx or 5xx fails it. The second recipe,
|
|
148
|
+
[`request-missing`](recipes/page-goto/request-missing.input.json), fetches the same missing page with a
|
|
149
|
+
`request` from the start page:
|
|
150
|
+
|
|
151
|
+
<!-- capture:page-goto summary -->
|
|
152
|
+
```text
|
|
153
|
+
goto-pages: 3 emitted, 0 rejected, 0 duplicates, 3 pages
|
|
154
|
+
request-missing: 0 emitted, 0 rejected, 0 duplicates, 2 pages, stopped: step steps.1 (request) failed: HTTP 404 for https://quotes.toscrape.com/does-not-exist
|
|
155
|
+
```
|
|
156
|
+
<!-- /capture -->
|
|
157
|
+
|
|
158
|
+
The run stopped at the `request`. The failed fetch still counts as a page (`2 pages`): the engine reports the
|
|
159
|
+
visit, with its status, before it fails the step.
|
|
160
|
+
|
|
161
|
+
Two more options change when `goto` returns. `waitUntil` picks the load event it waits for: `load` (the
|
|
162
|
+
default: the page and its images, scripts and stylesheets), `domcontentloaded` (the HTML parsed), `networkidle`
|
|
163
|
+
(no requests for 500 ms) or `commit` (the response started). `ready: { selector }` is for a site that sometimes
|
|
164
|
+
serves its shell without the content: the page is loaded again, up to `reloads` times (default 2), until the
|
|
165
|
+
element shows.
|
|
166
|
+
|
|
167
|
+
## `fill`, `press`, `click` and `wait`: a login
|
|
168
|
+
|
|
169
|
+
quotes.toscrape.com has a login form that accepts any name and password, and shows each author's Goodreads link
|
|
170
|
+
only to a logged-in visitor. The recipe [`login`](recipes/page-login/login.input.json) logs in, moves to page
|
|
171
|
+
2, and reads those links:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{ "type": "goto", "url": "{{start.url}}" },
|
|
175
|
+
{ "type": "fill", "selector": "#username", "value": "{{vars.user}}" },
|
|
176
|
+
{ "type": "fill", "selector": "#password", "value": "{{vars.password}}" },
|
|
177
|
+
{ "type": "press", "selector": "#password", "key": "Enter" },
|
|
178
|
+
{ "type": "wait", "selector": "a[href='/logout']" },
|
|
179
|
+
{ "type": "click", "selector": "li.next a" },
|
|
180
|
+
{ "type": "wait", "selector": ".quote" },
|
|
181
|
+
{ "type": "screenshot", "path": "docs/assets/how-it-works/page-login-step.png" },
|
|
182
|
+
{ "type": "extract", "id": "quotes", "selector": ".quote", "kind": "css", "take": "html", "many": true },
|
|
183
|
+
{ "type": "forEach", "over": "quotes", "as": "quote", "emit": true, "steps": ["…text, author, goodreads…"] }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
<!-- capture:page-login screenshot n=1 alt=The_login_form:_the_two_fields_filled,_and_the_button_Enter_submits -->
|
|
187
|
+

|
|
188
|
+
<!-- /capture -->
|
|
189
|
+
|
|
190
|
+
- **`fill`** finds the first element matching `selector`, waits until it can be typed into, clears it and sets
|
|
191
|
+
the value (a template: `{{vars.user}}` becomes `scraper`). It fires the `input` event, as typing does, but it
|
|
192
|
+
types nothing key by key; a widget that listens for key presses needs `press`.
|
|
193
|
+
- **`press`** sends one key (`Enter`, `Tab`, `ArrowDown`, `Control+a`) to the element, or to the page when the
|
|
194
|
+
step has no `selector`. Enter in a form field submits the form, as it does for a person.
|
|
195
|
+
- **`wait`** here is what makes the next step safe. A `press` or a `click` returns once the navigation it
|
|
196
|
+
started is under way (Playwright waits for the new page to commit), not once that page has loaded. Waiting for an element only the logged-in page has
|
|
197
|
+
(the Logout link) proves the login worked, and fails the run with a clear timeout if it didn't.
|
|
198
|
+
- **`click`** clicks the first match, after Playwright checks it is visible, stable and not covered. The "Next"
|
|
199
|
+
link navigates, so a `wait` follows it too.
|
|
200
|
+
|
|
201
|
+
<!-- capture:page-login screenshot n=2 alt=After_the_login:_the_Logout_link_the_wait_looks_for,_and_the_Goodreads_links -->
|
|
202
|
+

|
|
203
|
+
<!-- /capture -->
|
|
204
|
+
|
|
205
|
+
<!-- capture:page-login screenshot n=3 alt=The_link_the_click_follows -->
|
|
206
|
+

|
|
207
|
+
<!-- /capture -->
|
|
208
|
+
|
|
209
|
+
None of these steps binds a value, so the trace shows them without an id. There are three `⇢` lines: the
|
|
210
|
+
`goto`'s, then one for the navigation the key press caused (the form posts, and the site redirects to `/`) and one
|
|
211
|
+
for the click's (page 2). A navigation a `click`, `press` or `select` causes is followed like a `goto`: the engine
|
|
212
|
+
waits for its response and `load`, retries a failure in passing, applies the block rule, emits `page:visit` and
|
|
213
|
+
counts the page, so the run reports `3 pages`. Its `⇢` line comes before the step's own `·` line, which is written
|
|
214
|
+
when the step ends. `page.number` stays 1 throughout: only `paginate` moves it.
|
|
215
|
+
|
|
216
|
+
<!-- capture:page-login trace lines=12 grep=^(?!.*↺) -->
|
|
217
|
+
```text
|
|
218
|
+
▶ login (web)
|
|
219
|
+
⇄ access direct (direct)
|
|
220
|
+
⇢ page 1 https://quotes.toscrape.com/login
|
|
221
|
+
· steps.0 goto … ms
|
|
222
|
+
· steps.1 fill … ms
|
|
223
|
+
· steps.2 fill … ms
|
|
224
|
+
⇢ page 1 https://quotes.toscrape.com/
|
|
225
|
+
· steps.3 press … ms
|
|
226
|
+
· steps.4 wait … ms
|
|
227
|
+
⇢ page 1 https://quotes.toscrape.com/page/2/
|
|
228
|
+
· steps.5 click … ms
|
|
229
|
+
· steps.6 wait … ms
|
|
230
|
+
…
|
|
231
|
+
```
|
|
232
|
+
<!-- /capture -->
|
|
233
|
+
|
|
234
|
+
The records carry the Goodreads links, and `page.url` followed the click to page 2 while `page.number` stayed 1:
|
|
235
|
+
|
|
236
|
+
<!-- capture:page-login records n=3 -->
|
|
237
|
+
```json
|
|
238
|
+
{"text":"“This life is what you make it. No matter what, you're going to mess up sometimes, it's a universal truth. But the good part is you get to decide how you're …","author":"Marilyn Monroe","goodreads":"http://goodreads.com/author/show/82952.Marilyn_Monroe","pageUrl":"https://quotes.toscrape.com/page/2/","page":1}
|
|
239
|
+
{"text":"“It takes a great deal of bravery to stand up to our enemies, but just as much to stand up to our friends.”","author":"J.K. Rowling","goodreads":"http://goodreads.com/author/show/1077326.J_K_Rowling","pageUrl":"https://quotes.toscrape.com/page/2/","page":1}
|
|
240
|
+
{"text":"“If you can't explain it to a six year old, you don't understand it yourself.”","author":"Albert Einstein","goodreads":"http://goodreads.com/author/show/9810.Albert_Einstein","pageUrl":"https://quotes.toscrape.com/page/2/","page":1}
|
|
241
|
+
```
|
|
242
|
+
<!-- /capture -->
|
|
243
|
+
|
|
244
|
+
`selector` in these steps is a CSS selector (Playwright also takes `text=…` and `xpath=…`). It is never a
|
|
245
|
+
template: a selector that depends on a value goes in `target` instead (`"target": "#row-{{item.id}}"`), which
|
|
246
|
+
also takes a live element from a `forEach` over `selector` ([authoring.md §3.7](../recipes/authoring.md#37-live-elements-driving-a-configurator)).
|
|
247
|
+
`click` with `optional: true` waits 2 s for the element and does nothing if it never shows: a cookie banner
|
|
248
|
+
that appears on some visits only.
|
|
249
|
+
|
|
250
|
+
A login usually belongs in `session.bootstrap` rather than in the steps: it runs once before the crawl, and an
|
|
251
|
+
`api` recipe can use the cookies it leaves ([part 10](09-running.md#2-sessions)). In the steps, as here, it runs
|
|
252
|
+
again for every start point.
|
|
253
|
+
|
|
254
|
+
## `click` with `download`: a file the page hands over
|
|
255
|
+
|
|
256
|
+
Some pages give their data as a file behind a button: "Export to Excel", "Download CSV". `click` with
|
|
257
|
+
`download` clicks, catches the file the browser receives, and reads it like a fetched document: CSV,
|
|
258
|
+
spreadsheet, PDF, Word, JSON and the rest, by `as` or by the file's name. The step's `id` binds the result,
|
|
259
|
+
and it becomes the current document.
|
|
260
|
+
|
|
261
|
+
None of the practice sites has an export button, so [`export`](recipes/page-download/export.input.json) makes
|
|
262
|
+
one: an `evaluate` puts a link on the page to a CSV of the ten quotes it shows (the script builds the file in
|
|
263
|
+
the page, and the link carries `download="quotes.csv"`). From there on, the recipe is what it would be on a site
|
|
264
|
+
with a real button:
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{ "type": "goto", "url": "{{start.url}}" },
|
|
268
|
+
{ "type": "evaluate", "id": "rowsWritten", "script": "(() => { …adds a#export, returns the row count… })()" },
|
|
269
|
+
{ "type": "click", "id": "export", "selector": "#export", "download": {} },
|
|
270
|
+
{ "type": "extract", "id": "table", "from": "export", "kind": "table", "selector": "^text author$",
|
|
271
|
+
"columns": { "text": "^text$", "author": "^author$" } },
|
|
272
|
+
{ "type": "set", "id": "rows", "value": "{{table.rows}}" },
|
|
273
|
+
{ "type": "forEach", "over": "rows", "as": "row", "emit": true, "steps": [] }
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
<!-- capture:page-download screenshot alt=The_page_the_script_turns_into_a_CSV,_and_where_it_puts_the_link -->
|
|
277
|
+

|
|
278
|
+
<!-- /capture -->
|
|
279
|
+
|
|
280
|
+
What the click bound: the CSV, read into a workbook with one sheet named after the file.
|
|
281
|
+
|
|
282
|
+
<!-- capture:page-download scope ids=rowsWritten,export -->
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"rowsWritten": 10,
|
|
286
|
+
"export": {
|
|
287
|
+
"kind": "workbook",
|
|
288
|
+
"sheets": [
|
|
289
|
+
{
|
|
290
|
+
"name": "quotes",
|
|
291
|
+
"rows": [
|
|
292
|
+
[
|
|
293
|
+
"text",
|
|
294
|
+
"author"
|
|
295
|
+
],
|
|
296
|
+
[
|
|
297
|
+
"“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
|
|
298
|
+
"Albert Einstein"
|
|
299
|
+
],
|
|
300
|
+
[
|
|
301
|
+
"“It is our choices, Harry, that show what we truly are, far more than our abilities.”",
|
|
302
|
+
"J.K. Rowling"
|
|
303
|
+
],
|
|
304
|
+
"… 8 more"
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
],
|
|
308
|
+
"csv": {
|
|
309
|
+
"encoding": "utf-8",
|
|
310
|
+
"delimiter": ","
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
<!-- /capture -->
|
|
316
|
+
|
|
317
|
+
<!-- capture:page-download trace lines=9 -->
|
|
318
|
+
```text
|
|
319
|
+
▶ export (web)
|
|
320
|
+
⇄ access direct (direct)
|
|
321
|
+
⇢ page 1 https://quotes.toscrape.com/
|
|
322
|
+
· steps.0 goto … ms
|
|
323
|
+
· steps.1 evaluate rowsWritten … ms
|
|
324
|
+
· steps.2 click export … ms
|
|
325
|
+
· steps.3 extract table … ms
|
|
326
|
+
· steps.4 set rows … ms
|
|
327
|
+
✚ record ["“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”"]
|
|
328
|
+
…
|
|
329
|
+
```
|
|
330
|
+
<!-- /capture -->
|
|
331
|
+
|
|
332
|
+
The `extract` has `from: "export"`, and it must. In web mode an `extract` without `from` reads the live page,
|
|
333
|
+
even right after a download, so a `table` extract would look for an HTML table on the quotes page. The
|
|
334
|
+
download is only read when named. `download.saveTo` (a template) keeps a copy of the file; `download.timeoutMs`
|
|
335
|
+
bounds the wait for it to start.
|
|
336
|
+
|
|
337
|
+
## `select`: a form that posts itself back
|
|
338
|
+
|
|
339
|
+
[quotes.toscrape.com/search.aspx](https://quotes.toscrape.com/search.aspx) works like an old ASP.NET form. The
|
|
340
|
+
author list has `onchange="__doPostBack()"`: picking an author submits the whole form, with a hidden
|
|
341
|
+
`__VIEWSTATE` field that carries the page's state, and the server answers with a new page whose tag list holds
|
|
342
|
+
that author's tags. Only then can a tag be picked and the search run.
|
|
343
|
+
|
|
344
|
+
<!-- capture:page-select screenshot n=1 alt=The_search_form:_an_author_list,_and_a_tag_list_with_no_tags_yet -->
|
|
345
|
+

|
|
346
|
+
<!-- /capture -->
|
|
347
|
+
|
|
348
|
+
The recipe, [`search`](recipes/page-select/search.input.json):
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{ "type": "goto", "url": "{{start.url}}" },
|
|
352
|
+
{ "type": "select", "selector": "#author", "value": "{{vars.author}}" },
|
|
353
|
+
{ "type": "select", "selector": "#tag", "values": ["{{vars.tag}}"] },
|
|
354
|
+
{ "type": "click", "selector": "input[name=submit_button]" },
|
|
355
|
+
{ "type": "wait", "selector": ".results .quote" },
|
|
356
|
+
{ "type": "extract", "id": "quotes", "selector": ".results .quote", "kind": "css", "take": "html", "many": true },
|
|
357
|
+
{ "type": "forEach", "over": "quotes", "as": "quote", "emit": true, "steps": ["…text, author, tag…"] }
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The two `select` steps take the two paths the step has:
|
|
361
|
+
|
|
362
|
+
- **One `value`, `label` or `index`** goes straight to Playwright's `selectOption`, which picks the option as
|
|
363
|
+
a person does and fires `input` and `change`. The page's `change` handler posts the form back. The step does
|
|
364
|
+
not wait for the new page.
|
|
365
|
+
- **`values`** (or `multiple`, `force`, `search`) resolves each wanted entry to an option first, by value, then
|
|
366
|
+
by label, and **waits until every one exists**, checking every 250 ms, up to `timeoutMs`. That is what the
|
|
367
|
+
tag list needs: its options arrive with the page the postback brings back. When the time is up, the step
|
|
368
|
+
fails and names what matched nothing, with the first options it did find.
|
|
369
|
+
|
|
370
|
+
<!-- capture:page-select screenshot n=2 alt=After_the_postback:_the_author_kept,_and_the_author's_tags_in_the_tag_list -->
|
|
371
|
+

|
|
372
|
+
<!-- /capture -->
|
|
373
|
+
|
|
374
|
+
Each postback is a navigation to `/filter.aspx`, and after each step the engine re-reads the URL:
|
|
375
|
+
|
|
376
|
+
<!-- capture:page-select scope ids=page,text,author,tag -->
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"page": {
|
|
380
|
+
"url": "https://quotes.toscrape.com/filter.aspx",
|
|
381
|
+
"number": 1
|
|
382
|
+
},
|
|
383
|
+
"text": "“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”",
|
|
384
|
+
"author": "Albert Einstein",
|
|
385
|
+
"tag": "life"
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
<!-- /capture -->
|
|
389
|
+
|
|
390
|
+
<!-- capture:page-select screenshot n=3 alt=The_results_the_search_returned,_which_the_wait_and_the_extract_read -->
|
|
391
|
+

|
|
392
|
+
<!-- /capture -->
|
|
393
|
+
|
|
394
|
+
<!-- capture:page-select trace lines=9 -->
|
|
395
|
+
```text
|
|
396
|
+
▶ search (web)
|
|
397
|
+
⇄ access direct (direct)
|
|
398
|
+
⇢ page 1 https://quotes.toscrape.com/search.aspx
|
|
399
|
+
· steps.0 goto … ms
|
|
400
|
+
⇢ page 1 https://quotes.toscrape.com/filter.aspx
|
|
401
|
+
· steps.1 select … ms
|
|
402
|
+
· steps.2 select … ms
|
|
403
|
+
⇢ page 1 https://quotes.toscrape.com/filter.aspx
|
|
404
|
+
· steps.3 click … ms
|
|
405
|
+
…
|
|
406
|
+
```
|
|
407
|
+
<!-- /capture -->
|
|
408
|
+
|
|
409
|
+
The browser carries the ViewState from page to page without the recipe seeing it, which is the reason to drive
|
|
410
|
+
this form in a browser at all. A `request` with `form` (web mode, [part 4](03-data-steps.md#a-form-post-from-a-web-page))
|
|
411
|
+
can post the same form from the page instead, ViewState included, when the result is easier to read as a
|
|
412
|
+
response than as the next page.
|
|
413
|
+
|
|
414
|
+
The other options, for pages that hide their `<select>`:
|
|
415
|
+
|
|
416
|
+
| Option | What it does |
|
|
417
|
+
|---|---|
|
|
418
|
+
| `values: ["a", "b"]`, `multiple: true` | Picks several; `multiple` adds to what is already chosen. An item that renders a list adds each element. |
|
|
419
|
+
| `force: true` | Sets the options on the element itself, visible or not, and fires `input` and `change`: a hidden `<select>` behind a widget built by a script. |
|
|
420
|
+
| `search: { input, open?, close? }` | Types each value missing from the options into the widget's search box, key by key, and picks it once the page lists it. |
|
|
421
|
+
| `clear: true` | `values` that render to nothing clear the control instead of leaving it alone. |
|
|
422
|
+
| `ignoreCase: true` | Matches values and labels without case. |
|
|
423
|
+
|
|
424
|
+
## `scroll`: infinite lists
|
|
425
|
+
|
|
426
|
+
[quotes.toscrape.com/scroll](https://quotes.toscrape.com/scroll) has no "next" link. It fetches ten quotes from
|
|
427
|
+
`/api/quotes?page=1` when it loads, and ten more each time the window reaches the bottom.
|
|
428
|
+
|
|
429
|
+
<!-- capture:page-scroll screenshot alt=The_infinite_scroll_page_with_its_first_ten_quotes -->
|
|
430
|
+

|
|
431
|
+
<!-- /capture -->
|
|
432
|
+
|
|
433
|
+
`scroll` moves the window to the bottom (`to: "bottom"`) or brings an element into view (`to: "<selector>"`),
|
|
434
|
+
`times` times (default 1). After each round it waits `settleMs` (default **300 ms**) and reads the page's
|
|
435
|
+
height. With `untilStable: true` it keeps going until the height stops changing: the page has nothing more to
|
|
436
|
+
add, or `maxScrolls` rounds (default 50) have run, which ends the step with a `warning` event rather than a
|
|
437
|
+
failure.
|
|
438
|
+
|
|
439
|
+
That wait is the whole of its patience. A page whose next batch takes longer to arrive looks finished, and
|
|
440
|
+
the step ends early. Two recipes on the same page:
|
|
441
|
+
|
|
442
|
+
- [`scroll-until-stable`](recipes/page-scroll/scroll-until-stable.input.json): `wait` for the first quote, then
|
|
443
|
+
`scroll` with `untilStable`.
|
|
444
|
+
- [`scroll-and-wait`](recipes/page-scroll/scroll-and-wait.input.json): `scroll`, `wait` for the 11th quote,
|
|
445
|
+
`scroll`, `wait` for the 21st. Each round waits for the batch it asked for, however long it takes.
|
|
446
|
+
|
|
447
|
+
<!-- capture:page-scroll summary -->
|
|
448
|
+
```text
|
|
449
|
+
scroll-and-wait: 30 emitted, 0 rejected, 0 duplicates, 1 pages
|
|
450
|
+
scroll-until-stable: 10 emitted, 0 rejected, 0 duplicates, 1 pages
|
|
451
|
+
```
|
|
452
|
+
<!-- /capture -->
|
|
453
|
+
|
|
454
|
+
<!-- capture:page-scroll trace grep=▶|■|✖|·_steps\.[0-9]+__ -->
|
|
455
|
+
```text
|
|
456
|
+
▶ scroll-and-wait (web)
|
|
457
|
+
· steps.0 goto … ms
|
|
458
|
+
· steps.1 wait … ms
|
|
459
|
+
· steps.2 scroll … ms
|
|
460
|
+
· steps.3 wait … ms
|
|
461
|
+
· steps.4 scroll … ms
|
|
462
|
+
· steps.5 wait … ms
|
|
463
|
+
· steps.6 extract quotes … ms
|
|
464
|
+
· steps.7 forEach … ms
|
|
465
|
+
■ scroll-and-wait: 30 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
466
|
+
▶ scroll-until-stable (web)
|
|
467
|
+
· steps.0 goto … ms
|
|
468
|
+
· steps.1 wait … ms
|
|
469
|
+
· steps.2 scroll … ms
|
|
470
|
+
· steps.3 extract quotes … ms
|
|
471
|
+
· steps.4 forEach … ms
|
|
472
|
+
■ scroll-until-stable: 10 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
473
|
+
```
|
|
474
|
+
<!-- /capture -->
|
|
475
|
+
|
|
476
|
+
On the network this guide was captured on, the API took longer than 300 ms to answer, so `untilStable` saw
|
|
477
|
+
the same height twice and stopped before the second batch arrived. On a fast connection it may read further.
|
|
478
|
+
That is the point: `untilStable` measures the page, not the site's API, and the result depends on how fast the
|
|
479
|
+
site answers. When the count matters, pair each `scroll` with a `wait` for what it should bring, as the second
|
|
480
|
+
recipe does. Two more details:
|
|
481
|
+
|
|
482
|
+
- The first `wait` matters too. Right after `goto`, the first batch may still be on its way; a page shorter than
|
|
483
|
+
the window cannot scroll, so the scroll fires no event and nothing more loads.
|
|
484
|
+
- On a slow site, raise `settleMs`. On a list that never ends, `maxScrolls` is what stops the step
|
|
485
|
+
(`limits.timeoutMs` does not apply to `scroll`); set it to the number of batches you want.
|
|
486
|
+
|
|
487
|
+
A page like this one always has an API behind it. Reading `/api/quotes?page=N` with `request` and
|
|
488
|
+
`paginate` ([part 5](04-flow-steps.md)) is faster and gets every quote; scroll when the API is out of reach.
|
|
489
|
+
|
|
490
|
+
## `wait`
|
|
491
|
+
|
|
492
|
+
`wait` does one or more of three things, in this order, each only if given: `selector` waits for an element to
|
|
493
|
+
be **visible**, `ms` sleeps, `state: "networkidle"` waits until the page has made no request for 500 ms.
|
|
494
|
+
`timeoutMs` bounds the first and the last.
|
|
495
|
+
|
|
496
|
+
[quotes.toscrape.com/js](https://quotes.toscrape.com/js/) builds its quotes with a script that runs while the
|
|
497
|
+
page loads; [/js-delayed](https://quotes.toscrape.com/js-delayed/) runs the same script ten seconds later.
|
|
498
|
+
Three recipes read the first quote right after `goto`, two of them without a `wait`:
|
|
499
|
+
|
|
500
|
+
| Recipe | Page | Steps |
|
|
501
|
+
|---|---|---|
|
|
502
|
+
| [`js-no-wait`](recipes/page-wait/js-no-wait.input.json) | `/js/` | `goto`, `extract` |
|
|
503
|
+
| [`delayed-no-wait`](recipes/page-wait/delayed-no-wait.input.json) | `/js-delayed/` | `goto`, `extract` |
|
|
504
|
+
| [`delayed-wait`](recipes/page-wait/delayed-wait.input.json) | `/js-delayed/` | `goto`, `wait` for `.quote` (up to 15 s), `extract` |
|
|
505
|
+
|
|
506
|
+
<!-- capture:page-wait screenshot n=1 alt=js-delayed_when_goto_returns:_the_page_has_loaded,_the_quotes_have_not -->
|
|
507
|
+

|
|
508
|
+
<!-- /capture -->
|
|
509
|
+
|
|
510
|
+
<!-- capture:page-wait screenshot n=2 alt=js-delayed_ten_seconds_later,_when_the_wait_returns -->
|
|
511
|
+

|
|
512
|
+
<!-- /capture -->
|
|
513
|
+
|
|
514
|
+
<!-- capture:page-wait summary -->
|
|
515
|
+
```text
|
|
516
|
+
delayed-no-wait: 0 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.1 (extract) failed: no match for .quote .text
|
|
517
|
+
delayed-wait: 2 emitted, 0 rejected, 0 duplicates, 1 pages
|
|
518
|
+
js-no-wait: 2 emitted, 0 rejected, 0 duplicates, 1 pages
|
|
519
|
+
```
|
|
520
|
+
<!-- /capture -->
|
|
521
|
+
|
|
522
|
+
<!-- capture:page-wait trace grep=▶|■|✖|·_steps\.[0-9]+__ -->
|
|
523
|
+
```text
|
|
524
|
+
▶ delayed-no-wait (web)
|
|
525
|
+
· steps.0 goto … ms
|
|
526
|
+
✖ step steps.1 (extract) failed: no match for .quote .text
|
|
527
|
+
■ delayed-no-wait: 0 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
528
|
+
✖ stopped: step steps.1 (extract) failed: no match for .quote .text
|
|
529
|
+
▶ delayed-wait (web)
|
|
530
|
+
· steps.0 goto … ms
|
|
531
|
+
· steps.1 wait … ms
|
|
532
|
+
· steps.2 extract first … ms
|
|
533
|
+
· steps.3 extract quotes … ms
|
|
534
|
+
· steps.4 forEach … ms
|
|
535
|
+
■ delayed-wait: 2 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
536
|
+
▶ js-no-wait (web)
|
|
537
|
+
· steps.0 goto … ms
|
|
538
|
+
· steps.1 extract first … ms
|
|
539
|
+
· steps.2 extract quotes … ms
|
|
540
|
+
· steps.3 forEach … ms
|
|
541
|
+
■ js-no-wait: 2 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
542
|
+
```
|
|
543
|
+
<!-- /capture -->
|
|
544
|
+
|
|
545
|
+
`goto` waits for the `load` event: the HTML, scripts and stylesheets. On `/js/` the quotes are written while
|
|
546
|
+
the page is parsed, so they are there when `goto` returns. On `/js-delayed/` the load event comes ten seconds
|
|
547
|
+
before the quotes, so the `extract` right after `goto` found nothing and failed. With the `wait`, the same
|
|
548
|
+
`extract` ran once the first quote showed.
|
|
549
|
+
|
|
550
|
+
Waiting for the element you are about to read is the reliable form. `ms` is a guess that is either too long or
|
|
551
|
+
too short; `networkidle` fails on pages that poll or stream forever. `goto` with `ready` covers the other
|
|
552
|
+
case: a page that sometimes never shows the element, and needs loading again.
|
|
553
|
+
|
|
554
|
+
## `screenshot`
|
|
555
|
+
|
|
556
|
+
`screenshot` saves the whole page (`fullPage`, not just the window) as a PNG at `path`, a template, relative to
|
|
557
|
+
the directory the crawler runs in. It binds nothing and changes nothing: it is for seeing what the browser saw
|
|
558
|
+
when a recipe misbehaves. The `login` recipe takes one after its `wait` for `.quote`:
|
|
559
|
+
|
|
560
|
+
<p align="center">
|
|
561
|
+
<img src="../assets/how-it-works/page-login-step.png" alt="The full page the login recipe's screenshot step saved" width="480">
|
|
562
|
+
</p>
|
|
563
|
+
|
|
564
|
+
## `evaluate`
|
|
565
|
+
|
|
566
|
+
`evaluate` runs JavaScript in the page and binds what it returns under its `id`. The result must be
|
|
567
|
+
JSON-serialisable: numbers, strings, lists and plain objects, not DOM elements or functions. A promise is
|
|
568
|
+
awaited.
|
|
569
|
+
|
|
570
|
+
[quotes.toscrape.com/js](https://quotes.toscrape.com/js/) writes its quotes from a variable in one of its
|
|
571
|
+
scripts, `var data = [ … ]`. `data` holds the same quotes as objects, with what the markup leaves out: each
|
|
572
|
+
author's slug and Goodreads path. The recipe [`js-data`](recipes/page-evaluate/js-data.input.json) reads the
|
|
573
|
+
variable instead of the markup:
|
|
574
|
+
|
|
575
|
+
```json
|
|
576
|
+
{ "type": "goto", "url": "{{start.url}}" },
|
|
577
|
+
{ "type": "evaluate", "id": "total", "script": "data.length" },
|
|
578
|
+
{ "type": "evaluate", "id": "quotes", "script": "(options) => data.slice(0, options.count)",
|
|
579
|
+
"args": { "count": "{{vars.count}}" } },
|
|
580
|
+
{ "type": "forEach", "over": "quotes", "as": "quote", "emit": true, "steps": [] }
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
<!-- capture:page-evaluate screenshot alt=The_quotes_the_page_wrote_from_its_data_variable -->
|
|
584
|
+

|
|
585
|
+
<!-- /capture -->
|
|
586
|
+
|
|
587
|
+
<!-- capture:page-evaluate scope ids=total,quote -->
|
|
588
|
+
```json
|
|
589
|
+
{
|
|
590
|
+
"total": 10,
|
|
591
|
+
"quote": {
|
|
592
|
+
"tags": [
|
|
593
|
+
"change",
|
|
594
|
+
"deep-thoughts",
|
|
595
|
+
"thinking",
|
|
596
|
+
"world"
|
|
597
|
+
],
|
|
598
|
+
"author": {
|
|
599
|
+
"name": "Albert Einstein",
|
|
600
|
+
"goodreads_link": "/author/show/9810.Albert_Einstein",
|
|
601
|
+
"slug": "Albert-Einstein"
|
|
602
|
+
},
|
|
603
|
+
"text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”"
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
<!-- /capture -->
|
|
608
|
+
|
|
609
|
+
<!-- capture:page-evaluate trace lines=6 -->
|
|
610
|
+
```text
|
|
611
|
+
▶ js-data (web)
|
|
612
|
+
⇄ access direct (direct)
|
|
613
|
+
⇢ page 1 https://quotes.toscrape.com/js/
|
|
614
|
+
· steps.0 goto … ms
|
|
615
|
+
· steps.1 evaluate total … ms
|
|
616
|
+
· steps.2 evaluate quotes … ms
|
|
617
|
+
…
|
|
618
|
+
```
|
|
619
|
+
<!-- /capture -->
|
|
620
|
+
|
|
621
|
+
The two forms:
|
|
622
|
+
|
|
623
|
+
- **Without `args`**, `script` is an expression, evaluated as it is: `data.length` gives `10`.
|
|
624
|
+
- **With `args`**, `script` is a function, called with `args` after its templates are rendered. A string that is
|
|
625
|
+
one placeholder keeps its type: `"{{vars.count}}"` passes the number `3`, not `"3"`.
|
|
626
|
+
|
|
627
|
+
The mapping then reads the objects directly: `quote.author.name`, `quote.tags`. No selector touched the markup.
|
|
628
|
+
|
|
629
|
+
`script` is rendered as a template **before** it runs, so a literal `{{` in the JavaScript is read as a
|
|
630
|
+
placeholder. Put such values in `args`. And `evaluate` runs whatever the recipe says in the page, with the
|
|
631
|
+
page's cookies: run it only from recipes you trust.
|
|
632
|
+
|
|
633
|
+
## `captcha`
|
|
634
|
+
|
|
635
|
+
A captcha is a challenge a site puts between a visitor and the page. OpenCraw does not solve captchas itself:
|
|
636
|
+
it finds them, hands them to a **solver** you register with the crawler (`captchaSolvers`), checks the page
|
|
637
|
+
afterwards, retries, and caps what a run spends. Solving one can break a site's terms; read
|
|
638
|
+
[captcha.md](../recipes/captcha.md) first.
|
|
639
|
+
|
|
640
|
+
Three places start a solve, all in web mode:
|
|
641
|
+
|
|
642
|
+
- **`session.captcha`**: after every `goto`, every page `paginate` opens, every `click` and every `press`, the
|
|
643
|
+
engine looks for a visible challenge (reCAPTCHA, hCaptcha, Cloudflare Turnstile) and solves it before the next
|
|
644
|
+
step. Not after `fill`, `select` or `evaluate`, which rarely bring one up.
|
|
645
|
+
- **`onBlock.solve`**: a page the block rule calls a block, which shows a challenge, is solved on the spot.
|
|
646
|
+
- **The `captcha` step**: at one known point, such as a login form. It also detects reCAPTCHA v3, which has no
|
|
647
|
+
widget, and a page with no challenge is fine: the step does nothing. With `image`, `field` and `submit`, it
|
|
648
|
+
handles a picture captcha checked when its form is posted.
|
|
649
|
+
|
|
650
|
+
Each challenge gets 3 attempts by default, 120 s each, and a run gets 10 solves in all. The loop and its events
|
|
651
|
+
(`⚿`, `✓`, `✗` in a trace) are in [part 10](09-running.md#5-captchas).
|
|
652
|
+
|
|
653
|
+
This guide solves no real captcha. [`examples/captcha-solver`](../../examples/captcha-solver) is a working
|
|
654
|
+
solver for a paid service, with a demo that runs the whole loop on your machine: a local page with a
|
|
655
|
+
reCAPTCHA-shaped challenge and a fake solving API, so nothing leaves the machine.
|
|
656
|
+
|
|
657
|
+
## Summary: what each step leaves behind
|
|
658
|
+
|
|
659
|
+
| Step | Binds under `id` | Changes |
|
|
660
|
+
|---|---|---|
|
|
661
|
+
| `goto` | nothing | `page.url` (the final URL); emits `page:visit` (`⇢`) |
|
|
662
|
+
| `click` | nothing; with `download`, the file | `page.url` if it navigated, and then a `page:visit` (`⇢`), checked like a `goto`'s; with `download`, the current document |
|
|
663
|
+
| `press`, `select` | nothing | as `click`, without the download |
|
|
664
|
+
| `fill`, `scroll`, `wait` | nothing | `page.url` if the page navigated |
|
|
665
|
+
| `screenshot` | nothing | a file at `path` |
|
|
666
|
+
| `evaluate` | the script's result | whatever the script does to the page |
|
|
667
|
+
| `captcha` | nothing | the page, once solved |
|
|
668
|
+
|
|
669
|
+
Every one of them runs on the recipe's single tab, and after every one of them `page.url` is the tab's URL.
|
|
670
|
+
`page.number` belongs to `paginate`.
|
|
671
|
+
|
|
672
|
+
Next: [4. Data steps](03-data-steps.md), where the values come from.
|