proxy-scraper-cli 1.18.0__tar.gz → 1.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/PKG-INFO +16 -1
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/README.md +15 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/PKG-INFO +16 -1
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/SOURCES.txt +1 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/__init__.py +1 -1
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/agent.py +37 -13
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/api.py +80 -6
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/pages.py +5 -3
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/publish.py +8 -6
- proxy_scraper_cli-1.20.0/proxyscraper/ranking.py +44 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/server/pool.py +4 -14
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/site/index.html +8 -4
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_rotator.py +92 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_speed_ranking.py +50 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/LICENSE +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/__main__.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/action.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/app.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/asndb.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/blocklist.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/charts.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/checker.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/cli.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/compat.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/completion.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/exporters.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/fetchcache.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/geo.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/geodb.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/handshake.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/history.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/judges.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/mcp_entry.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/mcp_server.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/netio.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/options.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/output.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/parsing.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/paths.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/pipeline.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/preferences.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/proxypages.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/report.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/runsteps.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/server/__init__.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/server/core.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/server/http.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/server/socks.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/server/status.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/server/upstream.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/site/apple-touch-icon.png +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/site/logo.png +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/site/og.png +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/sites.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/sources.json +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/sources.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/speed.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/targets.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/ui/__init__.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/ui/dashboard.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/ui/keys.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/ui/report.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/ui/serve.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/ui/widgets.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/ui/wizard.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/pyproject.toml +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/setup.cfg +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_action.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_agent.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_api.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_app.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_asndb.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_auth.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_blocklist.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_charts.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_checker.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_compat.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_completion.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_discovery_growth.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_exporters.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_fetchcache.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_geodb.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_judges.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_learning.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_live_api.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_mcp_server.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_mine_sources.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_options.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_output_stdout.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_packaging.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_pages.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_parsing.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_parsing_fuzz.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_paths.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_pick.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_pipeline.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_proxy_pages.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_proxy_sources.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_publish.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_report.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_robustness.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_serve_password.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_serve_refill.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_serve_v2.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_server.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_server_closed_writer.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_server_fixes.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_sites.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_sources_json.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_speed.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_tamper.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_targets.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_ui.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_uptime.py +0 -0
- {proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/tests/test_wizard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.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
|
|
@@ -456,6 +456,21 @@ with ProxyRotator(country="DE") as rotator:
|
|
|
456
456
|
|
|
457
457
|
Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
|
|
458
458
|
|
|
459
|
+
Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
import requests
|
|
463
|
+
from proxyscraper import ProxyRotator
|
|
464
|
+
|
|
465
|
+
with ProxyRotator(country="DE") as rotator:
|
|
466
|
+
proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
|
|
467
|
+
requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
|
|
468
|
+
# httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
|
|
469
|
+
# Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
|
|
473
|
+
|
|
459
474
|
<a id="proxy-server"></a>
|
|
460
475
|
|
|
461
476
|
## Rotating proxy server
|
|
@@ -415,6 +415,21 @@ with ProxyRotator(country="DE") as rotator:
|
|
|
415
415
|
|
|
416
416
|
Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
|
|
417
417
|
|
|
418
|
+
Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
import requests
|
|
422
|
+
from proxyscraper import ProxyRotator
|
|
423
|
+
|
|
424
|
+
with ProxyRotator(country="DE") as rotator:
|
|
425
|
+
proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
|
|
426
|
+
requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
|
|
427
|
+
# httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
|
|
428
|
+
# Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
|
|
432
|
+
|
|
418
433
|
<a id="proxy-server"></a>
|
|
419
434
|
|
|
420
435
|
## Rotating proxy server
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.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
|
|
@@ -456,6 +456,21 @@ with ProxyRotator(country="DE") as rotator:
|
|
|
456
456
|
|
|
457
457
|
Usually a few seconds; when many proxies in a row are down it can take a minute (`timeout=` is per attempt). There's `aget()` with `async with` for asyncio code.
|
|
458
458
|
|
|
459
|
+
Already using requests, httpx, Playwright or Scrapy? `proxy_url()` gives you one local proxy address that rotates behind the scenes – every connection goes out through another proxy from the list, best first, dead ones skipped:
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
import requests
|
|
463
|
+
from proxyscraper import ProxyRotator
|
|
464
|
+
|
|
465
|
+
with ProxyRotator(country="DE") as rotator:
|
|
466
|
+
proxy = rotator.proxy_url() # http://country-de:…@127.0.0.1:…
|
|
467
|
+
requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy})
|
|
468
|
+
# httpx.Client(proxy=proxy) · scrapy: meta={"proxy": proxy} · curl -x "$proxy"
|
|
469
|
+
# Playwright wants the login apart: chromium.launch(proxy=rotator.playwright_proxy())
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
It runs on 127.0.0.1 with a random password, takes over each new hourly list by itself and stops with the `with` block.
|
|
473
|
+
|
|
459
474
|
<a id="proxy-server"></a>
|
|
460
475
|
|
|
461
476
|
## Rotating proxy server
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""proxy-scraper: collects public HTTP/SOCKS4/SOCKS5 proxies from hundreds of sources, checks them
|
|
2
2
|
in parallel with its own protocol handshakes and learns which sources deliver good proxies."""
|
|
3
3
|
|
|
4
|
-
__version__ = "1.
|
|
4
|
+
__version__ = "1.20.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,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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,
|
|
@@ -457,6 +458,22 @@ def _to_text(body: bytes, headers, content_type: str, raw_html: bool) -> str:
|
|
|
457
458
|
return text if raw_html or "html" not in content_type.lower() else html_to_text(text)
|
|
458
459
|
|
|
459
460
|
|
|
461
|
+
def _require_match(live: LiveList, protocol: str, countries: List[str], tls: bool) -> None:
|
|
462
|
+
if not any(True for _ in matching(live.rows, protocol, countries, https_only=tls, run_hours=live.run_hours)):
|
|
463
|
+
what = " ".join(x for x in (countries[0] if countries else "", "" if protocol == "any" else protocol,
|
|
464
|
+
"HTTPS-capable" if tls else "") if x)
|
|
465
|
+
raise AgentError(f"No {what} proxy in the live list right now."
|
|
466
|
+
+ (shortage_note(live.rows, 1, 0, countries) or ""))
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _wishes(protocol: str, countries: List[str]) -> List[str]:
|
|
470
|
+
"""What the rotating server reads from the proxy login: country-de, type-socks5."""
|
|
471
|
+
wishes = [f"country-{countries[0].lower()}"] if countries else []
|
|
472
|
+
if protocol != "any":
|
|
473
|
+
wishes.append(f"type-{protocol}")
|
|
474
|
+
return wishes
|
|
475
|
+
|
|
476
|
+
|
|
460
477
|
class PageFetcher:
|
|
461
478
|
"""Loads pages through the rotating server of this package, running on 127.0.0.1 with a random password:
|
|
462
479
|
the same failover as `--serve`, and HTTPS only through proxies that passed the verified-TLS test.
|
|
@@ -502,6 +519,19 @@ class PageFetcher:
|
|
|
502
519
|
self._list_id = list_id
|
|
503
520
|
return live
|
|
504
521
|
|
|
522
|
+
async def local_proxy(self, protocol: str = "any", country: str = "") -> str:
|
|
523
|
+
"""The URL of the rotating server behind this fetcher, with the login, for any HTTP client:
|
|
524
|
+
http://country-de:<password>@127.0.0.1:<port>. Every connection through it gets a proxy of its own."""
|
|
525
|
+
protocol = _check_protocol(protocol)
|
|
526
|
+
countries = normalize_countries([country] if country else [])
|
|
527
|
+
_require_match(await self._ready(), protocol, countries, tls=False)
|
|
528
|
+
user = "-".join(_wishes(protocol, countries)) or "any"
|
|
529
|
+
return f"http://{user}:{self._password}@127.0.0.1:{self.server.port}"
|
|
530
|
+
|
|
531
|
+
async def refresh(self) -> None:
|
|
532
|
+
"""Take over a new run's list, if there is one (fetch does that by itself)."""
|
|
533
|
+
await self._ready()
|
|
534
|
+
|
|
505
535
|
async def fetch(self, url: str, protocol: str = "any", country: str = "", max_chars: int = 20000,
|
|
506
536
|
raw_html: bool = False, progress: Optional[Progress] = None, tick: float = 5.0,
|
|
507
537
|
with_body: bool = False) -> dict:
|
|
@@ -510,15 +540,9 @@ class PageFetcher:
|
|
|
510
540
|
countries = normalize_countries([country] if country else [])
|
|
511
541
|
live = await self._ready()
|
|
512
542
|
tls = urlsplit(url).scheme == "https"
|
|
513
|
-
|
|
514
|
-
what = " ".join(x for x in (countries[0] if countries else "", "" if protocol == "any" else protocol,
|
|
515
|
-
"HTTPS-capable" if tls else "") if x)
|
|
516
|
-
raise AgentError(f"No {what} proxy in the live list right now."
|
|
517
|
-
+ (shortage_note(live.rows, 1, 0, countries) or ""))
|
|
543
|
+
_require_match(live, protocol, countries, tls)
|
|
518
544
|
session = secrets.token_hex(6)
|
|
519
|
-
wishes =
|
|
520
|
-
if protocol != "any":
|
|
521
|
-
wishes.append(f"type-{protocol}")
|
|
545
|
+
wishes = _wishes(protocol, countries)
|
|
522
546
|
pool, port = self.server.pool, self.server.port
|
|
523
547
|
started = time.monotonic()
|
|
524
548
|
result, faults, suspects = None, [], []
|
|
@@ -27,12 +27,14 @@ import asyncio
|
|
|
27
27
|
import contextlib
|
|
28
28
|
import email.message
|
|
29
29
|
import io
|
|
30
|
+
import logging
|
|
30
31
|
import os
|
|
31
32
|
import re
|
|
32
33
|
import tempfile
|
|
33
34
|
import threading
|
|
34
35
|
from dataclasses import dataclass, field
|
|
35
36
|
from typing import Dict, Iterable, List, Optional, Tuple
|
|
37
|
+
from urllib.parse import urlsplit
|
|
36
38
|
|
|
37
39
|
from rich.console import Console
|
|
38
40
|
|
|
@@ -42,6 +44,8 @@ from .parsing import PROXY_TYPES
|
|
|
42
44
|
from .targets import parse_target
|
|
43
45
|
from .ui import widgets
|
|
44
46
|
|
|
47
|
+
log = logging.getLogger(__name__)
|
|
48
|
+
|
|
45
49
|
__all__ = [
|
|
46
50
|
"CheckResult",
|
|
47
51
|
"LiveProxy",
|
|
@@ -216,7 +220,7 @@ async def live_proxies_async(*, types: Iterable[str] = PROXY_TYPES, countries: I
|
|
|
216
220
|
limit at most this many (0 = all)
|
|
217
221
|
"""
|
|
218
222
|
from .agent import AgentError, LiveSource
|
|
219
|
-
from .
|
|
223
|
+
from .ranking import page_ms, typical_speed
|
|
220
224
|
from .sites import SITE
|
|
221
225
|
|
|
222
226
|
types = _types(types)
|
|
@@ -242,8 +246,8 @@ async def live_proxies_async(*, types: Iterable[str] = PROXY_TYPES, countries: I
|
|
|
242
246
|
found = [_live_proxy(r, data.run_hours) for r in data.rows
|
|
243
247
|
if r.get("ptype") in types and isinstance(r.get("proxy"), str)]
|
|
244
248
|
# 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))
|
|
249
|
+
typical = typical_speed(p.speed_kbps for p in found)
|
|
250
|
+
found.sort(key=lambda p: page_ms(p.latency, p.speed_kbps, p.https, typical))
|
|
247
251
|
found = [p for p in found if opts.filters.accepts(p) and (not min_uptime or (p.uptime_7d or 0) >= min_uptime)
|
|
248
252
|
and all(p.sites.get(s) for s in works_on) and (not min_speed or (p.speed_kbps or 0) >= min_speed)]
|
|
249
253
|
return found[:limit] if limit else found
|
|
@@ -309,6 +313,15 @@ class ProxyResponse:
|
|
|
309
313
|
raise ConnectionError(f"the body from {self.final_url} was cut off at 2 MB")
|
|
310
314
|
|
|
311
315
|
|
|
316
|
+
REFRESH_SECONDS = 300 # proxy_url(): how often the pool behind it looks for a new run's list
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _playwright(url: str) -> Dict[str, str]:
|
|
320
|
+
parts = urlsplit(url)
|
|
321
|
+
return {"server": f"{parts.scheme}://{parts.hostname}:{parts.port}", "username": parts.username or "",
|
|
322
|
+
"password": parts.password or ""}
|
|
323
|
+
|
|
324
|
+
|
|
312
325
|
class ProxyRotator:
|
|
313
326
|
"""Loads URLs through proxies from the hourly list and switches to the next one when a proxy fails –
|
|
314
327
|
the loop every scraper writes by hand. HTTPS only through proxies with verified TLS.
|
|
@@ -330,6 +343,7 @@ class ProxyRotator:
|
|
|
330
343
|
self._loop: Optional[asyncio.AbstractEventLoop] = None # the sync API's own loop, in a thread
|
|
331
344
|
self._thread: Optional[threading.Thread] = None
|
|
332
345
|
self._start_lock = threading.Lock()
|
|
346
|
+
self._refresher: Optional[asyncio.Task] = None
|
|
333
347
|
|
|
334
348
|
def _use(self, mode: str) -> None:
|
|
335
349
|
if self._mode is None:
|
|
@@ -367,7 +381,43 @@ class ProxyRotator:
|
|
|
367
381
|
proxy_country=page["proxy_country"], elapsed_ms=page["elapsed_ms"],
|
|
368
382
|
truncated=page["body_truncated"])
|
|
369
383
|
|
|
384
|
+
async def aproxy_url(self) -> str:
|
|
385
|
+
self._use("async")
|
|
386
|
+
return await self._proxy_url()
|
|
387
|
+
|
|
388
|
+
async def _proxy_url(self) -> str:
|
|
389
|
+
from .agent import AgentError
|
|
390
|
+
|
|
391
|
+
if self._fetcher is None:
|
|
392
|
+
self._fetcher = self._make_fetcher()
|
|
393
|
+
try:
|
|
394
|
+
url = await self._fetcher.local_proxy(self.protocol, self.country)
|
|
395
|
+
except AgentError as e:
|
|
396
|
+
raise ConnectionError(str(e)) from None
|
|
397
|
+
if self._refresher is None: # nobody calls get() on this path, so the pool refreshes by itself
|
|
398
|
+
self._refresher = asyncio.ensure_future(self._keep_fresh())
|
|
399
|
+
return url
|
|
400
|
+
|
|
401
|
+
async def aplaywright_proxy(self) -> Dict[str, str]:
|
|
402
|
+
self._use("async")
|
|
403
|
+
return _playwright(await self._proxy_url())
|
|
404
|
+
|
|
405
|
+
async def _keep_fresh(self) -> None:
|
|
406
|
+
from .agent import AgentError
|
|
407
|
+
|
|
408
|
+
while True:
|
|
409
|
+
await asyncio.sleep(REFRESH_SECONDS)
|
|
410
|
+
try:
|
|
411
|
+
await self._fetcher.refresh()
|
|
412
|
+
except AgentError:
|
|
413
|
+
pass # GitHub not reachable right now: the pool keeps what it has, next try later
|
|
414
|
+
except Exception as e: # a broken list must not end the refreshing for good
|
|
415
|
+
log.warning("proxy-scraper: could not refresh the proxy list: %s", e)
|
|
416
|
+
|
|
370
417
|
async def aclose(self) -> None:
|
|
418
|
+
if self._refresher is not None:
|
|
419
|
+
self._refresher.cancel()
|
|
420
|
+
self._refresher = None
|
|
371
421
|
if self._fetcher is not None:
|
|
372
422
|
await self._fetcher.close()
|
|
373
423
|
self._fetcher = None
|
|
@@ -380,13 +430,37 @@ class ProxyRotator:
|
|
|
380
430
|
|
|
381
431
|
# sync: one event loop in a background thread keeps the proxy server alive between calls
|
|
382
432
|
def get(self, url: str) -> ProxyResponse:
|
|
383
|
-
|
|
384
|
-
|
|
433
|
+
return self._run_sync(self._aget(url))
|
|
434
|
+
|
|
435
|
+
def _run_sync(self, coro):
|
|
436
|
+
with self._start_lock: # several threads calling at once still share one loop
|
|
437
|
+
try:
|
|
438
|
+
self._use("sync")
|
|
439
|
+
except RuntimeError:
|
|
440
|
+
coro.close()
|
|
441
|
+
raise
|
|
385
442
|
if self._loop is None:
|
|
386
443
|
self._loop = asyncio.new_event_loop()
|
|
387
444
|
self._thread = threading.Thread(target=self._loop.run_forever, name="proxy-rotator", daemon=True)
|
|
388
445
|
self._thread.start()
|
|
389
|
-
return asyncio.run_coroutine_threadsafe(
|
|
446
|
+
return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
|
|
447
|
+
|
|
448
|
+
def proxy_url(self) -> str:
|
|
449
|
+
"""A local proxy URL for any library – requests, httpx, aiohttp, Playwright, Scrapy, curl:
|
|
450
|
+
|
|
451
|
+
with ProxyRotator(country="DE") as rotator:
|
|
452
|
+
proxy = rotator.proxy_url()
|
|
453
|
+
requests.get(url, proxies={"http": proxy, "https": proxy})
|
|
454
|
+
|
|
455
|
+
Every connection through it gets a proxy from the live list (best first, with failover), HTTPS only
|
|
456
|
+
through verified-TLS ones. It lives as long as the rotator and takes over each new run's list."""
|
|
457
|
+
return self._run_sync(self._proxy_url())
|
|
458
|
+
|
|
459
|
+
def playwright_proxy(self) -> Dict[str, str]:
|
|
460
|
+
"""proxy_url() the way Playwright wants it – it ignores a login inside the server address:
|
|
461
|
+
|
|
462
|
+
browser = p.chromium.launch(proxy=rotator.playwright_proxy())"""
|
|
463
|
+
return _playwright(self.proxy_url())
|
|
390
464
|
|
|
391
465
|
def close(self) -> None:
|
|
392
466
|
with self._start_lock:
|
|
@@ -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
|
|
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 =
|
|
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
|
|
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 –
|
|
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
|
-
|
|
284
|
-
for
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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"
|
|
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: "
|
|
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 !== "
|
|
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
|
|
@@ -127,3 +127,95 @@ def test_mixing_sync_and_async_is_refused(monkeypatch):
|
|
|
127
127
|
with pytest.raises(RuntimeError, match="async"):
|
|
128
128
|
asyncio.run(rotator.aget("http://x/"))
|
|
129
129
|
rotator.close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_through(proxy_url: str, url: str):
|
|
133
|
+
"""A plain GET through an HTTP proxy URL with a login, the way requests or httpx send it."""
|
|
134
|
+
import base64
|
|
135
|
+
import http.client
|
|
136
|
+
from urllib.parse import urlsplit
|
|
137
|
+
|
|
138
|
+
p = urlsplit(proxy_url)
|
|
139
|
+
login = base64.b64encode(f"{p.username}:{p.password}".encode()).decode()
|
|
140
|
+
conn = http.client.HTTPConnection(p.hostname, p.port, timeout=10)
|
|
141
|
+
conn.request("GET", url, headers={"Proxy-Authorization": f"Basic {login}"})
|
|
142
|
+
response = conn.getresponse()
|
|
143
|
+
return response.status, response.read()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_proxy_url_is_a_rotating_proxy_for_any_library():
|
|
147
|
+
loop = asyncio.new_event_loop()
|
|
148
|
+
target_srv, proxy_srv, port, rows = loop.run_until_complete(local_setup())
|
|
149
|
+
import threading
|
|
150
|
+
thread = threading.Thread(target=loop.run_forever, daemon=True)
|
|
151
|
+
thread.start()
|
|
152
|
+
try:
|
|
153
|
+
with ProxyRotator(_source=source_for(rows), _allow_private=True, timeout=5) as rotator:
|
|
154
|
+
url = rotator.proxy_url()
|
|
155
|
+
assert url.startswith("http://") and "@127.0.0.1:" in url
|
|
156
|
+
# the dead proxy is tried and skipped inside, the client just gets its page
|
|
157
|
+
assert get_through(url, f"http://127.0.0.1:{port}/ok") == (200, b"ok")
|
|
158
|
+
assert rotator.proxy_url() == url # the same server for the rotator's lifetime
|
|
159
|
+
finally:
|
|
160
|
+
loop.call_soon_threadsafe(target_srv.close)
|
|
161
|
+
loop.call_soon_threadsafe(proxy_srv.close)
|
|
162
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_proxy_url_carries_country_and_protocol_and_refuses_what_isnt_there():
|
|
166
|
+
async def go(**wishes):
|
|
167
|
+
rows = [row(1, country="DE", ptype="socks5")]
|
|
168
|
+
async with ProxyRotator(_source=source_for(rows), _allow_private=True, **wishes) as rotator:
|
|
169
|
+
return await rotator.aproxy_url()
|
|
170
|
+
assert "//country-de-type-socks5:" in asyncio.run(go(country="de", protocol="socks5"))
|
|
171
|
+
|
|
172
|
+
async def for_playwright():
|
|
173
|
+
async with ProxyRotator(_source=source_for([row(1)]), _allow_private=True) as rotator:
|
|
174
|
+
return await rotator.aproxy_url(), await rotator.aplaywright_proxy()
|
|
175
|
+
url, settings = asyncio.run(for_playwright())
|
|
176
|
+
# Playwright ignores a login inside "server": it wants the parts on their own
|
|
177
|
+
assert settings["server"] == "http://" + url.rsplit("@", 1)[1] and "@" not in settings["server"]
|
|
178
|
+
assert url == f"http://{settings['username']}:{settings['password']}@{settings['server'][7:]}"
|
|
179
|
+
with pytest.raises(ConnectionError, match="JP"):
|
|
180
|
+
asyncio.run(go(country="JP"))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_proxy_url_keeps_the_pool_fresh(monkeypatch):
|
|
184
|
+
from proxyscraper import api
|
|
185
|
+
|
|
186
|
+
monkeypatch.setattr(api, "REFRESH_SECONDS", 0.05)
|
|
187
|
+
runs = iter([agent.LiveList(rows=[row(1)], stats={**STATS, "updated": "run 1"}, loaded_at=0.0),
|
|
188
|
+
agent.LiveList(rows=[row(2)], stats={**STATS, "updated": "run 2"}, loaded_at=0.0)])
|
|
189
|
+
|
|
190
|
+
class Source: # the first call sees run 1, every later one run 2
|
|
191
|
+
current = None
|
|
192
|
+
|
|
193
|
+
async def get(self):
|
|
194
|
+
self.current = next(runs, self.current)
|
|
195
|
+
return self.current
|
|
196
|
+
|
|
197
|
+
async def go():
|
|
198
|
+
async with ProxyRotator(_source=Source(), _allow_private=True) as rotator:
|
|
199
|
+
await rotator.aproxy_url()
|
|
200
|
+
await asyncio.sleep(0.3)
|
|
201
|
+
return {e.result.proxy for e in rotator._fetcher.server.pool.entries}
|
|
202
|
+
assert asyncio.run(go()) == {row(2)["proxy"]}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_the_refresh_survives_a_surprise(monkeypatch):
|
|
206
|
+
from proxyscraper import api
|
|
207
|
+
|
|
208
|
+
monkeypatch.setattr(api, "REFRESH_SECONDS", 0.02)
|
|
209
|
+
calls = []
|
|
210
|
+
|
|
211
|
+
async def go():
|
|
212
|
+
async with ProxyRotator(_source=source_for([row(1)]), _allow_private=True) as rotator:
|
|
213
|
+
await rotator.aproxy_url()
|
|
214
|
+
|
|
215
|
+
async def broken():
|
|
216
|
+
calls.append(1)
|
|
217
|
+
raise KeyError("stats")
|
|
218
|
+
rotator._fetcher.refresh = broken
|
|
219
|
+
await asyncio.sleep(0.2)
|
|
220
|
+
asyncio.run(go())
|
|
221
|
+
assert len(calls) > 2 # kept trying after the first failure
|
|
@@ -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"]
|
|
File without changes
|
|
File without changes
|
{proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxy_scraper_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/site/apple-touch-icon.png
RENAMED
|
File without changes
|
{proxy_scraper_cli-1.18.0 → proxy_scraper_cli-1.20.0}/proxyscraper/site/googleaac1161b7853c5b5.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|