scrape-cli 1.3.2__tar.gz → 1.3.3__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.3}/PKG-INFO +2 -2
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/README.md +1 -1
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/pyproject.toml +1 -1
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli/__init__.py +1 -1
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli/scrape.py +66 -20
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli.egg-info/PKG-INFO +2 -2
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/tests/test_scrape.py +40 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli.egg-info/SOURCES.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli.egg-info/dependency_links.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli.egg-info/entry_points.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli.egg-info/requires.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/scrape_cli.egg-info/top_level.txt +0 -0
- {scrape_cli-1.3.2 → scrape_cli-1.3.3}/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.3
|
|
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
|
|
@@ -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. Other encodings need the meta
|
|
166
|
+
tag, or characters come out wrong.
|
|
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:
|
|
@@ -279,7 +321,11 @@ examples:
|
|
|
279
321
|
string_results = []
|
|
280
322
|
# For each expression, perform the search in the parsed HTML
|
|
281
323
|
for e in expression:
|
|
282
|
-
|
|
324
|
+
try:
|
|
325
|
+
result = document.xpath(e)
|
|
326
|
+
except etree.XPathError as err:
|
|
327
|
+
print(f"Error: invalid XPath expression '{e}': {err}", file=sys.stderr)
|
|
328
|
+
sys.exit(1)
|
|
283
329
|
|
|
284
330
|
# If check-existence is enabled, return 0 or 1 depending on the existence of elements
|
|
285
331
|
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.3
|
|
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
|
|
@@ -203,6 +203,46 @@ def test_order_checks_ignore_filenames_and_selectors(tmp_path):
|
|
|
203
203
|
assert "use -" not in result.stderr
|
|
204
204
|
|
|
205
205
|
|
|
206
|
+
def test_invalid_xpath_reports_clean_error():
|
|
207
|
+
result = run_scrape("-e", "//[bad", input_data="<p>x</p>")
|
|
208
|
+
|
|
209
|
+
assert result.returncode == 1
|
|
210
|
+
assert "Error: invalid XPath" in result.stderr
|
|
211
|
+
assert "Traceback" not in result.stderr
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_unreadable_input_reports_clean_error(tmp_path):
|
|
215
|
+
result = run_scrape("-e", "h1", str(tmp_path))
|
|
216
|
+
|
|
217
|
+
assert result.returncode == 1
|
|
218
|
+
assert "Error: cannot read" in result.stderr
|
|
219
|
+
assert "Traceback" not in result.stderr
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_help_documents_the_contract_for_agents():
|
|
223
|
+
result = run_scrape("--help")
|
|
224
|
+
|
|
225
|
+
assert result.returncode == 0
|
|
226
|
+
for section in ("output:", "exit codes:", "notes:", "examples:"):
|
|
227
|
+
assert section in result.stdout
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_exit_codes_match_help():
|
|
231
|
+
html = "<h1>x</h1>"
|
|
232
|
+
|
|
233
|
+
no_match = run_scrape("-e", "//nulla", input_data=html)
|
|
234
|
+
assert (no_match.returncode, no_match.stdout) == (0, "")
|
|
235
|
+
|
|
236
|
+
not_found = run_scrape("-x", "-e", "//nulla", input_data=html)
|
|
237
|
+
assert not_found.returncode == 1
|
|
238
|
+
assert "Error" not in not_found.stderr
|
|
239
|
+
|
|
240
|
+
assert run_scrape("--bogus", input_data=html).returncode == 2
|
|
241
|
+
|
|
242
|
+
first_only = run_scrape("-x", "-e", "//nulla", "-e", "//h1", input_data=html)
|
|
243
|
+
assert first_only.returncode == 1
|
|
244
|
+
|
|
245
|
+
|
|
206
246
|
def test_invalid_css_selector_fails_conversion():
|
|
207
247
|
result = run_scrape(str(TEST_HTML), "-e", "div[")
|
|
208
248
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|