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.
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/Cargo.lock +25 -3
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/Cargo.toml +2 -1
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/PKG-INFO +5 -5
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/README.md +4 -4
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/architecture.md +8 -9
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/async-api.md +10 -17
- scraper_rust-0.4.1/examples/demo_async_document.py +83 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/pyproject.toml +1 -1
- scraper_rust-0.4.1/scraper_rs/__init__.py +21 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/scraper_rs/asyncio.py +51 -121
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/scraper_rs/asyncio.pyi +3 -5
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/scraper_rs.pyi +21 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/src/lib.rs +879 -81
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/tests/test_asyncio.py +3 -9
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/tests/test_scraper.py +50 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/uv.lock +1 -1
- scraper_rust-0.3.3/.github/dependabot.yml +0 -16
- scraper_rust-0.3.3/scraper_rs/__init__.py +0 -14
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/benchmark.yml +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/bump-version.yml +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/release.yml +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.github/workflows/tests.yml +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/.gitignore +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/AGENTS.md +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/LICENSE +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/README.md +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/bench_parse_memory.py +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/bench_sync_async.py +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/benchmarks/bench_vs_markupever.py +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/README.md +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/api.md +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/development.md +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/docs/limits-and-errors.md +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/examples/demo.py +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/examples/demo_asyncio.py +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/examples/demo_prettify_url.py +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/justfile +0 -0
- {scraper_rust-0.3.3 → scraper_rust-0.4.1}/py.typed +0 -0
- {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.
|
|
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
|
+
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
|
+
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- `parse`
|
|
39
|
-
- `
|
|
40
|
-
-
|
|
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
|
|
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
|
|
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`:
|
|
8
|
-
- `AsyncElement`: wraps
|
|
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
|
-
-
|
|
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
|
|
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`
|
|
58
|
-
-
|
|
59
|
-
- `
|
|
60
|
-
-
|
|
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
|
|
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.
|
|
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__})
|