rolling-reader 0.5.2__tar.gz → 0.6.0__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.
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/PKG-INFO +1 -1
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/pyproject.toml +1 -1
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/cli.py +145 -6
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/.gitignore +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/README.md +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/__init__.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/cache/__init__.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/cache/profile.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/dispatcher.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/extractor/__init__.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/extractor/cdp.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/extractor/clean.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/extractor/http.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/extractor/state.py +0 -0
- {rolling_reader-0.5.2 → rolling_reader-0.6.0}/src/rolling_reader/models.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rolling-reader"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Local-first web scraper that automatically rolls through HTTP → browser → JS state extraction"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -261,14 +261,155 @@ async def _run_batch(
|
|
|
261
261
|
|
|
262
262
|
|
|
263
263
|
# ---------------------------------------------------------------------------
|
|
264
|
-
#
|
|
264
|
+
# rr chrome — 启动 Chrome(带调试端口)
|
|
265
265
|
# ---------------------------------------------------------------------------
|
|
266
266
|
|
|
267
|
+
@app.command(name="chrome")
|
|
268
|
+
def launch_chrome(
|
|
269
|
+
port: int = typer.Option(9222, "--port", "-p", help="Remote debugging port (default: 9222)"),
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Launch Chrome with remote debugging enabled.
|
|
272
|
+
|
|
273
|
+
Finds Chrome automatically and starts it in the background.
|
|
274
|
+
After running this, Level 2/3 scraping works immediately.
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
|
|
278
|
+
rr chrome
|
|
279
|
+
rr https://app.example.com/dashboard
|
|
280
|
+
"""
|
|
281
|
+
import subprocess
|
|
282
|
+
import platform
|
|
283
|
+
|
|
284
|
+
import asyncio
|
|
285
|
+
import time
|
|
286
|
+
import os
|
|
287
|
+
import tempfile
|
|
288
|
+
|
|
289
|
+
# 先检查端口是否已经在用(Chrome 已经以调试模式运行)
|
|
290
|
+
if asyncio.run(_check_cdp(port)):
|
|
291
|
+
typer.echo(f"Chrome is already running with remote debugging on port {port}.")
|
|
292
|
+
typer.echo("Ready — run: rr <url>")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
exe = _find_chrome()
|
|
296
|
+
if exe is None:
|
|
297
|
+
typer.echo(
|
|
298
|
+
"Error: Chrome not found. Install Google Chrome and try again.\n"
|
|
299
|
+
"Download: https://www.google.com/chrome/",
|
|
300
|
+
err=True,
|
|
301
|
+
)
|
|
302
|
+
raise typer.Exit(code=1)
|
|
303
|
+
|
|
304
|
+
# 用独立的 user-data-dir,避免被已有 Chrome 进程吞掉
|
|
305
|
+
debug_profile = os.path.join(tempfile.gettempdir(), "rolling-reader-chrome")
|
|
306
|
+
os.makedirs(debug_profile, exist_ok=True)
|
|
307
|
+
|
|
308
|
+
args = [
|
|
309
|
+
exe,
|
|
310
|
+
f"--remote-debugging-port={port}",
|
|
311
|
+
f"--user-data-dir={debug_profile}",
|
|
312
|
+
"--remote-allow-origins=*",
|
|
313
|
+
"--no-first-run",
|
|
314
|
+
"--no-default-browser-check",
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
system = platform.system()
|
|
318
|
+
try:
|
|
319
|
+
if system == "Windows":
|
|
320
|
+
subprocess.Popen(
|
|
321
|
+
args,
|
|
322
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
323
|
+
close_fds=True,
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
subprocess.Popen(args, start_new_session=True, close_fds=True)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
typer.echo(f"Error: failed to launch Chrome: {e}", err=True)
|
|
329
|
+
raise typer.Exit(code=1)
|
|
330
|
+
|
|
331
|
+
# 等待 Chrome 初始化调试端口(最多 8 秒)
|
|
332
|
+
typer.echo("Starting Chrome...", err=True)
|
|
333
|
+
deadline = time.time() + 8
|
|
334
|
+
while time.time() < deadline:
|
|
335
|
+
time.sleep(0.5)
|
|
336
|
+
if asyncio.run(_check_cdp(port)):
|
|
337
|
+
typer.echo(f"Chrome ready on port {port}.")
|
|
338
|
+
typer.echo("Now run: rr <url>")
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
typer.echo(
|
|
342
|
+
f"Warning: Chrome launched but port {port} is not responding yet.\n"
|
|
343
|
+
"Wait a moment and try your rr command — it may still be starting up.",
|
|
344
|
+
err=True,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def _check_cdp(port: int) -> bool:
|
|
349
|
+
"""检查 CDP 端口是否已经在响应。"""
|
|
350
|
+
import httpx
|
|
351
|
+
try:
|
|
352
|
+
async with httpx.AsyncClient(timeout=1.0) as client:
|
|
353
|
+
r = await client.get(f"http://localhost:{port}/json/version")
|
|
354
|
+
return r.status_code == 200
|
|
355
|
+
except Exception:
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _find_chrome() -> Optional[str]:
|
|
360
|
+
"""在各平台上自动定位 Chrome 可执行文件。"""
|
|
361
|
+
import platform
|
|
362
|
+
import shutil
|
|
363
|
+
|
|
364
|
+
system = platform.system()
|
|
365
|
+
|
|
366
|
+
if system == "Windows":
|
|
367
|
+
candidates = [
|
|
368
|
+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
369
|
+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
370
|
+
r"C:\Users\{}\AppData\Local\Google\Chrome\Application\chrome.exe".format(
|
|
371
|
+
__import__("os").environ.get("USERNAME", "")
|
|
372
|
+
),
|
|
373
|
+
]
|
|
374
|
+
for path in candidates:
|
|
375
|
+
if __import__("os.path", fromlist=["exists"]).exists(path):
|
|
376
|
+
return path
|
|
377
|
+
# 尝试 registry
|
|
378
|
+
try:
|
|
379
|
+
import winreg
|
|
380
|
+
key = winreg.OpenKey(
|
|
381
|
+
winreg.HKEY_LOCAL_MACHINE,
|
|
382
|
+
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
|
|
383
|
+
)
|
|
384
|
+
path, _ = winreg.QueryValueEx(key, "")
|
|
385
|
+
if path:
|
|
386
|
+
return path
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
elif system == "Darwin":
|
|
391
|
+
candidates = [
|
|
392
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
393
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
394
|
+
]
|
|
395
|
+
for path in candidates:
|
|
396
|
+
if __import__("os.path", fromlist=["exists"]).exists(path):
|
|
397
|
+
return path
|
|
398
|
+
|
|
399
|
+
else: # Linux
|
|
400
|
+
for name in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser"):
|
|
401
|
+
found = shutil.which(name)
|
|
402
|
+
if found:
|
|
403
|
+
return found
|
|
404
|
+
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
267
408
|
# ---------------------------------------------------------------------------
|
|
268
409
|
# 真正的 CLI 入口
|
|
269
410
|
# ---------------------------------------------------------------------------
|
|
270
411
|
|
|
271
|
-
_SUBCOMMANDS = {"batch", "scrape", "--help", "-h", "--version"}
|
|
412
|
+
_SUBCOMMANDS = {"batch", "scrape", "chrome", "--help", "-h", "--version"}
|
|
272
413
|
|
|
273
414
|
|
|
274
415
|
def main() -> None:
|
|
@@ -292,10 +433,8 @@ def _print_error(e: ExtractionError) -> None:
|
|
|
292
433
|
if "Chrome is not available" in reason or "Cannot connect to Chrome" in reason:
|
|
293
434
|
typer.echo(
|
|
294
435
|
"\nError: Chrome is not running with remote debugging enabled.\n\n"
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
" Windows: chrome --remote-debugging-port=9222\n"
|
|
298
|
-
" Linux: google-chrome --remote-debugging-port=9222\n",
|
|
436
|
+
"Fix: run this first, then retry:\n"
|
|
437
|
+
" rr chrome\n",
|
|
299
438
|
err=True,
|
|
300
439
|
)
|
|
301
440
|
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|