scrape-cli 1.2.4__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.2.4 → scrape_cli-1.3.1}/PKG-INFO +19 -13
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/README.md +17 -12
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/pyproject.toml +3 -2
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli/__init__.py +1 -1
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli/scrape.py +45 -4
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli.egg-info/PKG-INFO +19 -13
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli.egg-info/requires.txt +1 -0
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/tests/test_scrape.py +115 -0
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli.egg-info/SOURCES.txt +0 -0
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli.egg-info/dependency_links.txt +0 -0
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli.egg-info/entry_points.txt +0 -0
- {scrape_cli-1.2.4 → scrape_cli-1.3.1}/scrape_cli.egg-info/top_level.txt +0 -0
- {scrape_cli-1.2.4 → 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
|
+
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
|
|
@@ -11,6 +11,7 @@ Description-Content-Type: text/markdown
|
|
|
11
11
|
Requires-Dist: cssselect
|
|
12
12
|
Requires-Dist: lxml
|
|
13
13
|
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: xmltodict
|
|
14
15
|
|
|
15
16
|
[](https://pypi.org/project/scrape-cli/)
|
|
16
17
|
[](https://pypi.org/project/scrape-cli/)
|
|
@@ -71,6 +72,7 @@ pip install -e .
|
|
|
71
72
|
- requests
|
|
72
73
|
- lxml
|
|
73
74
|
- cssselect
|
|
75
|
+
- xmltodict
|
|
74
76
|
|
|
75
77
|
## How does it work?
|
|
76
78
|
|
|
@@ -230,13 +232,12 @@ scrape -te 'h1, h2, h3' resources/test.html
|
|
|
230
232
|
|
|
231
233
|
The `-t` option automatically excludes text from `<script>` and `<style>` tags and cleans up whitespace for better readability.
|
|
232
234
|
|
|
233
|
-
### JSON Output
|
|
235
|
+
### JSON Output
|
|
234
236
|
|
|
235
|
-
|
|
237
|
+
Use the `-j`/`--json` flag to get structured JSON natively, with no external tools:
|
|
236
238
|
|
|
237
239
|
```bash
|
|
238
|
-
|
|
239
|
-
scrape -be "a.external-link" resources/test.html | xq .
|
|
240
|
+
scrape -je "a.external-link" resources/test.html
|
|
240
241
|
```
|
|
241
242
|
|
|
242
243
|
Output:
|
|
@@ -258,7 +259,7 @@ Output:
|
|
|
258
259
|
Table extraction example:
|
|
259
260
|
|
|
260
261
|
```bash
|
|
261
|
-
scrape -
|
|
262
|
+
scrape -je "table.data-table td" resources/test.html
|
|
262
263
|
```
|
|
263
264
|
|
|
264
265
|
Output:
|
|
@@ -268,19 +269,23 @@ Output:
|
|
|
268
269
|
"html": {
|
|
269
270
|
"body": {
|
|
270
271
|
"td": [
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
272
|
+
"Italy",
|
|
273
|
+
"Rome",
|
|
274
|
+
"59",
|
|
275
|
+
"France",
|
|
276
|
+
"Paris",
|
|
277
|
+
"68"
|
|
277
278
|
]
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
```
|
|
282
283
|
|
|
283
|
-
|
|
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.
|
|
285
|
+
|
|
286
|
+
`-j` is mutually exclusive with `-t`, `-x` (`--check-existence`) and `-a` (`--argument`).
|
|
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`.
|
|
284
289
|
|
|
285
290
|
Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
|
|
286
291
|
|
|
@@ -288,6 +293,7 @@ Some notes on the commands:
|
|
|
288
293
|
|
|
289
294
|
- `-e` to set the query
|
|
290
295
|
- `-b` to add `<html>`, `<head>` and `<body>` tags to the HTML output
|
|
296
|
+
- `-j` to output structured JSON (built-in)
|
|
291
297
|
- `-t` to extract only text content (useful for LLMs and text processing)
|
|
292
298
|
|
|
293
299
|
|
|
@@ -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
|
|
|
@@ -216,13 +217,12 @@ scrape -te 'h1, h2, h3' resources/test.html
|
|
|
216
217
|
|
|
217
218
|
The `-t` option automatically excludes text from `<script>` and `<style>` tags and cleans up whitespace for better readability.
|
|
218
219
|
|
|
219
|
-
### JSON Output
|
|
220
|
+
### JSON Output
|
|
220
221
|
|
|
221
|
-
|
|
222
|
+
Use the `-j`/`--json` flag to get structured JSON natively, with no external tools:
|
|
222
223
|
|
|
223
224
|
```bash
|
|
224
|
-
|
|
225
|
-
scrape -be "a.external-link" resources/test.html | xq .
|
|
225
|
+
scrape -je "a.external-link" resources/test.html
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
Output:
|
|
@@ -244,7 +244,7 @@ Output:
|
|
|
244
244
|
Table extraction example:
|
|
245
245
|
|
|
246
246
|
```bash
|
|
247
|
-
scrape -
|
|
247
|
+
scrape -je "table.data-table td" resources/test.html
|
|
248
248
|
```
|
|
249
249
|
|
|
250
250
|
Output:
|
|
@@ -254,19 +254,23 @@ Output:
|
|
|
254
254
|
"html": {
|
|
255
255
|
"body": {
|
|
256
256
|
"td": [
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"
|
|
257
|
+
"Italy",
|
|
258
|
+
"Rome",
|
|
259
|
+
"59",
|
|
260
|
+
"France",
|
|
261
|
+
"Paris",
|
|
262
|
+
"68"
|
|
263
263
|
]
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
-
|
|
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.
|
|
270
|
+
|
|
271
|
+
`-j` is mutually exclusive with `-t`, `-x` (`--check-existence`) and `-a` (`--argument`).
|
|
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`.
|
|
270
274
|
|
|
271
275
|
Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
|
|
272
276
|
|
|
@@ -274,6 +278,7 @@ Some notes on the commands:
|
|
|
274
278
|
|
|
275
279
|
- `-e` to set the query
|
|
276
280
|
- `-b` to add `<html>`, `<head>` and `<body>` tags to the HTML output
|
|
281
|
+
- `-j` to output structured JSON (built-in)
|
|
277
282
|
- `-t` to extract only text content (useful for LLMs and text processing)
|
|
278
283
|
|
|
279
284
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scrape_cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.1"
|
|
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 = [
|
|
@@ -18,7 +18,8 @@ requires-python = ">=3.8"
|
|
|
18
18
|
dependencies = [
|
|
19
19
|
"cssselect",
|
|
20
20
|
"lxml",
|
|
21
|
-
"requests"
|
|
21
|
+
"requests",
|
|
22
|
+
"xmltodict"
|
|
22
23
|
]
|
|
23
24
|
|
|
24
25
|
[project.urls]
|
|
@@ -6,17 +6,21 @@
|
|
|
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
|
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
15
|
import re
|
|
16
|
+
import json
|
|
16
17
|
import argparse
|
|
17
18
|
import requests
|
|
19
|
+
import xmltodict
|
|
18
20
|
from lxml import etree
|
|
19
21
|
from cssselect import GenericTranslator
|
|
22
|
+
from xml.parsers.expat import ExpatError
|
|
23
|
+
from xml.sax.saxutils import escape
|
|
20
24
|
|
|
21
25
|
# Read version from __init__.py
|
|
22
26
|
init_file = os.path.join(os.path.dirname(__file__), '__init__.py')
|
|
@@ -109,15 +113,19 @@ examples:
|
|
|
109
113
|
cat file.html | scrape -e "//h1" read from stdin
|
|
110
114
|
scrape -e "//h1" -x file.html check existence (exit 0/1)
|
|
111
115
|
scrape -be "//article" file.html wrap output in <html><body>
|
|
116
|
+
scrape -je "//article" file.html output as JSON (built-in)
|
|
112
117
|
'''
|
|
113
118
|
)
|
|
114
119
|
|
|
115
120
|
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
|
|
116
121
|
|
|
117
|
-
# Check for incorrect argument order (-eb instead of -be)
|
|
118
|
-
if '-eb' in
|
|
122
|
+
# Check for incorrect argument order (-eb instead of -be, -ej instead of -je)
|
|
123
|
+
if '-eb' in sys.argv[1:]:
|
|
119
124
|
print("Error: use -be not -eb.\n scrape -be \"//article\" file.html", file=sys.stderr)
|
|
120
125
|
sys.exit(1)
|
|
126
|
+
if '-ej' in sys.argv[1:]:
|
|
127
|
+
print("Error: use -je not -ej.\n scrape -je \"//article\" file.html", file=sys.stderr)
|
|
128
|
+
sys.exit(1)
|
|
121
129
|
# Defines the HTML input argument (can be a file, URL or stdin)
|
|
122
130
|
parser.add_argument('html', nargs='?', type=str, default='',
|
|
123
131
|
help="HTML input (file, URL or stdin, default: stdin)", metavar="HTML")
|
|
@@ -130,6 +138,9 @@ examples:
|
|
|
130
138
|
# Option to extract only text content
|
|
131
139
|
parser.add_argument('-t', '--text', action='store_true', default=False,
|
|
132
140
|
help="Extract only text content (useful for LLMs)")
|
|
141
|
+
# Option to output structured JSON (built-in, no external xq needed)
|
|
142
|
+
parser.add_argument('-j', '--json', action='store_true', default=False,
|
|
143
|
+
help="Output result as structured JSON")
|
|
133
144
|
# Allows to specify one or more XPath or CSS3 selector expressions
|
|
134
145
|
parser.add_argument('-e', '--expression', default=[], action='append',
|
|
135
146
|
help="XPath query or CSS3 selector")
|
|
@@ -143,6 +154,17 @@ examples:
|
|
|
143
154
|
help="Custom User-Agent string for HTTP requests")
|
|
144
155
|
args = parser.parse_args()
|
|
145
156
|
|
|
157
|
+
# JSON flag is mutually exclusive with -t, -x, -a
|
|
158
|
+
if args.json and args.text:
|
|
159
|
+
print("Error: --json and --text are mutually exclusive.", file=sys.stderr)
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
if args.json and args.check_existence:
|
|
162
|
+
print("Error: --json and --check-existence are mutually exclusive.", file=sys.stderr)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
if args.json and args.argument:
|
|
165
|
+
print("Error: --json cannot be combined with -a/--argument.", file=sys.stderr)
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
|
|
146
168
|
# Check that at least one expression is provided by the user (unless using -t option)
|
|
147
169
|
if not args.expression and not args.text:
|
|
148
170
|
print(
|
|
@@ -241,6 +263,7 @@ examples:
|
|
|
241
263
|
sys.exit(1)
|
|
242
264
|
|
|
243
265
|
results = []
|
|
266
|
+
string_results = []
|
|
244
267
|
# For each expression, perform the search in the parsed HTML
|
|
245
268
|
for e in expression:
|
|
246
269
|
els = list(document.xpath(e))
|
|
@@ -257,6 +280,10 @@ examples:
|
|
|
257
280
|
# Clean the text when using -t option
|
|
258
281
|
if args.text:
|
|
259
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)
|
|
260
287
|
elif args.text:
|
|
261
288
|
# If -t option is used, extract only text content
|
|
262
289
|
if hasattr(el, 'text_content'):
|
|
@@ -281,7 +308,21 @@ examples:
|
|
|
281
308
|
results = [final_text] if final_text else []
|
|
282
309
|
|
|
283
310
|
# Output handling
|
|
284
|
-
if args.
|
|
311
|
+
if args.json:
|
|
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)
|
|
324
|
+
sys.stdout.write(json.dumps(parsed, indent=2, ensure_ascii=False) + "\n")
|
|
325
|
+
elif args.body and not args.text:
|
|
285
326
|
sys.stdout.write("<!DOCTYPE html>\n<html>\n<body>\n")
|
|
286
327
|
for result in results:
|
|
287
328
|
sys.stdout.write(result + "\n")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrape_cli
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -11,6 +11,7 @@ Description-Content-Type: text/markdown
|
|
|
11
11
|
Requires-Dist: cssselect
|
|
12
12
|
Requires-Dist: lxml
|
|
13
13
|
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: xmltodict
|
|
14
15
|
|
|
15
16
|
[](https://pypi.org/project/scrape-cli/)
|
|
16
17
|
[](https://pypi.org/project/scrape-cli/)
|
|
@@ -71,6 +72,7 @@ pip install -e .
|
|
|
71
72
|
- requests
|
|
72
73
|
- lxml
|
|
73
74
|
- cssselect
|
|
75
|
+
- xmltodict
|
|
74
76
|
|
|
75
77
|
## How does it work?
|
|
76
78
|
|
|
@@ -230,13 +232,12 @@ scrape -te 'h1, h2, h3' resources/test.html
|
|
|
230
232
|
|
|
231
233
|
The `-t` option automatically excludes text from `<script>` and `<style>` tags and cleans up whitespace for better readability.
|
|
232
234
|
|
|
233
|
-
### JSON Output
|
|
235
|
+
### JSON Output
|
|
234
236
|
|
|
235
|
-
|
|
237
|
+
Use the `-j`/`--json` flag to get structured JSON natively, with no external tools:
|
|
236
238
|
|
|
237
239
|
```bash
|
|
238
|
-
|
|
239
|
-
scrape -be "a.external-link" resources/test.html | xq .
|
|
240
|
+
scrape -je "a.external-link" resources/test.html
|
|
240
241
|
```
|
|
241
242
|
|
|
242
243
|
Output:
|
|
@@ -258,7 +259,7 @@ Output:
|
|
|
258
259
|
Table extraction example:
|
|
259
260
|
|
|
260
261
|
```bash
|
|
261
|
-
scrape -
|
|
262
|
+
scrape -je "table.data-table td" resources/test.html
|
|
262
263
|
```
|
|
263
264
|
|
|
264
265
|
Output:
|
|
@@ -268,19 +269,23 @@ Output:
|
|
|
268
269
|
"html": {
|
|
269
270
|
"body": {
|
|
270
271
|
"td": [
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
272
|
+
"Italy",
|
|
273
|
+
"Rome",
|
|
274
|
+
"59",
|
|
275
|
+
"France",
|
|
276
|
+
"Paris",
|
|
277
|
+
"68"
|
|
277
278
|
]
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
```
|
|
282
283
|
|
|
283
|
-
|
|
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.
|
|
285
|
+
|
|
286
|
+
`-j` is mutually exclusive with `-t`, `-x` (`--check-existence`) and `-a` (`--argument`).
|
|
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`.
|
|
284
289
|
|
|
285
290
|
Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
|
|
286
291
|
|
|
@@ -288,6 +293,7 @@ Some notes on the commands:
|
|
|
288
293
|
|
|
289
294
|
- `-e` to set the query
|
|
290
295
|
- `-b` to add `<html>`, `<head>` and `<body>` tags to the HTML output
|
|
296
|
+
- `-j` to output structured JSON (built-in)
|
|
291
297
|
- `-t` to extract only text content (useful for LLMs and text processing)
|
|
292
298
|
|
|
293
299
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import subprocess
|
|
2
3
|
import sys
|
|
3
4
|
import threading
|
|
@@ -182,6 +183,26 @@ def test_incorrect_eb_order_exits_with_specific_message():
|
|
|
182
183
|
assert "use -be not -eb" in result.stderr
|
|
183
184
|
|
|
184
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
|
+
|
|
185
206
|
def test_invalid_css_selector_fails_conversion():
|
|
186
207
|
result = run_scrape(str(TEST_HTML), "-e", "div[")
|
|
187
208
|
|
|
@@ -189,6 +210,100 @@ def test_invalid_css_selector_fails_conversion():
|
|
|
189
210
|
assert "Error converting CSS selector to XPath" in result.stderr
|
|
190
211
|
|
|
191
212
|
|
|
213
|
+
def test_json_flag_produces_valid_json_for_single_element():
|
|
214
|
+
result = run_scrape(str(TEST_HTML), "-je", "a.external-link")
|
|
215
|
+
|
|
216
|
+
assert result.returncode == 0
|
|
217
|
+
payload = json.loads(result.stdout)
|
|
218
|
+
a = payload["html"]["body"]["a"]
|
|
219
|
+
assert a["@href"] == "https://example.com"
|
|
220
|
+
assert a["@class"] == "external-link"
|
|
221
|
+
assert a["#text"] == "Example Link"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_json_flag_produces_array_for_multiple_elements():
|
|
225
|
+
result = run_scrape(str(TEST_HTML), "-je", "table.data-table td")
|
|
226
|
+
|
|
227
|
+
assert result.returncode == 0
|
|
228
|
+
payload = json.loads(result.stdout)
|
|
229
|
+
tds = payload["html"]["body"]["td"]
|
|
230
|
+
assert isinstance(tds, list)
|
|
231
|
+
assert "Italy" in tds
|
|
232
|
+
assert "Rome" in tds
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_json_flag_works_without_b_flag():
|
|
236
|
+
only_j = run_scrape(str(TEST_HTML), "-j", "-e", "a.external-link")
|
|
237
|
+
with_bj = run_scrape(str(TEST_HTML), "-bj", "-e", "a.external-link")
|
|
238
|
+
|
|
239
|
+
assert only_j.returncode == 0
|
|
240
|
+
assert with_bj.returncode == 0
|
|
241
|
+
assert only_j.stdout == with_bj.stdout
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_json_flag_conflicts_with_text_check_existence_argument():
|
|
245
|
+
jt = run_scrape(str(TEST_HTML), "-j", "-t")
|
|
246
|
+
assert jt.returncode == 1
|
|
247
|
+
assert "mutually exclusive" in jt.stderr
|
|
248
|
+
|
|
249
|
+
jx = run_scrape(str(TEST_HTML), "-j", "-e", "//h1", "-x")
|
|
250
|
+
assert jx.returncode == 1
|
|
251
|
+
assert "mutually exclusive" in jx.stderr
|
|
252
|
+
|
|
253
|
+
ja = run_scrape(str(TEST_HTML), "-j", "-e", "//a", "-a", "href")
|
|
254
|
+
assert ja.returncode == 1
|
|
255
|
+
assert "cannot be combined" in ja.stderr
|
|
256
|
+
|
|
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
|
+
|
|
192
307
|
def test_url_input_downloads_and_extracts_text():
|
|
193
308
|
html_bytes = TEST_HTML.read_bytes()
|
|
194
309
|
server, thread = run_test_server(html_bytes)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|