scrape-cli 1.3.2__tar.gz → 1.3.4__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.
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/PKG-INFO +2 -2
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/README.md +1 -1
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/pyproject.toml +2 -1
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli/__init__.py +1 -1
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli/scrape.py +76 -33
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli.egg-info/PKG-INFO +2 -2
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli.egg-info/entry_points.txt +1 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/tests/test_scrape.py +69 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli.egg-info/SOURCES.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli.egg-info/dependency_links.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli.egg-info/requires.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/scrape_cli.egg-info/top_level.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrape_cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: It's a command-line tool to extract HTML elements using an XPath query or CSS3 selector.
|
|
5
5
|
Author-email: Andrea Borruso <aborruso@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/aborruso/scrape-cli
|
|
@@ -68,7 +68,7 @@ pip install -e .
|
|
|
68
68
|
|
|
69
69
|
## Requirements
|
|
70
70
|
|
|
71
|
-
- Python >=3.
|
|
71
|
+
- Python >=3.8
|
|
72
72
|
- requests
|
|
73
73
|
- lxml
|
|
74
74
|
- cssselect
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scrape_cli"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.4"
|
|
8
8
|
description = "It's a command-line tool to extract HTML elements using an XPath query or CSS3 selector."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -27,6 +27,7 @@ Homepage = "https://github.com/aborruso/scrape-cli"
|
|
|
27
27
|
|
|
28
28
|
[project.scripts]
|
|
29
29
|
scrape = "scrape_cli.scrape:main"
|
|
30
|
+
scrape-cli = "scrape_cli.scrape:main"
|
|
30
31
|
|
|
31
32
|
[tool.setuptools.packages.find]
|
|
32
33
|
where = ["."]
|
|
@@ -116,19 +116,54 @@ def is_xpath(expression):
|
|
|
116
116
|
def main():
|
|
117
117
|
# Command line argument parser definition
|
|
118
118
|
parser = argparse.ArgumentParser(
|
|
119
|
-
description=
|
|
119
|
+
description=(
|
|
120
|
+
'Extract parts of an HTML page using an XPath query or CSS3 selector.\n'
|
|
121
|
+
'Reads static HTML and does not run JavaScript: content built in the\n'
|
|
122
|
+
'browser is not there. Never prompts for input.'
|
|
123
|
+
),
|
|
120
124
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
121
125
|
epilog='''\
|
|
122
126
|
examples:
|
|
123
|
-
scrape -e "//h1" file.html
|
|
124
|
-
scrape -e "h1.title" file.html
|
|
125
|
-
scrape -e "//a"
|
|
126
|
-
scrape -
|
|
127
|
-
scrape -e "//
|
|
128
|
-
|
|
129
|
-
scrape -
|
|
130
|
-
scrape -
|
|
131
|
-
scrape -je "
|
|
127
|
+
scrape -e "//h1" file.html XPath: matching elements as HTML
|
|
128
|
+
scrape -e "h1.title" file.html CSS selector, detected automatically
|
|
129
|
+
scrape -e "//a/@href" file.html attribute values as plain text
|
|
130
|
+
scrape -e "//a" -a href file.html same, elements without href skipped
|
|
131
|
+
scrape -e "count(//a)" file.html XPath function: one value (string,
|
|
132
|
+
number, true/false)
|
|
133
|
+
scrape -t file.html all body text, no script or style
|
|
134
|
+
scrape -te "//article" file.html text of the matching elements
|
|
135
|
+
scrape -je "a" file.html JSON
|
|
136
|
+
scrape -be "//article" file.html wrap output in <html><body>
|
|
137
|
+
scrape -x -e "//h1" file.html test only: exit code, no output
|
|
138
|
+
scrape -e "//h1" https://example.com fetch from URL
|
|
139
|
+
cat file.html | scrape -e "//h1" read from stdin
|
|
140
|
+
|
|
141
|
+
output:
|
|
142
|
+
default one result after the other, on stdout: elements as HTML, strings
|
|
143
|
+
(@attr, text(), XPath functions) as plain text
|
|
144
|
+
-e repeated: results follow the order of the expressions
|
|
145
|
+
-j always {"html": {"body": ...}}, where body is:
|
|
146
|
+
elements attributes as @name, text as #text; one match of a tag
|
|
147
|
+
is an object, several are a list
|
|
148
|
+
strings one is a string, several are a list
|
|
149
|
+
nothing null
|
|
150
|
+
|
|
151
|
+
exit codes:
|
|
152
|
+
0 success, also when nothing matches: empty output, or null body with -j
|
|
153
|
+
1 error, with a message on stderr; with -x, no match, with stderr empty
|
|
154
|
+
2 invalid usage: unknown option or missing option value
|
|
155
|
+
|
|
156
|
+
notes:
|
|
157
|
+
- No match is not an error: use -x to test for it.
|
|
158
|
+
- With short flags together, -e goes last: -be, -je, -te. As an argument on
|
|
159
|
+
its own, -eb or -ej is rejected, because -e would take "b" or "j" as value.
|
|
160
|
+
- -x evaluates only the first -e.
|
|
161
|
+
- -t with -e includes the text of any script or style inside the matches.
|
|
162
|
+
- -j fails with exit 1 on attribute names that are not valid XML, such as
|
|
163
|
+
@click or :bind. Use -e without -j on those pages.
|
|
164
|
+
- URLs: 30 s timeout, HTTP errors (4xx, 5xx) exit 1.
|
|
165
|
+
- Charset: from the meta tag, otherwise UTF-8, and ISO-8859-1 if the bytes
|
|
166
|
+
are not valid UTF-8. Other encodings need the meta tag.
|
|
132
167
|
'''
|
|
133
168
|
)
|
|
134
169
|
|
|
@@ -141,30 +176,34 @@ examples:
|
|
|
141
176
|
sys.exit(1)
|
|
142
177
|
# Defines the HTML input argument (can be a file, URL or stdin)
|
|
143
178
|
parser.add_argument('html', nargs='?', type=str, default='',
|
|
144
|
-
help="HTML input
|
|
179
|
+
help="HTML input: local file or http(s) URL. Stdin if omitted", metavar="HTML")
|
|
145
180
|
# Defines the optional argument to extract from the tag
|
|
146
181
|
parser.add_argument('-a', '--argument', default="",
|
|
147
|
-
help="
|
|
182
|
+
help="Output the value of this attribute instead of the element. "
|
|
183
|
+
"Elements without it are skipped")
|
|
148
184
|
# Option to include the result within HTML and BODY tags
|
|
149
185
|
parser.add_argument('-b', '--body', action='store_true', default=False,
|
|
150
|
-
help="
|
|
186
|
+
help="Wrap the output in a <!DOCTYPE html><html><body> document. "
|
|
187
|
+
"Ignored with -t and -j")
|
|
151
188
|
# Option to extract only text content
|
|
152
189
|
parser.add_argument('-t', '--text', action='store_true', default=False,
|
|
153
|
-
help="
|
|
190
|
+
help="Output text only, with whitespace cleaned. "
|
|
191
|
+
"Without -e, all body text except script and style")
|
|
154
192
|
# Option to output structured JSON (built-in, no external xq needed)
|
|
155
193
|
parser.add_argument('-j', '--json', action='store_true', default=False,
|
|
156
|
-
help="Output
|
|
194
|
+
help="Output structured JSON, see below. Not with -t, -x, -a")
|
|
157
195
|
# Allows to specify one or more XPath or CSS3 selector expressions
|
|
158
196
|
parser.add_argument('-e', '--expression', default=[], action='append',
|
|
159
|
-
help="XPath query or CSS3 selector")
|
|
197
|
+
help="XPath query or CSS3 selector, detected automatically. Repeatable")
|
|
160
198
|
# Option to verify the existence of elements matching the expression
|
|
161
199
|
parser.add_argument('-x', '--check-existence', dest='check_existence', action='store_true', default=False,
|
|
162
|
-
help="
|
|
200
|
+
help="Output nothing, exit 0 if the first expression matches, 1 if not")
|
|
163
201
|
# Option to avoid initial HTML parsing, useful in specific cases like CData
|
|
164
202
|
parser.add_argument('-r', '--rawinput', action='store_true', default=False,
|
|
165
|
-
help="
|
|
203
|
+
help="Parse the input as strict XML instead of HTML (useful for CData). "
|
|
204
|
+
"Malformed input is an error")
|
|
166
205
|
parser.add_argument('-u', '--user-agent', default=None,
|
|
167
|
-
help="
|
|
206
|
+
help="User-Agent for URL requests")
|
|
168
207
|
args = parser.parse_args()
|
|
169
208
|
|
|
170
209
|
# JSON flag is mutually exclusive with -t, -x, -a
|
|
@@ -208,6 +247,9 @@ examples:
|
|
|
208
247
|
except FileNotFoundError:
|
|
209
248
|
print(f"Error: The file '{args.html}' was not found.", file=sys.stderr)
|
|
210
249
|
sys.exit(1)
|
|
250
|
+
except OSError as e:
|
|
251
|
+
print(f"Error: cannot read '{args.html}': {e.strerror}", file=sys.stderr)
|
|
252
|
+
sys.exit(1)
|
|
211
253
|
else:
|
|
212
254
|
# If the input is from stdin
|
|
213
255
|
try:
|
|
@@ -257,19 +299,16 @@ examples:
|
|
|
257
299
|
# If detected charset fails, fall back to default behavior
|
|
258
300
|
pass
|
|
259
301
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
document = etree.fromstring(inp)
|
|
271
|
-
else:
|
|
272
|
-
document = etree.fromstring(inp, html_parser)
|
|
302
|
+
if args.rawinput:
|
|
303
|
+
# XML declares its own encoding: leave the bytes to the parser
|
|
304
|
+
document = etree.fromstring(inp)
|
|
305
|
+
else:
|
|
306
|
+
# The recovering parser never fails on bad bytes, so check them here
|
|
307
|
+
try:
|
|
308
|
+
inp.decode('utf-8')
|
|
309
|
+
except UnicodeDecodeError:
|
|
310
|
+
inp = inp.decode('iso-8859-1').encode('utf-8')
|
|
311
|
+
document = etree.fromstring(inp, html_parser)
|
|
273
312
|
except (etree.XMLSyntaxError, UnicodeDecodeError) as e:
|
|
274
313
|
# Print an error in case of syntax issues in the HTML
|
|
275
314
|
print(f"Error parsing HTML: {e}", file=sys.stderr)
|
|
@@ -279,7 +318,11 @@ examples:
|
|
|
279
318
|
string_results = []
|
|
280
319
|
# For each expression, perform the search in the parsed HTML
|
|
281
320
|
for e in expression:
|
|
282
|
-
|
|
321
|
+
try:
|
|
322
|
+
result = document.xpath(e)
|
|
323
|
+
except etree.XPathError as err:
|
|
324
|
+
print(f"Error: invalid XPath expression '{e}': {err}", file=sys.stderr)
|
|
325
|
+
sys.exit(1)
|
|
283
326
|
|
|
284
327
|
# If check-existence is enabled, return 0 or 1 depending on the existence of elements
|
|
285
328
|
if args.check_existence:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrape_cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: It's a command-line tool to extract HTML elements using an XPath query or CSS3 selector.
|
|
5
5
|
Author-email: Andrea Borruso <aborruso@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/aborruso/scrape-cli
|
|
@@ -68,7 +68,7 @@ pip install -e .
|
|
|
68
68
|
|
|
69
69
|
## Requirements
|
|
70
70
|
|
|
71
|
-
- Python >=3.
|
|
71
|
+
- Python >=3.8
|
|
72
72
|
- requests
|
|
73
73
|
- lxml
|
|
74
74
|
- cssselect
|
|
@@ -107,6 +107,35 @@ def test_encoding_meta_charset_iso_8859_1(tmp_path):
|
|
|
107
107
|
assert result.stdout.strip() == "Perché"
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def test_encoding_falls_back_to_iso_8859_1_without_meta(tmp_path):
|
|
111
|
+
html_file = tmp_path / "latin1.html"
|
|
112
|
+
html_file.write_bytes("<h1>Caffè</h1>".encode("iso-8859-1"))
|
|
113
|
+
|
|
114
|
+
result = run_scrape(str(html_file), "-e", "string(//h1)")
|
|
115
|
+
|
|
116
|
+
assert result.returncode == 0
|
|
117
|
+
assert result.stdout == "Caffè\n"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_encoding_utf8_without_meta_is_not_converted(tmp_path):
|
|
121
|
+
html_file = tmp_path / "utf8.html"
|
|
122
|
+
html_file.write_bytes("<h1>Caffè €</h1>".encode("utf-8"))
|
|
123
|
+
|
|
124
|
+
result = run_scrape(str(html_file), "-e", "string(//h1)")
|
|
125
|
+
|
|
126
|
+
assert result.stdout == "Caffè €\n"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_rawinput_keeps_the_encoding_declared_by_the_xml(tmp_path):
|
|
130
|
+
xml_file = tmp_path / "latin1.xml"
|
|
131
|
+
xml_file.write_bytes('<?xml version="1.0" encoding="iso-8859-1"?><a>Caffè</a>'.encode("iso-8859-1"))
|
|
132
|
+
|
|
133
|
+
result = run_scrape(str(xml_file), "-r", "-e", "string(//a)")
|
|
134
|
+
|
|
135
|
+
assert result.returncode == 0
|
|
136
|
+
assert result.stdout == "Caffè\n"
|
|
137
|
+
|
|
138
|
+
|
|
110
139
|
def test_argument_extracts_attribute_value():
|
|
111
140
|
result = run_scrape(str(TEST_HTML), "-e", "//a[@class='external-link']", "-a", "href")
|
|
112
141
|
|
|
@@ -203,6 +232,46 @@ def test_order_checks_ignore_filenames_and_selectors(tmp_path):
|
|
|
203
232
|
assert "use -" not in result.stderr
|
|
204
233
|
|
|
205
234
|
|
|
235
|
+
def test_invalid_xpath_reports_clean_error():
|
|
236
|
+
result = run_scrape("-e", "//[bad", input_data="<p>x</p>")
|
|
237
|
+
|
|
238
|
+
assert result.returncode == 1
|
|
239
|
+
assert "Error: invalid XPath" in result.stderr
|
|
240
|
+
assert "Traceback" not in result.stderr
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_unreadable_input_reports_clean_error(tmp_path):
|
|
244
|
+
result = run_scrape("-e", "h1", str(tmp_path))
|
|
245
|
+
|
|
246
|
+
assert result.returncode == 1
|
|
247
|
+
assert "Error: cannot read" in result.stderr
|
|
248
|
+
assert "Traceback" not in result.stderr
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_help_documents_the_contract_for_agents():
|
|
252
|
+
result = run_scrape("--help")
|
|
253
|
+
|
|
254
|
+
assert result.returncode == 0
|
|
255
|
+
for section in ("output:", "exit codes:", "notes:", "examples:"):
|
|
256
|
+
assert section in result.stdout
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_exit_codes_match_help():
|
|
260
|
+
html = "<h1>x</h1>"
|
|
261
|
+
|
|
262
|
+
no_match = run_scrape("-e", "//nulla", input_data=html)
|
|
263
|
+
assert (no_match.returncode, no_match.stdout) == (0, "")
|
|
264
|
+
|
|
265
|
+
not_found = run_scrape("-x", "-e", "//nulla", input_data=html)
|
|
266
|
+
assert not_found.returncode == 1
|
|
267
|
+
assert "Error" not in not_found.stderr
|
|
268
|
+
|
|
269
|
+
assert run_scrape("--bogus", input_data=html).returncode == 2
|
|
270
|
+
|
|
271
|
+
first_only = run_scrape("-x", "-e", "//nulla", "-e", "//h1", input_data=html)
|
|
272
|
+
assert first_only.returncode == 1
|
|
273
|
+
|
|
274
|
+
|
|
206
275
|
def test_invalid_css_selector_fails_conversion():
|
|
207
276
|
result = run_scrape(str(TEST_HTML), "-e", "div[")
|
|
208
277
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|