proxy-scraper-cli 1.10.0__tar.gz → 1.12.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.10.0 → proxy_scraper_cli-1.12.0}/PKG-INFO +5 -3
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/README.md +4 -2
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/PKG-INFO +5 -3
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/SOURCES.txt +3 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/__init__.py +1 -1
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/agent.py +7 -4
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/api.py +9 -5
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/checker.py +4 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/mcp_server.py +11 -3
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/proxypages.py +5 -1
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/publish.py +10 -4
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/report.py +64 -2
- proxy_scraper_cli-1.12.0/proxyscraper/runsteps.py +74 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/index.html +29 -8
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sites.py +25 -33
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sources.py +8 -2
- proxy_scraper_cli-1.12.0/proxyscraper/speed.py +118 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_agent.py +1 -1
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_proxy_sources.py +29 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_report.py +43 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_sites.py +21 -4
- proxy_scraper_cli-1.12.0/tests/test_speed.py +121 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/LICENSE +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/__main__.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/action.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/app.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/asndb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/blocklist.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/charts.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/cli.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/compat.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/completion.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/exporters.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/fetchcache.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/geo.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/geodb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/handshake.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/history.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/judges.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/mcp_entry.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/netio.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/options.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/output.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/pages.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/parsing.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/paths.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/pipeline.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/preferences.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/__init__.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/core.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/http.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/pool.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/socks.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/status.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/upstream.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/apple-touch-icon.png +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/logo.png +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/og.png +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sources.json +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/targets.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/__init__.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/dashboard.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/keys.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/report.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/serve.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/widgets.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/wizard.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/pyproject.toml +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/setup.cfg +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_action.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_api.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_app.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_asndb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_auth.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_blocklist.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_charts.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_checker.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_compat.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_completion.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_discovery_growth.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_exporters.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_fetchcache.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_geodb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_judges.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_learning.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_live_api.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_mcp_server.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_mine_sources.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_options.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_output_stdout.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_packaging.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_pages.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_parsing.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_parsing_fuzz.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_paths.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_pipeline.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_proxy_pages.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_publish.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_robustness.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_password.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_refill.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_v2.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_server.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_server_closed_writer.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_server_fixes.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_sources_json.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_tamper.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_targets.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_ui.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_uptime.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.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.12.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
|
|
@@ -205,7 +205,7 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
|
|
|
205
205
|
| HTTP · SOCKS4 · SOCKS5 | `1.2.3.4:8080` | [http.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/http.txt) · [socks4.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks4.txt) · [socks5.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt) |
|
|
206
206
|
| HTTPS-capable only | `type://ip:port` | [https.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/https.txt) |
|
|
207
207
|
| Elite only | `type://ip:port` | [elite.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/elite.txt) |
|
|
208
|
-
| Gets through to Google · Reddit · Amazon (no captcha, no
|
|
208
|
+
| Gets through to Google · Reddit · Amazon · Instagram · TikTok (no captcha, no block in the last run) | `type://ip:port` | [google.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/google.txt) · [reddit.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/reddit.txt) · [amazon.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/amazon.txt) · [instagram.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/instagram.txt) · [tiktok.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/tiktok.txt) |
|
|
209
209
|
| Stable, on the list in 90 %+ of this week's runs | `type://ip:port` | [stable.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/stable.txt) |
|
|
210
210
|
| With all details | latency, country, HTTPS, anonymity, exit IP, uptime, sites | [proxies.json](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/proxies.json) · [proxies.csv](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/proxies.csv) |
|
|
211
211
|
|
|
@@ -219,6 +219,8 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
|
|
|
219
219
|
curl -s https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt | head
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
**Over time:** every day the first run's `proxies.json` is kept for good as a gzipped asset of the release of that year, like [snapshots-2026](../../releases/tag/snapshots-2026) – `proxies-2026-09-28.json.gz` and so on, for anyone who wants to look at free proxies over weeks and months.
|
|
223
|
+
|
|
222
224
|
Every file is also on GitHub Pages, which sits behind a CDN, isn't rate limited like raw.githubusercontent and sends CORS headers, so it works straight from the browser: `https://maximilianfeix.github.io/proxy-scraper/socks5.txt`. [jsDelivr](https://cdn.jsdelivr.net/gh/maximilianfeix/proxy-scraper@proxy-list/) works too, but can lag behind by a few hours.
|
|
223
225
|
|
|
224
226
|
Or let the tool start from it: `proxy-scraper --recheck live` downloads the list and checks it again from **your** network – about 30 seconds instead of a full scan (517 of 1,169 worked from here). With `--serve` you have a rotating proxy in under a minute.
|
|
@@ -440,7 +442,7 @@ pool = itertools.cycle(p.url for p in proxies)
|
|
|
440
442
|
r = requests.get("https://api.ipify.org", proxies={"https": next(pool)}, timeout=15) # pip install "requests[socks]"
|
|
441
443
|
```
|
|
442
444
|
|
|
443
|
-
Same filters as `find_proxies`, plus `min_uptime`, `works_on` (`["google"]`, `"reddit"`, `"amazon"`) and `limit`. Every result also has `uptime_24h`, `uptime_7d`, `first_seen`, `up_for_hours` and `sites`.
|
|
445
|
+
Same filters as `find_proxies`, plus `min_uptime`, `works_on` (`["google"]`, `"reddit"`, `"amazon"`, `"instagram"`, `"tiktok"`) and `limit`. Every result also has `uptime_24h`, `uptime_7d`, `first_seen`, `up_for_hours` and `sites`.
|
|
444
446
|
|
|
445
447
|
<a id="proxy-server"></a>
|
|
446
448
|
|
|
@@ -164,7 +164,7 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
|
|
|
164
164
|
| HTTP · SOCKS4 · SOCKS5 | `1.2.3.4:8080` | [http.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/http.txt) · [socks4.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks4.txt) · [socks5.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt) |
|
|
165
165
|
| HTTPS-capable only | `type://ip:port` | [https.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/https.txt) |
|
|
166
166
|
| Elite only | `type://ip:port` | [elite.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/elite.txt) |
|
|
167
|
-
| Gets through to Google · Reddit · Amazon (no captcha, no
|
|
167
|
+
| Gets through to Google · Reddit · Amazon · Instagram · TikTok (no captcha, no block in the last run) | `type://ip:port` | [google.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/google.txt) · [reddit.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/reddit.txt) · [amazon.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/amazon.txt) · [instagram.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/instagram.txt) · [tiktok.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/tiktok.txt) |
|
|
168
168
|
| Stable, on the list in 90 %+ of this week's runs | `type://ip:port` | [stable.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/stable.txt) |
|
|
169
169
|
| With all details | latency, country, HTTPS, anonymity, exit IP, uptime, sites | [proxies.json](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/proxies.json) · [proxies.csv](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/proxies.csv) |
|
|
170
170
|
|
|
@@ -178,6 +178,8 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
|
|
|
178
178
|
curl -s https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt | head
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
**Over time:** every day the first run's `proxies.json` is kept for good as a gzipped asset of the release of that year, like [snapshots-2026](../../releases/tag/snapshots-2026) – `proxies-2026-09-28.json.gz` and so on, for anyone who wants to look at free proxies over weeks and months.
|
|
182
|
+
|
|
181
183
|
Every file is also on GitHub Pages, which sits behind a CDN, isn't rate limited like raw.githubusercontent and sends CORS headers, so it works straight from the browser: `https://maximilianfeix.github.io/proxy-scraper/socks5.txt`. [jsDelivr](https://cdn.jsdelivr.net/gh/maximilianfeix/proxy-scraper@proxy-list/) works too, but can lag behind by a few hours.
|
|
182
184
|
|
|
183
185
|
Or let the tool start from it: `proxy-scraper --recheck live` downloads the list and checks it again from **your** network – about 30 seconds instead of a full scan (517 of 1,169 worked from here). With `--serve` you have a rotating proxy in under a minute.
|
|
@@ -399,7 +401,7 @@ pool = itertools.cycle(p.url for p in proxies)
|
|
|
399
401
|
r = requests.get("https://api.ipify.org", proxies={"https": next(pool)}, timeout=15) # pip install "requests[socks]"
|
|
400
402
|
```
|
|
401
403
|
|
|
402
|
-
Same filters as `find_proxies`, plus `min_uptime`, `works_on` (`["google"]`, `"reddit"`, `"amazon"`) and `limit`. Every result also has `uptime_24h`, `uptime_7d`, `first_seen`, `up_for_hours` and `sites`.
|
|
404
|
+
Same filters as `find_proxies`, plus `min_uptime`, `works_on` (`["google"]`, `"reddit"`, `"amazon"`, `"instagram"`, `"tiktok"`) and `limit`. Every result also has `uptime_24h`, `uptime_7d`, `first_seen`, `up_for_hours` and `sites`.
|
|
403
405
|
|
|
404
406
|
<a id="proxy-server"></a>
|
|
405
407
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.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
|
|
@@ -205,7 +205,7 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
|
|
|
205
205
|
| HTTP · SOCKS4 · SOCKS5 | `1.2.3.4:8080` | [http.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/http.txt) · [socks4.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks4.txt) · [socks5.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt) |
|
|
206
206
|
| HTTPS-capable only | `type://ip:port` | [https.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/https.txt) |
|
|
207
207
|
| Elite only | `type://ip:port` | [elite.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/elite.txt) |
|
|
208
|
-
| Gets through to Google · Reddit · Amazon (no captcha, no
|
|
208
|
+
| Gets through to Google · Reddit · Amazon · Instagram · TikTok (no captcha, no block in the last run) | `type://ip:port` | [google.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/google.txt) · [reddit.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/reddit.txt) · [amazon.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/amazon.txt) · [instagram.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/instagram.txt) · [tiktok.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/works-with/tiktok.txt) |
|
|
209
209
|
| Stable, on the list in 90 %+ of this week's runs | `type://ip:port` | [stable.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/stable.txt) |
|
|
210
210
|
| With all details | latency, country, HTTPS, anonymity, exit IP, uptime, sites | [proxies.json](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/proxies.json) · [proxies.csv](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/proxies.csv) |
|
|
211
211
|
|
|
@@ -219,6 +219,8 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
|
|
|
219
219
|
curl -s https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt | head
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
**Over time:** every day the first run's `proxies.json` is kept for good as a gzipped asset of the release of that year, like [snapshots-2026](../../releases/tag/snapshots-2026) – `proxies-2026-09-28.json.gz` and so on, for anyone who wants to look at free proxies over weeks and months.
|
|
223
|
+
|
|
222
224
|
Every file is also on GitHub Pages, which sits behind a CDN, isn't rate limited like raw.githubusercontent and sends CORS headers, so it works straight from the browser: `https://maximilianfeix.github.io/proxy-scraper/socks5.txt`. [jsDelivr](https://cdn.jsdelivr.net/gh/maximilianfeix/proxy-scraper@proxy-list/) works too, but can lag behind by a few hours.
|
|
223
225
|
|
|
224
226
|
Or let the tool start from it: `proxy-scraper --recheck live` downloads the list and checks it again from **your** network – about 30 seconds instead of a full scan (517 of 1,169 worked from here). With `--serve` you have a rotating proxy in under a minute.
|
|
@@ -440,7 +442,7 @@ pool = itertools.cycle(p.url for p in proxies)
|
|
|
440
442
|
r = requests.get("https://api.ipify.org", proxies={"https": next(pool)}, timeout=15) # pip install "requests[socks]"
|
|
441
443
|
```
|
|
442
444
|
|
|
443
|
-
Same filters as `find_proxies`, plus `min_uptime`, `works_on` (`["google"]`, `"reddit"`, `"amazon"`) and `limit`. Every result also has `uptime_24h`, `uptime_7d`, `first_seen`, `up_for_hours` and `sites`.
|
|
445
|
+
Same filters as `find_proxies`, plus `min_uptime`, `works_on` (`["google"]`, `"reddit"`, `"amazon"`, `"instagram"`, `"tiktok"`) and `limit`. Every result also has `uptime_24h`, `uptime_7d`, `first_seen`, `up_for_hours` and `sites`.
|
|
444
446
|
|
|
445
447
|
<a id="proxy-server"></a>
|
|
446
448
|
|
{proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -40,9 +40,11 @@ proxyscraper/preferences.py
|
|
|
40
40
|
proxyscraper/proxypages.py
|
|
41
41
|
proxyscraper/publish.py
|
|
42
42
|
proxyscraper/report.py
|
|
43
|
+
proxyscraper/runsteps.py
|
|
43
44
|
proxyscraper/sites.py
|
|
44
45
|
proxyscraper/sources.json
|
|
45
46
|
proxyscraper/sources.py
|
|
47
|
+
proxyscraper/speed.py
|
|
46
48
|
proxyscraper/targets.py
|
|
47
49
|
proxyscraper/server/__init__.py
|
|
48
50
|
proxyscraper/server/core.py
|
|
@@ -104,6 +106,7 @@ tests/test_server_closed_writer.py
|
|
|
104
106
|
tests/test_server_fixes.py
|
|
105
107
|
tests/test_sites.py
|
|
106
108
|
tests/test_sources_json.py
|
|
109
|
+
tests/test_speed.py
|
|
107
110
|
tests/test_tamper.py
|
|
108
111
|
tests/test_targets.py
|
|
109
112
|
tests/test_ui.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.12.0"
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def __getattr__(name):
|
|
@@ -179,17 +179,18 @@ def normalize_countries(countries: Iterable[str]) -> List[str]:
|
|
|
179
179
|
def select(rows: Iterable[dict], protocol: str = "any", countries: Iterable[str] = (), https_only: bool = False,
|
|
180
180
|
elite_only: bool = False, exclude_datacenter: bool = False, exclude_blocklisted: bool = False,
|
|
181
181
|
stable_only: bool = False, max_latency_ms: int = 0, run_hours: int = 1, min_uptime: int = 0,
|
|
182
|
-
works_on: Iterable[str] = ()) -> List[dict]:
|
|
182
|
+
works_on: Iterable[str] = (), min_speed_kbps: int = 0) -> List[dict]:
|
|
183
183
|
"""The rows that pass every filter, fastest first – the same filters as on the website."""
|
|
184
184
|
return sorted(matching(rows, protocol, countries, https_only, elite_only, exclude_datacenter,
|
|
185
|
-
exclude_blocklisted, stable_only, max_latency_ms, run_hours, min_uptime, works_on
|
|
185
|
+
exclude_blocklisted, stable_only, max_latency_ms, run_hours, min_uptime, works_on,
|
|
186
|
+
min_speed_kbps),
|
|
186
187
|
key=lambda r: r.get("latency") or 0)
|
|
187
188
|
|
|
188
189
|
|
|
189
190
|
def matching(rows: Iterable[dict], protocol: str = "any", countries: Iterable[str] = (), https_only: bool = False,
|
|
190
191
|
elite_only: bool = False, exclude_datacenter: bool = False, exclude_blocklisted: bool = False,
|
|
191
192
|
stable_only: bool = False, max_latency_ms: int = 0, run_hours: int = 1,
|
|
192
|
-
min_uptime: int = 0, works_on: Iterable[str] = ()) -> Iterator[dict]:
|
|
193
|
+
min_uptime: int = 0, works_on: Iterable[str] = (), min_speed_kbps: int = 0) -> Iterator[dict]:
|
|
193
194
|
"""The rows that pass every filter, in list order – checks the filters before the first row is asked for."""
|
|
194
195
|
protocol = _check_protocol(protocol)
|
|
195
196
|
wanted = set(normalize_countries(countries))
|
|
@@ -205,7 +206,8 @@ def matching(rows: Iterable[dict], protocol: str = "any", countries: Iterable[st
|
|
|
205
206
|
and (not stable_only or (r.get("streak") or 0) >= stable_runs)
|
|
206
207
|
and (not max_latency_ms or (r.get("latency") or 0) <= max_latency_ms)
|
|
207
208
|
and (not min_uptime or (r.get("uptime_7d") or 0) >= min_uptime)
|
|
208
|
-
and all((r.get("sites") or {}).get(s) for s in sites)
|
|
209
|
+
and all((r.get("sites") or {}).get(s) for s in sites)
|
|
210
|
+
and (not min_speed_kbps or (r.get("speed_kbps") or 0) >= min_speed_kbps))
|
|
209
211
|
|
|
210
212
|
|
|
211
213
|
def describe(item: Union[dict, CheckResult], run_hours: Optional[int] = None) -> dict:
|
|
@@ -229,6 +231,7 @@ def describe(item: Union[dict, CheckResult], run_hours: Optional[int] = None) ->
|
|
|
229
231
|
"up_for_hours": streak * run_hours if streak and run_hours else None,
|
|
230
232
|
"uptime_7d_percent": item.get("uptime_7d"),
|
|
231
233
|
"works_on": [name for name, ok in (item.get("sites") or {}).items() if ok],
|
|
234
|
+
"speed_kbps": item.get("speed_kbps"),
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
|
|
@@ -166,7 +166,8 @@ class LiveProxy(CheckResult):
|
|
|
166
166
|
uptime_7d: Optional[int] = None # the same over the week
|
|
167
167
|
first_seen: str = "" # ISO time of the first run it was listed in
|
|
168
168
|
up_for_hours: int = 0 # listed without a gap for this long
|
|
169
|
-
sites: Dict[str, bool] = field(default_factory=dict) # "google", "reddit",
|
|
169
|
+
sites: Dict[str, bool] = field(default_factory=dict) # "google", "reddit", … -> got through in the last run?
|
|
170
|
+
speed_kbps: Optional[int] = None # download speed in KiB/s in the last run (HTTPS-capable ones only)
|
|
170
171
|
|
|
171
172
|
|
|
172
173
|
def _live_fetch(url: str, timeout: float = 20, headers=None):
|
|
@@ -184,19 +185,22 @@ def _live_proxy(row: dict, run_hours: int) -> LiveProxy:
|
|
|
184
185
|
org=row.get("org") or "", hosting=row.get("hosting"), blocklisted=row.get("blocklisted"),
|
|
185
186
|
uptime_24h=row.get("uptime_24h"), uptime_7d=row.get("uptime_7d"), first_seen=row.get("first_seen") or "",
|
|
186
187
|
up_for_hours=streak * run_hours,
|
|
187
|
-
sites={k: v for k, v in (row.get("sites") or {}).items() if isinstance(v, bool)}
|
|
188
|
+
sites={k: v for k, v in (row.get("sites") or {}).items() if isinstance(v, bool)},
|
|
189
|
+
speed_kbps=row.get("speed_kbps") if type(row.get("speed_kbps")) is int else None)
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
async def live_proxies_async(*, types: Iterable[str] = PROXY_TYPES, countries: Iterable[str] = (), https: bool = False,
|
|
191
193
|
anonymity: str = "", max_latency: int = 0, no_datacenter: bool = False,
|
|
192
194
|
no_blocklisted: bool = False, min_uptime: int = 0, works_on: Iterable[str] = (),
|
|
193
|
-
limit: int = 0) -> List[LiveProxy]:
|
|
195
|
+
min_speed: int = 0, limit: int = 0) -> List[LiveProxy]:
|
|
194
196
|
"""The proxies from the hourly list that pass the filters, fastest first. Nothing is checked here: they
|
|
195
197
|
worked from GitHub's servers in the last run (at most an hour ago). find_proxies checks from your network.
|
|
196
198
|
|
|
197
199
|
Same filters as find_proxies, plus
|
|
198
200
|
min_uptime only proxies listed in at least this share (percent) of the week's runs, e.g. 90
|
|
199
|
-
works_on only proxies that got through to these sites in the last run:
|
|
201
|
+
works_on only proxies that got through to these sites in the last run: google, reddit, amazon, instagram,
|
|
202
|
+
tiktok
|
|
203
|
+
min_speed only proxies that downloaded at least this many KiB/s in the last run
|
|
200
204
|
limit at most this many (0 = all)
|
|
201
205
|
"""
|
|
202
206
|
from .agent import AgentError, LiveSource
|
|
@@ -226,7 +230,7 @@ async def live_proxies_async(*, types: Iterable[str] = PROXY_TYPES, countries: I
|
|
|
226
230
|
if r.get("ptype") in types and isinstance(r.get("proxy"), str)),
|
|
227
231
|
key=lambda p: p.latency)
|
|
228
232
|
found = [p for p in found if opts.filters.accepts(p) and (not min_uptime or (p.uptime_7d or 0) >= min_uptime)
|
|
229
|
-
and all(p.sites.get(s) for s in works_on)]
|
|
233
|
+
and all(p.sites.get(s) for s in works_on) and (not min_speed or (p.speed_kbps or 0) >= min_speed)]
|
|
230
234
|
return found[:limit] if limit else found
|
|
231
235
|
|
|
232
236
|
|
|
@@ -334,6 +334,10 @@ class Checker:
|
|
|
334
334
|
writer.close()
|
|
335
335
|
return 200 <= status < 400
|
|
336
336
|
|
|
337
|
+
async def tls_tunnel(self, ptype: str, proxy: str, host: str, ip: str, port: int = 443):
|
|
338
|
+
"""Public side of _tls_tunnel for the steps after a run (sites, speed): ip as text."""
|
|
339
|
+
return await self._tls_tunnel(ptype, proxy, host, socket.inet_aton(ip), port)
|
|
340
|
+
|
|
337
341
|
async def _tls_tunnel(self, ptype: str, proxy: str, host: str, ip_bytes: bytes, port: int):
|
|
338
342
|
"""Tunnel through the proxy to host:port with verified TLS inside. None = tunnel refused or MITM."""
|
|
339
343
|
loop = asyncio.get_running_loop()
|
|
@@ -17,6 +17,7 @@ from mcp.types import ToolAnnotations
|
|
|
17
17
|
from pydantic import BaseModel, Field
|
|
18
18
|
|
|
19
19
|
from . import __version__, agent
|
|
20
|
+
from .sites import SITES
|
|
20
21
|
|
|
21
22
|
REPO = "https://github.com/maximilianfeix/proxy-scraper"
|
|
22
23
|
|
|
@@ -33,6 +34,7 @@ Free proxies are run by strangers: never send passwords, cookies, API keys or pe
|
|
|
33
34
|
and expect some of them to stop working at any time."""
|
|
34
35
|
|
|
35
36
|
Protocol = Literal["any", "http", "socks4", "socks5"]
|
|
37
|
+
SiteName = Literal[tuple(s.name for s in SITES)] # the sites the hourly check tries (sites.py)
|
|
36
38
|
Countries = Annotated[List[str], Field(description="Two-letter country codes of the exit IP, e.g. ['DE', 'NL']. "
|
|
37
39
|
"Empty = any country.")]
|
|
38
40
|
|
|
@@ -51,7 +53,10 @@ class Proxy(BaseModel):
|
|
|
51
53
|
blocklisted: Optional[bool] = Field(description="Exit IP on the SpamCop blocklist – expect captchas")
|
|
52
54
|
up_for_hours: Optional[int] = Field(description="How long it has worked without a gap (live list only)")
|
|
53
55
|
works_on: List[str] = Field(default_factory=list, description="Big sites it got through to in the last hourly "
|
|
54
|
-
"check:
|
|
56
|
+
f"check: {', '.join(s.name for s in SITES)} "
|
|
57
|
+
"(live list only)")
|
|
58
|
+
speed_kbps: Optional[int] = Field(default=None, description="Download speed in KiB/s in the last hourly check "
|
|
59
|
+
"(HTTPS-capable proxies, live list only)")
|
|
55
60
|
uptime_7d_percent: Optional[int] = Field(default=None, description="Share of the last 7 days' hourly checks it "
|
|
56
61
|
"passed (live list only)")
|
|
57
62
|
|
|
@@ -121,9 +126,12 @@ def build_server(live: Optional[agent.LiveSource] = None, fetcher: Optional[agen
|
|
|
121
126
|
min_uptime_percent: Annotated[int, Field(ge=0, le=100, description="Only proxies that passed at least this "
|
|
122
127
|
"share of the last 7 days' hourly checks, "
|
|
123
128
|
"e.g. 90 for the most reliable")] = 0,
|
|
124
|
-
works_on: Annotated[List[
|
|
129
|
+
works_on: Annotated[List[SiteName],
|
|
125
130
|
Field(description="Only proxies that got through to all of these sites in the last "
|
|
126
131
|
"hourly check (most free proxies get a captcha or a 403 there)")] = [], # noqa: B006
|
|
132
|
+
min_speed_kbps: Annotated[int, Field(ge=0, description="Only proxies that downloaded at least this many KiB/s "
|
|
133
|
+
"in the last hourly check, e.g. 300 for loading "
|
|
134
|
+
"real pages")] = 0,
|
|
127
135
|
limit: Annotated[int, Field(ge=1, le=100, description="How many to return (fastest first)")] = 10,
|
|
128
136
|
) -> ProxyList:
|
|
129
137
|
"""Get working free proxies right now, fastest first.
|
|
@@ -135,7 +143,7 @@ def build_server(live: Optional[agent.LiveSource] = None, fetcher: Optional[agen
|
|
|
135
143
|
data = await live.get()
|
|
136
144
|
rows = agent.select(data.rows, protocol, countries, https_only, elite_only, exclude_datacenter,
|
|
137
145
|
exclude_blocklisted, stable_only, max_latency_ms, data.run_hours, min_uptime_percent,
|
|
138
|
-
works_on)
|
|
146
|
+
works_on, min_speed_kbps)
|
|
139
147
|
except agent.AgentError as e:
|
|
140
148
|
_fail(e)
|
|
141
149
|
return ProxyList(proxies=[Proxy(**agent.describe(r, data.run_hours)) for r in rows[:limit]],
|
|
@@ -14,9 +14,10 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Dict, List, Optional, Sequence, Tuple
|
|
15
15
|
|
|
16
16
|
from .pages import COUNTRIES, CSS, SITE_URL, _short_name
|
|
17
|
+
from .sites import SITES
|
|
17
18
|
|
|
18
19
|
INDEX_MIN_UPTIME = 50 # percent of the week's runs
|
|
19
|
-
SITE_NAMES = {
|
|
20
|
+
SITE_NAMES = {s.name: s.title for s in SITES}
|
|
20
21
|
|
|
21
22
|
EXTRA_CSS = """
|
|
22
23
|
.wrap > * { min-width: 0; }
|
|
@@ -97,8 +98,10 @@ def _summary(entries: List[dict]) -> dict:
|
|
|
97
98
|
https = [r.get("https") for r in entries]
|
|
98
99
|
rank = {"elite": 3, "anonymous": 2, "transparent": 1}
|
|
99
100
|
sites = {k for r in entries for k, ok in (r.get("sites") or {}).items() if ok}
|
|
101
|
+
speeds = [r["speed_kbps"] for r in entries if r.get("speed_kbps")]
|
|
100
102
|
return {
|
|
101
103
|
"uptime": max(uptimes) if uptimes else None,
|
|
104
|
+
"speed": max(speeds) if speeds else None,
|
|
102
105
|
"latency": min(r["latency"] for r in entries) if all("latency" in r for r in entries) else None,
|
|
103
106
|
"https": True if True in https else False if False in https else None,
|
|
104
107
|
"anonymity": max((r.get("anonymity") or "" for r in entries), key=lambda a: rank.get(a, 0)) or None,
|
|
@@ -128,6 +131,7 @@ def _render(proxy: str, entries: List[dict], updated: datetime, bits: Optional[i
|
|
|
128
131
|
("Country", _country(cc)),
|
|
129
132
|
("Provider", f"{info['org'] or 'unknown'}" + (f" (AS{info['asn']})" if info["asn"] else "")),
|
|
130
133
|
("Latency", f"{info['latency']:,} ms" if info["latency"] is not None else "unknown"),
|
|
134
|
+
("Download speed", f"{info['speed']:,} KiB/s" if info["speed"] else "not measured"),
|
|
131
135
|
("HTTPS", _yes_no(info["https"], "yes, verified TLS", "no")),
|
|
132
136
|
("Anonymity", info["anonymity"] or "unknown"),
|
|
133
137
|
]
|
|
@@ -74,6 +74,9 @@ def stats_for(rows: List[dict], now: datetime) -> dict:
|
|
|
74
74
|
"run_hours": RUN_HOURS,
|
|
75
75
|
"countries": dict(Counter(r["country"] for r in rows if r.get("country")).most_common(15)),
|
|
76
76
|
"median_latency": round(statistics.median(r["latency"] for r in rows)) if rows else 0,
|
|
77
|
+
# download speed of the HTTPS-capable ones (speed.py), None when it wasn't measured
|
|
78
|
+
"median_speed_kbps": round(statistics.median(speeds)) if (speeds := [r["speed_kbps"] for r in rows
|
|
79
|
+
if r.get("speed_kbps")]) else None,
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
|
|
@@ -244,7 +247,8 @@ def load_streaks(path: Optional[Path]) -> Dict[str, int]:
|
|
|
244
247
|
|
|
245
248
|
|
|
246
249
|
def publish(run_dir: Path, out: Path, minimum: int = 20, now: Optional[datetime] = None,
|
|
247
|
-
history: Optional[Path] = None, streaks: Optional[Path] = None, seen: Optional[Path] = None
|
|
250
|
+
history: Optional[Path] = None, streaks: Optional[Path] = None, seen: Optional[Path] = None,
|
|
251
|
+
feed: Optional[Path] = None) -> int:
|
|
248
252
|
rows = load_rows(run_dir)
|
|
249
253
|
if len(rows) < minimum:
|
|
250
254
|
print(f"Only {len(rows)} hits (< {minimum}) – the old list stays online.")
|
|
@@ -270,7 +274,7 @@ def publish(run_dir: Path, out: Path, minimum: int = 20, now: Optional[datetime]
|
|
|
270
274
|
with (run_dir / "proxies.csv").open(newline="", encoding="utf-8") as src, \
|
|
271
275
|
(out / "proxies.csv").open("w", newline="", encoding="utf-8") as dst:
|
|
272
276
|
reader = csv.DictReader(src)
|
|
273
|
-
extra = ("first_seen", "uptime_24h", "uptime_7d", "works_with")
|
|
277
|
+
extra = ("first_seen", "uptime_24h", "uptime_7d", "works_with", "speed_kbps")
|
|
274
278
|
base = list(reader.fieldnames or ["proxy"])
|
|
275
279
|
writer = csv.DictWriter(dst, fieldnames=[*base, *(k for k in extra if k not in base)])
|
|
276
280
|
writer.writeheader()
|
|
@@ -309,7 +313,7 @@ def publish(run_dir: Path, out: Path, minimum: int = 20, now: Optional[datetime]
|
|
|
309
313
|
write_pages(rows, out, now, extra=proxy_pages)
|
|
310
314
|
write_json(out / "history.json", runs)
|
|
311
315
|
# the weekly report: lifetimes, sites, countries – a page, markdown and a short post (report.py)
|
|
312
|
-
write_report(weekly_facts(rows, runs, {url: e["bits"] for url, e in listed.items()}, now), out)
|
|
316
|
+
write_report(weekly_facts(rows, runs, {url: e["bits"] for url, e in listed.items()}, now), out, previous_feed=feed)
|
|
313
317
|
for theme in THEMES: # charts for the README, which embeds them from GitHub Pages
|
|
314
318
|
(out / f"chart-{theme}.svg").write_text(trend_svg(runs, now, theme), encoding="utf-8")
|
|
315
319
|
(out / f"countries-{theme}.svg").write_text(countries_svg(stats["countries"], theme), encoding="utf-8")
|
|
@@ -335,8 +339,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
335
339
|
p.add_argument("--streaks", type=Path, help="streaks.json from last time (how long each proxy has been listed)")
|
|
336
340
|
p.add_argument("--seen", type=Path,
|
|
337
341
|
help="seen.json from last time (which runs each proxy was listed in, for the uptime)")
|
|
342
|
+
p.add_argument("--feed", type=Path, help="report/feed.xml from last time (the weekly entries it keeps)")
|
|
338
343
|
args = p.parse_args(argv)
|
|
339
|
-
return publish(args.run_dir, args.out_dir, args.min, history=args.history, streaks=args.streaks, seen=args.seen
|
|
344
|
+
return publish(args.run_dir, args.out_dir, args.min, history=args.history, streaks=args.streaks, seen=args.seen,
|
|
345
|
+
feed=args.feed)
|
|
340
346
|
|
|
341
347
|
|
|
342
348
|
if __name__ == "__main__":
|
|
@@ -5,6 +5,7 @@ get through to the big sites, where they are. Rebuilt with every run from the sa
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
8
9
|
from collections import Counter
|
|
9
10
|
from datetime import datetime, timedelta
|
|
10
11
|
from html import escape
|
|
@@ -13,11 +14,12 @@ from typing import Dict, List, Optional, Sequence
|
|
|
13
14
|
|
|
14
15
|
from .charts import FONT, THEMES, WIDTH
|
|
15
16
|
from .pages import COUNTRIES, CSS, SITE_URL, _short_name
|
|
17
|
+
from .sites import SITES
|
|
16
18
|
|
|
17
19
|
DAYS = 7
|
|
18
20
|
RUN_HOURS = 1
|
|
19
21
|
REPORT_URL = f"{SITE_URL}report/"
|
|
20
|
-
SITE_NAMES = {
|
|
22
|
+
SITE_NAMES = {s.name: s.title for s in SITES}
|
|
21
23
|
# checks in a row a proxy was on the list for -> label (the runs are roughly hourly, but not always)
|
|
22
24
|
BUCKETS = [(1, 1, "1 check"), (2, 5, "2–5 checks"), (6, 23, "6–23 checks"), (24, 71, "24–71 checks"),
|
|
23
25
|
(72, 10 ** 6, "72 checks +")]
|
|
@@ -102,6 +104,7 @@ def weekly_facts(rows: List[dict], runs: Sequence[dict], seen: Dict[str, int], n
|
|
|
102
104
|
if any(r.get("hosting") is not None for r in rows) else None,
|
|
103
105
|
"countries": Counter(r["country"] for r in rows if r.get("country")).most_common(5),
|
|
104
106
|
"latency_now": window[-1][1].get("median_latency") if window else None,
|
|
107
|
+
"speeds": sorted(r["speed_kbps"] for r in rows if r.get("speed_kbps")),
|
|
105
108
|
"sites": sites,
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -141,6 +144,11 @@ def _lines(f: dict) -> List[str]:
|
|
|
141
144
|
line = (f"Between {f['low']:,} and {f['peak']:,} proxies worked at any one time. Right now: {f['total']:,}, "
|
|
142
145
|
f"{f['https']} % of them tunnel HTTPS")
|
|
143
146
|
out.append(line + (f", {f['datacenter']} % exit from a datacenter." if f["datacenter"] is not None else "."))
|
|
147
|
+
speeds = f.get("speeds") or []
|
|
148
|
+
if speeds:
|
|
149
|
+
median, top = speeds[len(speeds) // 2], speeds[9 * len(speeds) // 10]
|
|
150
|
+
out.append(f"Downloading 100 KB through them: half manage {median:,} KiB/s or more, "
|
|
151
|
+
f"the fastest tenth over {top:,} KiB/s.")
|
|
144
152
|
if f["countries"]:
|
|
145
153
|
out.append("Most of them sit in " + ", ".join(f"{_country(cc)} ({n:,})" for cc, n in f["countries"]) + ".")
|
|
146
154
|
return out
|
|
@@ -215,6 +223,7 @@ def page(f: dict) -> str:
|
|
|
215
223
|
<title>The week in free proxies ({escape(_period(f))}) | proxy-scraper</title>
|
|
216
224
|
<meta name="description" content="{escape(_lines(f)[0])}">
|
|
217
225
|
<link rel="canonical" href="{REPORT_URL}">
|
|
226
|
+
<link rel="alternate" type="application/atom+xml" title="The week in free proxies" href="{REPORT_URL}feed.xml">
|
|
218
227
|
<meta property="og:title" content="The week in free proxies">
|
|
219
228
|
<meta property="og:description" content="{escape(_lines(f)[0])}">
|
|
220
229
|
<meta property="og:image" content="{SITE_URL}og.png">
|
|
@@ -249,6 +258,7 @@ picture img {{ width: 100%; height: auto; border-radius: 16px; }}
|
|
|
249
258
|
<img src="{root}chart-dark.svg" alt="Working proxies over the week, by protocol">
|
|
250
259
|
</picture>
|
|
251
260
|
<footer><span>Want to share it? <a href="report.md">Markdown</a> · <a href="post.txt">a short post</a> ·
|
|
261
|
+
<a href="feed.xml">RSS/Atom feed</a> ·
|
|
252
262
|
<a href="https://github.com/maximilianfeix/proxy-scraper">the code</a></span></footer>
|
|
253
263
|
</div>
|
|
254
264
|
</body>
|
|
@@ -256,11 +266,63 @@ picture img {{ width: 100%; height: auto; border-radius: 16px; }}
|
|
|
256
266
|
"""
|
|
257
267
|
|
|
258
268
|
|
|
259
|
-
|
|
269
|
+
FEED_WEEKS = 12
|
|
270
|
+
_ATOM = "{http://www.w3.org/2005/Atom}"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def feed(f: dict, previous: Optional[str]) -> str:
|
|
274
|
+
"""Atom feed with one entry per finished ISO week. The first run of a week adds the entry for the week that just
|
|
275
|
+
ended; every later run that week hands back the feed unchanged – feed readers and Slack/Discord bridges see one
|
|
276
|
+
new entry a week, not an update every hour. `previous`: the feed from the last run (None or garbage: start over)."""
|
|
277
|
+
now = f["now"]
|
|
278
|
+
monday = (now - timedelta(days=now.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
279
|
+
year, week, _ = (monday - timedelta(days=1)).isocalendar()
|
|
280
|
+
entry_id = f"{REPORT_URL}{year}-W{week:02d}"
|
|
281
|
+
kept: List[str] = []
|
|
282
|
+
if previous:
|
|
283
|
+
try:
|
|
284
|
+
root = ET.fromstring(previous)
|
|
285
|
+
old = root.findall(f"{_ATOM}entry")
|
|
286
|
+
except ET.ParseError:
|
|
287
|
+
old = []
|
|
288
|
+
if any((e.findtext(f"{_ATOM}id") or "") == entry_id for e in old):
|
|
289
|
+
return previous # this week's entry is out already – leave it exactly as it is
|
|
290
|
+
ET.register_namespace("", _ATOM[1:-1])
|
|
291
|
+
kept = [ET.tostring(e, encoding="unicode") for e in old][:FEED_WEEKS - 1]
|
|
292
|
+
stamp = monday.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
293
|
+
text = "\n\n".join(_lines(f))
|
|
294
|
+
new = f"""<entry>
|
|
295
|
+
<title>The week in free proxies: {escape(_period(f))}</title>
|
|
296
|
+
<link rel="alternate" href="{REPORT_URL}"/>
|
|
297
|
+
<id>{entry_id}</id>
|
|
298
|
+
<updated>{stamp}</updated>
|
|
299
|
+
<content type="text">{escape(text)}</content>
|
|
300
|
+
</entry>"""
|
|
301
|
+
body = "\n ".join([new, *kept])
|
|
302
|
+
return f"""<?xml version="1.0" encoding="utf-8"?>
|
|
303
|
+
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
304
|
+
<title>The week in free proxies</title>
|
|
305
|
+
<subtitle>How long free proxies last and which sites let them through – from hourly checks by proxy-scraper</subtitle>
|
|
306
|
+
<link rel="self" href="{REPORT_URL}feed.xml"/>
|
|
307
|
+
<link rel="alternate" href="{REPORT_URL}"/>
|
|
308
|
+
<id>{REPORT_URL}</id>
|
|
309
|
+
<updated>{stamp}</updated>
|
|
310
|
+
<author><name>proxy-scraper</name></author>
|
|
311
|
+
{body}
|
|
312
|
+
</feed>
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def write_report(f: dict, out: Path, previous_feed: Optional[Path] = None) -> None:
|
|
260
317
|
folder = out / "report"
|
|
261
318
|
folder.mkdir(parents=True, exist_ok=True)
|
|
262
319
|
(folder / "index.html").write_text(page(f), encoding="utf-8")
|
|
263
320
|
(folder / "report.md").write_text(markdown(f), encoding="utf-8")
|
|
264
321
|
(folder / "post.txt").write_text(post(f) + "\n", encoding="utf-8")
|
|
322
|
+
try:
|
|
323
|
+
previous = previous_feed.read_text(encoding="utf-8") if previous_feed else None
|
|
324
|
+
except OSError:
|
|
325
|
+
previous = None
|
|
326
|
+
(folder / "feed.xml").write_text(feed(f, previous), encoding="utf-8")
|
|
265
327
|
for theme in THEMES:
|
|
266
328
|
(folder / f"lifetimes-{theme}.svg").write_text(lifetimes_svg(f, theme), encoding="utf-8")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Shared plumbing for the extra steps that run on a finished run's proxies.json in the hourly workflow –
|
|
2
|
+
which sites let a proxy through (sites.py), how fast it downloads (speed.py).
|
|
3
|
+
|
|
4
|
+
Each step loads the run's rows, tries the HTTPS-capable proxies through a verified TLS tunnel with a limit on how
|
|
5
|
+
many at once, and writes the file back in one go: if the workflow kills the step at its timeout, the publish step
|
|
6
|
+
still finds the old file intact.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import socket
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Awaitable, Callable, Iterable, List, Tuple
|
|
16
|
+
|
|
17
|
+
from .paths import atomic_write
|
|
18
|
+
|
|
19
|
+
# what a probe through a free proxy can run into – all of it just means "no answer from this one"
|
|
20
|
+
PROBE_ERRORS = (OSError, asyncio.TimeoutError, asyncio.IncompleteReadError, asyncio.LimitOverrunError, ValueError)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_rows(run_dir: Path) -> Tuple[Path, List[dict]]:
|
|
24
|
+
path = run_dir / "proxies.json"
|
|
25
|
+
return path, json.loads(path.read_text(encoding="utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_rows(path: Path, rows: List[dict]) -> None:
|
|
29
|
+
atomic_write(path, json.dumps(rows, indent=1, ensure_ascii=False))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def https_rows(rows: Iterable[dict]) -> List[dict]:
|
|
33
|
+
"""Only these can tunnel to an https:// site – the others failed the HTTPS test in the run."""
|
|
34
|
+
return [r for r in rows if r.get("https") is True]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_ipv4(host: str, port: int = 443) -> str:
|
|
38
|
+
"""Looked up once per step: SOCKS4 needs the IP, and a thousand lookups of the same host help nobody."""
|
|
39
|
+
return socket.getaddrinfo(host, port, family=socket.AF_INET)[0][4][0]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def run_limited(jobs: Iterable[Callable[[], Awaitable[None]]], concurrency: int) -> None:
|
|
43
|
+
"""Run the jobs with at most `concurrency` at a time."""
|
|
44
|
+
slots = asyncio.Semaphore(concurrency)
|
|
45
|
+
|
|
46
|
+
async def one(job: Callable[[], Awaitable[None]]) -> None:
|
|
47
|
+
async with slots:
|
|
48
|
+
await job()
|
|
49
|
+
|
|
50
|
+
await asyncio.gather(*(one(job) for job in jobs))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Tunnels:
|
|
54
|
+
"""Verified TLS tunnels to a host through a proxy – the same handshake code the checks use."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, timeout: float):
|
|
57
|
+
from .checker import Checker # only here: the checker pulls in more than these steps need
|
|
58
|
+
|
|
59
|
+
self.checker = Checker(judge_ip="127.0.0.1", own_ips=(), timeout=timeout, connect_timeout=timeout / 2)
|
|
60
|
+
|
|
61
|
+
async def open(self, row: dict, host: str, ip: str, port: int = 443):
|
|
62
|
+
"""(reader, writer) of the TLS connection, or None if the proxy refused the tunnel or broke up the TLS."""
|
|
63
|
+
return await self.checker.tls_tunnel(row["ptype"], row["proxy"], host, ip, port)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def browser_get(host: str, path: str, *extra: str) -> bytes:
|
|
67
|
+
"""A plain GET the way a browser would send it (so sites answer as they would to a person)."""
|
|
68
|
+
from .netio import USER_AGENT
|
|
69
|
+
|
|
70
|
+
lines = [f"GET {path} HTTP/1.1", f"Host: {host}", f"User-Agent: {USER_AGENT}",
|
|
71
|
+
"Accept: text/html,application/xhtml+xml,*/*;q=0.8", "Accept-Language: en-US,en;q=0.8", *extra,
|
|
72
|
+
"Connection: close"]
|
|
73
|
+
return ("\r\n".join(lines) + "\r\n\r\n").encode()
|
|
74
|
+
|