scrape-cli 1.3.0__tar.gz → 1.3.1__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.0 → scrape_cli-1.3.1}/PKG-INFO +5 -2
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/README.md +4 -1
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/pyproject.toml +1 -1
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli/__init__.py +1 -1
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli/scrape.py +22 -6
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli.egg-info/PKG-INFO +5 -2
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/tests/test_scrape.py +69 -0
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli.egg-info/SOURCES.txt +0 -0
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli.egg-info/dependency_links.txt +0 -0
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli.egg-info/entry_points.txt +0 -0
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli.egg-info/requires.txt +0 -0
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/scrape_cli.egg-info/top_level.txt +0 -0
- {scrape_cli-1.3.0 → scrape_cli-1.3.1}/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.1
|
|
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
|
|
@@ -72,6 +72,7 @@ pip install -e .
|
|
|
72
72
|
- requests
|
|
73
73
|
- lxml
|
|
74
74
|
- cssselect
|
|
75
|
+
- xmltodict
|
|
75
76
|
|
|
76
77
|
## How does it work?
|
|
77
78
|
|
|
@@ -280,10 +281,12 @@ Output:
|
|
|
280
281
|
}
|
|
281
282
|
```
|
|
282
283
|
|
|
283
|
-
`-j` automatically wraps the result in `<html>/<body>` before conversion, so you don't need to pass `-b`.
|
|
284
|
+
`-j` automatically wraps the result in `<html>/<body>` before conversion, so you don't need to pass `-b`. For elements, the output is the same you would get from `scrape -be ... | xq .` (the underlying converter is `xmltodict`, the same library used by `xq`). Expressions that return only strings, such as `//a/@href` or `//p/text()`, give the string in `body` when there is one result and an array when there are more.
|
|
284
285
|
|
|
285
286
|
`-j` is mutually exclusive with `-t`, `-x` (`--check-existence`) and `-a` (`--argument`).
|
|
286
287
|
|
|
288
|
+
`-j` exits with an error when a matched element has an attribute name that is not valid XML, such as `@click` or `:bind`.
|
|
289
|
+
|
|
287
290
|
Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
|
|
288
291
|
|
|
289
292
|
Some notes on the commands:
|
|
@@ -57,6 +57,7 @@ pip install -e .
|
|
|
57
57
|
- requests
|
|
58
58
|
- lxml
|
|
59
59
|
- cssselect
|
|
60
|
+
- xmltodict
|
|
60
61
|
|
|
61
62
|
## How does it work?
|
|
62
63
|
|
|
@@ -265,10 +266,12 @@ Output:
|
|
|
265
266
|
}
|
|
266
267
|
```
|
|
267
268
|
|
|
268
|
-
`-j` automatically wraps the result in `<html>/<body>` before conversion, so you don't need to pass `-b`.
|
|
269
|
+
`-j` automatically wraps the result in `<html>/<body>` before conversion, so you don't need to pass `-b`. For elements, the output is the same you would get from `scrape -be ... | xq .` (the underlying converter is `xmltodict`, the same library used by `xq`). Expressions that return only strings, such as `//a/@href` or `//p/text()`, give the string in `body` when there is one result and an array when there are more.
|
|
269
270
|
|
|
270
271
|
`-j` is mutually exclusive with `-t`, `-x` (`--check-existence`) and `-a` (`--argument`).
|
|
271
272
|
|
|
273
|
+
`-j` exits with an error when a matched element has an attribute name that is not valid XML, such as `@click` or `:bind`.
|
|
274
|
+
|
|
272
275
|
Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
|
|
273
276
|
|
|
274
277
|
Some notes on the commands:
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# $ curl 'https://en.wikipedia.org/wiki/List_of_sovereign_states' -s \
|
|
7
7
|
# | scrape -e 'table.wikitable > tbody > tr > td > b > a'
|
|
8
8
|
#
|
|
9
|
-
# Dependencies: lxml, cssselect, requests
|
|
9
|
+
# Dependencies: lxml, cssselect, requests, xmltodict
|
|
10
10
|
#
|
|
11
11
|
# Author: http://jeroenjanssens.com
|
|
12
12
|
|
|
@@ -19,6 +19,8 @@ import requests
|
|
|
19
19
|
import xmltodict
|
|
20
20
|
from lxml import etree
|
|
21
21
|
from cssselect import GenericTranslator
|
|
22
|
+
from xml.parsers.expat import ExpatError
|
|
23
|
+
from xml.sax.saxutils import escape
|
|
22
24
|
|
|
23
25
|
# Read version from __init__.py
|
|
24
26
|
init_file = os.path.join(os.path.dirname(__file__), '__init__.py')
|
|
@@ -118,11 +120,10 @@ examples:
|
|
|
118
120
|
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
|
|
119
121
|
|
|
120
122
|
# Check for incorrect argument order (-eb instead of -be, -ej instead of -je)
|
|
121
|
-
|
|
122
|
-
if '-eb' in joined_argv:
|
|
123
|
+
if '-eb' in sys.argv[1:]:
|
|
123
124
|
print("Error: use -be not -eb.\n scrape -be \"//article\" file.html", file=sys.stderr)
|
|
124
125
|
sys.exit(1)
|
|
125
|
-
if '-ej' in
|
|
126
|
+
if '-ej' in sys.argv[1:]:
|
|
126
127
|
print("Error: use -je not -ej.\n scrape -je \"//article\" file.html", file=sys.stderr)
|
|
127
128
|
sys.exit(1)
|
|
128
129
|
# Defines the HTML input argument (can be a file, URL or stdin)
|
|
@@ -262,6 +263,7 @@ examples:
|
|
|
262
263
|
sys.exit(1)
|
|
263
264
|
|
|
264
265
|
results = []
|
|
266
|
+
string_results = []
|
|
265
267
|
# For each expression, perform the search in the parsed HTML
|
|
266
268
|
for e in expression:
|
|
267
269
|
els = list(document.xpath(e))
|
|
@@ -278,6 +280,10 @@ examples:
|
|
|
278
280
|
# Clean the text when using -t option
|
|
279
281
|
if args.text:
|
|
280
282
|
text = clean_text(text)
|
|
283
|
+
# Strings are plain text, but -j parses the results as XML
|
|
284
|
+
if args.json:
|
|
285
|
+
string_results.append(text.strip())
|
|
286
|
+
text = escape(text)
|
|
281
287
|
elif args.text:
|
|
282
288
|
# If -t option is used, extract only text content
|
|
283
289
|
if hasattr(el, 'text_content'):
|
|
@@ -303,8 +309,18 @@ examples:
|
|
|
303
309
|
|
|
304
310
|
# Output handling
|
|
305
311
|
if args.json:
|
|
306
|
-
|
|
307
|
-
|
|
312
|
+
values = [s for s in string_results if s]
|
|
313
|
+
if values and len(string_results) == len(results):
|
|
314
|
+
# Only strings (@attr, text()): XML conversion would merge them into one
|
|
315
|
+
parsed = {"html": {"body": values[0] if len(values) == 1 else values}}
|
|
316
|
+
else:
|
|
317
|
+
wrapped = "<!DOCTYPE html>\n<html>\n<body>\n" + "".join(r + "\n" for r in results) + "</body>\n</html>\n"
|
|
318
|
+
try:
|
|
319
|
+
parsed = xmltodict.parse(wrapped)
|
|
320
|
+
except ExpatError as e:
|
|
321
|
+
print(f"Error: cannot convert result to JSON, it is not valid XML ({e}). "
|
|
322
|
+
"Attribute names like @click or :bind are not supported by -j.", file=sys.stderr)
|
|
323
|
+
sys.exit(1)
|
|
308
324
|
sys.stdout.write(json.dumps(parsed, indent=2, ensure_ascii=False) + "\n")
|
|
309
325
|
elif args.body and not args.text:
|
|
310
326
|
sys.stdout.write("<!DOCTYPE html>\n<html>\n<body>\n")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrape_cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.1
|
|
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
|
|
@@ -72,6 +72,7 @@ pip install -e .
|
|
|
72
72
|
- requests
|
|
73
73
|
- lxml
|
|
74
74
|
- cssselect
|
|
75
|
+
- xmltodict
|
|
75
76
|
|
|
76
77
|
## How does it work?
|
|
77
78
|
|
|
@@ -280,10 +281,12 @@ Output:
|
|
|
280
281
|
}
|
|
281
282
|
```
|
|
282
283
|
|
|
283
|
-
`-j` automatically wraps the result in `<html>/<body>` before conversion, so you don't need to pass `-b`.
|
|
284
|
+
`-j` automatically wraps the result in `<html>/<body>` before conversion, so you don't need to pass `-b`. For elements, the output is the same you would get from `scrape -be ... | xq .` (the underlying converter is `xmltodict`, the same library used by `xq`). Expressions that return only strings, such as `//a/@href` or `//p/text()`, give the string in `body` when there is one result and an array when there are more.
|
|
284
285
|
|
|
285
286
|
`-j` is mutually exclusive with `-t`, `-x` (`--check-existence`) and `-a` (`--argument`).
|
|
286
287
|
|
|
288
|
+
`-j` exits with an error when a matched element has an attribute name that is not valid XML, such as `@click` or `:bind`.
|
|
289
|
+
|
|
287
290
|
Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
|
|
288
291
|
|
|
289
292
|
Some notes on the commands:
|
|
@@ -183,6 +183,26 @@ def test_incorrect_eb_order_exits_with_specific_message():
|
|
|
183
183
|
assert "use -be not -eb" in result.stderr
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
def test_incorrect_ej_order_exits_with_specific_message():
|
|
187
|
+
result = run_scrape("-ej")
|
|
188
|
+
|
|
189
|
+
assert result.returncode == 1
|
|
190
|
+
assert "use -je not -ej" in result.stderr
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_order_checks_ignore_filenames_and_selectors(tmp_path):
|
|
194
|
+
for name in ("page-eb.html", "page-ej.html"):
|
|
195
|
+
html_file = tmp_path / name
|
|
196
|
+
html_file.write_text("<h1>ok</h1>", encoding="utf-8")
|
|
197
|
+
result = run_scrape(str(html_file), "-e", "h1")
|
|
198
|
+
assert result.returncode == 0, result.stderr
|
|
199
|
+
assert "ok" in result.stdout
|
|
200
|
+
|
|
201
|
+
for selector in (".page-ejected", ".page-ebook"):
|
|
202
|
+
result = run_scrape("-e", selector, input_data='<p class="x">ok</p>')
|
|
203
|
+
assert "use -" not in result.stderr
|
|
204
|
+
|
|
205
|
+
|
|
186
206
|
def test_invalid_css_selector_fails_conversion():
|
|
187
207
|
result = run_scrape(str(TEST_HTML), "-e", "div[")
|
|
188
208
|
|
|
@@ -235,6 +255,55 @@ def test_json_flag_conflicts_with_text_check_existence_argument():
|
|
|
235
255
|
assert "cannot be combined" in ja.stderr
|
|
236
256
|
|
|
237
257
|
|
|
258
|
+
def test_json_flag_handles_string_results_with_special_characters():
|
|
259
|
+
html = '<a href="x?a=1&b=2">AT&T <b></a>'
|
|
260
|
+
|
|
261
|
+
attr = run_scrape("-je", "//a/@href", input_data=html)
|
|
262
|
+
assert attr.returncode == 0, attr.stderr
|
|
263
|
+
assert json.loads(attr.stdout)["html"]["body"] == "x?a=1&b=2"
|
|
264
|
+
|
|
265
|
+
text = run_scrape("-je", "//a/text()", input_data=html)
|
|
266
|
+
assert text.returncode == 0, text.stderr
|
|
267
|
+
assert json.loads(text.stdout)["html"]["body"] == "AT&T <b>"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_json_flag_returns_array_for_multiple_string_results():
|
|
271
|
+
html = '<a href="x?a=1&b=2">uno</a> <a href="y">due</a>'
|
|
272
|
+
|
|
273
|
+
attrs = run_scrape("-je", "//a/@href", input_data=html)
|
|
274
|
+
assert attrs.returncode == 0, attrs.stderr
|
|
275
|
+
assert json.loads(attrs.stdout)["html"]["body"] == ["x?a=1&b=2", "y"]
|
|
276
|
+
|
|
277
|
+
# Whitespace-only text nodes are not results
|
|
278
|
+
texts = run_scrape("-je", "//body//text()", input_data=html)
|
|
279
|
+
assert json.loads(texts.stdout)["html"]["body"] == ["uno", "due"]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_json_flag_mixed_elements_and_strings_still_converts():
|
|
283
|
+
result = run_scrape("-je", "//a | //a/@href", input_data='<a href="x&y">uno</a>')
|
|
284
|
+
|
|
285
|
+
assert result.returncode == 0, result.stderr
|
|
286
|
+
body = json.loads(result.stdout)["html"]["body"]
|
|
287
|
+
assert body["a"]["#text"] == "uno"
|
|
288
|
+
assert body["#text"] == "x&y"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_json_flag_text_results_work_on_test_file():
|
|
292
|
+
result = run_scrape(str(TEST_HTML), "-je", "//p/text()")
|
|
293
|
+
|
|
294
|
+
assert result.returncode == 0, result.stderr
|
|
295
|
+
assert json.loads(result.stdout)["html"]["body"]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_json_flag_reports_clean_error_on_non_xml_attribute_name():
|
|
299
|
+
result = run_scrape("-je", "#v", input_data='<div id="v" @click="go">x</div>')
|
|
300
|
+
|
|
301
|
+
assert result.returncode == 1
|
|
302
|
+
assert "Error:" in result.stderr
|
|
303
|
+
assert "Traceback" not in result.stderr
|
|
304
|
+
assert result.stdout == ""
|
|
305
|
+
|
|
306
|
+
|
|
238
307
|
def test_url_input_downloads_and_extracts_text():
|
|
239
308
|
html_bytes = TEST_HTML.read_bytes()
|
|
240
309
|
server, thread = run_test_server(html_bytes)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|