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.
Files changed (42) hide show
  1. nettle_html-0.5.0/LICENSE +21 -0
  2. nettle_html-0.5.0/PKG-INFO +278 -0
  3. nettle_html-0.5.0/README.md +245 -0
  4. nettle_html-0.5.0/nettle/__init__.py +123 -0
  5. nettle_html-0.5.0/nettle/cdp.py +521 -0
  6. nettle_html-0.5.0/nettle/clean.py +90 -0
  7. nettle_html-0.5.0/nettle/compat.py +32 -0
  8. nettle_html-0.5.0/nettle/css.py +539 -0
  9. nettle_html-0.5.0/nettle/discover.py +225 -0
  10. nettle_html-0.5.0/nettle/exceptions.py +36 -0
  11. nettle_html-0.5.0/nettle/extract.py +168 -0
  12. nettle_html-0.5.0/nettle/format.py +137 -0
  13. nettle_html-0.5.0/nettle/http.py +468 -0
  14. nettle_html-0.5.0/nettle/network.py +533 -0
  15. nettle_html-0.5.0/nettle/nodes.py +462 -0
  16. nettle_html-0.5.0/nettle/parse.py +502 -0
  17. nettle_html-0.5.0/nettle/py.typed +0 -0
  18. nettle_html-0.5.0/nettle/query.py +149 -0
  19. nettle_html-0.5.0/nettle/registry.py +226 -0
  20. nettle_html-0.5.0/nettle/serialize.py +81 -0
  21. nettle_html-0.5.0/nettle/soup.py +127 -0
  22. nettle_html-0.5.0/nettle/table.py +155 -0
  23. nettle_html-0.5.0/nettle/text.py +240 -0
  24. nettle_html-0.5.0/nettle/urls.py +285 -0
  25. nettle_html-0.5.0/nettle_html.egg-info/PKG-INFO +278 -0
  26. nettle_html-0.5.0/nettle_html.egg-info/SOURCES.txt +40 -0
  27. nettle_html-0.5.0/nettle_html.egg-info/dependency_links.txt +1 -0
  28. nettle_html-0.5.0/nettle_html.egg-info/top_level.txt +1 -0
  29. nettle_html-0.5.0/pyproject.toml +53 -0
  30. nettle_html-0.5.0/setup.cfg +4 -0
  31. nettle_html-0.5.0/tests/test_api.py +66 -0
  32. nettle_html-0.5.0/tests/test_crossplatform_fixes.py +103 -0
  33. nettle_html-0.5.0/tests/test_css.py +137 -0
  34. nettle_html-0.5.0/tests/test_css_extra.py +57 -0
  35. nettle_html-0.5.0/tests/test_extract_query.py +81 -0
  36. nettle_html-0.5.0/tests/test_format_table.py +69 -0
  37. nettle_html-0.5.0/tests/test_http_clean.py +95 -0
  38. nettle_html-0.5.0/tests/test_parse.py +171 -0
  39. nettle_html-0.5.0/tests/test_registry_discover.py +108 -0
  40. nettle_html-0.5.0/tests/test_text.py +80 -0
  41. nettle_html-0.5.0/tests/test_transversal.py +208 -0
  42. 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
+ [![PyPI](https://img.shields.io/pypi/v/nettle-html)](https://pypi.org/project/nettle-html/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/nettle-html)](https://pypi.org/project/nettle-html/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/nettle-html)](https://pypi.org/project/nettle-html/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/nettle-html)](https://pypi.org/project/nettle-html/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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"