webreader 2.3.8__py3-none-any.whl

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/__init__.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
+ ]
webreader/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running the package as `python -m webreader`."""
2
+
3
+ from .cli import run
4
+
5
+ if __name__ == '__main__':
6
+ run()
webreader/cli.py ADDED
@@ -0,0 +1,166 @@
1
+ """Command line interface for webreader."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from pafer.exporters import DataExporter
9
+
10
+
11
+ def run(argv: Optional[List[str]] = None) -> None:
12
+ """Entry point for the webreader CLI."""
13
+ cli = argparse.ArgumentParser(
14
+ prog='webreader',
15
+ description='webreader - HTML processing tool built on the reader package',
16
+ epilog="Example: python -m webreader parse -f input.html",
17
+ )
18
+ cli.add_argument('--config', help='load this config.config as the new default configuration')
19
+ sub = cli.add_subparsers(dest='command', required=True)
20
+
21
+ def with_file(cmd):
22
+ cmd.add_argument('-f', '--file', required=True, help="Path to HTML file ('-' for stdin)")
23
+ return cmd
24
+
25
+ def with_out(cmd):
26
+ cmd.add_argument('--out', default=None, help='Write output to this file')
27
+ return cmd
28
+
29
+ with_out(with_file(sub.add_parser('parse', help='Parse a file and print the structured result')))
30
+
31
+ select_cmd = with_out(with_file(sub.add_parser('select', help='Run a CSS selector and print matches')))
32
+ select_cmd.add_argument('-s', '--selector', required=True, help='CSS selector expression')
33
+
34
+ links_cmd = with_out(with_file(sub.add_parser('links', help='List links and asset references')))
35
+ links_cmd.add_argument('--base-url', default=None, help='Absolutize URLs against this base')
36
+
37
+ with_out(with_file(sub.add_parser('meta', help='Extract page metadata')))
38
+
39
+ tables_cmd = with_out(with_file(sub.add_parser('tables', help='Extract tables')))
40
+ tables_cmd.add_argument('--format', choices=['json', 'csv', 'md'], default='json')
41
+
42
+ with_out(with_file(sub.add_parser('text', help='Extract readable text')))
43
+
44
+ with_out(with_file(sub.add_parser('images', help='List images')))
45
+
46
+ with_out(with_file(sub.add_parser('forms', help='Extract forms and fields')))
47
+
48
+ with_out(with_file(sub.add_parser('minify', help='Minify HTML')))
49
+
50
+ with_out(with_file(sub.add_parser('pretty', help='Reformat HTML with indentation')))
51
+
52
+ with_out(with_file(sub.add_parser('md', help='Convert HTML to Markdown')))
53
+
54
+ sanitize_cmd = with_out(with_file(sub.add_parser('sanitize', help='Sanitize HTML and print or save the result')))
55
+ sanitize_cmd.add_argument('--allowed', default=None, help='Comma-separated allowed tags')
56
+
57
+ args = cli.parse_args(argv)
58
+
59
+ import webreader
60
+
61
+ if args.config:
62
+ try:
63
+ webreader.load_config(args.config)
64
+ print(f"[+] Loaded config from '{args.config}' (persisted as webreader default).")
65
+ except Exception as exc:
66
+ print(f'[-] Config error: {exc}')
67
+ sys.exit(1)
68
+
69
+ try:
70
+ html_text = _read_input(args.file)
71
+ except OSError as exc:
72
+ print(f"[-] Error: cannot read '{args.file}': {exc}")
73
+ sys.exit(1)
74
+
75
+ try:
76
+ if args.command == 'parse':
77
+ _output(_dumps(webreader.parse(html_text).to_dict()), args.out)
78
+ elif args.command == 'select':
79
+ _output(_dumps(webreader.select(html_text, args.selector)), args.out)
80
+ elif args.command == 'links':
81
+ result = webreader.extract_links(html_text, base_url=args.base_url)
82
+ _output(_dumps(result), args.out)
83
+ elif args.command == 'meta':
84
+ _output(_dumps(webreader.extract_metadata(html_text)), args.out)
85
+ elif args.command == 'tables':
86
+ _output(_tables_output(webreader.extract_tables(html_text), args.format), args.out)
87
+ elif args.command == 'text':
88
+ _output(webreader.extract_text(html_text), args.out)
89
+ elif args.command == 'images':
90
+ _output(_dumps(webreader.extract_images(html_text)), args.out)
91
+ elif args.command == 'forms':
92
+ _output(_dumps(webreader.extract_forms(html_text)), args.out)
93
+ elif args.command == 'minify':
94
+ _output(webreader.minify(html_text), args.out)
95
+ elif args.command == 'pretty':
96
+ _output(webreader.pretty(html_text), args.out)
97
+ elif args.command == 'md':
98
+ _output(webreader.to_markdown(html_text), args.out)
99
+ elif args.command == 'sanitize':
100
+ allowed: Optional[List[str]] = None
101
+ if args.allowed:
102
+ allowed = [tag.strip() for tag in args.allowed.split(',') if tag.strip()]
103
+ clean, report = webreader.sanitize(html_text, allowed_tags=allowed)
104
+ if args.out:
105
+ _write_file(args.out, clean)
106
+ print(f"[+] Sanitized HTML written to '{args.out}'.")
107
+ else:
108
+ print(clean)
109
+ print('[+] Report: ' + json.dumps(report, default=str))
110
+ except Exception as exc:
111
+ print(f'[-] Error: {exc}')
112
+ sys.exit(1)
113
+
114
+
115
+ def _read_input(path: str) -> str:
116
+ if path == '-':
117
+ return sys.stdin.read()
118
+ with open(path, 'r', encoding='utf-8-sig', errors='replace') as handle:
119
+ return handle.read()
120
+
121
+
122
+ def _write_file(path: str, text: str) -> None:
123
+ with open(path, 'w', encoding='utf-8') as handle:
124
+ handle.write(text)
125
+
126
+
127
+ def _output(text: str, out: Optional[str]) -> None:
128
+ if out:
129
+ _write_file(out, text if text.endswith('\n') else text + '\n')
130
+ print(f"[+] Written to '{out}'.")
131
+ else:
132
+ print(text)
133
+
134
+
135
+ def _dumps(data: Any) -> str:
136
+ return json.dumps(data, indent=2, default=str)
137
+
138
+
139
+ def _tables_output(tables: List[Dict[str, Any]], fmt: str) -> str:
140
+ if fmt == 'json':
141
+ return _dumps(tables)
142
+ rows: List[List[str]] = []
143
+ for table in tables:
144
+ rows.extend(table.get('headers', []))
145
+ rows.extend(table.get('rows', []))
146
+ rows.append([])
147
+ if not rows:
148
+ return ''
149
+ if fmt == 'csv':
150
+ return DataExporter.to_csv(rows).rstrip('\n')
151
+ return _markdown_table(rows)
152
+
153
+
154
+ def _markdown_table(rows: List[List[str]]) -> str:
155
+ rows = [row for row in rows if row]
156
+ if not rows:
157
+ return ''
158
+ width = max(len(row) for row in rows)
159
+ padded = [list(row) + [''] * (width - len(row)) for row in rows]
160
+ lines = ['| ' + ' | '.join(padded[0]) + ' |', '|' + '---|' * width]
161
+ lines.extend('| ' + ' | '.join(row) + ' |' for row in padded[1:])
162
+ return '\n'.join(lines)
163
+
164
+
165
+ if __name__ == '__main__':
166
+ run()