scraper-rust 0.3.3__tar.gz → 0.4.1__tar.gz

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.
Files changed (39) hide show
  1. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/Cargo.lock +25 -3
  2. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/Cargo.toml +2 -1
  3. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/PKG-INFO +5 -5
  4. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/README.md +4 -4
  5. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/architecture.md +8 -9
  6. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/async-api.md +10 -17
  7. scraper_rust-0.4.1/examples/demo_async_document.py +83 -0
  8. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/pyproject.toml +1 -1
  9. scraper_rust-0.4.1/scraper_rs/__init__.py +21 -0
  10. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/scraper_rs/asyncio.py +51 -121
  11. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/scraper_rs/asyncio.pyi +3 -5
  12. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/scraper_rs.pyi +21 -0
  13. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/src/lib.rs +879 -81
  14. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/tests/test_asyncio.py +3 -9
  15. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/tests/test_scraper.py +50 -0
  16. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/uv.lock +1 -1
  17. scraper_rust-0.3.3/.github/dependabot.yml +0 -16
  18. scraper_rust-0.3.3/scraper_rs/__init__.py +0 -14
  19. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/benchmark.yml +0 -0
  20. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/bump-version.yml +0 -0
  21. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/release.yml +0 -0
  22. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/tests.yml +0 -0
  23. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.gitignore +0 -0
  24. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/AGENTS.md +0 -0
  25. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/LICENSE +0 -0
  26. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/README.md +0 -0
  27. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/bench_parse_memory.py +0 -0
  28. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/bench_sync_async.py +0 -0
  29. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/bench_vs_markupever.py +0 -0
  30. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/README.md +0 -0
  31. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/api.md +0 -0
  32. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/development.md +0 -0
  33. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/limits-and-errors.md +0 -0
  34. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/examples/demo.py +0 -0
  35. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/examples/demo_asyncio.py +0 -0
  36. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/examples/demo_prettify_url.py +0 -0
  37. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/justfile +0 -0
  38. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/py.typed +0 -0
  39. {scraper_rust-0.3.3 → scraper_rust-0.4.1}/tests/test_performance.py +0 -0
@@ -545,7 +545,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
545
545
  checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
546
546
  dependencies = [
547
547
  "log",
548
- "markup5ever",
548
+ "markup5ever 0.38.0",
549
+ ]
550
+
551
+ [[package]]
552
+ name = "html5ever"
553
+ version = "0.39.0"
554
+ source = "registry+https://github.com/rust-lang/crates.io-index"
555
+ checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
556
+ dependencies = [
557
+ "log",
558
+ "markup5ever 0.39.0",
549
559
  ]
550
560
 
551
561
  [[package]]
@@ -1215,6 +1225,17 @@ dependencies = [
1215
1225
  "web_atoms",
1216
1226
  ]
1217
1227
 
1228
+ [[package]]
1229
+ name = "markup5ever"
1230
+ version = "0.39.0"
1231
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1232
+ checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
1233
+ dependencies = [
1234
+ "log",
1235
+ "tendril",
1236
+ "web_atoms",
1237
+ ]
1238
+
1218
1239
  [[package]]
1219
1240
  name = "memchr"
1220
1241
  version = "2.8.0"
@@ -1820,7 +1841,7 @@ dependencies = [
1820
1841
  "cssparser",
1821
1842
  "ego-tree",
1822
1843
  "getopts",
1823
- "html5ever",
1844
+ "html5ever 0.38.0",
1824
1845
  "precomputed-hash",
1825
1846
  "selectors",
1826
1847
  "tendril",
@@ -1828,8 +1849,9 @@ dependencies = [
1828
1849
 
1829
1850
  [[package]]
1830
1851
  name = "scraper-rs"
1831
- version = "0.3.3"
1852
+ version = "0.4.1"
1832
1853
  dependencies = [
1854
+ "html5ever 0.39.0",
1833
1855
  "pyo3",
1834
1856
  "pyo3-async-runtimes",
1835
1857
  "scraper",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "scraper-rs"
3
- version = "0.3.3"
3
+ version = "0.4.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -20,6 +20,7 @@ tokio = { version = "1", features = ["rt", "macros"] }
20
20
  scraper = { version = "0.25.0", git = "https://github.com/rust-scraper/scraper", features = ["atomic"], rev = "d07444effb9ca192d9b3919fb920ab9c6c701c4f" }
21
21
 
22
22
  xee-xpath = "0.1.5"
23
+ html5ever = "0.39.0"
23
24
 
24
25
  [profile.release]
25
26
  lto = "fat"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scraper-rust
3
- Version: 0.3.3
3
+ Version: 0.4.1
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -80,7 +80,7 @@ python examples/demo_prettify_url.py https://example.com --max-lines 80
80
80
 
81
81
  ### Async usage
82
82
 
83
- The `scraper_rs.asyncio` module keeps the event loop responsive. `parse` yields once (`asyncio.sleep(0)`) before constructing a sync `Document` in the current thread, while `select`/`xpath` helpers run in a thread pool. Parsed documents and elements are wrapped with awaitable selector methods for nested queries:
83
+ The `scraper_rs.asyncio` module exposes an async-first surface for coroutine code. `AsyncDocument` stores shareable HTML/text state instead of a thread-affine sync `Document`, and all selectors are awaitable for consistent async calling style:
84
84
 
85
85
  ```py
86
86
  import asyncio
@@ -103,8 +103,7 @@ asyncio.run(main())
103
103
  ```
104
104
 
105
105
  All async functions accept the same keyword arguments as their sync counterparts (`max_size_bytes`, `truncate_on_limit`, etc.).
106
- Async wrappers expose the underlying sync objects via `.document` and `.element` if you need direct access.
107
- `AsyncDocument` supports `async with` for automatic cleanup in coroutine code.
106
+ `AsyncDocument` supports `async with` for automatic cleanup in coroutine code, and `AsyncElement` / `AsyncDocument` both expose async `.prettify()` helpers.
108
107
 
109
108
  ### Large documents and memory safety
110
109
 
@@ -137,7 +136,7 @@ Note: Truncation happens at valid UTF-8 character boundaries to prevent encoding
137
136
  - `.xpath(expr)` / `.xpath_first(expr)` evaluate XPath expressions that return element nodes.
138
137
  - `.prettify()` renders the current DOM as an indented string for readable output/debugging.
139
138
  - `.text` returns normalized text; `Document.html` is the original input HTML; `Element.html` is inner HTML.
140
- - `scraper_rs.asyncio` exposes async `parse`/`select`/`xpath` wrappers to keep the event loop responsive.
139
+ - `scraper_rs.asyncio` exposes async `parse`/`select`/`xpath` wrappers plus awaitable `AsyncDocument` / `AsyncElement` methods.
141
140
  - `Element` exposes `.tag`, `.text`, `.html`, `.attrs` plus helpers `.attr(name)`, `.get(name, default)`, `.to_dict()`.
142
141
  - Elements support nested CSS and XPath selection via `.select(css)`, `.select_first(css)`, `.find(css)`, `.css(css)`, `.xpath(expr)`, `.xpath_first(expr)`.
143
142
  - Elements also expose `.prettify()` to format element HTML with indentation.
@@ -167,6 +166,7 @@ If you have `just` installed, the repo includes helpers: `just build` (local whe
167
166
  ## Projects Using scraper-rs
168
167
 
169
168
  - [**silkworm**](https://github.com/BitingSnakes/silkworm) - Async web scraping framework on top of Rust
169
+ - [**silkworm-mcp**](https://github.com/BitingSnakes/silkworm-mcp) - An MCP server for silkworm
170
170
 
171
171
  ## Development
172
172
 
@@ -53,7 +53,7 @@ python examples/demo_prettify_url.py https://example.com --max-lines 80
53
53
 
54
54
  ### Async usage
55
55
 
56
- The `scraper_rs.asyncio` module keeps the event loop responsive. `parse` yields once (`asyncio.sleep(0)`) before constructing a sync `Document` in the current thread, while `select`/`xpath` helpers run in a thread pool. Parsed documents and elements are wrapped with awaitable selector methods for nested queries:
56
+ The `scraper_rs.asyncio` module exposes an async-first surface for coroutine code. `AsyncDocument` stores shareable HTML/text state instead of a thread-affine sync `Document`, and all selectors are awaitable for consistent async calling style:
57
57
 
58
58
  ```py
59
59
  import asyncio
@@ -76,8 +76,7 @@ asyncio.run(main())
76
76
  ```
77
77
 
78
78
  All async functions accept the same keyword arguments as their sync counterparts (`max_size_bytes`, `truncate_on_limit`, etc.).
79
- Async wrappers expose the underlying sync objects via `.document` and `.element` if you need direct access.
80
- `AsyncDocument` supports `async with` for automatic cleanup in coroutine code.
79
+ `AsyncDocument` supports `async with` for automatic cleanup in coroutine code, and `AsyncElement` / `AsyncDocument` both expose async `.prettify()` helpers.
81
80
 
82
81
  ### Large documents and memory safety
83
82
 
@@ -110,7 +109,7 @@ Note: Truncation happens at valid UTF-8 character boundaries to prevent encoding
110
109
  - `.xpath(expr)` / `.xpath_first(expr)` evaluate XPath expressions that return element nodes.
111
110
  - `.prettify()` renders the current DOM as an indented string for readable output/debugging.
112
111
  - `.text` returns normalized text; `Document.html` is the original input HTML; `Element.html` is inner HTML.
113
- - `scraper_rs.asyncio` exposes async `parse`/`select`/`xpath` wrappers to keep the event loop responsive.
112
+ - `scraper_rs.asyncio` exposes async `parse`/`select`/`xpath` wrappers plus awaitable `AsyncDocument` / `AsyncElement` methods.
114
113
  - `Element` exposes `.tag`, `.text`, `.html`, `.attrs` plus helpers `.attr(name)`, `.get(name, default)`, `.to_dict()`.
115
114
  - Elements support nested CSS and XPath selection via `.select(css)`, `.select_first(css)`, `.find(css)`, `.css(css)`, `.xpath(expr)`, `.xpath_first(expr)`.
116
115
  - Elements also expose `.prettify()` to format element HTML with indentation.
@@ -140,6 +139,7 @@ If you have `just` installed, the repo includes helpers: `just build` (local whe
140
139
  ## Projects Using scraper-rs
141
140
 
142
141
  - [**silkworm**](https://github.com/BitingSnakes/silkworm) - Async web scraping framework on top of Rust
142
+ - [**silkworm-mcp**](https://github.com/BitingSnakes/silkworm-mcp) - An MCP server for silkworm
143
143
 
144
144
  ## Development
145
145
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  `scraper_rs` is a Python extension module implemented in Rust with PyO3. The Rust code in `src/lib.rs` defines the public API (`Document`, `Element`, and top-level helper functions) and is exported as the Python module `scraper_rs`. The Python package in `scraper_rs/__init__.py` simply re-exports the extension module.
6
6
 
7
- Async support lives in pure Python in `scraper_rs/asyncio.py`. It wraps the Rust async helpers defined in `src/lib.rs` and exposes `AsyncDocument` and `AsyncElement` wrappers to make selectors awaitable.
7
+ Async support lives in pure Python in `scraper_rs/asyncio.py`. It exposes `AsyncDocument` and `AsyncElement` wrappers to make selectors awaitable without storing a sync `Document` object in coroutine-facing state.
8
8
 
9
9
  ## Data flow (sync)
10
10
 
@@ -33,16 +33,15 @@ Key code references:
33
33
  The async layer is a thin wrapper over the sync API:
34
34
 
35
35
  - The Python module `scraper_rs/asyncio.py` exposes `AsyncDocument` and `AsyncElement`.
36
- - Top-level async functions (`select`, `xpath`, `select_first`, `first`, etc) call Rust helpers in `src/lib.rs` such as `select_async`, `first_async`, and `xpath_async`.
37
- - The Rust async helpers use `pyo3_async_runtimes::tokio::future_into_py_with_locals` and `tokio::task::spawn_blocking` to run blocking parsing and selection on a thread pool.
38
- - `parse` is implemented in Python (`asyncio.sleep(0)` + sync `Document(...)` construction).
39
- - `AsyncDocument` / `AsyncElement` selector methods pass HTML strings into Rust async helpers, so selectors parse per call rather than reusing the wrapped `Document` DOM.
40
- - Nested async selection on elements is implemented via `_select_fragment_async` and `_xpath_fragment_async` in `src/lib.rs`, which parse the element's inner HTML as a fragment.
36
+ - `AsyncDocument` stores only shareable `html` and `text` state; it does not retain a sync `Document`.
37
+ - Top-level async functions (`select`, `xpath`, `select_first`, `first`, etc) yield once and then call the corresponding synchronous Rust helper from `src/lib.rs`.
38
+ - `parse` yields once, constructs a temporary sync `Document`, copies out `html` and `text`, then drops the sync object.
39
+ - `AsyncElement` wraps an immutable sync `Element` snapshot and forwards nested selectors/prettify through awaitable methods.
40
+ - Selector calls still parse per call rather than reusing a persistent DOM, which keeps async objects thread-safe and lightweight.
41
41
 
42
42
  Code references:
43
43
  - Async wrappers: `scraper_rs/asyncio.py`
44
- - Rust async entry points: `src/lib.rs` (`select_async`, `select_first_async`, `first_async`, `xpath_async`, `xpath_first_async`)
45
- - Fragment helpers: `src/lib.rs` (`_select_fragment_async`, `_xpath_fragment_async`)
44
+ - Rust sync entry points: `src/lib.rs` (`Document`, `Element`, `select`, `select_first`, `first`, `xpath`, `xpath_first`)
46
45
 
47
46
  ## Module wiring
48
47
 
@@ -50,7 +49,7 @@ The Rust module initializer in `src/lib.rs` registers all classes and functions
50
49
 
51
50
  - `#[pymodule] fn scraper_rs(...)` adds `Document`, `Element`, and the top-level functions.
52
51
  - The version string is exposed as `__version__` from `env!("CARGO_PKG_VERSION")`.
53
- - `scraper_rs/__init__.py` re-exports the extension module and forwards `__all__` if it exists.
52
+ - `scraper_rs/__init__.py` re-exports the supported sync API explicitly.
54
53
 
55
54
  ## Types and public surface
56
55
 
@@ -4,13 +4,12 @@ The async API lives in `scraper_rs/asyncio.py` and provides awaitable wrappers a
4
4
 
5
5
  ## Key types
6
6
 
7
- - `AsyncDocument`: wraps a synchronous `Document` and exposes awaitable selectors.
8
- - `AsyncElement`: wraps a synchronous `Element` and exposes awaitable selectors.
9
- - `AsyncDocument.document` and `AsyncElement.element` provide access to the underlying sync objects.
7
+ - `AsyncDocument`: stores shareable document state (`html`, `text`) and exposes awaitable selectors.
8
+ - `AsyncElement`: wraps an immutable element snapshot and exposes awaitable nested selectors.
10
9
 
11
10
  Implementation references:
12
11
  - Python wrappers: `scraper_rs/asyncio.py`
13
- - Rust async helpers: `src/lib.rs` (`select_async`, `select_first_async`, `first_async`, `xpath_async`, `xpath_first_async`)
12
+ - Sync primitives: `src/lib.rs` (`Document`, `Element`, `select`, `select_first`, `first`, `xpath`, `xpath_first`)
14
13
 
15
14
  ## Top-level async functions
16
15
 
@@ -43,7 +42,7 @@ asyncio.run(main())
43
42
 
44
43
  ## Context management and cleanup
45
44
 
46
- - `AsyncDocument` supports `async with` and closes the underlying `Document` on exit (including exception paths).
45
+ - `AsyncDocument` supports `async with` and clears its stored HTML/text state on exit (including exception paths).
47
46
  - `AsyncDocument` also supports sync `with`, but `async with` is preferred inside coroutine code.
48
47
 
49
48
  ```py
@@ -54,21 +53,15 @@ async with doc:
54
53
 
55
54
  ## How async execution works
56
55
 
57
- - `scraper_rs/asyncio.py` calls Rust helpers such as `select_async` and `xpath_async`.
58
- - In `src/lib.rs`, each async helper uses `pyo3_async_runtimes::tokio::future_into_py_with_locals` and `tokio::task::spawn_blocking` to run parsing and selection on a thread pool.
59
- - `parse` is implemented in Python: it yields to the event loop with `asyncio.sleep(0)` and then constructs a sync `Document` in the current thread.
60
- - `AsyncDocument` and `AsyncElement` methods pass HTML strings into Rust async helpers, so each async selector call re-parses HTML/fragment input.
56
+ - `scraper_rs/asyncio.py` presents an async API even though the parsing primitives remain synchronous in Rust.
57
+ - Each async entry point yields to the event loop once before invoking the corresponding synchronous Rust helper.
58
+ - `AsyncDocument` stores only `html` and `text`, so coroutine code does not hold a thread-affine sync `Document`.
59
+ - Selector calls still parse per call, just like the sync top-level helper functions.
61
60
 
62
61
  ## Nested selection on AsyncElement
63
62
 
64
- Nested selectors on `AsyncElement` call fragment helpers defined in `src/lib.rs`:
65
-
66
- - `_select_fragment_async`
67
- - `_select_first_fragment_async`
68
- - `_xpath_fragment_async`
69
- - `_xpath_first_fragment_async`
70
-
71
- These helpers parse the element's inner HTML fragment each time (see `scraper_rs/asyncio.py` and `src/lib.rs`). This keeps the API simple but means repeated async queries re-parse the fragment.
63
+ Nested selectors on `AsyncElement` call the wrapped sync `Element` snapshot methods from `src/lib.rs`.
64
+ Those methods operate on the stored element HTML fragment each time, which keeps the async wrapper simple but means repeated nested queries re-parse the fragment.
72
65
 
73
66
  ## Performance notes
74
67
 
@@ -0,0 +1,83 @@
1
+ """Example showing the async document-oriented API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ from scraper_rs import asyncio as async_scraper
8
+
9
+ HTML = """
10
+ <html>
11
+ <body>
12
+ <main>
13
+ <section class="card" data-id="1">
14
+ <h2>Alpha</h2>
15
+ <a href="/alpha">Read more</a>
16
+ </section>
17
+ <section class="card featured" data-id="2">
18
+ <h2>Beta</h2>
19
+ <a href="/beta">Read more</a>
20
+ </section>
21
+ <section class="card" data-id="3">
22
+ <h2>Gamma</h2>
23
+ <a href="/gamma">Read more</a>
24
+ </section>
25
+ </main>
26
+ </body>
27
+ </html>
28
+ """
29
+
30
+
31
+ async def main() -> None:
32
+ print("AsyncDocument workflow")
33
+ print()
34
+
35
+ async with await async_scraper.parse(HTML) as doc:
36
+ print(doc)
37
+ print(f"Document text: {doc.text}")
38
+ print()
39
+
40
+ cards = await doc.select(".card")
41
+ print(f"Found {len(cards)} cards")
42
+
43
+ for card in cards:
44
+ title = await card.select_first("h2")
45
+ link = await card.select_first("a[href]")
46
+ print(
47
+ f" - id={card.attr('data-id')}: "
48
+ f"{title.text if title else 'missing title'} "
49
+ f"-> {link.attr('href') if link else 'missing link'}"
50
+ )
51
+
52
+ print()
53
+
54
+ featured = await doc.find(".featured")
55
+ if featured:
56
+ featured_link = await featured.find("a[href]")
57
+ print("Featured card")
58
+ print(f" text: {featured.text}")
59
+ print(
60
+ f" href: {featured_link.attr('href') if featured_link else 'missing'}"
61
+ )
62
+ print()
63
+
64
+ first_link, first_featured = await asyncio.gather(
65
+ doc.select_first("a[href]"),
66
+ doc.xpath_first("//section[contains(@class, 'featured')]"),
67
+ )
68
+ print("Concurrent queries")
69
+ print(f" first link: {first_link.attr('href') if first_link else 'missing'}")
70
+ print(
71
+ " featured via xpath: "
72
+ f"{first_featured.attr('data-id') if first_featured else 'missing'}"
73
+ )
74
+ print()
75
+
76
+ pretty = await doc.prettify()
77
+ print("Prettified HTML preview")
78
+ for line in pretty.splitlines()[:10]:
79
+ print(f" {line}")
80
+
81
+
82
+ if __name__ == "__main__":
83
+ asyncio.run(main())
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scraper-rust"
7
- version = "0.3.3"
7
+ version = "0.4.1"
8
8
  description = "Python bindings around rust-scraper/scraper with PyO3"
9
9
  authors = [{ name = "Yehor Smoliakov", email = "egorsmkv@gmail.com" }]
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,21 @@
1
+ """scraper_rs - Python bindings for the Rust scraper crate."""
2
+
3
+ from . import scraper_rs as _scraper_rs
4
+
5
+ __doc__ = _scraper_rs.__doc__
6
+ __all__ = [
7
+ "Document",
8
+ "Element",
9
+ "__version__",
10
+ "first",
11
+ "parse",
12
+ "parse_document",
13
+ "parse_fragment",
14
+ "prettify",
15
+ "select",
16
+ "select_first",
17
+ "xpath",
18
+ "xpath_first",
19
+ ]
20
+
21
+ globals().update({name: getattr(_scraper_rs, name) for name in __all__})