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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrape_cli
3
- Version: 1.3.2
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.6
71
+ - Python >=3.8
72
72
  - requests
73
73
  - lxml
74
74
  - cssselect
@@ -53,7 +53,7 @@ pip install -e .
53
53
 
54
54
  ## Requirements
55
55
 
56
- - Python >=3.6
56
+ - Python >=3.8
57
57
  - requests
58
58
  - lxml
59
59
  - cssselect
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scrape_cli"
7
- version = "1.3.2"
7
+ version = "1.3.3"
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 = [
@@ -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.3.2"
7
+ __version__ = "1.3.3"
8
8
  __author__ = "Andrea Borruso"
9
9
  __author_email__ = "aborruso@gmail.com"
10
10
 
@@ -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='Extract HTML elements using an XPath query or CSS3 selector.',
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 XPath expression
124
- scrape -e "h1.title" file.html CSS selector
125
- scrape -e "//a" -a href file.html extract attribute
126
- scrape -t file.html extract all text
127
- scrape -e "//h1" https://example.com fetch from URL
128
- cat file.html | scrape -e "//h1" read from stdin
129
- scrape -e "//h1" -x file.html check existence (exit 0/1)
130
- scrape -be "//article" file.html wrap output in <html><body>
131
- scrape -je "//article" file.html output as JSON (built-in)
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 (file, URL or stdin, default: stdin)", metavar="HTML")
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="Argument to extract from the tag")
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="Include result in HTML and BODY tags")
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="Extract only text content (useful for LLMs)")
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 result as structured JSON")
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="Returns an exit value indicating existence")
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="Do not parse HTML before passing to etree (useful for CData)")
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="Custom User-Agent string for HTTP requests")
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
- result = document.xpath(e)
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.2
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.6
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