proxy-scraper-cli 1.18.0__tar.gz → 1.19.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 (120) hide show
  1. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/PKG-INFO +1 -1
  2. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxy_scraper_cli.egg-info/PKG-INFO +1 -1
  3. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxy_scraper_cli.egg-info/SOURCES.txt +1 -0
  4. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/__init__.py +1 -1
  5. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/agent.py +6 -5
  6. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/api.py +3 -3
  7. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/pages.py +5 -3
  8. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/publish.py +8 -6
  9. proxy_scraper_cli-1.19.0/proxyscraper/ranking.py +44 -0
  10. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/server/pool.py +4 -14
  11. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/site/index.html +8 -4
  12. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_speed_ranking.py +50 -0
  13. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/LICENSE +0 -0
  14. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/README.md +0 -0
  15. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  16. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  17. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
  18. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  19. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/__main__.py +0 -0
  20. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/action.py +0 -0
  21. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/app.py +0 -0
  22. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/asndb.py +0 -0
  23. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/blocklist.py +0 -0
  24. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/charts.py +0 -0
  25. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/checker.py +0 -0
  26. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/cli.py +0 -0
  27. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/compat.py +0 -0
  28. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/completion.py +0 -0
  29. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/exporters.py +0 -0
  30. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/fetchcache.py +0 -0
  31. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/geo.py +0 -0
  32. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/geodb.py +0 -0
  33. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/handshake.py +0 -0
  34. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/history.py +0 -0
  35. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/judges.py +0 -0
  36. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/mcp_entry.py +0 -0
  37. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/mcp_server.py +0 -0
  38. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/netio.py +0 -0
  39. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/options.py +0 -0
  40. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/output.py +0 -0
  41. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/parsing.py +0 -0
  42. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/paths.py +0 -0
  43. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/pipeline.py +0 -0
  44. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/preferences.py +0 -0
  45. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/proxypages.py +0 -0
  46. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/report.py +0 -0
  47. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/runsteps.py +0 -0
  48. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/server/__init__.py +0 -0
  49. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/server/core.py +0 -0
  50. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/server/http.py +0 -0
  51. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/server/socks.py +0 -0
  52. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/server/status.py +0 -0
  53. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/server/upstream.py +0 -0
  54. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  55. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  56. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/site/logo.png +0 -0
  57. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/site/og.png +0 -0
  58. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/sites.py +0 -0
  59. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/sources.json +0 -0
  60. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/sources.py +0 -0
  61. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/speed.py +0 -0
  62. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/targets.py +0 -0
  63. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/ui/__init__.py +0 -0
  64. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/ui/dashboard.py +0 -0
  65. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/ui/keys.py +0 -0
  66. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/ui/report.py +0 -0
  67. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/ui/serve.py +0 -0
  68. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/ui/widgets.py +0 -0
  69. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/proxyscraper/ui/wizard.py +0 -0
  70. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/pyproject.toml +0 -0
  71. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/setup.cfg +0 -0
  72. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_action.py +0 -0
  73. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_agent.py +0 -0
  74. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_api.py +0 -0
  75. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_app.py +0 -0
  76. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_asndb.py +0 -0
  77. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_auth.py +0 -0
  78. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_blocklist.py +0 -0
  79. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_charts.py +0 -0
  80. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_checker.py +0 -0
  81. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_compat.py +0 -0
  82. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_completion.py +0 -0
  83. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_discovery_growth.py +0 -0
  84. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_exporters.py +0 -0
  85. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_fetchcache.py +0 -0
  86. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_geodb.py +0 -0
  87. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_judges.py +0 -0
  88. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_learning.py +0 -0
  89. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_live_api.py +0 -0
  90. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_mcp_server.py +0 -0
  91. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_mine_sources.py +0 -0
  92. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_options.py +0 -0
  93. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_output_stdout.py +0 -0
  94. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_packaging.py +0 -0
  95. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_pages.py +0 -0
  96. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_parsing.py +0 -0
  97. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_parsing_fuzz.py +0 -0
  98. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_paths.py +0 -0
  99. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_pick.py +0 -0
  100. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_pipeline.py +0 -0
  101. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_proxy_pages.py +0 -0
  102. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_proxy_sources.py +0 -0
  103. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_publish.py +0 -0
  104. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_report.py +0 -0
  105. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_robustness.py +0 -0
  106. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_rotator.py +0 -0
  107. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_serve_password.py +0 -0
  108. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_serve_refill.py +0 -0
  109. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_serve_v2.py +0 -0
  110. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_server.py +0 -0
  111. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_server_closed_writer.py +0 -0
  112. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_server_fixes.py +0 -0
  113. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_sites.py +0 -0
  114. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_sources_json.py +0 -0
  115. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_speed.py +0 -0
  116. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_tamper.py +0 -0
  117. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_targets.py +0 -0
  118. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_ui.py +0 -0
  119. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.0}/tests/test_uptime.py +0 -0
  120. {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.19.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.18.0
3
+ Version: 1.19.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-scraper-cli
3
- Version: 1.18.0
3
+ Version: 1.19.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
@@ -39,6 +39,7 @@ proxyscraper/pipeline.py
39
39
  proxyscraper/preferences.py
40
40
  proxyscraper/proxypages.py
41
41
  proxyscraper/publish.py
42
+ proxyscraper/ranking.py
42
43
  proxyscraper/report.py
43
44
  proxyscraper/runsteps.py
44
45
  proxyscraper/sites.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.18.0"
4
+ __version__ = "1.19.0"
5
5
 
6
6
 
7
7
  def __getattr__(name):
@@ -34,6 +34,7 @@ from .netio import http_get
34
34
  from .pages import SITE_URL
35
35
  from .parsing import PROXY_TYPES, make_key
36
36
  from .publish import RAW_BASE
37
+ from .ranking import best_first
37
38
  from .server import ProxyPool, RotatingServer
38
39
  from .sites import SITES
39
40
 
@@ -180,11 +181,11 @@ def select(rows: Iterable[dict], protocol: str = "any", countries: Iterable[str]
180
181
  elite_only: bool = False, exclude_datacenter: bool = False, exclude_blocklisted: bool = False,
181
182
  stable_only: bool = False, max_latency_ms: int = 0, run_hours: int = 1, min_uptime: int = 0,
182
183
  works_on: Iterable[str] = (), min_speed_kbps: int = 0) -> List[dict]:
183
- """The rows that pass every filter, fastest first – the same filters as on the website."""
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,
186
- min_speed_kbps),
187
- key=lambda r: r.get("latency") or 0)
184
+ """The rows that pass every filter, best first (answer plus download speed) – the same filters as on the
185
+ website."""
186
+ return best_first(matching(rows, protocol, countries, https_only, elite_only, exclude_datacenter,
187
+ exclude_blocklisted, stable_only, max_latency_ms, run_hours, min_uptime, works_on,
188
+ min_speed_kbps))
188
189
 
189
190
 
190
191
  def matching(rows: Iterable[dict], protocol: str = "any", countries: Iterable[str] = (), https_only: bool = False,
@@ -216,7 +216,7 @@ async def live_proxies_async(*, types: Iterable[str] = PROXY_TYPES, countries: I
216
216
  limit at most this many (0 = all)
217
217
  """
218
218
  from .agent import AgentError, LiveSource
219
- from .server.pool import page_ms, typical_speed
219
+ from .ranking import page_ms, typical_speed
220
220
  from .sites import SITE
221
221
 
222
222
  types = _types(types)
@@ -242,8 +242,8 @@ async def live_proxies_async(*, types: Iterable[str] = PROXY_TYPES, countries: I
242
242
  found = [_live_proxy(r, data.run_hours) for r in data.rows
243
243
  if r.get("ptype") in types and isinstance(r.get("proxy"), str)]
244
244
  # fastest first – by how quickly a page comes through when the list has download speeds (#186)
245
- typical = typical_speed(found)
246
- found.sort(key=lambda p: page_ms(p, typical))
245
+ typical = typical_speed(p.speed_kbps for p in found)
246
+ found.sort(key=lambda p: page_ms(p.latency, p.speed_kbps, p.https, typical))
247
247
  found = [p for p in found if opts.filters.accepts(p) and (not min_uptime or (p.uptime_7d or 0) >= min_uptime)
248
248
  and all(p.sites.get(s) for s in works_on) and (not min_speed or (p.speed_kbps or 0) >= min_speed)]
249
249
  return found[:limit] if limit else found
@@ -14,6 +14,8 @@ from html import escape
14
14
  from pathlib import Path
15
15
  from typing import Callable, Dict, List, Sequence, Tuple
16
16
 
17
+ from .ranking import best_first
18
+
17
19
  SITE_URL = "https://maximilianfeix.github.io/proxy-scraper/"
18
20
  REPO_URL = "https://github.com/maximilianfeix/proxy-scraper"
19
21
  ROWS_PER_PAGE = 200
@@ -203,8 +205,8 @@ def _render(path: str, title: str, what: str, rows: List[dict], updated: datetim
203
205
  f"<td>{'yes' if r.get('https') else 'no'}</td><td>{escape(through)}</td>"
204
206
  f"<td>{escape((r.get('org') or '–')[:40])}</td></tr>")
205
207
  body_rows = "\n".join(cells(r) for r in rows[:ROWS_PER_PAGE])
206
- table = (f"<div class=\"table\"><table><caption>The {min(total, ROWS_PER_PAGE):,} fastest, as ip:port. "
207
- f"The download has all {total:,} as type://ip:port.</caption>"
208
+ table = (f"<div class=\"table\"><table><caption>The {min(total, ROWS_PER_PAGE):,} fastest to load a page, "
209
+ f"as ip:port. The download has all {total:,} as type://ip:port.</caption>"
208
210
  "<thead><tr><th scope=\"col\">Proxy</th><th scope=\"col\">Type</th><th scope=\"col\">Country</th>"
209
211
  "<th scope=\"col\">Latency</th><th scope=\"col\">Speed</th><th scope=\"col\">Uptime</th>"
210
212
  "<th scope=\"col\">HTTPS</th><th scope=\"col\">Gets through</th>"
@@ -307,7 +309,7 @@ def _nav(specs, counts: Dict[str, int], current: str, root: str) -> str:
307
309
  def write_pages(rows: List[dict], out: Path, updated: datetime, extra: Sequence[str] = ()) -> List[str]:
308
310
  """Writes every page with its proxies.txt and the sitemap (plus the `extra` paths, e.g. the proxy pages)
309
311
  -> the paths listed in the sitemap."""
310
- rows = sorted(rows, key=lambda r: r["latency"])
312
+ rows = best_first(rows)
311
313
  specs = page_specs()
312
314
  selected = {p: [r for r in rows if keep(r)] for p, _, _, keep in specs}
313
315
  counts = {p: len(v) for p, v in selected.items()}
@@ -25,6 +25,7 @@ from .charts import THEMES, countries_svg, trend_svg
25
25
  from .pages import write_pages
26
26
  from .parsing import PROXY_TYPES
27
27
  from .proxypages import write_proxy_pages
28
+ from .ranking import best_first
28
29
  from .report import weekly_facts, write_report
29
30
  from .sites import SITES
30
31
 
@@ -43,7 +44,7 @@ def is_public(row: dict) -> bool:
43
44
 
44
45
  def load_rows(run_dir: Path) -> List[dict]:
45
46
  rows = json.loads((run_dir / "proxies.json").read_text(encoding="utf-8"))
46
- return sorted((r for r in rows if is_public(r)), key=lambda r: r["latency"])
47
+ return best_first(r for r in rows if is_public(r))
47
48
 
48
49
 
49
50
  def num(n: int) -> str:
@@ -116,7 +117,8 @@ def readme(stats: dict, counts: Dict[str, int]) -> str:
116
117
  return f"""# Live proxy list
117
118
 
118
119
  Generated automatically by [proxy-scraper](https://github.com/maximilianfeix/proxy-scraper) with GitHub Actions.
119
- Every proxy here really worked in the last run – sorted by latency, fastest first.
120
+ Every proxy here really worked in the last run – fastest first, by first answer plus the download speed measured
121
+ right after the run.
120
122
  Browse and filter it on the [website](https://maximilianfeix.github.io/proxy-scraper/).
121
123
 
122
124
  **Updated:** {updated} · **{num(stats["total"])} proxies** · median latency {num(stats["median_latency"])} ms
@@ -280,10 +282,10 @@ def publish(run_dir: Path, out: Path, minimum: int = 20, now: Optional[datetime]
280
282
  base = list(reader.fieldnames or ["proxy"])
281
283
  writer = csv.DictWriter(dst, fieldnames=[*base, *(k for k in extra if k not in base)])
282
284
  writer.writeheader()
283
- by_url = {r["url"]: r for r in rows}
284
- for r in reader:
285
- if is_public(r):
286
- listed_row = by_url.get(r.get("url", ""), {})
285
+ csv_rows = {r.get("url", ""): r for r in reader if is_public(r)}
286
+ for listed_row in rows: # in the order of proxies.json: best first
287
+ r = csv_rows.get(listed_row.get("url", ""))
288
+ if r is not None:
287
289
  works = " ".join(name for name, ok in (listed_row.get("sites") or {}).items() if ok)
288
290
  writer.writerow({**r, **{k: listed_row.get(k, "") for k in extra}, "works_with": works})
289
291
  stats = stats_for(rows, now)
@@ -0,0 +1,44 @@
1
+ """Which proxy is "fastest": how quickly a page comes through, not just the first answer (#186).
2
+
3
+ A quick answer to one tiny request says little about loading a page – in a test on the live list, the 25 HTTPS
4
+ proxies quickest to answer loaded 4 of 50 real pages, the first 25 ranked by answer plus the download speed of the
5
+ hourly speed step 26 of 50. Everything that orders proxies of the live list uses this: the published lists, the
6
+ pages, --pick, live_proxies, the rotating pool.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import statistics
12
+ from typing import Iterable, List, Optional
13
+
14
+ PAGE_KB = 100 # roughly one page – the download the speed step measures
15
+ UNMEASURED_KBPS = 20 # tried in the speed step, no download got through: counted as slow (it passed the checks)
16
+
17
+
18
+ def page_ms(latency: float, speed_kbps: Optional[int], https: Optional[bool], typical_kbps: Optional[float]) -> float:
19
+ """Roughly how long a page takes through a proxy: the first answer plus the download.
20
+
21
+ typical_kbps is the median measured speed of the set, None when it has no speeds at all (an own scan) – then
22
+ latency decides alone. The speed step only tries HTTPS-capable proxies: one of those without a speed didn't
23
+ get the download through, the others were never tried and count as typical."""
24
+ if not typical_kbps:
25
+ return latency
26
+ kbps = speed_kbps or (UNMEASURED_KBPS if https else typical_kbps)
27
+ return latency + PAGE_KB * 1000 / kbps
28
+
29
+
30
+ def typical_speed(speeds: Iterable[Optional[int]]) -> Optional[float]:
31
+ measured = [s for s in speeds if s]
32
+ return statistics.median(measured) if measured else None
33
+
34
+
35
+ def best_first(rows: Iterable[dict]) -> List[dict]:
36
+ """Rows of a run (dicts as in proxies.json), the fastest to load a page first."""
37
+ rows = list(rows)
38
+ typical = typical_speed(_speed(r) for r in rows)
39
+ return sorted(rows, key=lambda r: page_ms(r["latency"], _speed(r), r.get("https") is True, typical))
40
+
41
+
42
+ def _speed(row: dict) -> Optional[int]:
43
+ value = row.get("speed_kbps")
44
+ return value if type(value) is int and value > 0 else None
@@ -13,36 +13,26 @@ from __future__ import annotations
13
13
 
14
14
  import random
15
15
  import re
16
- import statistics
17
16
  import time
18
17
  from dataclasses import dataclass
19
18
  from typing import Callable, Dict, List, Optional, Set, Tuple
20
19
 
20
+ from .. import ranking
21
21
  from ..checker import CheckResult
22
22
 
23
23
  DISABLE_AFTER = 3 # this many failures in a row -> out of the rotation
24
24
  STRATEGIES = ("weighted", "random", "round-robin", "fastest")
25
25
  SESSION_SECONDS = 600 # how long a session (session-…) keeps its proxy when --sticky isn't set
26
26
  _TOKEN_RE = re.compile(r"(country|type|session)[-_]([A-Za-z0-9]+)")
27
- PAGE_KB = 100 # roughly one page – the download the speed step measures
28
- UNMEASURED_KBPS = 20 # tried in the speed step, no download got through: counted as slow (it passed the checks)
29
27
 
30
28
 
31
29
  def page_ms(result, typical_kbps: Optional[float]) -> float:
32
- """Roughly how long a page takes through this proxy: the first answer plus the download.
33
-
34
- typical_kbps is the median measured speed of the set, None when it has no speeds at all (an own scan) – then
35
- latency decides alone. The speed step only tries HTTPS-capable proxies: one of those without a speed didn't
36
- get the download through, the others were never tried and count as typical."""
37
- if not typical_kbps:
38
- return result.latency
39
- kbps = result.speed_kbps or (UNMEASURED_KBPS if result.https else typical_kbps)
40
- return result.latency + PAGE_KB * 1000 / kbps
30
+ """ranking.page_ms for a CheckResult (or anything with latency, speed_kbps and https)."""
31
+ return ranking.page_ms(result.latency, result.speed_kbps, result.https, typical_kbps)
41
32
 
42
33
 
43
34
  def typical_speed(results) -> Optional[float]:
44
- speeds = [r.speed_kbps for r in results if r.speed_kbps]
45
- return statistics.median(speeds) if speeds else None
35
+ return ranking.typical_speed(r.speed_kbps for r in results)
46
36
 
47
37
 
48
38
  @dataclass(frozen=True)
@@ -597,7 +597,7 @@
597
597
  <section class="block wrap" id="list" aria-labelledby="list-title">
598
598
  <div class="head-row">
599
599
  <h2 class="h2" id="list-title">Pick your proxies</h2>
600
- <p class="intro-text">Filter, sort, then copy or download exactly the ones you need. Click any address to copy it.</p>
600
+ <p class="intro-text">Best first: the ones that answer quickly and download fast. Filter, sort, then copy or download exactly the ones you need. Click any address to copy it.</p>
601
601
  </div>
602
602
  <div class="filters" role="group" aria-label="Filters">
603
603
  <button class="chip" type="button" data-type="http" aria-pressed="true"><span class="dot http"></span>HTTP</button>
@@ -623,7 +623,7 @@
623
623
  <th scope="col" data-key="ptype"><button class="sort" type="button" aria-label="Type"><span class="name">Type</span></button></th>
624
624
  <th scope="col" data-key="proxy"><button class="sort" type="button">Proxy</button></th>
625
625
  <th scope="col" data-key="country"><button class="sort" type="button">Country</button></th>
626
- <th scope="col" data-key="latency" aria-sort="ascending"><button class="sort" type="button">Latency</button></th>
626
+ <th scope="col" data-key="latency"><button class="sort" type="button">Latency</button></th>
627
627
  <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>
628
628
  <th scope="col" data-key="https" class="hide-sm"><button class="sort" type="button">HTTPS</button></th>
629
629
  <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>
@@ -637,6 +637,7 @@
637
637
  <div class="list-foot">
638
638
  <span class="count" id="count" aria-live="polite"></span>
639
639
  <div class="actions">
640
+ <button class="btn small" id="best" type="button" hidden title="The ones that answer quickly and download fast">Best first</button>
640
641
  <button class="btn small" id="more" type="button" hidden>Show more</button>
641
642
  <button class="btn small" id="share" type="button" title="The filters are in the address, so the link opens this exact view">Share view</button>
642
643
  <button class="btn small" id="copy-all" type="button">Copy filtered</button>
@@ -871,7 +872,7 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
871
872
  const speed = (k) => !k ? '<span class="quiet">–</span>'
872
873
  : `<span class="${k >= 500 ? "fast" : k >= 100 ? "mid" : "slow"}">${k >= 1024 ? (k / 1024).toFixed(1) + " MB/s" : fmt(k) + " KB/s"}</span>`;
873
874
  const latency = (ms) => `<span class="${ms < 1000 ? "fast" : ms < 3000 ? "mid" : "slow"}">${fmt(ms)} ms</span>`;
874
- const state = { rows: [], shown: PAGE, sort: "latency", dir: 1, types: new Set(["http", "socks4", "socks5"]),
875
+ const state = { rows: [], shown: PAGE, sort: "rank", dir: 1, types: new Set(["http", "socks4", "socks5"]),
875
876
  https: false, elite: false, noDc: false, noBl: false, stable: false, country: "", site: "", maxLat: 8000, q: "" };
876
877
 
877
878
  // Theme
@@ -1037,6 +1038,7 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
1037
1038
  || '<tr><td colspan="9" class="empty">No proxy matches these filters. Try a higher latency limit or fewer filters.</td></tr>';
1038
1039
  $("#count").textContent = rows.length ? `Showing ${fmt(Math.min(state.shown, rows.length))} of ${fmt(rows.length)} matching proxies` : "";
1039
1040
  $("#more").hidden = rows.length <= state.shown;
1041
+ $("#best").hidden = state.sort === "rank";
1040
1042
  saveView();
1041
1043
  $$("th[data-key]").forEach(th => th.dataset.key === state.sort ? th.setAttribute("aria-sort", state.dir > 0 ? "ascending" : "descending") : th.removeAttribute("aria-sort"));
1042
1044
  }
@@ -1052,7 +1054,7 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
1052
1054
  if (state.site) q.set("site", state.site);
1053
1055
  if (state.maxLat < 8000) q.set("latency", state.maxLat);
1054
1056
  if (state.q.trim()) q.set("q", state.q.trim());
1055
- if (state.sort !== "latency" || state.dir !== 1) q.set("sort", (state.dir < 0 ? "-" : "") + state.sort);
1057
+ if (state.sort !== "rank") q.set("sort", (state.dir < 0 ? "-" : "") + state.sort);
1056
1058
  return location.pathname + (q.toString() ? `?${q}` : "") + location.hash;
1057
1059
  }
1058
1060
  // debounced: browsers limit replaceState (Safari throws after ~100 calls in 30 s, e.g. while dragging the slider)
@@ -1117,6 +1119,7 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
1117
1119
  $("#f-lat").oninput = (e) => { state.maxLat = +e.target.value; $("#f-lat-v").textContent = state.maxLat >= 8000 ? "any" : `${fmt(state.maxLat)} ms`; refresh(); };
1118
1120
  $("#f-q").oninput = (e) => { state.q = e.target.value; refresh(); };
1119
1121
  $("#more").onclick = () => { state.shown += PAGE * 4; render(); };
1122
+ $("#best").onclick = () => { state.sort = "rank"; state.dir = 1; morph(render); };
1120
1123
  $$("th[data-key]").forEach(th => {
1121
1124
  th.querySelector(".sort").onclick = () => { const k = th.dataset.key; state.dir = state.sort === k ? -state.dir : 1; state.sort = k; morph(render); };
1122
1125
  });
@@ -1353,6 +1356,7 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
1353
1356
 
1354
1357
  let started = false;
1355
1358
  function apply(rows, st, history) {
1359
+ rows.forEach((r, i) => { r.rank = i; }); // proxies.json comes best first: first answer plus download speed
1356
1360
  state.rows = rows;
1357
1361
  shownRun = st.updated;
1358
1362
  RUN_HOURS = st.run_hours || 6; // lists from before run_hours existed ran every 6 hours
@@ -56,3 +56,53 @@ def test_proxies_the_speed_step_never_tried_count_as_typical_not_slow():
56
56
  pool = ProxyPool([plain_only, measured], strategy="fastest")
57
57
  assert pool.pick(set()).result is plain_only
58
58
  assert pool.pick(set(), tls=True).result is measured
59
+
60
+
61
+ def published(n, latency, speed=None, https=True, ptype="http"):
62
+ row = {"proxy": f"10.0.0.{n}:80", "ptype": ptype, "url": f"{ptype}://10.0.0.{n}:80", "latency": latency,
63
+ "https": https, "country": "DE", "exit_ip": f"10.0.0.{n}", "anonymity": "elite"}
64
+ return {**row, "speed_kbps": speed} if speed else row
65
+
66
+
67
+ def test_the_published_lists_start_with_the_ones_that_load_pages_fastest(tmp_path):
68
+ import json
69
+
70
+ from proxyscraper import publish
71
+
72
+ (tmp_path / "proxies.json").write_text(json.dumps([published(1, 100), published(2, 400, speed=500)]))
73
+ assert [r["proxy"] for r in publish.load_rows(tmp_path)] == ["10.0.0.2:80", "10.0.0.1:80"]
74
+
75
+
76
+ def test_landing_pages_list_the_best_first(tmp_path):
77
+ from datetime import datetime, timezone
78
+
79
+ from proxyscraper import pages
80
+
81
+ pages.write_pages([published(1, 100), published(2, 400, speed=500)], tmp_path, datetime.now(timezone.utc))
82
+ assert (tmp_path / "country" / "de" / "proxies.txt").read_text().splitlines()[0] == "http://10.0.0.2:80"
83
+
84
+
85
+ def test_the_csv_comes_in_the_same_order_as_the_json(tmp_path):
86
+ import csv
87
+ import json
88
+
89
+ from proxyscraper import publish
90
+ from proxyscraper.output import ResultWriter
91
+
92
+ results = [CheckResult(f"http 1.1.1.{i}:80", "http", f"1.1.1.{i}:80", 100 * (i + 1), "9.9.9.9", True, "elite", "DE")
93
+ for i in range(3)]
94
+ ResultWriter(run_dir=tmp_path / "run").finalize(results)
95
+ rows = json.loads((tmp_path / "run" / "proxies.json").read_text())
96
+ rows[2]["speed_kbps"] = 900 # the slowest to answer downloads fastest
97
+ (tmp_path / "run" / "proxies.json").write_text(json.dumps(rows))
98
+ publish.publish(tmp_path / "run", tmp_path / "out", minimum=1)
99
+ order = [r["proxy"] for r in json.loads((tmp_path / "out" / "proxies.json").read_text())]
100
+ with (tmp_path / "out" / "proxies.csv").open(newline="") as fh:
101
+ assert [r["proxy"] for r in csv.DictReader(fh)] == order and order[0] == "1.1.1.2:80"
102
+
103
+
104
+ def test_the_mcp_tools_list_best_first_too():
105
+ from proxyscraper.agent import select
106
+
107
+ rows = [published(1, 100), published(2, 400, speed=500)]
108
+ assert [r["proxy"] for r in select(rows)] == ["10.0.0.2:80", "10.0.0.1:80"]