proxy-scraper-cli 1.19.0__tar.gz → 1.21.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.
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/PKG-INFO +75 -3
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/README.md +74 -2
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/PKG-INFO +75 -3
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/SOURCES.txt +3 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/__init__.py +1 -1
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/agent.py +31 -8
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/api.py +77 -3
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/app.py +5 -3
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/cli.py +6 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/options.py +9 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/pipeline.py +22 -8
- proxy_scraper_cli-1.21.0/proxyscraper/server/api.py +186 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/core.py +28 -7
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/pool.py +9 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/sources.py +24 -0
- proxy_scraper_cli-1.21.0/tests/test_own_sources.py +98 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_rotator.py +92 -0
- proxy_scraper_cli-1.21.0/tests/test_serve_api.py +153 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_server.py +2 -1
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/LICENSE +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/__main__.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/action.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/asndb.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/blocklist.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/charts.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/checker.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/compat.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/completion.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/exporters.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/fetchcache.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/geo.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/geodb.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/handshake.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/history.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/judges.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/mcp_entry.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/mcp_server.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/netio.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/output.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/pages.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/parsing.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/paths.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/preferences.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/proxypages.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/publish.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ranking.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/report.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/runsteps.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/__init__.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/http.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/socks.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/status.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/server/upstream.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/apple-touch-icon.png +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/index.html +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/logo.png +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/site/og.png +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/sites.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/sources.json +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/speed.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/targets.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/__init__.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/dashboard.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/keys.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/report.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/serve.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/widgets.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxyscraper/ui/wizard.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/pyproject.toml +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/setup.cfg +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_action.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_agent.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_api.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_app.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_asndb.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_auth.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_blocklist.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_charts.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_checker.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_compat.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_completion.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_discovery_growth.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_exporters.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_fetchcache.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_geodb.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_judges.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_learning.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_live_api.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_mcp_server.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_mine_sources.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_options.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_output_stdout.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_packaging.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_pages.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_parsing.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_parsing_fuzz.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_paths.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_pick.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_pipeline.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_proxy_pages.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_proxy_sources.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_publish.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_report.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_robustness.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_serve_password.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_serve_refill.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_serve_v2.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_server_closed_writer.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_server_fixes.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_sites.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_sources_json.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_speed.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_speed_ranking.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_tamper.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_targets.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_ui.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_uptime.py +0 -0
- {proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/tests/test_wizard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.21.0
|
|
4
4
|
Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
|
|
5
5
|
Author: Maximilian Feix
|
|
6
6
|
License-Expression: MIT
|
|
@@ -52,6 +52,7 @@ Dynamic: license-file
|
|
|
52
52
|
[](#live-list)
|
|
53
53
|
[](https://pypi.org/project/proxy-scraper-cli/)
|
|
54
54
|
[](pyproject.toml)
|
|
55
|
+
[](https://github.com/maximilianfeix/proxy-scraper/stargazers)
|
|
55
56
|
[](LICENSE)
|
|
56
57
|
|
|
57
58
|
<a href="https://maximilianfeix.github.io/proxy-scraper/"><img src="https://img.shields.io/badge/Browse_the_live_list-D4F77A?style=for-the-badge&labelColor=121113" alt="Browse the live list"></a>
|
|
@@ -94,7 +95,7 @@ Most free proxy lists are 95 % dead, and a good part of the rest are honeypots o
|
|
|
94
95
|
- [Options](#options)
|
|
95
96
|
- [GitHub Actions](#github-actions)
|
|
96
97
|
- [FAQ](#faq)
|
|
97
|
-
- [Roadmap](#roadmap) · [Contributing](#contributing) · [Acknowledgements](#acknowledgements)
|
|
98
|
+
- [Roadmap](#roadmap) · [Contributing](#contributing) · [Community](#community) · [Acknowledgements](#acknowledgements)
|
|
98
99
|
|
|
99
100
|
</details>
|
|
100
101
|
|
|
@@ -384,6 +385,7 @@ macOS, Linux and Windows, Python 3.9 to 3.13. Only two dependencies: `rich` and
|
|
|
384
385
|
| Only proxies that reach *your* target site | ❌ | ✅ `--target` |
|
|
385
386
|
| Learns which sources are worth it | ❌ | ✅ |
|
|
386
387
|
| Usable as a single rotating proxy | ❌ | ✅ `--serve` |
|
|
388
|
+
| API to fetch a proxy (proxy_pool compatible) | ❌ | ✅ [`/get`](#pool-api) |
|
|
387
389
|
| Ready-made list without running anything | ✅ | ✅ [live list](#live-list) |
|
|
388
390
|
|
|
389
391
|
<a id="examples"></a>
|
|
@@ -456,6 +458,21 @@ with ProxyRotator(country="DE") as rotator:
|
|
|
456
458
|
|
|
457
459
|
Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
|
|
458
460
|
|
|
461
|
+
Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
import requests
|
|
465
|
+
from proxyscraper import ProxyRotator
|
|
466
|
+
|
|
467
|
+
with ProxyRotator(country="DE") as rotator:
|
|
468
|
+
proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
|
|
469
|
+
requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
|
|
470
|
+
# httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
|
|
471
|
+
# Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
|
|
475
|
+
|
|
459
476
|
<a id="proxy-server"></a>
|
|
460
477
|
|
|
461
478
|
## Rotating proxy server
|
|
@@ -504,6 +521,38 @@ Without a password, `--serve-host` means **anyone who reaches the port can use i
|
|
|
504
521
|
|
|
505
522
|
In testing: 20 of 20 HTTPS requests succeeded, over 15 different exit IPs. In the wizard this is **Proxy server right away**.
|
|
506
523
|
|
|
524
|
+
<a id="pool-api"></a>
|
|
525
|
+
|
|
526
|
+
### Proxy pool API
|
|
527
|
+
|
|
528
|
+
Some programs want a proxy *address*, not a proxy – to hand it to a browser, a worker or a queue. The same port answers plain HTTP requests with one:
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
curl http://127.0.0.1:8899/get # one proxy as JSON, chosen like a connection would be
|
|
532
|
+
curl "http://127.0.0.1:8899/get?country=DE&https=1&format=txt" # → http://203.0.113.7:8080
|
|
533
|
+
curl "http://127.0.0.1:8899/all?protocol=socks5&limit=20" # the 20 best SOCKS5 proxies
|
|
534
|
+
curl "http://127.0.0.1:8899/report?proxy=203.0.113.7:8080&ok=0" # it failed you – three times and it's out
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
| Endpoint | What it does |
|
|
538
|
+
|---|---|
|
|
539
|
+
| `/get` | one proxy, picked by the `--rotate` strategy |
|
|
540
|
+
| `/pop` | like `/get`, and the proxy leaves the pool |
|
|
541
|
+
| `/all` | every usable proxy, best first (`limit=N`) |
|
|
542
|
+
| `/count` | totals by type and country |
|
|
543
|
+
| `/delete?proxy=IP:PORT` | take a proxy out |
|
|
544
|
+
| `/report?proxy=IP:PORT&ok=0` | feedback from your own requests |
|
|
545
|
+
|
|
546
|
+
Filters work on `/get`, `/pop` and `/all`: `country=DE,AT`, `protocol=socks5`, `https=1`, `anonymity=elite`, `max_latency=1500`, and `format=txt` for plain URLs. With `--serve-password` the API wants it as Basic auth, like the status page.
|
|
547
|
+
|
|
548
|
+
**Coming from [jhao104/proxy_pool](https://github.com/jhao104/proxy_pool)?** The endpoints, `type=https` and the JSON fields (`proxy`, `https`, `region`, `anonymous`, `check_count`, `fail_count`, …) are the same, so point your code at port 8899 and it keeps working – without Redis, and with proxies that passed the honeypot, tampering and TLS checks:
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
import requests
|
|
552
|
+
proxy = requests.get("http://127.0.0.1:8899/get?type=https").json()["url"] # e.g. socks5://…, type included
|
|
553
|
+
requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
|
|
554
|
+
```
|
|
555
|
+
|
|
507
556
|
<a id="discord-bot"></a>
|
|
508
557
|
|
|
509
558
|
## Discord and Telegram bot
|
|
@@ -543,6 +592,15 @@ flowchart LR
|
|
|
543
592
|
</div>
|
|
544
593
|
</details>
|
|
545
594
|
|
|
595
|
+
### Your own lists
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
proxy-scraper --source https://example.com/my-list.txt --source socks5=./socks.txt # on top of the 700+ sources
|
|
599
|
+
proxy-scraper --only-sources --source bought.txt --want 50 # only yours
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Any text with `ip:port` works; lines like `socks5://user:pass@host:port` keep their type, bare ones are tried as HTTP and SOCKS5 unless you write `http=…`.
|
|
603
|
+
|
|
546
604
|
<a id="output"></a>
|
|
547
605
|
|
|
548
606
|
## Output
|
|
@@ -909,7 +967,7 @@ proxyscraper/
|
|
|
909
967
|
├── targets.py target sites for --target
|
|
910
968
|
├── output.py result files
|
|
911
969
|
├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
|
|
912
|
-
├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
|
|
970
|
+
├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · api · core
|
|
913
971
|
├── api.py find_proxies() / check_proxies() for Python
|
|
914
972
|
├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
|
|
915
973
|
├── mcp_server.py MCP server (proxy-scraper-mcp) for AI agents
|
|
@@ -922,6 +980,20 @@ proxyscraper/
|
|
|
922
980
|
|
|
923
981
|
</details>
|
|
924
982
|
|
|
983
|
+
<a id="community"></a>
|
|
984
|
+
|
|
985
|
+
## Community
|
|
986
|
+
|
|
987
|
+
Questions, ideas and things you built with it go to [Discussions](https://github.com/maximilianfeix/proxy-scraper/discussions) – bugs to the [issues](../../issues). If proxy-scraper saves you time, a ⭐ helps others find it.
|
|
988
|
+
|
|
989
|
+
<a href="https://star-history.com/#maximilianfeix/proxy-scraper&Date">
|
|
990
|
+
<picture>
|
|
991
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date&theme=dark">
|
|
992
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date">
|
|
993
|
+
<img alt="Star history of proxy-scraper" src="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date" width="600">
|
|
994
|
+
</picture>
|
|
995
|
+
</a>
|
|
996
|
+
|
|
925
997
|
<a id="acknowledgements"></a>
|
|
926
998
|
|
|
927
999
|
## Acknowledgements
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
[](#live-list)
|
|
12
12
|
[](https://pypi.org/project/proxy-scraper-cli/)
|
|
13
13
|
[](pyproject.toml)
|
|
14
|
+
[](https://github.com/maximilianfeix/proxy-scraper/stargazers)
|
|
14
15
|
[](LICENSE)
|
|
15
16
|
|
|
16
17
|
<a href="https://maximilianfeix.github.io/proxy-scraper/"><img src="https://img.shields.io/badge/Browse_the_live_list-D4F77A?style=for-the-badge&labelColor=121113" alt="Browse the live list"></a>
|
|
@@ -53,7 +54,7 @@ Most free proxy lists are 95 % dead, and a good part of the rest are honeypots o
|
|
|
53
54
|
- [Options](#options)
|
|
54
55
|
- [GitHub Actions](#github-actions)
|
|
55
56
|
- [FAQ](#faq)
|
|
56
|
-
- [Roadmap](#roadmap) · [Contributing](#contributing) · [Acknowledgements](#acknowledgements)
|
|
57
|
+
- [Roadmap](#roadmap) · [Contributing](#contributing) · [Community](#community) · [Acknowledgements](#acknowledgements)
|
|
57
58
|
|
|
58
59
|
</details>
|
|
59
60
|
|
|
@@ -343,6 +344,7 @@ macOS, Linux and Windows, Python 3.9 to 3.13. Only two dependencies: `rich` and
|
|
|
343
344
|
| Only proxies that reach *your* target site | ❌ | ✅ `--target` |
|
|
344
345
|
| Learns which sources are worth it | ❌ | ✅ |
|
|
345
346
|
| Usable as a single rotating proxy | ❌ | ✅ `--serve` |
|
|
347
|
+
| API to fetch a proxy (proxy_pool compatible) | ❌ | ✅ [`/get`](#pool-api) |
|
|
346
348
|
| Ready-made list without running anything | ✅ | ✅ [live list](#live-list) |
|
|
347
349
|
|
|
348
350
|
<a id="examples"></a>
|
|
@@ -415,6 +417,21 @@ with ProxyRotator(country="DE") as rotator:
|
|
|
415
417
|
|
|
416
418
|
Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
|
|
417
419
|
|
|
420
|
+
Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
import requests
|
|
424
|
+
from proxyscraper import ProxyRotator
|
|
425
|
+
|
|
426
|
+
with ProxyRotator(country="DE") as rotator:
|
|
427
|
+
proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
|
|
428
|
+
requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
|
|
429
|
+
# httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
|
|
430
|
+
# Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
|
|
434
|
+
|
|
418
435
|
<a id="proxy-server"></a>
|
|
419
436
|
|
|
420
437
|
## Rotating proxy server
|
|
@@ -463,6 +480,38 @@ Without a password, `--serve-host` means **anyone who reaches the port can use i
|
|
|
463
480
|
|
|
464
481
|
In testing: 20 of 20 HTTPS requests succeeded, over 15 different exit IPs. In the wizard this is **Proxy server right away**.
|
|
465
482
|
|
|
483
|
+
<a id="pool-api"></a>
|
|
484
|
+
|
|
485
|
+
### Proxy pool API
|
|
486
|
+
|
|
487
|
+
Some programs want a proxy *address*, not a proxy – to hand it to a browser, a worker or a queue. The same port answers plain HTTP requests with one:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
curl http://127.0.0.1:8899/get # one proxy as JSON, chosen like a connection would be
|
|
491
|
+
curl "http://127.0.0.1:8899/get?country=DE&https=1&format=txt" # → http://203.0.113.7:8080
|
|
492
|
+
curl "http://127.0.0.1:8899/all?protocol=socks5&limit=20" # the 20 best SOCKS5 proxies
|
|
493
|
+
curl "http://127.0.0.1:8899/report?proxy=203.0.113.7:8080&ok=0" # it failed you – three times and it's out
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
| Endpoint | What it does |
|
|
497
|
+
|---|---|
|
|
498
|
+
| `/get` | one proxy, picked by the `--rotate` strategy |
|
|
499
|
+
| `/pop` | like `/get`, and the proxy leaves the pool |
|
|
500
|
+
| `/all` | every usable proxy, best first (`limit=N`) |
|
|
501
|
+
| `/count` | totals by type and country |
|
|
502
|
+
| `/delete?proxy=IP:PORT` | take a proxy out |
|
|
503
|
+
| `/report?proxy=IP:PORT&ok=0` | feedback from your own requests |
|
|
504
|
+
|
|
505
|
+
Filters work on `/get`, `/pop` and `/all`: `country=DE,AT`, `protocol=socks5`, `https=1`, `anonymity=elite`, `max_latency=1500`, and `format=txt` for plain URLs. With `--serve-password` the API wants it as Basic auth, like the status page.
|
|
506
|
+
|
|
507
|
+
**Coming from [jhao104/proxy_pool](https://github.com/jhao104/proxy_pool)?** The endpoints, `type=https` and the JSON fields (`proxy`, `https`, `region`, `anonymous`, `check_count`, `fail_count`, …) are the same, so point your code at port 8899 and it keeps working – without Redis, and with proxies that passed the honeypot, tampering and TLS checks:
|
|
508
|
+
|
|
509
|
+
```python
|
|
510
|
+
import requests
|
|
511
|
+
proxy = requests.get("http://127.0.0.1:8899/get?type=https").json()["url"] # e.g. socks5://…, type included
|
|
512
|
+
requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
|
|
513
|
+
```
|
|
514
|
+
|
|
466
515
|
<a id="discord-bot"></a>
|
|
467
516
|
|
|
468
517
|
## Discord and Telegram bot
|
|
@@ -502,6 +551,15 @@ flowchart LR
|
|
|
502
551
|
</div>
|
|
503
552
|
</details>
|
|
504
553
|
|
|
554
|
+
### Your own lists
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
proxy-scraper --source https://example.com/my-list.txt --source socks5=./socks.txt # on top of the 700+ sources
|
|
558
|
+
proxy-scraper --only-sources --source bought.txt --want 50 # only yours
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Any text with `ip:port` works; lines like `socks5://user:pass@host:port` keep their type, bare ones are tried as HTTP and SOCKS5 unless you write `http=…`.
|
|
562
|
+
|
|
505
563
|
<a id="output"></a>
|
|
506
564
|
|
|
507
565
|
## Output
|
|
@@ -868,7 +926,7 @@ proxyscraper/
|
|
|
868
926
|
├── targets.py target sites for --target
|
|
869
927
|
├── output.py result files
|
|
870
928
|
├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
|
|
871
|
-
├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
|
|
929
|
+
├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · api · core
|
|
872
930
|
├── api.py find_proxies() / check_proxies() for Python
|
|
873
931
|
├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
|
|
874
932
|
├── mcp_server.py MCP server (proxy-scraper-mcp) for AI agents
|
|
@@ -881,6 +939,20 @@ proxyscraper/
|
|
|
881
939
|
|
|
882
940
|
</details>
|
|
883
941
|
|
|
942
|
+
<a id="community"></a>
|
|
943
|
+
|
|
944
|
+
## Community
|
|
945
|
+
|
|
946
|
+
Questions, ideas and things you built with it go to [Discussions](https://github.com/maximilianfeix/proxy-scraper/discussions) – bugs to the [issues](../../issues). If proxy-scraper saves you time, a ⭐ helps others find it.
|
|
947
|
+
|
|
948
|
+
<a href="https://star-history.com/#maximilianfeix/proxy-scraper&Date">
|
|
949
|
+
<picture>
|
|
950
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date&theme=dark">
|
|
951
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date">
|
|
952
|
+
<img alt="Star history of proxy-scraper" src="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date" width="600">
|
|
953
|
+
</picture>
|
|
954
|
+
</a>
|
|
955
|
+
|
|
884
956
|
<a id="acknowledgements"></a>
|
|
885
957
|
|
|
886
958
|
## Acknowledgements
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.21.0
|
|
4
4
|
Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
|
|
5
5
|
Author: Maximilian Feix
|
|
6
6
|
License-Expression: MIT
|
|
@@ -52,6 +52,7 @@ Dynamic: license-file
|
|
|
52
52
|
[](#live-list)
|
|
53
53
|
[](https://pypi.org/project/proxy-scraper-cli/)
|
|
54
54
|
[](pyproject.toml)
|
|
55
|
+
[](https://github.com/maximilianfeix/proxy-scraper/stargazers)
|
|
55
56
|
[](LICENSE)
|
|
56
57
|
|
|
57
58
|
<a href="https://maximilianfeix.github.io/proxy-scraper/"><img src="https://img.shields.io/badge/Browse_the_live_list-D4F77A?style=for-the-badge&labelColor=121113" alt="Browse the live list"></a>
|
|
@@ -94,7 +95,7 @@ Most free proxy lists are 95 % dead, and a good part of the rest are honeypots o
|
|
|
94
95
|
- [Options](#options)
|
|
95
96
|
- [GitHub Actions](#github-actions)
|
|
96
97
|
- [FAQ](#faq)
|
|
97
|
-
- [Roadmap](#roadmap) · [Contributing](#contributing) · [Acknowledgements](#acknowledgements)
|
|
98
|
+
- [Roadmap](#roadmap) · [Contributing](#contributing) · [Community](#community) · [Acknowledgements](#acknowledgements)
|
|
98
99
|
|
|
99
100
|
</details>
|
|
100
101
|
|
|
@@ -384,6 +385,7 @@ macOS, Linux and Windows, Python 3.9 to 3.13. Only two dependencies: `rich` and
|
|
|
384
385
|
| Only proxies that reach *your* target site | ❌ | ✅ `--target` |
|
|
385
386
|
| Learns which sources are worth it | ❌ | ✅ |
|
|
386
387
|
| Usable as a single rotating proxy | ❌ | ✅ `--serve` |
|
|
388
|
+
| API to fetch a proxy (proxy_pool compatible) | ❌ | ✅ [`/get`](#pool-api) |
|
|
387
389
|
| Ready-made list without running anything | ✅ | ✅ [live list](#live-list) |
|
|
388
390
|
|
|
389
391
|
<a id="examples"></a>
|
|
@@ -456,6 +458,21 @@ with ProxyRotator(country="DE") as rotator:
|
|
|
456
458
|
|
|
457
459
|
Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
|
|
458
460
|
|
|
461
|
+
Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
import requests
|
|
465
|
+
from proxyscraper import ProxyRotator
|
|
466
|
+
|
|
467
|
+
with ProxyRotator(country="DE") as rotator:
|
|
468
|
+
proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
|
|
469
|
+
requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
|
|
470
|
+
# httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
|
|
471
|
+
# Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
|
|
475
|
+
|
|
459
476
|
<a id="proxy-server"></a>
|
|
460
477
|
|
|
461
478
|
## Rotating proxy server
|
|
@@ -504,6 +521,38 @@ Without a password, `--serve-host` means **anyone who reaches the port can use i
|
|
|
504
521
|
|
|
505
522
|
In testing: 20 of 20 HTTPS requests succeeded, over 15 different exit IPs. In the wizard this is **Proxy server right away**.
|
|
506
523
|
|
|
524
|
+
<a id="pool-api"></a>
|
|
525
|
+
|
|
526
|
+
### Proxy pool API
|
|
527
|
+
|
|
528
|
+
Some programs want a proxy *address*, not a proxy – to hand it to a browser, a worker or a queue. The same port answers plain HTTP requests with one:
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
curl http://127.0.0.1:8899/get # one proxy as JSON, chosen like a connection would be
|
|
532
|
+
curl "http://127.0.0.1:8899/get?country=DE&https=1&format=txt" # → http://203.0.113.7:8080
|
|
533
|
+
curl "http://127.0.0.1:8899/all?protocol=socks5&limit=20" # the 20 best SOCKS5 proxies
|
|
534
|
+
curl "http://127.0.0.1:8899/report?proxy=203.0.113.7:8080&ok=0" # it failed you – three times and it's out
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
| Endpoint | What it does |
|
|
538
|
+
|---|---|
|
|
539
|
+
| `/get` | one proxy, picked by the `--rotate` strategy |
|
|
540
|
+
| `/pop` | like `/get`, and the proxy leaves the pool |
|
|
541
|
+
| `/all` | every usable proxy, best first (`limit=N`) |
|
|
542
|
+
| `/count` | totals by type and country |
|
|
543
|
+
| `/delete?proxy=IP:PORT` | take a proxy out |
|
|
544
|
+
| `/report?proxy=IP:PORT&ok=0` | feedback from your own requests |
|
|
545
|
+
|
|
546
|
+
Filters work on `/get`, `/pop` and `/all`: `country=DE,AT`, `protocol=socks5`, `https=1`, `anonymity=elite`, `max_latency=1500`, and `format=txt` for plain URLs. With `--serve-password` the API wants it as Basic auth, like the status page.
|
|
547
|
+
|
|
548
|
+
**Coming from [jhao104/proxy_pool](https://github.com/jhao104/proxy_pool)?** The endpoints, `type=https` and the JSON fields (`proxy`, `https`, `region`, `anonymous`, `check_count`, `fail_count`, …) are the same, so point your code at port 8899 and it keeps working – without Redis, and with proxies that passed the honeypot, tampering and TLS checks:
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
import requests
|
|
552
|
+
proxy = requests.get("http://127.0.0.1:8899/get?type=https").json()["url"] # e.g. socks5://…, type included
|
|
553
|
+
requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
|
|
554
|
+
```
|
|
555
|
+
|
|
507
556
|
<a id="discord-bot"></a>
|
|
508
557
|
|
|
509
558
|
## Discord and Telegram bot
|
|
@@ -543,6 +592,15 @@ flowchart LR
|
|
|
543
592
|
</div>
|
|
544
593
|
</details>
|
|
545
594
|
|
|
595
|
+
### Your own lists
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
proxy-scraper --source https://example.com/my-list.txt --source socks5=./socks.txt # on top of the 700+ sources
|
|
599
|
+
proxy-scraper --only-sources --source bought.txt --want 50 # only yours
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Any text with `ip:port` works; lines like `socks5://user:pass@host:port` keep their type, bare ones are tried as HTTP and SOCKS5 unless you write `http=…`.
|
|
603
|
+
|
|
546
604
|
<a id="output"></a>
|
|
547
605
|
|
|
548
606
|
## Output
|
|
@@ -909,7 +967,7 @@ proxyscraper/
|
|
|
909
967
|
├── targets.py target sites for --target
|
|
910
968
|
├── output.py result files
|
|
911
969
|
├── exporters.py proxychains, Clash, sing-box and curl formats (--export)
|
|
912
|
-
├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · core
|
|
970
|
+
├── server/ rotating proxy server (--serve): pool · http · upstream · socks · status · api · core
|
|
913
971
|
├── api.py find_proxies() / check_proxies() for Python
|
|
914
972
|
├── agent.py MCP tools without the SDK: live list, filters, fetch through proxies
|
|
915
973
|
├── mcp_server.py MCP server (proxy-scraper-mcp) for AI agents
|
|
@@ -922,6 +980,20 @@ proxyscraper/
|
|
|
922
980
|
|
|
923
981
|
</details>
|
|
924
982
|
|
|
983
|
+
<a id="community"></a>
|
|
984
|
+
|
|
985
|
+
## Community
|
|
986
|
+
|
|
987
|
+
Questions, ideas and things you built with it go to [Discussions](https://github.com/maximilianfeix/proxy-scraper/discussions) – bugs to the [issues](../../issues). If proxy-scraper saves you time, a ⭐ helps others find it.
|
|
988
|
+
|
|
989
|
+
<a href="https://star-history.com/#maximilianfeix/proxy-scraper&Date">
|
|
990
|
+
<picture>
|
|
991
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date&theme=dark">
|
|
992
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date">
|
|
993
|
+
<img alt="Star history of proxy-scraper" src="https://api.star-history.com/svg?repos=maximilianfeix/proxy-scraper&type=Date" width="600">
|
|
994
|
+
</picture>
|
|
995
|
+
</a>
|
|
996
|
+
|
|
925
997
|
<a id="acknowledgements"></a>
|
|
926
998
|
|
|
927
999
|
## Acknowledgements
|
{proxy_scraper_cli-1.19.0 → proxy_scraper_cli-1.21.0}/proxy_scraper_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -48,6 +48,7 @@ proxyscraper/sources.py
|
|
|
48
48
|
proxyscraper/speed.py
|
|
49
49
|
proxyscraper/targets.py
|
|
50
50
|
proxyscraper/server/__init__.py
|
|
51
|
+
proxyscraper/server/api.py
|
|
51
52
|
proxyscraper/server/core.py
|
|
52
53
|
proxyscraper/server/http.py
|
|
53
54
|
proxyscraper/server/pool.py
|
|
@@ -88,6 +89,7 @@ tests/test_mcp_server.py
|
|
|
88
89
|
tests/test_mine_sources.py
|
|
89
90
|
tests/test_options.py
|
|
90
91
|
tests/test_output_stdout.py
|
|
92
|
+
tests/test_own_sources.py
|
|
91
93
|
tests/test_packaging.py
|
|
92
94
|
tests/test_pages.py
|
|
93
95
|
tests/test_parsing.py
|
|
@@ -101,6 +103,7 @@ tests/test_publish.py
|
|
|
101
103
|
tests/test_report.py
|
|
102
104
|
tests/test_robustness.py
|
|
103
105
|
tests/test_rotator.py
|
|
106
|
+
tests/test_serve_api.py
|
|
104
107
|
tests/test_serve_password.py
|
|
105
108
|
tests/test_serve_refill.py
|
|
106
109
|
tests/test_serve_v2.py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""proxy-scraper: collects public HTTP/SOCKS4/SOCKS5 proxies from hundreds of sources, checks them
|
|
2
2
|
in parallel with its own protocol handshakes and learns which sources deliver good proxies."""
|
|
3
3
|
|
|
4
|
-
__version__ = "1.
|
|
4
|
+
__version__ = "1.21.0"
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def __getattr__(name):
|
|
@@ -458,6 +458,22 @@ def _to_text(body: bytes, headers, content_type: str, raw_html: bool) -> str:
|
|
|
458
458
|
return text if raw_html or "html" not in content_type.lower() else html_to_text(text)
|
|
459
459
|
|
|
460
460
|
|
|
461
|
+
def _require_match(live: LiveList, protocol: str, countries: List[str], tls: bool) -> None:
|
|
462
|
+
if not any(True for _ in matching(live.rows, protocol, countries, https_only=tls, run_hours=live.run_hours)):
|
|
463
|
+
what = " ".join(x for x in (countries[0] if countries else "", "" if protocol == "any" else protocol,
|
|
464
|
+
"HTTPS-capable" if tls else "") if x)
|
|
465
|
+
raise AgentError(f"No {what} proxy in the live list right now."
|
|
466
|
+
+ (shortage_note(live.rows, 1, 0, countries) or ""))
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _wishes(protocol: str, countries: List[str]) -> List[str]:
|
|
470
|
+
"""What the rotating server reads from the proxy login: country-de, type-socks5."""
|
|
471
|
+
wishes = [f"country-{countries[0].lower()}"] if countries else []
|
|
472
|
+
if protocol != "any":
|
|
473
|
+
wishes.append(f"type-{protocol}")
|
|
474
|
+
return wishes
|
|
475
|
+
|
|
476
|
+
|
|
461
477
|
class PageFetcher:
|
|
462
478
|
"""Loads pages through the rotating server of this package, running on 127.0.0.1 with a random password:
|
|
463
479
|
the same failover as `--serve`, and HTTPS only through proxies that passed the verified-TLS test.
|
|
@@ -503,6 +519,19 @@ class PageFetcher:
|
|
|
503
519
|
self._list_id = list_id
|
|
504
520
|
return live
|
|
505
521
|
|
|
522
|
+
async def local_proxy(self, protocol: str = "any", country: str = "") -> str:
|
|
523
|
+
"""The URL of the rotating server behind this fetcher, with the login, for any HTTP client:
|
|
524
|
+
http://country-de:<password>@127.0.0.1:<port>. Every connection through it gets a proxy of its own."""
|
|
525
|
+
protocol = _check_protocol(protocol)
|
|
526
|
+
countries = normalize_countries([country] if country else [])
|
|
527
|
+
_require_match(await self._ready(), protocol, countries, tls=False)
|
|
528
|
+
user = "-".join(_wishes(protocol, countries)) or "any"
|
|
529
|
+
return f"http://{user}:{self._password}@127.0.0.1:{self.server.port}"
|
|
530
|
+
|
|
531
|
+
async def refresh(self) -> None:
|
|
532
|
+
"""Take over a new run's list, if there is one (fetch does that by itself)."""
|
|
533
|
+
await self._ready()
|
|
534
|
+
|
|
506
535
|
async def fetch(self, url: str, protocol: str = "any", country: str = "", max_chars: int = 20000,
|
|
507
536
|
raw_html: bool = False, progress: Optional[Progress] = None, tick: float = 5.0,
|
|
508
537
|
with_body: bool = False) -> dict:
|
|
@@ -511,15 +540,9 @@ class PageFetcher:
|
|
|
511
540
|
countries = normalize_countries([country] if country else [])
|
|
512
541
|
live = await self._ready()
|
|
513
542
|
tls = urlsplit(url).scheme == "https"
|
|
514
|
-
|
|
515
|
-
what = " ".join(x for x in (countries[0] if countries else "", "" if protocol == "any" else protocol,
|
|
516
|
-
"HTTPS-capable" if tls else "") if x)
|
|
517
|
-
raise AgentError(f"No {what} proxy in the live list right now."
|
|
518
|
-
+ (shortage_note(live.rows, 1, 0, countries) or ""))
|
|
543
|
+
_require_match(live, protocol, countries, tls)
|
|
519
544
|
session = secrets.token_hex(6)
|
|
520
|
-
wishes =
|
|
521
|
-
if protocol != "any":
|
|
522
|
-
wishes.append(f"type-{protocol}")
|
|
545
|
+
wishes = _wishes(protocol, countries)
|
|
523
546
|
pool, port = self.server.pool, self.server.port
|
|
524
547
|
started = time.monotonic()
|
|
525
548
|
result, faults, suspects = None, [], []
|
|
@@ -27,12 +27,14 @@ import asyncio
|
|
|
27
27
|
import contextlib
|
|
28
28
|
import email.message
|
|
29
29
|
import io
|
|
30
|
+
import logging
|
|
30
31
|
import os
|
|
31
32
|
import re
|
|
32
33
|
import tempfile
|
|
33
34
|
import threading
|
|
34
35
|
from dataclasses import dataclass, field
|
|
35
36
|
from typing import Dict, Iterable, List, Optional, Tuple
|
|
37
|
+
from urllib.parse import urlsplit
|
|
36
38
|
|
|
37
39
|
from rich.console import Console
|
|
38
40
|
|
|
@@ -42,6 +44,8 @@ from .parsing import PROXY_TYPES
|
|
|
42
44
|
from .targets import parse_target
|
|
43
45
|
from .ui import widgets
|
|
44
46
|
|
|
47
|
+
log = logging.getLogger(__name__)
|
|
48
|
+
|
|
45
49
|
__all__ = [
|
|
46
50
|
"CheckResult",
|
|
47
51
|
"LiveProxy",
|
|
@@ -309,6 +313,15 @@ class ProxyResponse:
|
|
|
309
313
|
raise ConnectionError(f"the body from {self.final_url} was cut off at 2 MB")
|
|
310
314
|
|
|
311
315
|
|
|
316
|
+
REFRESH_SECONDS = 300 # proxy_url(): how often the pool behind it looks for a new run's list
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _playwright(url: str) -> Dict[str, str]:
|
|
320
|
+
parts = urlsplit(url)
|
|
321
|
+
return {"server": f"{parts.scheme}://{parts.hostname}:{parts.port}", "username": parts.username or "",
|
|
322
|
+
"password": parts.password or ""}
|
|
323
|
+
|
|
324
|
+
|
|
312
325
|
class ProxyRotator:
|
|
313
326
|
"""Loads URLs through proxies from the hourly list and switches to the next one when a proxy fails –
|
|
314
327
|
the loop every scraper writes by hand. HTTPS only through proxies with verified TLS.
|
|
@@ -330,6 +343,7 @@ class ProxyRotator:
|
|
|
330
343
|
self._loop: Optional[asyncio.AbstractEventLoop] = None # the sync API's own loop, in a thread
|
|
331
344
|
self._thread: Optional[threading.Thread] = None
|
|
332
345
|
self._start_lock = threading.Lock()
|
|
346
|
+
self._refresher: Optional[asyncio.Task] = None
|
|
333
347
|
|
|
334
348
|
def _use(self, mode: str) -> None:
|
|
335
349
|
if self._mode is None:
|
|
@@ -367,7 +381,43 @@ class ProxyRotator:
|
|
|
367
381
|
proxy_country=page["proxy_country"], elapsed_ms=page["elapsed_ms"],
|
|
368
382
|
truncated=page["body_truncated"])
|
|
369
383
|
|
|
384
|
+
async def aproxy_url(self) -> str:
|
|
385
|
+
self._use("async")
|
|
386
|
+
return await self._proxy_url()
|
|
387
|
+
|
|
388
|
+
async def _proxy_url(self) -> str:
|
|
389
|
+
from .agent import AgentError
|
|
390
|
+
|
|
391
|
+
if self._fetcher is None:
|
|
392
|
+
self._fetcher = self._make_fetcher()
|
|
393
|
+
try:
|
|
394
|
+
url = await self._fetcher.local_proxy(self.protocol, self.country)
|
|
395
|
+
except AgentError as e:
|
|
396
|
+
raise ConnectionError(str(e)) from None
|
|
397
|
+
if self._refresher is None: # nobody calls get() on this path, so the pool refreshes by itself
|
|
398
|
+
self._refresher = asyncio.ensure_future(self._keep_fresh())
|
|
399
|
+
return url
|
|
400
|
+
|
|
401
|
+
async def aplaywright_proxy(self) -> Dict[str, str]:
|
|
402
|
+
self._use("async")
|
|
403
|
+
return _playwright(await self._proxy_url())
|
|
404
|
+
|
|
405
|
+
async def _keep_fresh(self) -> None:
|
|
406
|
+
from .agent import AgentError
|
|
407
|
+
|
|
408
|
+
while True:
|
|
409
|
+
await asyncio.sleep(REFRESH_SECONDS)
|
|
410
|
+
try:
|
|
411
|
+
await self._fetcher.refresh()
|
|
412
|
+
except AgentError:
|
|
413
|
+
pass # GitHub not reachable right now: the pool keeps what it has, next try later
|
|
414
|
+
except Exception as e: # a broken list must not end the refreshing for good
|
|
415
|
+
log.warning("proxy-scraper: could not refresh the proxy list: %s", e)
|
|
416
|
+
|
|
370
417
|
async def aclose(self) -> None:
|
|
418
|
+
if self._refresher is not None:
|
|
419
|
+
self._refresher.cancel()
|
|
420
|
+
self._refresher = None
|
|
371
421
|
if self._fetcher is not None:
|
|
372
422
|
await self._fetcher.close()
|
|
373
423
|
self._fetcher = None
|
|
@@ -380,13 +430,37 @@ class ProxyRotator:
|
|
|
380
430
|
|
|
381
431
|
# sync: one event loop in a background thread keeps the proxy server alive between calls
|
|
382
432
|
def get(self, url: str) -> ProxyResponse:
|
|
383
|
-
|
|
384
|
-
|
|
433
|
+
return self._run_sync(self._aget(url))
|
|
434
|
+
|
|
435
|
+
def _run_sync(self, coro):
|
|
436
|
+
with self._start_lock: # several threads calling at once still share one loop
|
|
437
|
+
try:
|
|
438
|
+
self._use("sync")
|
|
439
|
+
except RuntimeError:
|
|
440
|
+
coro.close()
|
|
441
|
+
raise
|
|
385
442
|
if self._loop is None:
|
|
386
443
|
self._loop = asyncio.new_event_loop()
|
|
387
444
|
self._thread = threading.Thread(target=self._loop.run_forever, name="proxy-rotator", daemon=True)
|
|
388
445
|
self._thread.start()
|
|
389
|
-
return asyncio.run_coroutine_threadsafe(
|
|
446
|
+
return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
|
|
447
|
+
|
|
448
|
+
def proxy_url(self) -> str:
|
|
449
|
+
"""A local proxy URL for any library – requests, httpx, aiohttp, Playwright, Scrapy, curl:
|
|
450
|
+
|
|
451
|
+
with ProxyRotator(country="DE") as rotator:
|
|
452
|
+
proxy = rotator.proxy_url()
|
|
453
|
+
requests.get(url, proxies={"http": proxy, "https": proxy})
|
|
454
|
+
|
|
455
|
+
Every connection through it gets a proxy from the live list (best first, with failover), HTTPS only
|
|
456
|
+
through verified-TLS ones. It lives as long as the rotator and takes over each new run's list."""
|
|
457
|
+
return self._run_sync(self._proxy_url())
|
|
458
|
+
|
|
459
|
+
def playwright_proxy(self) -> Dict[str, str]:
|
|
460
|
+
"""proxy_url() the way Playwright wants it – it ignores a login inside the server address:
|
|
461
|
+
|
|
462
|
+
browser = p.chromium.launch(proxy=rotator.playwright_proxy())"""
|
|
463
|
+
return _playwright(self.proxy_url())
|
|
390
464
|
|
|
391
465
|
def close(self) -> None:
|
|
392
466
|
with self._start_lock:
|
|
@@ -327,8 +327,10 @@ class Run:
|
|
|
327
327
|
plan = await collect_sources(self.opts, self.quality)
|
|
328
328
|
info("Sources", Text.assemble(
|
|
329
329
|
(fmt(len(plan.sources)), f"bold {ACCENT}"), " active ",
|
|
330
|
-
(f"({fmt(plan.
|
|
331
|
-
f"
|
|
330
|
+
(f"({fmt(plan.n_own)} own)" if self.opts.only_sources else
|
|
331
|
+
f"({fmt(plan.n_curated)} curated · {fmt(plan.n_meta)} from {plan.meta_ok}/{plan.meta_total} "
|
|
332
|
+
f"meta lists · {fmt(plan.n_discovered)} discovered"
|
|
333
|
+
+ (f" · {fmt(plan.n_own)} own)" if plan.n_own else ")"), MUTED),
|
|
332
334
|
))
|
|
333
335
|
if plan.discovery_ran and not plan.discovery_token:
|
|
334
336
|
note("GitHub discovery is limited without a token – run `gh auth login` or set GITHUB_TOKEN.",
|
|
@@ -355,7 +357,7 @@ class Run:
|
|
|
355
357
|
hosts = sorted(INSECURE_HOSTS)
|
|
356
358
|
note(f"Certificate could not be verified (TLS inspection on this network?), loaded anyway: "
|
|
357
359
|
f"{', '.join(hosts[:4])}{' …' if len(hosts) > 4 else ''}", MUTED, "ℹ")
|
|
358
|
-
return prioritize(res, self.quality, self.history, self.opts.types)
|
|
360
|
+
return prioritize(res, self.quality, self.history, self.opts.types, listed_only=self.opts.only_sources)
|
|
359
361
|
|
|
360
362
|
# ------------------------------------------------------------------ phase 3+4: check, learn, report
|
|
361
363
|
|