webreader 2.3.8__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.
- webreader-2.3.8/LICENSE +21 -0
- webreader-2.3.8/PKG-INFO +188 -0
- webreader-2.3.8/README.md +164 -0
- webreader-2.3.8/pyproject.toml +44 -0
- webreader-2.3.8/setup.cfg +4 -0
- webreader-2.3.8/setup.py +6 -0
- webreader-2.3.8/webreader/__init__.py +165 -0
- webreader-2.3.8/webreader/__main__.py +6 -0
- webreader-2.3.8/webreader/cli.py +166 -0
- webreader-2.3.8/webreader/core.py +501 -0
- webreader-2.3.8/webreader/default.config +0 -0
- webreader-2.3.8/webreader/extractors.py +447 -0
- webreader-2.3.8/webreader/formatter.py +89 -0
- webreader-2.3.8/webreader/markdown.py +237 -0
- webreader-2.3.8/webreader/sanitizer.py +117 -0
- webreader-2.3.8/webreader/selectors.py +462 -0
- webreader-2.3.8/webreader.egg-info/PKG-INFO +188 -0
- webreader-2.3.8/webreader.egg-info/SOURCES.txt +19 -0
- webreader-2.3.8/webreader.egg-info/dependency_links.txt +1 -0
- webreader-2.3.8/webreader.egg-info/requires.txt +5 -0
- webreader-2.3.8/webreader.egg-info/top_level.txt +1 -0
webreader-2.3.8/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webreader
|
|
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.
|
webreader-2.3.8/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webreader
|
|
3
|
+
Version: 2.3.8
|
|
4
|
+
Summary: Tree-based HTML reader for Python.
|
|
5
|
+
Author-email: morichan <morichan@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: pafer
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Tree-based parsing into an editable document tree
|
|
32
|
+
- CSS selectors: combinators, attributes, pseudo-classes, selector lists
|
|
33
|
+
- Extractors: metadata, links, tables, text, images, forms, headings,
|
|
34
|
+
scripts, stylesheets
|
|
35
|
+
- Sanitization with a removal report
|
|
36
|
+
- Minify, pretty-print, Markdown conversion
|
|
37
|
+
- JSON / CSV / Markdown export
|
|
38
|
+
- CLI tool
|
|
39
|
+
- Standard library only at runtime
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install webreader
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import webreader
|
|
51
|
+
|
|
52
|
+
html = '<h1 class="title">Hello</h1><a href="/x">link</a>'
|
|
53
|
+
doc = webreader.parse(html)
|
|
54
|
+
|
|
55
|
+
webreader.select(doc, 'h1.title') # [{'tag': ..., 'attrs': ..., 'text': ..., 'html': ...}]
|
|
56
|
+
doc.find('a').get_attr('href') # '/x'
|
|
57
|
+
|
|
58
|
+
webreader.minify(html) # collapsed whitespace
|
|
59
|
+
webreader.pretty(html) # re-indented, readable markup
|
|
60
|
+
webreader.to_markdown(html) # convert to Markdown
|
|
61
|
+
|
|
62
|
+
clean, report = webreader.sanitize(html)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Bytes input is decoded using detected encoding: BOM sniffing first,
|
|
66
|
+
then `<meta charset>` in the first 2 KB, then the configured
|
|
67
|
+
`encoding` fallback.
|
|
68
|
+
|
|
69
|
+
In non-strict mode, malformed HTML does not raise; the warnings are
|
|
70
|
+
available on the document:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
doc = webreader.parse('<div><p>x</p>') # unclosed <div>
|
|
74
|
+
doc.errors # ['unclosed tag(s) at end of input: <div>']
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Extraction
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
webreader.extract_metadata(html) # title, OG/Twitter, canonical, charset, JSON-LD
|
|
81
|
+
webreader.extract_links(html) # links and asset references
|
|
82
|
+
webreader.extract_tables(html) # matrices with colspan/rowspan expanded
|
|
83
|
+
webreader.extract_text(html) # readable text without boilerplate
|
|
84
|
+
webreader.extract_images(html) # src, alt, dimensions, srcset candidates
|
|
85
|
+
webreader.extract_forms(html) # form fields with types and defaults
|
|
86
|
+
webreader.extract_headings(html) # h1-h6 outline + word count + reading time
|
|
87
|
+
webreader.extract_scripts(html) # inline and external script inventory
|
|
88
|
+
webreader.extract_stylesheets(html) # link[rel~=stylesheet] and inline styles
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## CSS selectors
|
|
92
|
+
|
|
93
|
+
Supported syntax: type selectors, `*`, `#id`, `.class`, attribute
|
|
94
|
+
selectors (`[attr]`, `[attr=value]` and the `^=`, `$=`, `*=`, `~=`,
|
|
95
|
+
`|=` operators), the pseudo-classes `:first-child`, `:last-child`,
|
|
96
|
+
`:nth-child(an+b)`, `:not(compound)` and `:contains(text)`, compounds
|
|
97
|
+
such as `div.content#main[href="x"]`, the combinators whitespace
|
|
98
|
+
(descendant), `>` (child), `+` (adjacent sibling) and `~` (general
|
|
99
|
+
sibling), and comma-separated selector lists.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
webreader.select(doc, 'a[href^="https://"]')
|
|
103
|
+
webreader.select(doc, 'ul > li:nth-child(2n+1):not(.skip)')
|
|
104
|
+
webreader.select(doc, 'h2 + p, blockquote p:contains("note")')
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Invalid or unsupported selectors raise `SelectorSyntaxError`.
|
|
108
|
+
|
|
109
|
+
## Tree editing
|
|
110
|
+
|
|
111
|
+
Nodes support a mutation API, and the edited tree can be
|
|
112
|
+
re-serialized:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
doc = webreader.parse('<div><a href="/x">link</a></div>')
|
|
116
|
+
link = doc.find('a')
|
|
117
|
+
link.set_attr('rel', 'noopener').remove_attr('class')
|
|
118
|
+
|
|
119
|
+
new = webreader.Node('p')
|
|
120
|
+
new.children.append('added text')
|
|
121
|
+
doc.find('div').append_child(new)
|
|
122
|
+
|
|
123
|
+
link.unwrap() # replace the <a> with its inner text
|
|
124
|
+
doc.to_html() # '<div>link<p>added text</p></div>'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`Node` also provides `remove()` and `replace_with(*nodes)`.
|
|
128
|
+
|
|
129
|
+
## Markdown conversion
|
|
130
|
+
|
|
131
|
+
`to_markdown` renders headings, paragraphs, links, images,
|
|
132
|
+
emphasis/strong/strikethrough, inline and fenced code (with language
|
|
133
|
+
detection from `class="language-..."`), nested lists, blockquotes,
|
|
134
|
+
horizontal rules and tables.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
webreader.to_markdown('<h1>T</h1><p>Hi <b>bold</b></p>')
|
|
138
|
+
# '# T\n\nHi **bold**'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Sanitization
|
|
142
|
+
|
|
143
|
+
`sanitize()` returns `(clean_html, report)`:
|
|
144
|
+
|
|
145
|
+
- tags outside the whitelist are unwrapped, inner content kept
|
|
146
|
+
- `script`, `style`, `noscript`, `template` are dropped entirely
|
|
147
|
+
- attributes filtered through a safe-list, `on*` handlers removed
|
|
148
|
+
- `javascript:`, `vbscript:` and non-image `data:` URLs blocked
|
|
149
|
+
- the report lists removed tags/attributes, blocked URLs and detected threats
|
|
150
|
+
|
|
151
|
+
## CLI
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
python -m webreader parse -f input.html
|
|
155
|
+
python -m webreader select -f input.html -s 'a[href]'
|
|
156
|
+
python -m webreader links -f input.html --base-url https://example.com
|
|
157
|
+
python -m webreader meta -f input.html
|
|
158
|
+
python -m webreader tables -f input.html --format csv
|
|
159
|
+
python -m webreader text -f input.html
|
|
160
|
+
python -m webreader images -f input.html
|
|
161
|
+
python -m webreader forms -f input.html
|
|
162
|
+
python -m webreader minify -f input.html --out min.html
|
|
163
|
+
python -m webreader pretty -f input.html --out pretty.html
|
|
164
|
+
python -m webreader md -f input.html --out page.md
|
|
165
|
+
python -m webreader sanitize -f input.html --out clean.html --allowed p,a,ul
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Use `-f -` to read from stdin. Every output command accepts `--out`
|
|
169
|
+
to write to a file instead of stdout.
|
|
170
|
+
|
|
171
|
+
### Default settings
|
|
172
|
+
|
|
173
|
+
| Key | Default | Purpose |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `strip_whitespace` | `True` | collapse whitespace in text nodes |
|
|
176
|
+
| `encoding` | `utf-8` | fallback when bytes input has no detectable encoding |
|
|
177
|
+
| `max_depth` | `100` | maximum element nesting |
|
|
178
|
+
| `preserve_comments` | `False` | keep comments in the tree |
|
|
179
|
+
| `strict_mode` | `False` | raise on malformed HTML |
|
|
180
|
+
| `remove_comments` | `True` | strip comments when minifying |
|
|
181
|
+
| `allowed_tags` | `p, br, b, i, a, ul, ol, li, div, span` | sanitize whitelist |
|
|
182
|
+
| `max_file_size_mb` | `10` | reject oversized input |
|
|
183
|
+
| `cache_size` | `50` | parse cache capacity |
|
|
184
|
+
|
|
185
|
+
## Exceptions
|
|
186
|
+
|
|
187
|
+
`HTMLPaferError` (base), `ConfigLoadError`, `SanitizationError`,
|
|
188
|
+
`SelectorSyntaxError`.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Tree-based parsing into an editable document tree
|
|
8
|
+
- CSS selectors: combinators, attributes, pseudo-classes, selector lists
|
|
9
|
+
- Extractors: metadata, links, tables, text, images, forms, headings,
|
|
10
|
+
scripts, stylesheets
|
|
11
|
+
- Sanitization with a removal report
|
|
12
|
+
- Minify, pretty-print, Markdown conversion
|
|
13
|
+
- JSON / CSV / Markdown export
|
|
14
|
+
- CLI tool
|
|
15
|
+
- Standard library only at runtime
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install webreader
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import webreader
|
|
27
|
+
|
|
28
|
+
html = '<h1 class="title">Hello</h1><a href="/x">link</a>'
|
|
29
|
+
doc = webreader.parse(html)
|
|
30
|
+
|
|
31
|
+
webreader.select(doc, 'h1.title') # [{'tag': ..., 'attrs': ..., 'text': ..., 'html': ...}]
|
|
32
|
+
doc.find('a').get_attr('href') # '/x'
|
|
33
|
+
|
|
34
|
+
webreader.minify(html) # collapsed whitespace
|
|
35
|
+
webreader.pretty(html) # re-indented, readable markup
|
|
36
|
+
webreader.to_markdown(html) # convert to Markdown
|
|
37
|
+
|
|
38
|
+
clean, report = webreader.sanitize(html)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Bytes input is decoded using detected encoding: BOM sniffing first,
|
|
42
|
+
then `<meta charset>` in the first 2 KB, then the configured
|
|
43
|
+
`encoding` fallback.
|
|
44
|
+
|
|
45
|
+
In non-strict mode, malformed HTML does not raise; the warnings are
|
|
46
|
+
available on the document:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
doc = webreader.parse('<div><p>x</p>') # unclosed <div>
|
|
50
|
+
doc.errors # ['unclosed tag(s) at end of input: <div>']
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Extraction
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
webreader.extract_metadata(html) # title, OG/Twitter, canonical, charset, JSON-LD
|
|
57
|
+
webreader.extract_links(html) # links and asset references
|
|
58
|
+
webreader.extract_tables(html) # matrices with colspan/rowspan expanded
|
|
59
|
+
webreader.extract_text(html) # readable text without boilerplate
|
|
60
|
+
webreader.extract_images(html) # src, alt, dimensions, srcset candidates
|
|
61
|
+
webreader.extract_forms(html) # form fields with types and defaults
|
|
62
|
+
webreader.extract_headings(html) # h1-h6 outline + word count + reading time
|
|
63
|
+
webreader.extract_scripts(html) # inline and external script inventory
|
|
64
|
+
webreader.extract_stylesheets(html) # link[rel~=stylesheet] and inline styles
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## CSS selectors
|
|
68
|
+
|
|
69
|
+
Supported syntax: type selectors, `*`, `#id`, `.class`, attribute
|
|
70
|
+
selectors (`[attr]`, `[attr=value]` and the `^=`, `$=`, `*=`, `~=`,
|
|
71
|
+
`|=` operators), the pseudo-classes `:first-child`, `:last-child`,
|
|
72
|
+
`:nth-child(an+b)`, `:not(compound)` and `:contains(text)`, compounds
|
|
73
|
+
such as `div.content#main[href="x"]`, the combinators whitespace
|
|
74
|
+
(descendant), `>` (child), `+` (adjacent sibling) and `~` (general
|
|
75
|
+
sibling), and comma-separated selector lists.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
webreader.select(doc, 'a[href^="https://"]')
|
|
79
|
+
webreader.select(doc, 'ul > li:nth-child(2n+1):not(.skip)')
|
|
80
|
+
webreader.select(doc, 'h2 + p, blockquote p:contains("note")')
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Invalid or unsupported selectors raise `SelectorSyntaxError`.
|
|
84
|
+
|
|
85
|
+
## Tree editing
|
|
86
|
+
|
|
87
|
+
Nodes support a mutation API, and the edited tree can be
|
|
88
|
+
re-serialized:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
doc = webreader.parse('<div><a href="/x">link</a></div>')
|
|
92
|
+
link = doc.find('a')
|
|
93
|
+
link.set_attr('rel', 'noopener').remove_attr('class')
|
|
94
|
+
|
|
95
|
+
new = webreader.Node('p')
|
|
96
|
+
new.children.append('added text')
|
|
97
|
+
doc.find('div').append_child(new)
|
|
98
|
+
|
|
99
|
+
link.unwrap() # replace the <a> with its inner text
|
|
100
|
+
doc.to_html() # '<div>link<p>added text</p></div>'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`Node` also provides `remove()` and `replace_with(*nodes)`.
|
|
104
|
+
|
|
105
|
+
## Markdown conversion
|
|
106
|
+
|
|
107
|
+
`to_markdown` renders headings, paragraphs, links, images,
|
|
108
|
+
emphasis/strong/strikethrough, inline and fenced code (with language
|
|
109
|
+
detection from `class="language-..."`), nested lists, blockquotes,
|
|
110
|
+
horizontal rules and tables.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
webreader.to_markdown('<h1>T</h1><p>Hi <b>bold</b></p>')
|
|
114
|
+
# '# T\n\nHi **bold**'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Sanitization
|
|
118
|
+
|
|
119
|
+
`sanitize()` returns `(clean_html, report)`:
|
|
120
|
+
|
|
121
|
+
- tags outside the whitelist are unwrapped, inner content kept
|
|
122
|
+
- `script`, `style`, `noscript`, `template` are dropped entirely
|
|
123
|
+
- attributes filtered through a safe-list, `on*` handlers removed
|
|
124
|
+
- `javascript:`, `vbscript:` and non-image `data:` URLs blocked
|
|
125
|
+
- the report lists removed tags/attributes, blocked URLs and detected threats
|
|
126
|
+
|
|
127
|
+
## CLI
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
python -m webreader parse -f input.html
|
|
131
|
+
python -m webreader select -f input.html -s 'a[href]'
|
|
132
|
+
python -m webreader links -f input.html --base-url https://example.com
|
|
133
|
+
python -m webreader meta -f input.html
|
|
134
|
+
python -m webreader tables -f input.html --format csv
|
|
135
|
+
python -m webreader text -f input.html
|
|
136
|
+
python -m webreader images -f input.html
|
|
137
|
+
python -m webreader forms -f input.html
|
|
138
|
+
python -m webreader minify -f input.html --out min.html
|
|
139
|
+
python -m webreader pretty -f input.html --out pretty.html
|
|
140
|
+
python -m webreader md -f input.html --out page.md
|
|
141
|
+
python -m webreader sanitize -f input.html --out clean.html --allowed p,a,ul
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use `-f -` to read from stdin. Every output command accepts `--out`
|
|
145
|
+
to write to a file instead of stdout.
|
|
146
|
+
|
|
147
|
+
### Default settings
|
|
148
|
+
|
|
149
|
+
| Key | Default | Purpose |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `strip_whitespace` | `True` | collapse whitespace in text nodes |
|
|
152
|
+
| `encoding` | `utf-8` | fallback when bytes input has no detectable encoding |
|
|
153
|
+
| `max_depth` | `100` | maximum element nesting |
|
|
154
|
+
| `preserve_comments` | `False` | keep comments in the tree |
|
|
155
|
+
| `strict_mode` | `False` | raise on malformed HTML |
|
|
156
|
+
| `remove_comments` | `True` | strip comments when minifying |
|
|
157
|
+
| `allowed_tags` | `p, br, b, i, a, ul, ol, li, div, span` | sanitize whitelist |
|
|
158
|
+
| `max_file_size_mb` | `10` | reject oversized input |
|
|
159
|
+
| `cache_size` | `50` | parse cache capacity |
|
|
160
|
+
|
|
161
|
+
## Exceptions
|
|
162
|
+
|
|
163
|
+
`HTMLPaferError` (base), `ConfigLoadError`, `SanitizationError`,
|
|
164
|
+
`SelectorSyntaxError`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=78.0.2", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "webreader"
|
|
7
|
+
version = "2.3.8"
|
|
8
|
+
description = "Tree-based HTML reader for Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "morichan", email = "morichan@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: User Interfaces",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"pafer"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest",
|
|
35
|
+
"pytest-cov",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
webreader = [
|
|
40
|
+
"*.config"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools]
|
|
44
|
+
packages = ["webreader"]
|
webreader-2.3.8/setup.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Tree-based HTML parsing, extraction and sanitization."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import pafer
|
|
6
|
+
from pafer.exceptions import (
|
|
7
|
+
ConfigLoadError,
|
|
8
|
+
HTMLPaferError,
|
|
9
|
+
SanitizationError,
|
|
10
|
+
SelectorSyntaxError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from . import core as _core
|
|
14
|
+
from .core import Engine, Node, ParsedDocument, get_config, get_engine, load_config
|
|
15
|
+
|
|
16
|
+
def _as_doc(target: Union[str, bytes, ParsedDocument]) -> ParsedDocument:
|
|
17
|
+
if isinstance(target, ParsedDocument):
|
|
18
|
+
return target
|
|
19
|
+
return _core.get_engine().parse(target)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse(html: Union[str, bytes]) -> ParsedDocument:
|
|
23
|
+
"""Parse HTML into a ParsedDocument using the active configuration."""
|
|
24
|
+
return _core.get_engine().parse(html)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def select(target: Union[str, bytes, ParsedDocument],
|
|
28
|
+
selector: str) -> List[Dict[str, Any]]:
|
|
29
|
+
"""Run a CSS selector; returns dicts with tag/attrs/text/html per match."""
|
|
30
|
+
return [node.to_dict() for node in _as_doc(target).find_all(selector)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_metadata(target: Union[str, bytes, ParsedDocument]) -> Dict[str, Any]:
|
|
34
|
+
"""Extract title, OG/Twitter, canonical, lang, charset and JSON-LD."""
|
|
35
|
+
return _as_doc(target).metadata()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_links(target: Union[str, bytes, ParsedDocument],
|
|
39
|
+
base_url: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
40
|
+
"""Extract links and asset references, absolutized against base_url."""
|
|
41
|
+
return _as_doc(target).links(base_url=base_url)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_tables(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
|
|
45
|
+
"""Extract tables as header/data matrices with span expansion."""
|
|
46
|
+
return _as_doc(target).tables()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_text(target: Union[str, bytes, ParsedDocument]) -> str:
|
|
50
|
+
"""Extract readable text with boilerplate removal."""
|
|
51
|
+
return _as_doc(target).text()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sanitize(target: Union[str, bytes, ParsedDocument],
|
|
55
|
+
allowed_tags: Optional[Set[str]] = None) -> Tuple[str, Dict[str, Any]]:
|
|
56
|
+
"""Return (clean_html, removal_report) using config allowed_tags by default."""
|
|
57
|
+
from .sanitizer import sanitize_document
|
|
58
|
+
return sanitize_document(_as_doc(target), allowed_tags=allowed_tags)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def minify(html: str) -> str:
|
|
62
|
+
"""Minify HTML honoring remove_comments from the active configuration."""
|
|
63
|
+
return _core.get_engine().minify(html)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def pretty(html: Union[str, bytes], indent: str = ' ') -> str:
|
|
67
|
+
"""Reformat HTML into indented, readable markup."""
|
|
68
|
+
from .formatter import pretty_html
|
|
69
|
+
return pretty_html(html, indent=indent, config=_core.config)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def to_markdown(target: Union[str, bytes, ParsedDocument]) -> str:
|
|
73
|
+
"""Convert HTML or a parsed document to Markdown."""
|
|
74
|
+
from .markdown import to_markdown as _to_md
|
|
75
|
+
return _to_md(target, config=_core.config)
|
|
76
|
+
|
|
77
|
+
def extract_images(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
|
|
78
|
+
"""Extract img elements with src, alt, dimensions and srcset candidates."""
|
|
79
|
+
return _as_doc(target).images()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def extract_forms(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
|
|
83
|
+
"""Extract forms with their input fields and default values."""
|
|
84
|
+
return _as_doc(target).forms()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def extract_headings(target: Union[str, bytes, ParsedDocument]) -> Dict[str, Any]:
|
|
88
|
+
"""Extract the h1-h6 outline plus word count and reading time."""
|
|
89
|
+
return _as_doc(target).headings()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def extract_scripts(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
|
|
93
|
+
"""Inventory script elements, both inline and external."""
|
|
94
|
+
return _as_doc(target).scripts()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_stylesheets(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
|
|
98
|
+
"""Inventory stylesheets: link[rel~=stylesheet] and inline styles."""
|
|
99
|
+
return _as_doc(target).stylesheets()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def export(data: Any, fmt: str = 'json', indent: int = 2) -> str:
|
|
103
|
+
"""Export data to json, csv or markdown using pafer.exporters."""
|
|
104
|
+
if fmt == 'json':
|
|
105
|
+
return pafer.exporters.DataExporter.to_json(data, indent=indent)
|
|
106
|
+
if fmt == 'csv':
|
|
107
|
+
if isinstance(data, list) and data and all(isinstance(row, dict) for row in data):
|
|
108
|
+
headers = list(data[0].keys())
|
|
109
|
+
rows = [[str(row.get(header, '')) for header in headers] for row in data]
|
|
110
|
+
return pafer.exporters.DataExporter.to_csv([headers] + rows)
|
|
111
|
+
return pafer.exporters.DataExporter.to_csv(data)
|
|
112
|
+
if fmt == 'md':
|
|
113
|
+
return _export_markdown(data)
|
|
114
|
+
raise ValueError(f'unsupported export format: {fmt!r}')
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _export_markdown(data: Any) -> str:
|
|
118
|
+
if isinstance(data, list) and data and all(isinstance(item, dict) and 'rows' in item
|
|
119
|
+
for item in data):
|
|
120
|
+
lines: List[str] = []
|
|
121
|
+
for table in data:
|
|
122
|
+
matrix = list(table.get('headers', [])) + list(table.get('rows', []))
|
|
123
|
+
width = max((len(row) for row in matrix), default=0)
|
|
124
|
+
if not width:
|
|
125
|
+
continue
|
|
126
|
+
padded = [list(row) + [''] * (width - len(row)) for row in matrix]
|
|
127
|
+
lines.append('| ' + ' | '.join(str(cell) for cell in padded[0]) + ' |')
|
|
128
|
+
lines.append('|' + '---|' * width)
|
|
129
|
+
lines.extend(
|
|
130
|
+
'| ' + ' | '.join(str(cell) for cell in row) + ' |'
|
|
131
|
+
for row in padded[1:]
|
|
132
|
+
)
|
|
133
|
+
lines.append('')
|
|
134
|
+
return '\n'.join(lines)
|
|
135
|
+
return '```json\n' + pafer.exporters.DataExporter.to_json(data) + '\n```'
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
__all__ = [
|
|
139
|
+
'Engine',
|
|
140
|
+
'Node',
|
|
141
|
+
'ParsedDocument',
|
|
142
|
+
'load_config',
|
|
143
|
+
'get_config',
|
|
144
|
+
'get_engine',
|
|
145
|
+
'parse',
|
|
146
|
+
'select',
|
|
147
|
+
'extract_metadata',
|
|
148
|
+
'extract_links',
|
|
149
|
+
'extract_tables',
|
|
150
|
+
'extract_text',
|
|
151
|
+
'sanitize',
|
|
152
|
+
'minify',
|
|
153
|
+
'pretty',
|
|
154
|
+
'to_markdown',
|
|
155
|
+
'extract_images',
|
|
156
|
+
'extract_forms',
|
|
157
|
+
'extract_headings',
|
|
158
|
+
'extract_scripts',
|
|
159
|
+
'extract_stylesheets',
|
|
160
|
+
'export',
|
|
161
|
+
'HTMLPaferError',
|
|
162
|
+
'ConfigLoadError',
|
|
163
|
+
'SanitizationError',
|
|
164
|
+
'SelectorSyntaxError',
|
|
165
|
+
]
|