nettle-html 0.5.0__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.
- nettle_html-0.5.0/LICENSE +21 -0
- nettle_html-0.5.0/PKG-INFO +278 -0
- nettle_html-0.5.0/README.md +245 -0
- nettle_html-0.5.0/nettle/__init__.py +123 -0
- nettle_html-0.5.0/nettle/cdp.py +521 -0
- nettle_html-0.5.0/nettle/clean.py +90 -0
- nettle_html-0.5.0/nettle/compat.py +32 -0
- nettle_html-0.5.0/nettle/css.py +539 -0
- nettle_html-0.5.0/nettle/discover.py +225 -0
- nettle_html-0.5.0/nettle/exceptions.py +36 -0
- nettle_html-0.5.0/nettle/extract.py +168 -0
- nettle_html-0.5.0/nettle/format.py +137 -0
- nettle_html-0.5.0/nettle/http.py +468 -0
- nettle_html-0.5.0/nettle/network.py +533 -0
- nettle_html-0.5.0/nettle/nodes.py +462 -0
- nettle_html-0.5.0/nettle/parse.py +502 -0
- nettle_html-0.5.0/nettle/py.typed +0 -0
- nettle_html-0.5.0/nettle/query.py +149 -0
- nettle_html-0.5.0/nettle/registry.py +226 -0
- nettle_html-0.5.0/nettle/serialize.py +81 -0
- nettle_html-0.5.0/nettle/soup.py +127 -0
- nettle_html-0.5.0/nettle/table.py +155 -0
- nettle_html-0.5.0/nettle/text.py +240 -0
- nettle_html-0.5.0/nettle/urls.py +285 -0
- nettle_html-0.5.0/nettle_html.egg-info/PKG-INFO +278 -0
- nettle_html-0.5.0/nettle_html.egg-info/SOURCES.txt +40 -0
- nettle_html-0.5.0/nettle_html.egg-info/dependency_links.txt +1 -0
- nettle_html-0.5.0/nettle_html.egg-info/top_level.txt +1 -0
- nettle_html-0.5.0/pyproject.toml +53 -0
- nettle_html-0.5.0/setup.cfg +4 -0
- nettle_html-0.5.0/tests/test_api.py +66 -0
- nettle_html-0.5.0/tests/test_crossplatform_fixes.py +103 -0
- nettle_html-0.5.0/tests/test_css.py +137 -0
- nettle_html-0.5.0/tests/test_css_extra.py +57 -0
- nettle_html-0.5.0/tests/test_extract_query.py +81 -0
- nettle_html-0.5.0/tests/test_format_table.py +69 -0
- nettle_html-0.5.0/tests/test_http_clean.py +95 -0
- nettle_html-0.5.0/tests/test_parse.py +171 -0
- nettle_html-0.5.0/tests/test_registry_discover.py +108 -0
- nettle_html-0.5.0/tests/test_text.py +80 -0
- nettle_html-0.5.0/tests/test_transversal.py +208 -0
- nettle_html-0.5.0/tests/test_urls_network.py +164 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ldikay99
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nettle-html
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Pure-Python, cross-platform scraping toolkit: clean declarative extract, one-call endpoint discovery, browser-fingerprint HTTP, and real network sniffing via CDP. Stdlib only, zero dependencies.
|
|
5
|
+
Author: ldikay99
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ldikay99/nettle
|
|
8
|
+
Project-URL: Repository, https://github.com/ldikay99/nettle
|
|
9
|
+
Project-URL: Issues, https://github.com/ldikay99/nettle/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/ldikay99/nettle/commits
|
|
11
|
+
Keywords: html,parser,css,selector,scrape,scraping,beautifulsoup,endpoints,discovery,anti-detection,cdp,cross-platform
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Nettle
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/nettle-html/)
|
|
37
|
+
[](https://pypi.org/project/nettle-html/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
**El toolkit de scraping que Python esperaba.** Una sola librería para parsear HTML, extraer datos limpios, llamar cualquier endpoint, descubrir APIs ocultas y sniffiear tráfico real de navegador — con **cero dependencias externas**.
|
|
41
|
+
|
|
42
|
+
Nettle existe porque el scraping real no termina en "seleccionar un nodo": termina peleando con `\xa0`, entidades crudas, JSON escondido en scripts, endpoints ocultos en JavaScript y sitios que te bloquean por parecer bot. Nettle resuelve **todo el pipeline**, no solo el primer paso.
|
|
43
|
+
|
|
44
|
+
- **Corre en todas partes**: Windows, Linux, macOS y Android (Termux). Python puro + stdlib, sin compilaciones ni binarios raros. El sniffing con navegador encuentra solo tu Chrome/Chromium/Edge/Brave en cualquier sistema.
|
|
45
|
+
- **Gratis y libre**: licencia MIT, uso comercial incluido.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from nettle import fetch
|
|
49
|
+
|
|
50
|
+
doc = fetch("https://quotes.toscrape.com/")
|
|
51
|
+
data = doc.extract({
|
|
52
|
+
"quotes": {
|
|
53
|
+
"select": "div.quote",
|
|
54
|
+
"each": {
|
|
55
|
+
"text": {"css": "span.text", "clean": "plain"},
|
|
56
|
+
"author": {"css": "small.author", "clean": "plain"},
|
|
57
|
+
"tags": {"css": "a.tag", "all": True, "clean": "plain"},
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Eso es todo. Sin `replace("\xa0", " ")`, sin `html.unescape`, sin `re.sub(r"\s+", ...)` por cada campo, sin armar dicts a mano. **Texto sucio entra, datos limpios salen.**
|
|
64
|
+
|
|
65
|
+
- Antes: `"Hello\xa0world's & friends"`
|
|
66
|
+
- Con Nettle: `"Hello world's & friends"`
|
|
67
|
+
|
|
68
|
+
## Instalación
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install nettle-html
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
O desde el código fuente:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/ldikay99/nettle.git
|
|
78
|
+
pip install ./nettle
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Requiere **Python 3.9 o superior** y nada más — `pip install nettle-html` no instala una sola dependencia. Opcional: un navegador basado en Chromium (Chrome, Edge, Brave) si quieres capturar tráfico de red real; Nettle lo detecta solo en tu sistema.
|
|
82
|
+
|
|
83
|
+
## Por qué Nettle y no BeautifulSoup
|
|
84
|
+
|
|
85
|
+
| Dolor con BS4 + requests | Nettle |
|
|
86
|
+
|---|---|
|
|
87
|
+
| Texto sucio (`\xa0`, entidades, whitespace loco) — limpias a mano por cada campo | Limpieza integrada: `clean_text()` y `clean: "plain"` en cada extracción |
|
|
88
|
+
| Soup no habla HTTP — necesitas `requests` aparte | Cliente HTTP propio: `fetch()`, `request()`, sesiones con cookies y reintentos |
|
|
89
|
+
| Nada de endpoints — solo ves el HTML renderizado | `discover_endpoints()` los encuentra y verifica en una llamada |
|
|
90
|
+
| No ves el tráfico que genera la página | `sniff_network()` captura XHR/fetch con un Chrome real, como DevTools |
|
|
91
|
+
| Fingerprints de bot detectados | Rotación de perfiles de navegador reales con cabeceras `Sec-Ch-Ua`/`Sec-Fetch` coherentes |
|
|
92
|
+
| JSON embebido hay que sacarlo con regex frágiles | `sniff_embedded_json()` extrae cualquier `variable = {...}` que parsee como JSON |
|
|
93
|
+
| CSV/JSON los armas tú | `to_json()`, `to_csv()`, `to_dicts()` listos |
|
|
94
|
+
| Heurísticas fijas — si tu sitio no encaja, sufres | `nettle.registry`: enseñas tus convenciones en runtime, sin fork |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 1. Scrape declarativo — describe el dato, no el proceso
|
|
99
|
+
|
|
100
|
+
`doc.extract(esquema)` mapea selectores CSS a diccionarios ya limpios. Anida, itera registros, saca atributos, absolutiza URLs:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from nettle import fetch
|
|
104
|
+
|
|
105
|
+
doc = fetch("https://books.toscrape.com/")
|
|
106
|
+
libros = doc.extract({
|
|
107
|
+
"libros": {
|
|
108
|
+
"select": "article.product_pod",
|
|
109
|
+
"each": {
|
|
110
|
+
"titulo": {"css": "h3 a", "attr": "title"},
|
|
111
|
+
"precio": {"css": ".price_color", "clean": "plain"},
|
|
112
|
+
"stock": {"css": ".instock.availability", "clean": "plain"},
|
|
113
|
+
"link": {"css": "h3 a", "attr": "href", "abs": True},
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
})["libros"]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Cada campo acepta `attr` (o lista de atributos fallback tipo `["data-src", "src"]` para imágenes lazy), `all=True` para listas, `abs=True` para URLs absolutas, `default=` para valores por defecto y `clean=` con los modos `plain`, `strict`, `keep_newlines` o `raw`.
|
|
120
|
+
|
|
121
|
+
Atajos rápidos: `doc.values("h1", ".precio")` para varios textos de una, `doc.record({...})` por elemento, `doc.table("table")` para tablas HTML → lista de dicts, `doc.lists()` para listas con items.
|
|
122
|
+
|
|
123
|
+
## 2. HTTP directo — cualquier método, cualquier endpoint
|
|
124
|
+
|
|
125
|
+
Si ya tienes la URL, la llamas. Nettle no asume rutas ni exige descubrir nada:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from nettle import request, call_endpoint
|
|
129
|
+
|
|
130
|
+
request("POST", "https://tienda.example/catalog/load", json={"q": "zapatos"})
|
|
131
|
+
request("PUT", "https://tienda.example/items/42", json={"precio": 10})
|
|
132
|
+
call_endpoint("https://tienda.example/items/42", "DELETE")
|
|
133
|
+
call_endpoint("https://tienda.example/search", "GET", params={"page": 2})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS — con JSON, form-data, bytes o texto. La respuesta trae `.text`, `.json()`, `.status`, `.headers`, `.ok` y `.doc` (el HTML ya parseado). Los errores HTTP (401, 404, 500…) devuelven la respuesta para inspeccionarla; los fallos de red lanzan `FetchError` con reintentos y backoff exponencial incluidos.
|
|
137
|
+
|
|
138
|
+
Para varias llamadas relacionadas, `Session` mantiene cookies entre requests y hereda tus defaults globales.
|
|
139
|
+
|
|
140
|
+
## 3. Descubre endpoints con una llamada
|
|
141
|
+
|
|
142
|
+
No sabes dónde está la API? Pásale la página y Nettle te devuelve los endpoints ordenados por confianza, **cada uno con su evidencia**:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from nettle import discover_endpoints
|
|
146
|
+
|
|
147
|
+
res = discover_endpoints("https://techcrunch.com/")
|
|
148
|
+
for e in res["endpoints"][:5]:
|
|
149
|
+
print(e["score"], e.get("status"), e["url"], e["evidence"])
|
|
150
|
+
# 22 200 https://techcrunch.com/wp-json/ ['classified-api', 'probe:json-ok', ...]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Qué consulta: llamadas `fetch`/`axios`/`XHR`/`$.ajax` literales en el JavaScript, asignaciones de config (`baseURL = "..."`), JSON embebido, atributos `data-api`/`data-endpoint`, `link[rel=preload]`, descriptores estándar (`/openapi.json`, `/graphql`, `/v3/api-docs`...) y `robots.txt`. Luego verifica los mejores candidatos con un GET barato — un endpoint que responde JSON gana; uno que contesta 401/405 también puntúa, porque demuestra que existe. Con `probe=False` es análisis 100% estático: un solo request.
|
|
154
|
+
|
|
155
|
+
## 4. Tráfico real, como el panel Network de DevTools
|
|
156
|
+
|
|
157
|
+
Las URLs construidas en runtime y las SPAs no aparecen en el HTML. `sniff_network()` abre la página en un Chrome real vía CDP y captura todo lo que pasa por la red:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from nettle import sniff_network
|
|
161
|
+
|
|
162
|
+
tráfico = sniff_network("https://www.bbc.com/news")
|
|
163
|
+
for req in tráfico["xhr_fetch"]:
|
|
164
|
+
print(req["method"], req["url"], req["status"])
|
|
165
|
+
for j in tráfico["json"]:
|
|
166
|
+
print(j["url"], j.get("body", "")[:80])
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
El tráfico lo genera un navegador de verdad — no hay fingerprint de bot que detectar. Nettle lanza su propio navegador headless si no encuentra uno corriendo (perfil aislado, y cierra la pestaña al terminar), y agrupa lo capturado en `xhr_fetch`, `json` y `media`.
|
|
170
|
+
|
|
171
|
+
## 5. JSON escondido en la página
|
|
172
|
+
|
|
173
|
+
Los datos que nunca llegan al DOM: estado de frameworks, JSON-LD, payloads precargados:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from nettle import fetch, sniff_embedded_json
|
|
177
|
+
|
|
178
|
+
doc = fetch("https://cualquier-sitio.com/")
|
|
179
|
+
for blob in sniff_embedded_json(doc):
|
|
180
|
+
print(blob["source"], "→", str(blob["data"])[:100])
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Reconoce `script[type*=json]`, JSON-LD y **cualquier** asignación `nombre = {...}` que parsee como JSON — conoce los globales típicos (`__NEXT_DATA__`, `__NUXT__`, `initialState`, ...) pero no depende de ellos: cualquier framework que metas, lo encuentra.
|
|
184
|
+
|
|
185
|
+
## 6. URLs: encuéntralas, clasifícalas, fíltralas
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from nettle import fetch, find_urls, classify_url, filter_urls
|
|
189
|
+
|
|
190
|
+
doc = fetch("https://example.com/")
|
|
191
|
+
todas = find_urls(doc) # hrefs, srcs, srcset, data-*, JSON-LD, meta og:, refresh...
|
|
192
|
+
apis = find_urls(doc, kind="api") # solo las que parecen endpoints
|
|
193
|
+
media = filter_urls(todas, ext=[".png", ".webp"])
|
|
194
|
+
mias = find_urls(doc, same_host=True)
|
|
195
|
+
|
|
196
|
+
classify_url("https://cdn.example.com/img.webp") # "media"
|
|
197
|
+
classify_url("https://example.com/gql", use_hints=False) # heurísticas off
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Descubre URLs de `href`, `src`, `srcset`, atributos lazy (`data-src`, `data-original`...), meta tags Open Graph, meta refresh y strings dentro de scripts — todo absolutizado y deduplicado.
|
|
201
|
+
|
|
202
|
+
## 7. Exporta sin fricción
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from nettle import to_json, to_csv, write_json, write_csv
|
|
206
|
+
|
|
207
|
+
to_json(libros) # JSON string con unicode legible
|
|
208
|
+
write_csv(libros, "libros.csv") # columnas deducidas de los dicts, UTF-8 garantizado
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Los archivos siempre se escriben en UTF-8 con finales de línea normales — sin sorpresas de encoding en Windows.
|
|
212
|
+
|
|
213
|
+
## 8. Adáptalo a tu sitio — nada está quemado
|
|
214
|
+
|
|
215
|
+
Esta es la promesa central: si tu sitio usa convenciones que Nettle no conoce, **las enseñas tú en runtime**, sin fork ni monkey-patching:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from nettle import registry
|
|
219
|
+
|
|
220
|
+
registry.add_api_hints("/tienda-service/", "/catalogo/") # tus rutas de API
|
|
221
|
+
registry.add_state_globals("MI_APP_STATE") # tu framework
|
|
222
|
+
registry.add_url_keywords("shopApi") # tus configs JS
|
|
223
|
+
registry.add_data_endpoint_attrs("data-x-endpoint") # tus atributos HTML
|
|
224
|
+
registry.add_media_exts(".weirdfmt") # tus formatos
|
|
225
|
+
registry.add_well_known("/api/swagger.json") # tus descriptores
|
|
226
|
+
registry.register_classifier(lambda u: "api" if "/loquesea" in u else None)
|
|
227
|
+
|
|
228
|
+
registry.http.update(timeout=10, retries=1) # defaults HTTP globales
|
|
229
|
+
registry.reset() # volver a fábrica
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Cada heurística de la librería consulta el registry **en cada llamada**, así que tus reglas aplican en todas partes: descubrimiento, clasificación, sniffing, CDP. Tus clasificadores corren antes que los built-in.
|
|
233
|
+
|
|
234
|
+
## 9. Anti-detección integrada
|
|
235
|
+
|
|
236
|
+
`fetch()` y `Session` se presentan como navegador real por defecto: rotación de perfiles Chrome (Windows/Linux/macOS) con cabeceras `User-Agent`, `Sec-Ch-Ua` y `Sec-Fetch-*` coherentes entre sí. Si necesitas control total: `Session(user_agent="...", headers={...})` o `registry.http["user_agent"]` para hacerlo global. Y cuando el sitio exige un navegador de verdad, `sniff_network()` lo ejecuta por ti.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Cross-platform de verdad
|
|
241
|
+
|
|
242
|
+
Nettle es Python 100% puro — el mismo código corre idéntico en:
|
|
243
|
+
|
|
244
|
+
| Sistema | Estado |
|
|
245
|
+
|---|---|
|
|
246
|
+
| Linux | Soportado (desarrollo principal) |
|
|
247
|
+
| Windows | Soportado — rutas de navegador, temp dir y procesos nativos |
|
|
248
|
+
| macOS | Soportado — detecta Chrome/Chromium/Edge/Brave en `/Applications` |
|
|
249
|
+
| Android (Termux) | Soportado — detecta binarios bajo `$PREFIX` |
|
|
250
|
+
|
|
251
|
+
El único componente que toca el sistema es el opcional `sniff_network()`: Nettle encuentra navegadores Chromium en las rutas estándar de cada OS, y si el tuyo vive en un lugar raro, apúntalo con la variable de entorno `NETTLE_CHROME_BIN`. Todo lo demás — parse, select, extract, HTTP, descubrimiento, formato — es stdlib puro y funciona en cualquier parte donde corra Python 3.9+.
|
|
252
|
+
|
|
253
|
+
## Preguntas frecuentes
|
|
254
|
+
|
|
255
|
+
**¿Necesito instalar Chrome?** No. Solo para `sniff_network()` (captura de tráfico real). Todo lo demás funciona con Python solo.
|
|
256
|
+
|
|
257
|
+
**¿Qué dependencias instala?** Cero. Ni lxml, ni requests, ni bs4. Todo es stdlib — auditable, liviano y sin conflictos de versiones.
|
|
258
|
+
|
|
259
|
+
**¿Sirve para SPAs (React/Vue/Svelte)?** Sí: `discover_endpoints()` y `sniff_embedded_json()` encuentran los datos precargados, y `sniff_network()` captura el tráfico del navegador para lo que se carga dinámicamente.
|
|
260
|
+
|
|
261
|
+
**¿Me van a bloquear como bot?** El cliente HTTP imita navegadores reales por defecto (perfiles rotativos, cabeceras coherentes), y `sniff_network()` usa un navegador de verdad, así que no hay fingerprint de bot. Los sitios con protección extrema pueden seguir filtrando — para esos, el tráfico de navegador real es tu mejor arma.
|
|
262
|
+
|
|
263
|
+
**¿Licencia?** MIT — gratis para cualquier uso, comercial incluido. Ver [LICENSE](LICENSE).
|
|
264
|
+
|
|
265
|
+
## API en una mirada
|
|
266
|
+
|
|
267
|
+
| Quiero... | Usa |
|
|
268
|
+
|---|---|
|
|
269
|
+
| Parsear HTML | `parse(html)` o `fetch(url)` |
|
|
270
|
+
| Extraer datos limpios | `doc.extract(esquema)`, `doc.record()`, `doc.values()` |
|
|
271
|
+
| Tablas | `doc.table(selector)` |
|
|
272
|
+
| Llamar un endpoint | `request(método, url, json=...)`, `call_endpoint()` |
|
|
273
|
+
| Descubrir endpoints | `discover_endpoints(url)` |
|
|
274
|
+
| Ver tráfico real | `sniff_network(url)` |
|
|
275
|
+
| JSON embebido | `sniff_embedded_json(doc)` |
|
|
276
|
+
| URLs | `find_urls()`, `classify_url()`, `filter_urls()` |
|
|
277
|
+
| Exportar | `to_json()`, `to_csv()`, `write_csv()` |
|
|
278
|
+
| Enseñar mis reglas | `nettle.registry` |
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Nettle
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/nettle-html/)
|
|
4
|
+
[](https://pypi.org/project/nettle-html/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**El toolkit de scraping que Python esperaba.** Una sola librería para parsear HTML, extraer datos limpios, llamar cualquier endpoint, descubrir APIs ocultas y sniffiear tráfico real de navegador — con **cero dependencias externas**.
|
|
8
|
+
|
|
9
|
+
Nettle existe porque el scraping real no termina en "seleccionar un nodo": termina peleando con `\xa0`, entidades crudas, JSON escondido en scripts, endpoints ocultos en JavaScript y sitios que te bloquean por parecer bot. Nettle resuelve **todo el pipeline**, no solo el primer paso.
|
|
10
|
+
|
|
11
|
+
- **Corre en todas partes**: Windows, Linux, macOS y Android (Termux). Python puro + stdlib, sin compilaciones ni binarios raros. El sniffing con navegador encuentra solo tu Chrome/Chromium/Edge/Brave en cualquier sistema.
|
|
12
|
+
- **Gratis y libre**: licencia MIT, uso comercial incluido.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from nettle import fetch
|
|
16
|
+
|
|
17
|
+
doc = fetch("https://quotes.toscrape.com/")
|
|
18
|
+
data = doc.extract({
|
|
19
|
+
"quotes": {
|
|
20
|
+
"select": "div.quote",
|
|
21
|
+
"each": {
|
|
22
|
+
"text": {"css": "span.text", "clean": "plain"},
|
|
23
|
+
"author": {"css": "small.author", "clean": "plain"},
|
|
24
|
+
"tags": {"css": "a.tag", "all": True, "clean": "plain"},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Eso es todo. Sin `replace("\xa0", " ")`, sin `html.unescape`, sin `re.sub(r"\s+", ...)` por cada campo, sin armar dicts a mano. **Texto sucio entra, datos limpios salen.**
|
|
31
|
+
|
|
32
|
+
- Antes: `"Hello\xa0world's & friends"`
|
|
33
|
+
- Con Nettle: `"Hello world's & friends"`
|
|
34
|
+
|
|
35
|
+
## Instalación
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install nettle-html
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
O desde el código fuente:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/ldikay99/nettle.git
|
|
45
|
+
pip install ./nettle
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requiere **Python 3.9 o superior** y nada más — `pip install nettle-html` no instala una sola dependencia. Opcional: un navegador basado en Chromium (Chrome, Edge, Brave) si quieres capturar tráfico de red real; Nettle lo detecta solo en tu sistema.
|
|
49
|
+
|
|
50
|
+
## Por qué Nettle y no BeautifulSoup
|
|
51
|
+
|
|
52
|
+
| Dolor con BS4 + requests | Nettle |
|
|
53
|
+
|---|---|
|
|
54
|
+
| Texto sucio (`\xa0`, entidades, whitespace loco) — limpias a mano por cada campo | Limpieza integrada: `clean_text()` y `clean: "plain"` en cada extracción |
|
|
55
|
+
| Soup no habla HTTP — necesitas `requests` aparte | Cliente HTTP propio: `fetch()`, `request()`, sesiones con cookies y reintentos |
|
|
56
|
+
| Nada de endpoints — solo ves el HTML renderizado | `discover_endpoints()` los encuentra y verifica en una llamada |
|
|
57
|
+
| No ves el tráfico que genera la página | `sniff_network()` captura XHR/fetch con un Chrome real, como DevTools |
|
|
58
|
+
| Fingerprints de bot detectados | Rotación de perfiles de navegador reales con cabeceras `Sec-Ch-Ua`/`Sec-Fetch` coherentes |
|
|
59
|
+
| JSON embebido hay que sacarlo con regex frágiles | `sniff_embedded_json()` extrae cualquier `variable = {...}` que parsee como JSON |
|
|
60
|
+
| CSV/JSON los armas tú | `to_json()`, `to_csv()`, `to_dicts()` listos |
|
|
61
|
+
| Heurísticas fijas — si tu sitio no encaja, sufres | `nettle.registry`: enseñas tus convenciones en runtime, sin fork |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 1. Scrape declarativo — describe el dato, no el proceso
|
|
66
|
+
|
|
67
|
+
`doc.extract(esquema)` mapea selectores CSS a diccionarios ya limpios. Anida, itera registros, saca atributos, absolutiza URLs:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from nettle import fetch
|
|
71
|
+
|
|
72
|
+
doc = fetch("https://books.toscrape.com/")
|
|
73
|
+
libros = doc.extract({
|
|
74
|
+
"libros": {
|
|
75
|
+
"select": "article.product_pod",
|
|
76
|
+
"each": {
|
|
77
|
+
"titulo": {"css": "h3 a", "attr": "title"},
|
|
78
|
+
"precio": {"css": ".price_color", "clean": "plain"},
|
|
79
|
+
"stock": {"css": ".instock.availability", "clean": "plain"},
|
|
80
|
+
"link": {"css": "h3 a", "attr": "href", "abs": True},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
})["libros"]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Cada campo acepta `attr` (o lista de atributos fallback tipo `["data-src", "src"]` para imágenes lazy), `all=True` para listas, `abs=True` para URLs absolutas, `default=` para valores por defecto y `clean=` con los modos `plain`, `strict`, `keep_newlines` o `raw`.
|
|
87
|
+
|
|
88
|
+
Atajos rápidos: `doc.values("h1", ".precio")` para varios textos de una, `doc.record({...})` por elemento, `doc.table("table")` para tablas HTML → lista de dicts, `doc.lists()` para listas con items.
|
|
89
|
+
|
|
90
|
+
## 2. HTTP directo — cualquier método, cualquier endpoint
|
|
91
|
+
|
|
92
|
+
Si ya tienes la URL, la llamas. Nettle no asume rutas ni exige descubrir nada:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from nettle import request, call_endpoint
|
|
96
|
+
|
|
97
|
+
request("POST", "https://tienda.example/catalog/load", json={"q": "zapatos"})
|
|
98
|
+
request("PUT", "https://tienda.example/items/42", json={"precio": 10})
|
|
99
|
+
call_endpoint("https://tienda.example/items/42", "DELETE")
|
|
100
|
+
call_endpoint("https://tienda.example/search", "GET", params={"page": 2})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS — con JSON, form-data, bytes o texto. La respuesta trae `.text`, `.json()`, `.status`, `.headers`, `.ok` y `.doc` (el HTML ya parseado). Los errores HTTP (401, 404, 500…) devuelven la respuesta para inspeccionarla; los fallos de red lanzan `FetchError` con reintentos y backoff exponencial incluidos.
|
|
104
|
+
|
|
105
|
+
Para varias llamadas relacionadas, `Session` mantiene cookies entre requests y hereda tus defaults globales.
|
|
106
|
+
|
|
107
|
+
## 3. Descubre endpoints con una llamada
|
|
108
|
+
|
|
109
|
+
No sabes dónde está la API? Pásale la página y Nettle te devuelve los endpoints ordenados por confianza, **cada uno con su evidencia**:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from nettle import discover_endpoints
|
|
113
|
+
|
|
114
|
+
res = discover_endpoints("https://techcrunch.com/")
|
|
115
|
+
for e in res["endpoints"][:5]:
|
|
116
|
+
print(e["score"], e.get("status"), e["url"], e["evidence"])
|
|
117
|
+
# 22 200 https://techcrunch.com/wp-json/ ['classified-api', 'probe:json-ok', ...]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Qué consulta: llamadas `fetch`/`axios`/`XHR`/`$.ajax` literales en el JavaScript, asignaciones de config (`baseURL = "..."`), JSON embebido, atributos `data-api`/`data-endpoint`, `link[rel=preload]`, descriptores estándar (`/openapi.json`, `/graphql`, `/v3/api-docs`...) y `robots.txt`. Luego verifica los mejores candidatos con un GET barato — un endpoint que responde JSON gana; uno que contesta 401/405 también puntúa, porque demuestra que existe. Con `probe=False` es análisis 100% estático: un solo request.
|
|
121
|
+
|
|
122
|
+
## 4. Tráfico real, como el panel Network de DevTools
|
|
123
|
+
|
|
124
|
+
Las URLs construidas en runtime y las SPAs no aparecen en el HTML. `sniff_network()` abre la página en un Chrome real vía CDP y captura todo lo que pasa por la red:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from nettle import sniff_network
|
|
128
|
+
|
|
129
|
+
tráfico = sniff_network("https://www.bbc.com/news")
|
|
130
|
+
for req in tráfico["xhr_fetch"]:
|
|
131
|
+
print(req["method"], req["url"], req["status"])
|
|
132
|
+
for j in tráfico["json"]:
|
|
133
|
+
print(j["url"], j.get("body", "")[:80])
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
El tráfico lo genera un navegador de verdad — no hay fingerprint de bot que detectar. Nettle lanza su propio navegador headless si no encuentra uno corriendo (perfil aislado, y cierra la pestaña al terminar), y agrupa lo capturado en `xhr_fetch`, `json` y `media`.
|
|
137
|
+
|
|
138
|
+
## 5. JSON escondido en la página
|
|
139
|
+
|
|
140
|
+
Los datos que nunca llegan al DOM: estado de frameworks, JSON-LD, payloads precargados:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from nettle import fetch, sniff_embedded_json
|
|
144
|
+
|
|
145
|
+
doc = fetch("https://cualquier-sitio.com/")
|
|
146
|
+
for blob in sniff_embedded_json(doc):
|
|
147
|
+
print(blob["source"], "→", str(blob["data"])[:100])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Reconoce `script[type*=json]`, JSON-LD y **cualquier** asignación `nombre = {...}` que parsee como JSON — conoce los globales típicos (`__NEXT_DATA__`, `__NUXT__`, `initialState`, ...) pero no depende de ellos: cualquier framework que metas, lo encuentra.
|
|
151
|
+
|
|
152
|
+
## 6. URLs: encuéntralas, clasifícalas, fíltralas
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from nettle import fetch, find_urls, classify_url, filter_urls
|
|
156
|
+
|
|
157
|
+
doc = fetch("https://example.com/")
|
|
158
|
+
todas = find_urls(doc) # hrefs, srcs, srcset, data-*, JSON-LD, meta og:, refresh...
|
|
159
|
+
apis = find_urls(doc, kind="api") # solo las que parecen endpoints
|
|
160
|
+
media = filter_urls(todas, ext=[".png", ".webp"])
|
|
161
|
+
mias = find_urls(doc, same_host=True)
|
|
162
|
+
|
|
163
|
+
classify_url("https://cdn.example.com/img.webp") # "media"
|
|
164
|
+
classify_url("https://example.com/gql", use_hints=False) # heurísticas off
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Descubre URLs de `href`, `src`, `srcset`, atributos lazy (`data-src`, `data-original`...), meta tags Open Graph, meta refresh y strings dentro de scripts — todo absolutizado y deduplicado.
|
|
168
|
+
|
|
169
|
+
## 7. Exporta sin fricción
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from nettle import to_json, to_csv, write_json, write_csv
|
|
173
|
+
|
|
174
|
+
to_json(libros) # JSON string con unicode legible
|
|
175
|
+
write_csv(libros, "libros.csv") # columnas deducidas de los dicts, UTF-8 garantizado
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Los archivos siempre se escriben en UTF-8 con finales de línea normales — sin sorpresas de encoding en Windows.
|
|
179
|
+
|
|
180
|
+
## 8. Adáptalo a tu sitio — nada está quemado
|
|
181
|
+
|
|
182
|
+
Esta es la promesa central: si tu sitio usa convenciones que Nettle no conoce, **las enseñas tú en runtime**, sin fork ni monkey-patching:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from nettle import registry
|
|
186
|
+
|
|
187
|
+
registry.add_api_hints("/tienda-service/", "/catalogo/") # tus rutas de API
|
|
188
|
+
registry.add_state_globals("MI_APP_STATE") # tu framework
|
|
189
|
+
registry.add_url_keywords("shopApi") # tus configs JS
|
|
190
|
+
registry.add_data_endpoint_attrs("data-x-endpoint") # tus atributos HTML
|
|
191
|
+
registry.add_media_exts(".weirdfmt") # tus formatos
|
|
192
|
+
registry.add_well_known("/api/swagger.json") # tus descriptores
|
|
193
|
+
registry.register_classifier(lambda u: "api" if "/loquesea" in u else None)
|
|
194
|
+
|
|
195
|
+
registry.http.update(timeout=10, retries=1) # defaults HTTP globales
|
|
196
|
+
registry.reset() # volver a fábrica
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Cada heurística de la librería consulta el registry **en cada llamada**, así que tus reglas aplican en todas partes: descubrimiento, clasificación, sniffing, CDP. Tus clasificadores corren antes que los built-in.
|
|
200
|
+
|
|
201
|
+
## 9. Anti-detección integrada
|
|
202
|
+
|
|
203
|
+
`fetch()` y `Session` se presentan como navegador real por defecto: rotación de perfiles Chrome (Windows/Linux/macOS) con cabeceras `User-Agent`, `Sec-Ch-Ua` y `Sec-Fetch-*` coherentes entre sí. Si necesitas control total: `Session(user_agent="...", headers={...})` o `registry.http["user_agent"]` para hacerlo global. Y cuando el sitio exige un navegador de verdad, `sniff_network()` lo ejecuta por ti.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Cross-platform de verdad
|
|
208
|
+
|
|
209
|
+
Nettle es Python 100% puro — el mismo código corre idéntico en:
|
|
210
|
+
|
|
211
|
+
| Sistema | Estado |
|
|
212
|
+
|---|---|
|
|
213
|
+
| Linux | Soportado (desarrollo principal) |
|
|
214
|
+
| Windows | Soportado — rutas de navegador, temp dir y procesos nativos |
|
|
215
|
+
| macOS | Soportado — detecta Chrome/Chromium/Edge/Brave en `/Applications` |
|
|
216
|
+
| Android (Termux) | Soportado — detecta binarios bajo `$PREFIX` |
|
|
217
|
+
|
|
218
|
+
El único componente que toca el sistema es el opcional `sniff_network()`: Nettle encuentra navegadores Chromium en las rutas estándar de cada OS, y si el tuyo vive en un lugar raro, apúntalo con la variable de entorno `NETTLE_CHROME_BIN`. Todo lo demás — parse, select, extract, HTTP, descubrimiento, formato — es stdlib puro y funciona en cualquier parte donde corra Python 3.9+.
|
|
219
|
+
|
|
220
|
+
## Preguntas frecuentes
|
|
221
|
+
|
|
222
|
+
**¿Necesito instalar Chrome?** No. Solo para `sniff_network()` (captura de tráfico real). Todo lo demás funciona con Python solo.
|
|
223
|
+
|
|
224
|
+
**¿Qué dependencias instala?** Cero. Ni lxml, ni requests, ni bs4. Todo es stdlib — auditable, liviano y sin conflictos de versiones.
|
|
225
|
+
|
|
226
|
+
**¿Sirve para SPAs (React/Vue/Svelte)?** Sí: `discover_endpoints()` y `sniff_embedded_json()` encuentran los datos precargados, y `sniff_network()` captura el tráfico del navegador para lo que se carga dinámicamente.
|
|
227
|
+
|
|
228
|
+
**¿Me van a bloquear como bot?** El cliente HTTP imita navegadores reales por defecto (perfiles rotativos, cabeceras coherentes), y `sniff_network()` usa un navegador de verdad, así que no hay fingerprint de bot. Los sitios con protección extrema pueden seguir filtrando — para esos, el tráfico de navegador real es tu mejor arma.
|
|
229
|
+
|
|
230
|
+
**¿Licencia?** MIT — gratis para cualquier uso, comercial incluido. Ver [LICENSE](LICENSE).
|
|
231
|
+
|
|
232
|
+
## API en una mirada
|
|
233
|
+
|
|
234
|
+
| Quiero... | Usa |
|
|
235
|
+
|---|---|
|
|
236
|
+
| Parsear HTML | `parse(html)` o `fetch(url)` |
|
|
237
|
+
| Extraer datos limpios | `doc.extract(esquema)`, `doc.record()`, `doc.values()` |
|
|
238
|
+
| Tablas | `doc.table(selector)` |
|
|
239
|
+
| Llamar un endpoint | `request(método, url, json=...)`, `call_endpoint()` |
|
|
240
|
+
| Descubrir endpoints | `discover_endpoints(url)` |
|
|
241
|
+
| Ver tráfico real | `sniff_network(url)` |
|
|
242
|
+
| JSON embebido | `sniff_embedded_json(doc)` |
|
|
243
|
+
| URLs | `find_urls()`, `classify_url()`, `filter_urls()` |
|
|
244
|
+
| Exportar | `to_json()`, `to_csv()`, `write_csv()` |
|
|
245
|
+
| Enseñar mis reglas | `nettle.registry` |
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Nettle — pure-Python HTML toolkit for clean scrape pipelines.
|
|
2
|
+
|
|
3
|
+
Stdlib only. Parse, CSS-select, clean text, declarative extract, tables,
|
|
4
|
+
URL discovery, embedded-JSON sniffing, and direct HTTP to any endpoint.
|
|
5
|
+
|
|
6
|
+
from nettle import parse, fetch, request, call_endpoint
|
|
7
|
+
|
|
8
|
+
# scrape HTML
|
|
9
|
+
doc = parse(html)
|
|
10
|
+
data = doc.extract({...})
|
|
11
|
+
|
|
12
|
+
# you already have an endpoint — just call it (any method, any path)
|
|
13
|
+
request("POST", "https://shop.example/catalog/load", json={"q": "shoes"})
|
|
14
|
+
call_endpoint("https://shop.example/items/42", "DELETE")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .nodes import Comment, Document, Element, Node, Text
|
|
18
|
+
from .parse import parse, detect_charset
|
|
19
|
+
from .soup import Nettle
|
|
20
|
+
from .text import (
|
|
21
|
+
clean_text,
|
|
22
|
+
collapse_ws,
|
|
23
|
+
decode_entities,
|
|
24
|
+
normalize_unicode,
|
|
25
|
+
remove_invisible,
|
|
26
|
+
strip_noise,
|
|
27
|
+
)
|
|
28
|
+
from .extract import fields, records, table, lists, links, meta, values
|
|
29
|
+
from .clean import CleanPipeline, clean_tree
|
|
30
|
+
from .format import to_json, to_csv, to_tsv, to_dicts, pretty, write_json, write_csv
|
|
31
|
+
from .query import extract, ExtractQuery
|
|
32
|
+
from .http import fetch, fetch_response, fetch_html, Session, Response, request, call
|
|
33
|
+
from .urls import find_urls, classify_url, filter_urls, absolutize
|
|
34
|
+
from .network import sniff_embedded_json, sniff_api_candidates, probe_apis, har_from_cdp, call_endpoint
|
|
35
|
+
from .cdp import sniff_network, CDPSession, list_targets, ensure_debugging_chrome, find_debugging_port
|
|
36
|
+
from .discover import discover_endpoints
|
|
37
|
+
from .registry import registry
|
|
38
|
+
from .serialize import html as serialize_html, prettify
|
|
39
|
+
from .exceptions import (
|
|
40
|
+
NettleError,
|
|
41
|
+
ParseError,
|
|
42
|
+
SelectorError,
|
|
43
|
+
ExtractError,
|
|
44
|
+
FetchError,
|
|
45
|
+
FormatError,
|
|
46
|
+
JsonBodyError,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
# core
|
|
51
|
+
"parse",
|
|
52
|
+
"detect_charset",
|
|
53
|
+
"Nettle",
|
|
54
|
+
"Node",
|
|
55
|
+
"Element",
|
|
56
|
+
"Text",
|
|
57
|
+
"Comment",
|
|
58
|
+
"Document",
|
|
59
|
+
# text
|
|
60
|
+
"clean_text",
|
|
61
|
+
"collapse_ws",
|
|
62
|
+
"decode_entities",
|
|
63
|
+
"normalize_unicode",
|
|
64
|
+
"remove_invisible",
|
|
65
|
+
"strip_noise",
|
|
66
|
+
# extract
|
|
67
|
+
"fields",
|
|
68
|
+
"records",
|
|
69
|
+
"table",
|
|
70
|
+
"lists",
|
|
71
|
+
"links",
|
|
72
|
+
"meta",
|
|
73
|
+
"values",
|
|
74
|
+
"extract",
|
|
75
|
+
"ExtractQuery",
|
|
76
|
+
# clean / format
|
|
77
|
+
"CleanPipeline",
|
|
78
|
+
"clean_tree",
|
|
79
|
+
"to_json",
|
|
80
|
+
"to_csv",
|
|
81
|
+
"to_tsv",
|
|
82
|
+
"to_dicts",
|
|
83
|
+
"pretty",
|
|
84
|
+
"write_json",
|
|
85
|
+
"write_csv",
|
|
86
|
+
# http / urls / network
|
|
87
|
+
"fetch",
|
|
88
|
+
"fetch_response",
|
|
89
|
+
"fetch_html",
|
|
90
|
+
"request",
|
|
91
|
+
"call",
|
|
92
|
+
"Session",
|
|
93
|
+
"Response",
|
|
94
|
+
"find_urls",
|
|
95
|
+
"classify_url",
|
|
96
|
+
"filter_urls",
|
|
97
|
+
"absolutize",
|
|
98
|
+
"sniff_embedded_json",
|
|
99
|
+
"sniff_api_candidates",
|
|
100
|
+
"probe_apis",
|
|
101
|
+
"call_endpoint",
|
|
102
|
+
"har_from_cdp",
|
|
103
|
+
"discover_endpoints",
|
|
104
|
+
"registry",
|
|
105
|
+
"sniff_network",
|
|
106
|
+
"ensure_debugging_chrome",
|
|
107
|
+
"find_debugging_port",
|
|
108
|
+
"CDPSession",
|
|
109
|
+
"list_targets",
|
|
110
|
+
# serialize
|
|
111
|
+
"serialize_html",
|
|
112
|
+
"prettify",
|
|
113
|
+
# errors
|
|
114
|
+
"NettleError",
|
|
115
|
+
"ParseError",
|
|
116
|
+
"SelectorError",
|
|
117
|
+
"ExtractError",
|
|
118
|
+
"FetchError",
|
|
119
|
+
"FormatError",
|
|
120
|
+
"JsonBodyError",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
__version__ = "0.5.0"
|