scrape-cli 1.2.0__tar.gz → 1.2.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.
@@ -1,21 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrape_cli
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: It's a command-line tool to extract HTML elements using an XPath query or CSS3 selector.
5
- Home-page: https://github.com/aborruso/scrape-cli
6
- Author: Andrea Borruso
7
5
  Author-email: Andrea Borruso <aborruso@gmail.com>
8
6
  Project-URL: Homepage, https://github.com/aborruso/scrape-cli
9
7
  Classifier: Programming Language :: Python :: 3
10
8
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
9
+ Requires-Python: >=3.8
12
10
  Description-Content-Type: text/markdown
13
11
  Requires-Dist: cssselect
14
12
  Requires-Dist: lxml
15
13
  Requires-Dist: requests
16
- Dynamic: author
17
- Dynamic: home-page
18
- Dynamic: requires-python
19
14
 
20
15
  [![PyPI version](https://img.shields.io/pypi/v/scrape-cli.svg?label=PyPI%20version)](https://pypi.org/project/scrape-cli/)
21
16
  [![Python Versions](https://img.shields.io/pypi/pyversions/scrape-cli.svg)](https://pypi.org/project/scrape-cli/)
@@ -52,7 +47,7 @@ uv tool install scrape-cli
52
47
  uv pip install scrape-cli
53
48
 
54
49
  # Or run temporarily without installing
55
- uvx scrape-cli --help
50
+ uvx --from scrape-cli scrape --help
56
51
  ```
57
52
 
58
53
  ### Using pip
@@ -80,7 +75,15 @@ pip install -e .
80
75
 
81
76
  ### Using the Test HTML File
82
77
 
83
- In the `resources` directory you'll find a `test.html` file that you can use to test various scraping scenarios. Here are some examples:
78
+ In the `resources` directory you'll find a `test.html` file that you can use to test various scraping scenarios.
79
+
80
+ **Note**: You can also test directly from the URL without cloning the repository:
81
+
82
+ ```bash
83
+ scrape -e "h1" https://raw.githubusercontent.com/aborruso/scrape-cli/refs/heads/master/resources/test.html
84
+ ```
85
+
86
+ Here are some examples:
84
87
 
85
88
  1. Extract all table data:
86
89
 
@@ -226,6 +229,60 @@ scrape -te 'h1, h2, h3' resources/test.html
226
229
 
227
230
  The `-t` option automatically excludes text from `<script>` and `<style>` tags and cleans up whitespace for better readability.
228
231
 
232
+ ### JSON Output Integration
233
+
234
+ You can integrate scrape-cli with [xq](https://github.com/kislyuk/yq) (part of yq) to convert HTML output to structured JSON:
235
+
236
+ ```bash
237
+ # Extract and convert to JSON (requires -b for complete HTML)
238
+ scrape -be "a.external-link" resources/test.html | xq .
239
+ ```
240
+
241
+ Output:
242
+
243
+ ```json
244
+ {
245
+ "html": {
246
+ "body": {
247
+ "a": {
248
+ "@href": "https://example.com",
249
+ "@class": "external-link",
250
+ "#text": "Example Link"
251
+ }
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ Table extraction example:
258
+
259
+ ```bash
260
+ scrape -be "table.data-table td" resources/test.html | xq .
261
+ ```
262
+
263
+ Output:
264
+
265
+ ```json
266
+ {
267
+ "html": {
268
+ "body": {
269
+ "td": [
270
+ "1",
271
+ "John Doe",
272
+ "john@example.com",
273
+ "2",
274
+ "Jane Smith",
275
+ "jane@example.com"
276
+ ]
277
+ }
278
+ }
279
+ }
280
+ ```
281
+
282
+ **Note**: The `-b` flag is mandatory to produce valid HTML with `<html>`, `<head>` and `<body>` tags.
283
+
284
+ Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
285
+
229
286
  Some notes on the commands:
230
287
 
231
288
  - `-e` to set the query
@@ -33,7 +33,7 @@ uv tool install scrape-cli
33
33
  uv pip install scrape-cli
34
34
 
35
35
  # Or run temporarily without installing
36
- uvx scrape-cli --help
36
+ uvx --from scrape-cli scrape --help
37
37
  ```
38
38
 
39
39
  ### Using pip
@@ -61,7 +61,15 @@ pip install -e .
61
61
 
62
62
  ### Using the Test HTML File
63
63
 
64
- In the `resources` directory you'll find a `test.html` file that you can use to test various scraping scenarios. Here are some examples:
64
+ In the `resources` directory you'll find a `test.html` file that you can use to test various scraping scenarios.
65
+
66
+ **Note**: You can also test directly from the URL without cloning the repository:
67
+
68
+ ```bash
69
+ scrape -e "h1" https://raw.githubusercontent.com/aborruso/scrape-cli/refs/heads/master/resources/test.html
70
+ ```
71
+
72
+ Here are some examples:
65
73
 
66
74
  1. Extract all table data:
67
75
 
@@ -207,6 +215,60 @@ scrape -te 'h1, h2, h3' resources/test.html
207
215
 
208
216
  The `-t` option automatically excludes text from `<script>` and `<style>` tags and cleans up whitespace for better readability.
209
217
 
218
+ ### JSON Output Integration
219
+
220
+ You can integrate scrape-cli with [xq](https://github.com/kislyuk/yq) (part of yq) to convert HTML output to structured JSON:
221
+
222
+ ```bash
223
+ # Extract and convert to JSON (requires -b for complete HTML)
224
+ scrape -be "a.external-link" resources/test.html | xq .
225
+ ```
226
+
227
+ Output:
228
+
229
+ ```json
230
+ {
231
+ "html": {
232
+ "body": {
233
+ "a": {
234
+ "@href": "https://example.com",
235
+ "@class": "external-link",
236
+ "#text": "Example Link"
237
+ }
238
+ }
239
+ }
240
+ }
241
+ ```
242
+
243
+ Table extraction example:
244
+
245
+ ```bash
246
+ scrape -be "table.data-table td" resources/test.html | xq .
247
+ ```
248
+
249
+ Output:
250
+
251
+ ```json
252
+ {
253
+ "html": {
254
+ "body": {
255
+ "td": [
256
+ "1",
257
+ "John Doe",
258
+ "john@example.com",
259
+ "2",
260
+ "Jane Smith",
261
+ "jane@example.com"
262
+ ]
263
+ }
264
+ }
265
+ }
266
+ ```
267
+
268
+ **Note**: The `-b` flag is mandatory to produce valid HTML with `<html>`, `<head>` and `<body>` tags.
269
+
270
+ Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
271
+
210
272
  Some notes on the commands:
211
273
 
212
274
  - `-e` to set the query
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scrape_cli"
7
- version = "1.2.0"
7
+ version = "1.2.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 = [
@@ -14,7 +14,7 @@ classifiers = [
14
14
  "Programming Language :: Python :: 3",
15
15
  "Operating System :: OS Independent",
16
16
  ]
17
- requires-python = ">=3.6"
17
+ requires-python = ">=3.8"
18
18
  dependencies = [
19
19
  "cssselect",
20
20
  "lxml",
@@ -4,7 +4,7 @@ scrape-cli - A command-line tool to extract HTML elements using XPath or CSS3 se
4
4
 
5
5
  from scrape_cli.scrape import main
6
6
 
7
- __version__ = "1.2.0"
7
+ __version__ = "1.2.1"
8
8
  __author__ = "Andrea Borruso"
9
9
  __author_email__ = "aborruso@gmail.com"
10
10
 
@@ -63,13 +63,13 @@ def is_xpath(expression):
63
63
  - Expressions wrapped in parentheses that contain XPath syntax
64
64
  """
65
65
  expr = expression.strip()
66
-
66
+
67
67
  # Direct XPath patterns
68
68
  if expr.startswith('/') or expr.startswith('//'):
69
69
  return True
70
70
  if '::' in expr:
71
71
  return True
72
-
72
+
73
73
  # Handle expressions wrapped in parentheses
74
74
  if expr.startswith('(') and expr.endswith(')'):
75
75
  # Remove outer parentheses and check inner content
@@ -78,7 +78,7 @@ def is_xpath(expression):
78
78
  return True
79
79
  if '::' in inner_expr:
80
80
  return True
81
-
81
+
82
82
  # Additional XPath indicators
83
83
  # Check for XPath-specific patterns that CSS doesn't have
84
84
  if '//' in expr or expr.startswith('/'):
@@ -91,7 +91,7 @@ def is_xpath(expression):
91
91
  return True
92
92
  if re.search(r'\b(ancestor|descendant|following|preceding|parent|child)::', expr): # XPath axes
93
93
  return True
94
-
94
+
95
95
  return False
96
96
 
97
97
  def main():
@@ -142,7 +142,7 @@ def main():
142
142
  if args.html.startswith('http://') or args.html.startswith('https://'):
143
143
  # If the input is a URL, download the HTML content
144
144
  try:
145
- response = requests.get(args.html)
145
+ response = requests.get(args.html, timeout=30)
146
146
  response.raise_for_status()
147
147
  inp = response.content
148
148
  except requests.RequestException as e:
@@ -189,7 +189,7 @@ def main():
189
189
  meta = re.search(r'<meta[^>]+charset=["\']?([\w-]+)', head)
190
190
  if meta:
191
191
  return meta.group(1)
192
- except:
192
+ except Exception:
193
193
  pass
194
194
  return None
195
195
 
@@ -1,21 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrape_cli
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: It's a command-line tool to extract HTML elements using an XPath query or CSS3 selector.
5
- Home-page: https://github.com/aborruso/scrape-cli
6
- Author: Andrea Borruso
7
5
  Author-email: Andrea Borruso <aborruso@gmail.com>
8
6
  Project-URL: Homepage, https://github.com/aborruso/scrape-cli
9
7
  Classifier: Programming Language :: Python :: 3
10
8
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
9
+ Requires-Python: >=3.8
12
10
  Description-Content-Type: text/markdown
13
11
  Requires-Dist: cssselect
14
12
  Requires-Dist: lxml
15
13
  Requires-Dist: requests
16
- Dynamic: author
17
- Dynamic: home-page
18
- Dynamic: requires-python
19
14
 
20
15
  [![PyPI version](https://img.shields.io/pypi/v/scrape-cli.svg?label=PyPI%20version)](https://pypi.org/project/scrape-cli/)
21
16
  [![Python Versions](https://img.shields.io/pypi/pyversions/scrape-cli.svg)](https://pypi.org/project/scrape-cli/)
@@ -52,7 +47,7 @@ uv tool install scrape-cli
52
47
  uv pip install scrape-cli
53
48
 
54
49
  # Or run temporarily without installing
55
- uvx scrape-cli --help
50
+ uvx --from scrape-cli scrape --help
56
51
  ```
57
52
 
58
53
  ### Using pip
@@ -80,7 +75,15 @@ pip install -e .
80
75
 
81
76
  ### Using the Test HTML File
82
77
 
83
- In the `resources` directory you'll find a `test.html` file that you can use to test various scraping scenarios. Here are some examples:
78
+ In the `resources` directory you'll find a `test.html` file that you can use to test various scraping scenarios.
79
+
80
+ **Note**: You can also test directly from the URL without cloning the repository:
81
+
82
+ ```bash
83
+ scrape -e "h1" https://raw.githubusercontent.com/aborruso/scrape-cli/refs/heads/master/resources/test.html
84
+ ```
85
+
86
+ Here are some examples:
84
87
 
85
88
  1. Extract all table data:
86
89
 
@@ -226,6 +229,60 @@ scrape -te 'h1, h2, h3' resources/test.html
226
229
 
227
230
  The `-t` option automatically excludes text from `<script>` and `<style>` tags and cleans up whitespace for better readability.
228
231
 
232
+ ### JSON Output Integration
233
+
234
+ You can integrate scrape-cli with [xq](https://github.com/kislyuk/yq) (part of yq) to convert HTML output to structured JSON:
235
+
236
+ ```bash
237
+ # Extract and convert to JSON (requires -b for complete HTML)
238
+ scrape -be "a.external-link" resources/test.html | xq .
239
+ ```
240
+
241
+ Output:
242
+
243
+ ```json
244
+ {
245
+ "html": {
246
+ "body": {
247
+ "a": {
248
+ "@href": "https://example.com",
249
+ "@class": "external-link",
250
+ "#text": "Example Link"
251
+ }
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ Table extraction example:
258
+
259
+ ```bash
260
+ scrape -be "table.data-table td" resources/test.html | xq .
261
+ ```
262
+
263
+ Output:
264
+
265
+ ```json
266
+ {
267
+ "html": {
268
+ "body": {
269
+ "td": [
270
+ "1",
271
+ "John Doe",
272
+ "john@example.com",
273
+ "2",
274
+ "Jane Smith",
275
+ "jane@example.com"
276
+ ]
277
+ }
278
+ }
279
+ }
280
+ ```
281
+
282
+ **Note**: The `-b` flag is mandatory to produce valid HTML with `<html>`, `<head>` and `<body>` tags.
283
+
284
+ Useful for JSON-based pipelines, APIs, databases, and processing with jq/DuckDB.
285
+
229
286
  Some notes on the commands:
230
287
 
231
288
  - `-e` to set the query
@@ -1,6 +1,5 @@
1
1
  README.md
2
2
  pyproject.toml
3
- setup.py
4
3
  scrape_cli/__init__.py
5
4
  scrape_cli/scrape.py
6
5
  scrape_cli.egg-info/PKG-INFO
@@ -8,4 +7,5 @@ scrape_cli.egg-info/SOURCES.txt
8
7
  scrape_cli.egg-info/dependency_links.txt
9
8
  scrape_cli.egg-info/entry_points.txt
10
9
  scrape_cli.egg-info/requires.txt
11
- scrape_cli.egg-info/top_level.txt
10
+ scrape_cli.egg-info/top_level.txt
11
+ tests/test_scrape.py
@@ -0,0 +1,205 @@
1
+ import subprocess
2
+ import sys
3
+ import threading
4
+ from pathlib import Path
5
+ from http.server import BaseHTTPRequestHandler, HTTPServer
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+ TEST_HTML = ROOT / "resources" / "test.html"
9
+ sys.path.insert(0, str(ROOT))
10
+
11
+ from scrape_cli.scrape import is_xpath
12
+
13
+
14
+ def run_scrape(*args, input_data=None):
15
+ cmd = [sys.executable, "-m", "scrape_cli.scrape", *args]
16
+ return subprocess.run(
17
+ cmd,
18
+ capture_output=True,
19
+ text=True,
20
+ cwd=ROOT,
21
+ input=input_data,
22
+ )
23
+
24
+
25
+ def run_test_server(html_bytes):
26
+ class Handler(BaseHTTPRequestHandler):
27
+ def do_GET(self):
28
+ self.send_response(200)
29
+ self.send_header("Content-Type", "text/html; charset=utf-8")
30
+ self.end_headers()
31
+ self.wfile.write(html_bytes)
32
+
33
+ def log_message(self, format, *args):
34
+ return
35
+
36
+ server = HTTPServer(("127.0.0.1", 0), Handler)
37
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
38
+ thread.start()
39
+ return server, thread
40
+
41
+
42
+ def test_is_xpath_true_patterns():
43
+ candidates = [
44
+ "//div",
45
+ "/html/body/div",
46
+ "(//div)[1]",
47
+ "//a/@href",
48
+ "//li[2]",
49
+ "ancestor::div",
50
+ "descendant::span",
51
+ "//p/text()",
52
+ ]
53
+
54
+ for expression in candidates:
55
+ assert is_xpath(expression) is True
56
+
57
+
58
+ def test_is_xpath_false_css_patterns():
59
+ candidates = [
60
+ "div.content > a.link",
61
+ "a[href*='/about']",
62
+ "input[type='email']",
63
+ "ul.items-list li:first-child",
64
+ "div.class1.class2",
65
+ ]
66
+
67
+ for expression in candidates:
68
+ assert is_xpath(expression) is False
69
+
70
+
71
+ def test_xpath_parentheses_extracts_first_match():
72
+ result = run_scrape(str(TEST_HTML), "-e", "(//ul[@class='items-list']/li)[1]", "-t")
73
+
74
+ assert result.returncode == 0
75
+ assert result.stdout.strip() == "First item"
76
+
77
+
78
+ def test_css_attribute_selector_is_not_misclassified_as_xpath():
79
+ result = run_scrape(str(TEST_HTML), "-e", "a[href*='/about']", "-t")
80
+
81
+ assert result.returncode == 0
82
+ assert result.stdout.strip() == "About Page"
83
+
84
+
85
+ def test_check_existence_true_and_false():
86
+ found = run_scrape(str(TEST_HTML), "-e", "//h1", "--check-existence")
87
+ missing = run_scrape(str(TEST_HTML), "-e", "//this-node-does-not-exist", "--check-existence")
88
+
89
+ assert found.returncode == 0
90
+ assert missing.returncode == 1
91
+
92
+
93
+ def test_encoding_meta_charset_iso_8859_1(tmp_path):
94
+ html = """<!doctype html>
95
+ <html>
96
+ <head><meta charset=\"iso-8859-1\"></head>
97
+ <body><p>Perch\xe9</p></body>
98
+ </html>
99
+ """.encode("iso-8859-1")
100
+ sample = tmp_path / "latin1.html"
101
+ sample.write_bytes(html)
102
+
103
+ result = run_scrape(str(sample), "-e", "//p/text()", "-t")
104
+
105
+ assert result.returncode == 0
106
+ assert result.stdout.strip() == "Perché"
107
+
108
+
109
+ def test_argument_extracts_attribute_value():
110
+ result = run_scrape(str(TEST_HTML), "-e", "//a[@class='external-link']", "-a", "href")
111
+
112
+ assert result.returncode == 0
113
+ assert result.stdout.strip() == "https://example.com"
114
+
115
+
116
+ def test_body_flag_wraps_output_in_html_body():
117
+ result = run_scrape(str(TEST_HTML), "-e", "//h1", "-b")
118
+
119
+ assert result.returncode == 0
120
+ assert result.stdout.startswith("<!DOCTYPE html>\n<html>\n<body>\n")
121
+ assert result.stdout.strip().endswith("</body>\n</html>")
122
+ assert "<h1 id=\"main-title\">Welcome to the Test Page</h1>" in result.stdout
123
+
124
+
125
+ def test_text_flag_without_expression_extracts_body_and_skips_script():
126
+ result = run_scrape(str(TEST_HTML), "-t")
127
+
128
+ assert result.returncode == 0
129
+ assert "Welcome to the Test Page" in result.stdout
130
+ assert "document.getElementById('dynamic-content')" not in result.stdout
131
+
132
+
133
+ def test_short_check_existence_flag_x():
134
+ found = run_scrape(str(TEST_HTML), "-e", "//table", "-x")
135
+ missing = run_scrape(str(TEST_HTML), "-e", "//definitely-not-here", "-x")
136
+
137
+ assert found.returncode == 0
138
+ assert missing.returncode == 1
139
+
140
+
141
+ def test_rawinput_parses_xml_without_html_parser():
142
+ xml_data = "<root><item>one</item><item>two</item></root>"
143
+ result = run_scrape("-e", "//item[2]/text()", "-r", input_data=xml_data)
144
+
145
+ assert result.returncode == 0
146
+ assert result.stdout.strip() == "two"
147
+
148
+
149
+ def test_stdin_input_works_when_no_html_argument():
150
+ html_data = "<html><body><p>stdin-ok</p></body></html>"
151
+ result = run_scrape("-e", "//p/text()", "-t", input_data=html_data)
152
+
153
+ assert result.returncode == 0
154
+ assert result.stdout.strip() == "stdin-ok"
155
+
156
+
157
+ def test_empty_stdin_returns_error():
158
+ result = run_scrape("-e", "//p", input_data="")
159
+
160
+ assert result.returncode == 1
161
+ assert "Error: No input received from stdin" in result.stdout
162
+
163
+
164
+ def test_missing_file_returns_error():
165
+ result = run_scrape("resources/this-file-does-not-exist.html", "-e", "//p")
166
+
167
+ assert result.returncode == 1
168
+ assert "was not found" in result.stdout
169
+
170
+
171
+ def test_missing_expression_without_text_returns_error():
172
+ result = run_scrape(str(TEST_HTML))
173
+
174
+ assert result.returncode == 1
175
+ assert "you must provide at least one XPath query or CSS3 selector" in result.stderr
176
+
177
+
178
+ def test_incorrect_eb_order_exits_with_specific_message():
179
+ result = run_scrape("-eb")
180
+
181
+ assert result.returncode == 1
182
+ assert "Please use -be instead of -eb." in result.stderr
183
+
184
+
185
+ def test_invalid_css_selector_fails_conversion():
186
+ result = run_scrape(str(TEST_HTML), "-e", "div[")
187
+
188
+ assert result.returncode == 1
189
+ assert "Error converting CSS selector to XPath" in result.stdout
190
+
191
+
192
+ def test_url_input_downloads_and_extracts_text():
193
+ html_bytes = TEST_HTML.read_bytes()
194
+ server, thread = run_test_server(html_bytes)
195
+
196
+ try:
197
+ url = f"http://127.0.0.1:{server.server_address[1]}"
198
+ result = run_scrape(url, "-e", "//h1/text()", "-t")
199
+ finally:
200
+ server.shutdown()
201
+ server.server_close()
202
+ thread.join(timeout=2)
203
+
204
+ assert result.returncode == 0
205
+ assert result.stdout.strip() == "Welcome to the Test Page"
scrape_cli-1.2.0/setup.py DELETED
@@ -1,37 +0,0 @@
1
- # setup.py
2
- from setuptools import setup
3
- from pathlib import Path
4
-
5
- # Leggi il README
6
- this_directory = Path(__file__).parent
7
- long_description = (this_directory / "README.md").read_text(encoding="utf-8")
8
-
9
- setup(
10
- name="scrape_cli",
11
- version="1.1.9",
12
- description="It's a command-line tool to extract HTML elements using an XPath query or CSS3 selector.",
13
- long_description=long_description,
14
- long_description_content_type="text/markdown",
15
- author="Andrea Borruso",
16
- author_email="aborruso@gmail.com",
17
- url="https://github.com/aborruso/scrape-cli",
18
- license="MIT",
19
- packages=["scrape_cli"],
20
- package_dir={"scrape_cli": "scrape_cli"},
21
- entry_points={
22
- 'console_scripts': [
23
- 'scrape=scrape_cli.scrape:main',
24
- ],
25
- },
26
- install_requires=[
27
- "cssselect",
28
- "lxml",
29
- "requests"
30
- ],
31
- classifiers=[
32
- "Programming Language :: Python :: 3",
33
- "License :: OSI Approved :: MIT License",
34
- "Operating System :: OS Independent",
35
- ],
36
- python_requires='>=3.6',
37
- )
File without changes