proxy-scraper-cli 1.10.0__tar.gz → 1.11.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.11.0}/PKG-INFO +5 -3
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/README.md +4 -2
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/PKG-INFO +5 -3
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/SOURCES.txt +2 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/__init__.py +1 -1
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/agent.py +7 -4
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/api.py +9 -5
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/mcp_server.py +11 -3
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/proxypages.py +5 -1
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/publish.py +10 -4
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/report.py +64 -2
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/index.html +11 -6
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/sites.py +11 -3
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/sources.py +8 -2
- proxy_scraper_cli-1.11.0/proxyscraper/speed.py +129 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_agent.py +1 -1
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_proxy_sources.py +29 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_report.py +43 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_sites.py +21 -4
- proxy_scraper_cli-1.11.0/tests/test_speed.py +121 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/LICENSE +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/__main__.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/action.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/app.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/asndb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/blocklist.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/charts.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/checker.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/cli.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/compat.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/completion.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/exporters.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/fetchcache.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/geo.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/geodb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/handshake.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/history.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/judges.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/mcp_entry.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/netio.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/options.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/output.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/pages.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/parsing.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/paths.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/pipeline.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/preferences.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/__init__.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/core.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/http.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/pool.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/socks.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/status.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/upstream.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/apple-touch-icon.png +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/logo.png +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/og.png +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/sources.json +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/targets.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/__init__.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/dashboard.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/keys.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/report.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/serve.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/widgets.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/wizard.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/pyproject.toml +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/setup.cfg +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_action.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_api.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_app.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_asndb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_auth.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_blocklist.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_charts.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_checker.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_compat.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_completion.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_discovery_growth.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_exporters.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_fetchcache.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_geodb.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_judges.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_learning.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_live_api.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_mcp_server.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_mine_sources.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_options.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_output_stdout.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_packaging.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_pages.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_parsing.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_parsing_fuzz.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_paths.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_pipeline.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_proxy_pages.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_publish.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_robustness.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_serve_password.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_serve_refill.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_serve_v2.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_server.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_server_closed_writer.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_server_fixes.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_sources_json.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_tamper.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_targets.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_ui.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_uptime.py +0 -0
- {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.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.11.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.11.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.11.0}/proxy_scraper_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -43,6 +43,7 @@ proxyscraper/report.py
|
|
|
43
43
|
proxyscraper/sites.py
|
|
44
44
|
proxyscraper/sources.json
|
|
45
45
|
proxyscraper/sources.py
|
|
46
|
+
proxyscraper/speed.py
|
|
46
47
|
proxyscraper/targets.py
|
|
47
48
|
proxyscraper/server/__init__.py
|
|
48
49
|
proxyscraper/server/core.py
|
|
@@ -104,6 +105,7 @@ tests/test_server_closed_writer.py
|
|
|
104
105
|
tests/test_server_fixes.py
|
|
105
106
|
tests/test_sites.py
|
|
106
107
|
tests/test_sources_json.py
|
|
108
|
+
tests/test_speed.py
|
|
107
109
|
tests/test_tamper.py
|
|
108
110
|
tests/test_targets.py
|
|
109
111
|
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.11.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
|
|
|
@@ -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")
|
|
@@ -582,7 +582,7 @@
|
|
|
582
582
|
<button class="chip" type="button" id="f-bl" aria-pressed="false" title="Exit IP not on the SpamCop blocklist – fewer captchas">Not blocklisted</button>
|
|
583
583
|
<select class="field" id="f-country" aria-label="Country"><option value="">All countries</option></select>
|
|
584
584
|
<select class="field" id="f-site" aria-label="Gets through to" title="Checked every hour: most free proxies get a captcha or a 403 on these sites">
|
|
585
|
-
<option value="">Any site</option><option value="google">Gets through to Google</option><option value="reddit">Gets through to Reddit</option><option value="amazon">Gets through to Amazon</option></select>
|
|
585
|
+
<option value="">Any site</option><option value="google">Gets through to Google</option><option value="reddit">Gets through to Reddit</option><option value="amazon">Gets through to Amazon</option><option value="instagram">Gets through to Instagram</option><option value="tiktok">Gets through to TikTok</option></select>
|
|
586
586
|
<label class="range">Up to <input type="range" id="f-lat" min="200" max="8000" step="100" value="8000" aria-label="Maximum latency"><output id="f-lat-v" for="f-lat">any</output></label>
|
|
587
587
|
<input class="field" type="search" id="f-q" placeholder="Search IP, country or provider /" aria-label="Search" aria-keyshortcuts="/">
|
|
588
588
|
</div>
|
|
@@ -595,10 +595,10 @@
|
|
|
595
595
|
<th scope="col" data-key="proxy"><button class="sort" type="button">Proxy</button></th>
|
|
596
596
|
<th scope="col" data-key="country"><button class="sort" type="button">Country</button></th>
|
|
597
597
|
<th scope="col" data-key="latency" aria-sort="ascending"><button class="sort" type="button">Latency</button></th>
|
|
598
|
+
<th scope="col" data-key="speed_kbps" class="hide-sm"><button class="sort" type="button" title="Download speed in the last run – 100 KB through the proxy">Speed</button></th>
|
|
598
599
|
<th scope="col" data-key="https" class="hide-sm"><button class="sort" type="button">HTTPS</button></th>
|
|
599
600
|
<th scope="col" data-key="uptime_7d" class="hide-sm"><button class="sort" type="button" title="Share of this week's runs it was on the list">Uptime</button></th>
|
|
600
601
|
<th scope="col" class="hide-sm"><span class="th-plain" title="Big sites it got through to in the last hourly check">Gets through</span></th>
|
|
601
|
-
<th scope="col" data-key="anonymity" class="hide-sm"><button class="sort" type="button">Anonymity</button></th>
|
|
602
602
|
<th scope="col" data-key="org" class="hide-sm"><button class="sort" type="button">Provider</button></th>
|
|
603
603
|
</tr></thead>
|
|
604
604
|
<tbody id="rows"><tr><td colspan="9" class="empty">Loading the list</td></tr></tbody>
|
|
@@ -716,6 +716,7 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
|
|
|
716
716
|
<a href="history.json"><span class="mono">history.json</span><span>the runs of the last 30 days, for charts</span></a>
|
|
717
717
|
<a href="streaks.json"><span class="mono">streaks.json</span><span>how many runs in a row each proxy worked</span></a>
|
|
718
718
|
<a href="seen.json"><span class="mono">seen.json</span><span>which of the last 168 runs each proxy was listed in</span></a>
|
|
719
|
+
<a href="https://github.com/maximilianfeix/proxy-scraper/releases?q=snapshots&expanded=true"><span class="mono">snapshots</span><span>one proxies.json a day, kept for good</span></a>
|
|
719
720
|
</div>
|
|
720
721
|
</div>
|
|
721
722
|
<div class="cmd"><pre>curl -s https://maximilianfeix.github.io/proxy-scraper/proxies.json | jq '.[0]'
|
|
@@ -799,11 +800,13 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
|
|
|
799
800
|
};
|
|
800
801
|
const share = (r) => r.uptime_7d == null ? uptime(r.streak) // lists from before the uptime existed
|
|
801
802
|
: `<span class="uptime${(r.streak || 0) >= STABLE ? " stable-badge" : ""}" title="On the list in ${r.uptime_7d} % of this week's runs and ${r.uptime_24h} % of today's – up without a gap for ${upFor(r.streak)}"><span class="meter" style="--v:${r.uptime_7d}%"></span>${r.uptime_7d} %</span>`;
|
|
802
|
-
const SITE_NAMES = { google: "Google", reddit: "Reddit", amazon: "Amazon" };
|
|
803
|
+
const SITE_NAMES = { google: "Google", reddit: "Reddit", amazon: "Amazon", instagram: "Instagram", tiktok: "TikTok" };
|
|
803
804
|
const through = (r) => {
|
|
804
805
|
const ok = Object.keys(SITE_NAMES).filter(k => (r.sites || {})[k] === true);
|
|
805
806
|
return ok.length ? `<span class="sites">${ok.map(k => `<span class="site">${SITE_NAMES[k]}</span>`).join("")}</span>` : '<span class="quiet">–</span>';
|
|
806
807
|
};
|
|
808
|
+
const speed = (k) => !k ? '<span class="quiet">–</span>'
|
|
809
|
+
: `<span class="${k >= 500 ? "fast" : k >= 100 ? "mid" : "slow"}">${k >= 1024 ? (k / 1024).toFixed(1) + " MB/s" : fmt(k) + " KB/s"}</span>`;
|
|
807
810
|
const latency = (ms) => `<span class="${ms < 1000 ? "fast" : ms < 3000 ? "mid" : "slow"}">${fmt(ms)} ms</span>`;
|
|
808
811
|
const state = { rows: [], shown: PAGE, sort: "latency", dir: 1, types: new Set(["http", "socks4", "socks5"]),
|
|
809
812
|
https: false, elite: false, noDc: false, noBl: false, stable: false, country: "", site: "", maxLat: 8000, q: "" };
|
|
@@ -952,7 +955,9 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
|
|
|
952
955
|
&& (state.maxLat >= 8000 || r.latency <= state.maxLat) // all the way right = no limit
|
|
953
956
|
&& (!q || `${r.ptype} ${r.proxy} ${r.country} ${r.country ? countryName(r.country) : ""} ${r.exit_ip} ${r.org || ""} AS${r.asn || ""}`.toLowerCase().includes(q)));
|
|
954
957
|
const k = state.sort, d = state.dir;
|
|
955
|
-
|
|
958
|
+
const missing = (v) => v == null || v === ""; // not measured: always last, whichever way it's sorted
|
|
959
|
+
return rows.sort((a, b) => (missing(a[k]) - missing(b[k]))
|
|
960
|
+
|| (a[k] > b[k] ? 1 : a[k] < b[k] ? -1 : 0) * d || a.latency - b.latency);
|
|
956
961
|
}
|
|
957
962
|
function render() {
|
|
958
963
|
const rows = filtered();
|
|
@@ -961,10 +966,10 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
|
|
|
961
966
|
<td><button class="addr-btn" type="button" data-url="${esc(r.url)}" aria-label="Copy ${esc(r.url)}">${esc(r.proxy)}<svg aria-hidden="true"><use href="#i-copy"/></svg></button><a class="detail" href="proxy/${esc(r.proxy.replace(":", "-"))}/" aria-label="Details and week of ${esc(r.proxy)}" title="Details and this week's checks"><svg aria-hidden="true"><use href="#i-open"/></svg></a></td>
|
|
962
967
|
<td title="${esc(r.country ? countryName(r.country) : "")}">${flag(r.country)} ${esc(r.country || "–")}</td>
|
|
963
968
|
<td>${latency(r.latency)}</td>
|
|
969
|
+
<td class="hide-sm">${speed(r.speed_kbps)}</td>
|
|
964
970
|
<td class="hide-sm">${r.https ? "Yes" : '<span class="quiet">No</span>'}</td>
|
|
965
971
|
<td class="hide-sm">${share(r)}</td>
|
|
966
972
|
<td class="hide-sm">${through(r)}</td>
|
|
967
|
-
<td class="hide-sm">${esc(r.anonymity || "–")}</td>
|
|
968
973
|
<td class="hide-sm provider" title="${esc(r.org ? `AS${r.asn} ${r.org}, exit ${r.exit_ip}` : r.exit_ip)}">${r.hosting ? '<span class="tag" title="Datacenter">DC</span>' : ""}${r.blocklisted ? '<span class="tag bl" title="Exit IP on the SpamCop blocklist">BL</span>' : ""}${esc(short(r.org) || r.exit_ip)}</td></tr>`).join("")
|
|
969
974
|
|| '<tr><td colspan="9" class="empty">No proxy matches these filters. Try a higher latency limit or fewer filters.</td></tr>';
|
|
970
975
|
$("#count").textContent = rows.length ? `Showing ${fmt(Math.min(state.shown, rows.length))} of ${fmt(rows.length)} matching proxies` : "";
|
|
@@ -1003,7 +1008,7 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
|
|
|
1003
1008
|
if (q.has("types")) state.types = new Set(q.get("types").split(",").filter(t => ALL_TYPES.includes(t)));
|
|
1004
1009
|
Object.entries(FLAGS).forEach(([key, name]) => { state[key] = q.get(name) === "1"; });
|
|
1005
1010
|
state.country = /^[A-Z]{2}$/.test(q.get("country") || "") ? q.get("country") : "";
|
|
1006
|
-
state.site =
|
|
1011
|
+
state.site = Object.keys(SITE_NAMES).includes(q.get("site")) ? q.get("site") : "";
|
|
1007
1012
|
const lat = parseInt(q.get("latency"), 10);
|
|
1008
1013
|
state.maxLat = lat >= 200 && lat < 8000 ? Math.round(lat / 100) * 100 : 8000;
|
|
1009
1014
|
state.q = (q.get("q") || "").slice(0, 100);
|
|
@@ -36,6 +36,7 @@ class Site:
|
|
|
36
36
|
blocked_redirect: str = "" # a redirect to a URL containing this is a block too (a captcha page)
|
|
37
37
|
ok_redirect: str = "" # ... and one containing this is fine (a consent screen)
|
|
38
38
|
min_body: int = 0 # an "ok" answer must come with at least this much page, else it's a stub
|
|
39
|
+
title: str = "" # for people: "Google"
|
|
39
40
|
|
|
40
41
|
def verdict(self, status: int, location: str = "", body: Optional[int] = None) -> Optional[bool]:
|
|
41
42
|
"""True = got through, False = blocked, None = the answer says neither. body: bytes of page read after
|
|
@@ -54,11 +55,18 @@ class Site:
|
|
|
54
55
|
|
|
55
56
|
SITES = (
|
|
56
57
|
Site("google", "www.google.com", "/search?q=weather", ok=(200,), blocked=(429,),
|
|
57
|
-
blocked_redirect="/sorry/", ok_redirect="consent.google."),
|
|
58
|
-
Site("reddit", "www.reddit.com", "/", ok=(200,), blocked=(403, 429)),
|
|
58
|
+
blocked_redirect="/sorry/", ok_redirect="consent.google.", title="Google"),
|
|
59
|
+
Site("reddit", "www.reddit.com", "/", ok=(200,), blocked=(403, 429), title="Reddit"),
|
|
59
60
|
# Amazon answers bots with a 2-4 KB stub or captcha page, and since 2026-09 with status 200 on the home page
|
|
60
61
|
# too – even without a proxy. The search tells them apart: real results are well over 100 KB.
|
|
61
|
-
Site("amazon", "www.amazon.com", "/s?k=usb+cable", ok=(200,), blocked=(202, 503), min_body=32_000
|
|
62
|
+
Site("amazon", "www.amazon.com", "/s?k=usb+cable", ok=(200,), blocked=(202, 503), min_body=32_000,
|
|
63
|
+
title="Amazon"),
|
|
64
|
+
# measured on 60 live proxies (2026-09-27): most get Instagram's login wall, a few the real profile page;
|
|
65
|
+
# TikTok gives some a 403 or a small stub. ChatGPT, eBay and Indeed block everyone (the last two even without
|
|
66
|
+
# a proxy) and LinkedIn and Twitch let everyone through – those say nothing, so they're not in here.
|
|
67
|
+
Site("instagram", "www.instagram.com", "/instagram/", ok=(200,), blocked=(429,), blocked_redirect="accounts/login",
|
|
68
|
+
min_body=30_000, title="Instagram"),
|
|
69
|
+
Site("tiktok", "www.tiktok.com", "/@tiktok", ok=(200,), blocked=(403, 429), min_body=30_000, title="TikTok"),
|
|
62
70
|
)
|
|
63
71
|
SITE = {s.name: s for s in SITES}
|
|
64
72
|
|