proxy-scraper-cli 1.10.0__tar.gz → 1.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/PKG-INFO +5 -3
  2. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/README.md +4 -2
  3. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/PKG-INFO +5 -3
  4. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/SOURCES.txt +3 -0
  5. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/__init__.py +1 -1
  6. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/agent.py +7 -4
  7. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/api.py +9 -5
  8. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/checker.py +4 -0
  9. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/mcp_server.py +11 -3
  10. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/proxypages.py +5 -1
  11. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/publish.py +10 -4
  12. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/report.py +64 -2
  13. proxy_scraper_cli-1.12.0/proxyscraper/runsteps.py +74 -0
  14. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/index.html +29 -8
  15. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sites.py +25 -33
  16. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sources.py +8 -2
  17. proxy_scraper_cli-1.12.0/proxyscraper/speed.py +118 -0
  18. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_agent.py +1 -1
  19. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_proxy_sources.py +29 -0
  20. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_report.py +43 -0
  21. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_sites.py +21 -4
  22. proxy_scraper_cli-1.12.0/tests/test_speed.py +121 -0
  23. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/LICENSE +0 -0
  24. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  25. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  26. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
  27. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  28. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/__main__.py +0 -0
  29. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/action.py +0 -0
  30. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/app.py +0 -0
  31. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/asndb.py +0 -0
  32. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/blocklist.py +0 -0
  33. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/charts.py +0 -0
  34. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/cli.py +0 -0
  35. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/compat.py +0 -0
  36. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/completion.py +0 -0
  37. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/exporters.py +0 -0
  38. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/fetchcache.py +0 -0
  39. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/geo.py +0 -0
  40. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/geodb.py +0 -0
  41. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/handshake.py +0 -0
  42. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/history.py +0 -0
  43. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/judges.py +0 -0
  44. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/mcp_entry.py +0 -0
  45. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/netio.py +0 -0
  46. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/options.py +0 -0
  47. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/output.py +0 -0
  48. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/pages.py +0 -0
  49. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/parsing.py +0 -0
  50. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/paths.py +0 -0
  51. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/pipeline.py +0 -0
  52. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/preferences.py +0 -0
  53. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/__init__.py +0 -0
  54. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/core.py +0 -0
  55. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/http.py +0 -0
  56. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/pool.py +0 -0
  57. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/socks.py +0 -0
  58. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/status.py +0 -0
  59. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/upstream.py +0 -0
  60. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  61. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  62. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/logo.png +0 -0
  63. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/og.png +0 -0
  64. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sources.json +0 -0
  65. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/targets.py +0 -0
  66. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/__init__.py +0 -0
  67. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/dashboard.py +0 -0
  68. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/keys.py +0 -0
  69. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/report.py +0 -0
  70. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/serve.py +0 -0
  71. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/widgets.py +0 -0
  72. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/wizard.py +0 -0
  73. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/pyproject.toml +0 -0
  74. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/setup.cfg +0 -0
  75. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_action.py +0 -0
  76. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_api.py +0 -0
  77. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_app.py +0 -0
  78. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_asndb.py +0 -0
  79. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_auth.py +0 -0
  80. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_blocklist.py +0 -0
  81. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_charts.py +0 -0
  82. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_checker.py +0 -0
  83. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_compat.py +0 -0
  84. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_completion.py +0 -0
  85. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_discovery_growth.py +0 -0
  86. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_exporters.py +0 -0
  87. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_fetchcache.py +0 -0
  88. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_geodb.py +0 -0
  89. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_judges.py +0 -0
  90. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_learning.py +0 -0
  91. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_live_api.py +0 -0
  92. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_mcp_server.py +0 -0
  93. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_mine_sources.py +0 -0
  94. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_options.py +0 -0
  95. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_output_stdout.py +0 -0
  96. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_packaging.py +0 -0
  97. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_pages.py +0 -0
  98. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_parsing.py +0 -0
  99. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_parsing_fuzz.py +0 -0
  100. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_paths.py +0 -0
  101. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_pipeline.py +0 -0
  102. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_proxy_pages.py +0 -0
  103. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_publish.py +0 -0
  104. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_robustness.py +0 -0
  105. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_password.py +0 -0
  106. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_refill.py +0 -0
  107. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_v2.py +0 -0
  108. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_server.py +0 -0
  109. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_server_closed_writer.py +0 -0
  110. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_server_fixes.py +0 -0
  111. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_sources_json.py +0 -0
  112. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_tamper.py +0 -0
  113. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_targets.py +0 -0
  114. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_ui.py +0 -0
  115. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_uptime.py +0 -0
  116. {proxy_scraper_cli-1.10.0 → proxy_scraper_cli-1.12.0}/tests/test_wizard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-scraper-cli
3
- Version: 1.10.0
3
+ Version: 1.12.0
4
4
  Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
5
5
  Author: Maximilian Feix
6
6
  License-Expression: MIT
@@ -205,7 +205,7 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
205
205
  | HTTP · SOCKS4 · SOCKS5 | `1.2.3.4:8080` | [http.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/http.txt) · [socks4.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks4.txt) · [socks5.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt) |
206
206
  | HTTPS-capable only | `type://ip:port` | [https.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/https.txt) |
207
207
  | Elite only | `type://ip:port` | [elite.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/elite.txt) |
208
- | Gets through to Google · Reddit · Amazon (no captcha, no 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.12.0
4
4
  Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
5
5
  Author: Maximilian Feix
6
6
  License-Expression: MIT
@@ -205,7 +205,7 @@ Every protocol and country also has its own page with a plain download, e.g. [SO
205
205
  | HTTP · SOCKS4 · SOCKS5 | `1.2.3.4:8080` | [http.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/http.txt) · [socks4.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks4.txt) · [socks5.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/socks5.txt) |
206
206
  | HTTPS-capable only | `type://ip:port` | [https.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/https.txt) |
207
207
  | Elite only | `type://ip:port` | [elite.txt](https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list/elite.txt) |
208
- | Gets through to Google · Reddit · Amazon (no captcha, no 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
 
@@ -40,9 +40,11 @@ proxyscraper/preferences.py
40
40
  proxyscraper/proxypages.py
41
41
  proxyscraper/publish.py
42
42
  proxyscraper/report.py
43
+ proxyscraper/runsteps.py
43
44
  proxyscraper/sites.py
44
45
  proxyscraper/sources.json
45
46
  proxyscraper/sources.py
47
+ proxyscraper/speed.py
46
48
  proxyscraper/targets.py
47
49
  proxyscraper/server/__init__.py
48
50
  proxyscraper/server/core.py
@@ -104,6 +106,7 @@ tests/test_server_closed_writer.py
104
106
  tests/test_server_fixes.py
105
107
  tests/test_sites.py
106
108
  tests/test_sources_json.py
109
+ tests/test_speed.py
107
110
  tests/test_tamper.py
108
111
  tests/test_targets.py
109
112
  tests/test_ui.py
@@ -1,7 +1,7 @@
1
1
  """proxy-scraper: collects public HTTP/SOCKS4/SOCKS5 proxies from hundreds of sources, checks them
2
2
  in parallel with its own protocol handshakes and learns which sources deliver good proxies."""
3
3
 
4
- __version__ = "1.10.0"
4
+ __version__ = "1.12.0"
5
5
 
6
6
 
7
7
  def __getattr__(name):
@@ -179,17 +179,18 @@ def normalize_countries(countries: Iterable[str]) -> List[str]:
179
179
  def select(rows: Iterable[dict], protocol: str = "any", countries: Iterable[str] = (), https_only: bool = False,
180
180
  elite_only: bool = False, exclude_datacenter: bool = False, exclude_blocklisted: bool = False,
181
181
  stable_only: bool = False, max_latency_ms: int = 0, run_hours: int = 1, min_uptime: int = 0,
182
- works_on: Iterable[str] = ()) -> List[dict]:
182
+ works_on: Iterable[str] = (), min_speed_kbps: int = 0) -> List[dict]:
183
183
  """The rows that pass every filter, fastest first – the same filters as on the website."""
184
184
  return sorted(matching(rows, protocol, countries, https_only, elite_only, exclude_datacenter,
185
- exclude_blocklisted, stable_only, max_latency_ms, run_hours, min_uptime, works_on),
185
+ exclude_blocklisted, stable_only, max_latency_ms, run_hours, min_uptime, works_on,
186
+ min_speed_kbps),
186
187
  key=lambda r: r.get("latency") or 0)
187
188
 
188
189
 
189
190
  def matching(rows: Iterable[dict], protocol: str = "any", countries: Iterable[str] = (), https_only: bool = False,
190
191
  elite_only: bool = False, exclude_datacenter: bool = False, exclude_blocklisted: bool = False,
191
192
  stable_only: bool = False, max_latency_ms: int = 0, run_hours: int = 1,
192
- min_uptime: int = 0, works_on: Iterable[str] = ()) -> Iterator[dict]:
193
+ min_uptime: int = 0, works_on: Iterable[str] = (), min_speed_kbps: int = 0) -> Iterator[dict]:
193
194
  """The rows that pass every filter, in list order – checks the filters before the first row is asked for."""
194
195
  protocol = _check_protocol(protocol)
195
196
  wanted = set(normalize_countries(countries))
@@ -205,7 +206,8 @@ def matching(rows: Iterable[dict], protocol: str = "any", countries: Iterable[st
205
206
  and (not stable_only or (r.get("streak") or 0) >= stable_runs)
206
207
  and (not max_latency_ms or (r.get("latency") or 0) <= max_latency_ms)
207
208
  and (not min_uptime or (r.get("uptime_7d") or 0) >= min_uptime)
208
- and all((r.get("sites") or {}).get(s) for s in sites))
209
+ and all((r.get("sites") or {}).get(s) for s in sites)
210
+ and (not min_speed_kbps or (r.get("speed_kbps") or 0) >= min_speed_kbps))
209
211
 
210
212
 
211
213
  def describe(item: Union[dict, CheckResult], run_hours: Optional[int] = None) -> dict:
@@ -229,6 +231,7 @@ def describe(item: Union[dict, CheckResult], run_hours: Optional[int] = None) ->
229
231
  "up_for_hours": streak * run_hours if streak and run_hours else None,
230
232
  "uptime_7d_percent": item.get("uptime_7d"),
231
233
  "works_on": [name for name, ok in (item.get("sites") or {}).items() if ok],
234
+ "speed_kbps": item.get("speed_kbps"),
232
235
  }
233
236
 
234
237
 
@@ -166,7 +166,8 @@ class LiveProxy(CheckResult):
166
166
  uptime_7d: Optional[int] = None # the same over the week
167
167
  first_seen: str = "" # ISO time of the first run it was listed in
168
168
  up_for_hours: int = 0 # listed without a gap for this long
169
- sites: Dict[str, bool] = field(default_factory=dict) # "google", "reddit", "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
 
@@ -334,6 +334,10 @@ class Checker:
334
334
  writer.close()
335
335
  return 200 <= status < 400
336
336
 
337
+ async def tls_tunnel(self, ptype: str, proxy: str, host: str, ip: str, port: int = 443):
338
+ """Public side of _tls_tunnel for the steps after a run (sites, speed): ip as text."""
339
+ return await self._tls_tunnel(ptype, proxy, host, socket.inet_aton(ip), port)
340
+
337
341
  async def _tls_tunnel(self, ptype: str, proxy: str, host: str, ip_bytes: bytes, port: int):
338
342
  """Tunnel through the proxy to host:port with verified TLS inside. None = tunnel refused or MITM."""
339
343
  loop = asyncio.get_running_loop()
@@ -17,6 +17,7 @@ from mcp.types import ToolAnnotations
17
17
  from pydantic import BaseModel, Field
18
18
 
19
19
  from . import __version__, agent
20
+ from .sites import SITES
20
21
 
21
22
  REPO = "https://github.com/maximilianfeix/proxy-scraper"
22
23
 
@@ -33,6 +34,7 @@ Free proxies are run by strangers: never send passwords, cookies, API keys or pe
33
34
  and expect some of them to stop working at any time."""
34
35
 
35
36
  Protocol = Literal["any", "http", "socks4", "socks5"]
37
+ SiteName = Literal[tuple(s.name for s in SITES)] # the sites the hourly check tries (sites.py)
36
38
  Countries = Annotated[List[str], Field(description="Two-letter country codes of the exit IP, e.g. ['DE', 'NL']. "
37
39
  "Empty = any country.")]
38
40
 
@@ -51,7 +53,10 @@ class Proxy(BaseModel):
51
53
  blocklisted: Optional[bool] = Field(description="Exit IP on the SpamCop blocklist – expect captchas")
52
54
  up_for_hours: Optional[int] = Field(description="How long it has worked without a gap (live list only)")
53
55
  works_on: List[str] = Field(default_factory=list, description="Big sites it got through to in the last hourly "
54
- "check: 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")
@@ -0,0 +1,74 @@
1
+ """Shared plumbing for the extra steps that run on a finished run's proxies.json in the hourly workflow –
2
+ which sites let a proxy through (sites.py), how fast it downloads (speed.py).
3
+
4
+ Each step loads the run's rows, tries the HTTPS-capable proxies through a verified TLS tunnel with a limit on how
5
+ many at once, and writes the file back in one go: if the workflow kills the step at its timeout, the publish step
6
+ still finds the old file intact.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import socket
14
+ from pathlib import Path
15
+ from typing import Awaitable, Callable, Iterable, List, Tuple
16
+
17
+ from .paths import atomic_write
18
+
19
+ # what a probe through a free proxy can run into – all of it just means "no answer from this one"
20
+ PROBE_ERRORS = (OSError, asyncio.TimeoutError, asyncio.IncompleteReadError, asyncio.LimitOverrunError, ValueError)
21
+
22
+
23
+ def load_rows(run_dir: Path) -> Tuple[Path, List[dict]]:
24
+ path = run_dir / "proxies.json"
25
+ return path, json.loads(path.read_text(encoding="utf-8"))
26
+
27
+
28
+ def save_rows(path: Path, rows: List[dict]) -> None:
29
+ atomic_write(path, json.dumps(rows, indent=1, ensure_ascii=False))
30
+
31
+
32
+ def https_rows(rows: Iterable[dict]) -> List[dict]:
33
+ """Only these can tunnel to an https:// site – the others failed the HTTPS test in the run."""
34
+ return [r for r in rows if r.get("https") is True]
35
+
36
+
37
+ def resolve_ipv4(host: str, port: int = 443) -> str:
38
+ """Looked up once per step: SOCKS4 needs the IP, and a thousand lookups of the same host help nobody."""
39
+ return socket.getaddrinfo(host, port, family=socket.AF_INET)[0][4][0]
40
+
41
+
42
+ async def run_limited(jobs: Iterable[Callable[[], Awaitable[None]]], concurrency: int) -> None:
43
+ """Run the jobs with at most `concurrency` at a time."""
44
+ slots = asyncio.Semaphore(concurrency)
45
+
46
+ async def one(job: Callable[[], Awaitable[None]]) -> None:
47
+ async with slots:
48
+ await job()
49
+
50
+ await asyncio.gather(*(one(job) for job in jobs))
51
+
52
+
53
+ class Tunnels:
54
+ """Verified TLS tunnels to a host through a proxy – the same handshake code the checks use."""
55
+
56
+ def __init__(self, timeout: float):
57
+ from .checker import Checker # only here: the checker pulls in more than these steps need
58
+
59
+ self.checker = Checker(judge_ip="127.0.0.1", own_ips=(), timeout=timeout, connect_timeout=timeout / 2)
60
+
61
+ async def open(self, row: dict, host: str, ip: str, port: int = 443):
62
+ """(reader, writer) of the TLS connection, or None if the proxy refused the tunnel or broke up the TLS."""
63
+ return await self.checker.tls_tunnel(row["ptype"], row["proxy"], host, ip, port)
64
+
65
+
66
+ def browser_get(host: str, path: str, *extra: str) -> bytes:
67
+ """A plain GET the way a browser would send it (so sites answer as they would to a person)."""
68
+ from .netio import USER_AGENT
69
+
70
+ lines = [f"GET {path} HTTP/1.1", f"Host: {host}", f"User-Agent: {USER_AGENT}",
71
+ "Accept: text/html,application/xhtml+xml,*/*;q=0.8", "Accept-Language: en-US,en;q=0.8", *extra,
72
+ "Connection: close"]
73
+ return ("\r\n".join(lines) + "\r\n\r\n").encode()
74
+