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.
Files changed (115) hide show
  1. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/PKG-INFO +5 -3
  2. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/README.md +4 -2
  3. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/PKG-INFO +5 -3
  4. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/SOURCES.txt +2 -0
  5. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/__init__.py +1 -1
  6. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/agent.py +7 -4
  7. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/api.py +9 -5
  8. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/mcp_server.py +11 -3
  9. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/proxypages.py +5 -1
  10. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/publish.py +10 -4
  11. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/report.py +64 -2
  12. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/index.html +11 -6
  13. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/sites.py +11 -3
  14. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/sources.py +8 -2
  15. proxy_scraper_cli-1.11.0/proxyscraper/speed.py +129 -0
  16. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_agent.py +1 -1
  17. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_proxy_sources.py +29 -0
  18. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_report.py +43 -0
  19. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_sites.py +21 -4
  20. proxy_scraper_cli-1.11.0/tests/test_speed.py +121 -0
  21. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/LICENSE +0 -0
  22. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  23. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  24. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
  25. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  26. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/__main__.py +0 -0
  27. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/action.py +0 -0
  28. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/app.py +0 -0
  29. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/asndb.py +0 -0
  30. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/blocklist.py +0 -0
  31. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/charts.py +0 -0
  32. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/checker.py +0 -0
  33. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/cli.py +0 -0
  34. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/compat.py +0 -0
  35. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/completion.py +0 -0
  36. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/exporters.py +0 -0
  37. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/fetchcache.py +0 -0
  38. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/geo.py +0 -0
  39. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/geodb.py +0 -0
  40. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/handshake.py +0 -0
  41. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/history.py +0 -0
  42. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/judges.py +0 -0
  43. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/mcp_entry.py +0 -0
  44. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/netio.py +0 -0
  45. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/options.py +0 -0
  46. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/output.py +0 -0
  47. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/pages.py +0 -0
  48. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/parsing.py +0 -0
  49. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/paths.py +0 -0
  50. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/pipeline.py +0 -0
  51. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/preferences.py +0 -0
  52. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/__init__.py +0 -0
  53. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/core.py +0 -0
  54. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/http.py +0 -0
  55. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/pool.py +0 -0
  56. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/socks.py +0 -0
  57. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/status.py +0 -0
  58. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/server/upstream.py +0 -0
  59. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  60. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  61. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/logo.png +0 -0
  62. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/site/og.png +0 -0
  63. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/sources.json +0 -0
  64. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/targets.py +0 -0
  65. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/__init__.py +0 -0
  66. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/dashboard.py +0 -0
  67. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/keys.py +0 -0
  68. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/report.py +0 -0
  69. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/serve.py +0 -0
  70. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/widgets.py +0 -0
  71. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/proxyscraper/ui/wizard.py +0 -0
  72. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/pyproject.toml +0 -0
  73. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/setup.cfg +0 -0
  74. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_action.py +0 -0
  75. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_api.py +0 -0
  76. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_app.py +0 -0
  77. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_asndb.py +0 -0
  78. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_auth.py +0 -0
  79. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_blocklist.py +0 -0
  80. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_charts.py +0 -0
  81. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_checker.py +0 -0
  82. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_compat.py +0 -0
  83. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_completion.py +0 -0
  84. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_discovery_growth.py +0 -0
  85. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_exporters.py +0 -0
  86. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_fetchcache.py +0 -0
  87. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_geodb.py +0 -0
  88. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_judges.py +0 -0
  89. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_learning.py +0 -0
  90. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_live_api.py +0 -0
  91. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_mcp_server.py +0 -0
  92. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_mine_sources.py +0 -0
  93. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_options.py +0 -0
  94. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_output_stdout.py +0 -0
  95. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_packaging.py +0 -0
  96. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_pages.py +0 -0
  97. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_parsing.py +0 -0
  98. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_parsing_fuzz.py +0 -0
  99. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_paths.py +0 -0
  100. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_pipeline.py +0 -0
  101. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_proxy_pages.py +0 -0
  102. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_publish.py +0 -0
  103. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_robustness.py +0 -0
  104. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_serve_password.py +0 -0
  105. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_serve_refill.py +0 -0
  106. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_serve_v2.py +0 -0
  107. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_server.py +0 -0
  108. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_server_closed_writer.py +0 -0
  109. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_server_fixes.py +0 -0
  110. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_sources_json.py +0 -0
  111. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_tamper.py +0 -0
  112. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_targets.py +0 -0
  113. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_ui.py +0 -0
  114. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.11.0}/tests/test_uptime.py +0 -0
  115. {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.10.0
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 403 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) |
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 403 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) |
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.10.0
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 403 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) |
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
 
@@ -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.10.0"
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", "amazon" -> got through last run?
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: "google", "reddit", "amazon"
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: google, reddit, amazon (live list only)")
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[Literal["google", "reddit", "amazon"]],
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 = {"google": "Google", "reddit": "Reddit", "amazon": "Amazon"}
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) -> int:
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 = {"google": "Google", "reddit": "Reddit", "amazon": "Amazon"}
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
- def write_report(f: dict, out: Path) -> None:
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&amp;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
- return rows.sort((a, b) => (a[k] > b[k] ? 1 : a[k] < b[k] ? -1 : 0) * d || a.latency - b.latency);
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 = ["google", "reddit", "amazon"].includes(q.get("site")) ? q.get("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