proxy-scraper-cli 1.8.1__tar.gz → 1.9.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.8.1 → proxy_scraper_cli-1.9.0}/PKG-INFO +79 -1
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/README.md +78 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/PKG-INFO +79 -1
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/SOURCES.txt +4 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/__init__.py +1 -1
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/pages.py +7 -4
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/parsing.py +7 -7
- proxy_scraper_cli-1.9.0/proxyscraper/proxypages.py +241 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/publish.py +15 -1
- proxy_scraper_cli-1.9.0/proxyscraper/report.py +264 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/index.html +6 -2
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.py +3 -1
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_mine_sources.py +8 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_parsing.py +7 -5
- proxy_scraper_cli-1.9.0/tests/test_proxy_pages.py +93 -0
- proxy_scraper_cli-1.9.0/tests/test_report.py +113 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_uptime.py +12 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/LICENSE +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/__main__.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/agent.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/api.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/app.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/asndb.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/blocklist.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/charts.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/checker.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/cli.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/compat.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/completion.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/exporters.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/fetchcache.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/geo.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/geodb.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/handshake.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/history.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/judges.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_entry.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_server.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/netio.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/options.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/output.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/paths.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/pipeline.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/preferences.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/__init__.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/core.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/http.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/pool.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/socks.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/status.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/server/upstream.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/apple-touch-icon.png +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/logo.png +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/site/og.png +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/sites.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.json +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/targets.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/__init__.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/dashboard.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/keys.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/report.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/serve.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/widgets.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/wizard.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/pyproject.toml +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/setup.cfg +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_agent.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_api.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_app.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_asndb.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_auth.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_blocklist.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_charts.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_checker.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_compat.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_completion.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_discovery_growth.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_exporters.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_fetchcache.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_geodb.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_judges.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_learning.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_live_api.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_mcp_server.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_options.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_output_stdout.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_packaging.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_pages.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_paths.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_pipeline.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_proxy_sources.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_publish.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_robustness.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_serve_password.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_serve_refill.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_serve_v2.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_server.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_server_fixes.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_sites.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_tamper.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_targets.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.0}/tests/test_ui.py +0 -0
- {proxy_scraper_cli-1.8.1 → proxy_scraper_cli-1.9.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.9.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
|
|
@@ -578,6 +578,84 @@ for p in proxies:
|
|
|
578
578
|
continue # free proxies come and go – just take the next one
|
|
579
579
|
```
|
|
580
580
|
|
|
581
|
+
**httpx** (`pip install "httpx[socks]"`) – straight from the hourly list, no scan
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
import httpx
|
|
585
|
+
from proxyscraper import live_proxies
|
|
586
|
+
|
|
587
|
+
for p in live_proxies(types=["http", "socks5"], https=True, min_uptime=90, limit=10): # httpx can't do SOCKS4
|
|
588
|
+
try:
|
|
589
|
+
with httpx.Client(proxy=p.url, timeout=10) as client:
|
|
590
|
+
print(p.url, "→", client.get("https://api.ipify.org").text)
|
|
591
|
+
break
|
|
592
|
+
except httpx.HTTPError:
|
|
593
|
+
continue # next one
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**aiohttp** (`pip install aiohttp aiohttp-socks` – aiohttp alone can't do SOCKS)
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
import asyncio
|
|
600
|
+
|
|
601
|
+
import aiohttp
|
|
602
|
+
from aiohttp_socks import ProxyConnector, ProxyError
|
|
603
|
+
from proxyscraper import live_proxies_async
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
async def main():
|
|
607
|
+
for p in await live_proxies_async(https=True, min_uptime=90, limit=10):
|
|
608
|
+
try:
|
|
609
|
+
async with aiohttp.ClientSession(connector=ProxyConnector.from_url(p.url)) as session:
|
|
610
|
+
async with session.get("https://api.ipify.org", timeout=aiohttp.ClientTimeout(total=10)) as r:
|
|
611
|
+
print(p.url, "→", await r.text())
|
|
612
|
+
return
|
|
613
|
+
except (aiohttp.ClientError, ProxyError, asyncio.TimeoutError, OSError):
|
|
614
|
+
continue # free proxies come and go – just take the next one
|
|
615
|
+
|
|
616
|
+
asyncio.run(main())
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Scrapy** – a downloader middleware that sends every request, retries included, through the next proxy. Scrapy only speaks HTTP proxies, `https=True` picks the ones that can tunnel `https://` pages
|
|
620
|
+
|
|
621
|
+
```python
|
|
622
|
+
import itertools
|
|
623
|
+
|
|
624
|
+
import scrapy
|
|
625
|
+
from scrapy.crawler import CrawlerProcess
|
|
626
|
+
from proxyscraper import live_proxies
|
|
627
|
+
|
|
628
|
+
PROXIES = [p.url for p in live_proxies(types="http", https=True, min_uptime=50)]
|
|
629
|
+
if not PROXIES:
|
|
630
|
+
raise SystemExit("No proxy matches right now – loosen the filters (e.g. min_uptime)")
|
|
631
|
+
POOL = itertools.cycle(PROXIES)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class RotatingProxy:
|
|
635
|
+
def process_request(self, request, spider=None): # newer Scrapy leaves out spider
|
|
636
|
+
request.meta["proxy"] = next(POOL)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class IpSpider(scrapy.Spider):
|
|
640
|
+
name = "ip"
|
|
641
|
+
start_urls = [f"https://api.ipify.org/?n={i}" for i in range(3)]
|
|
642
|
+
custom_settings = {
|
|
643
|
+
"DOWNLOADER_MIDDLEWARES": {f"{__name__}.RotatingProxy": 350},
|
|
644
|
+
"RETRY_TIMES": 5, "DOWNLOAD_TIMEOUT": 15,
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
def parse(self, response):
|
|
648
|
+
print(response.meta["proxy"], "→", response.text)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
if __name__ == "__main__":
|
|
652
|
+
process = CrawlerProcess()
|
|
653
|
+
process.crawl(IpSpider)
|
|
654
|
+
process.start()
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
In a Scrapy project, put `RotatingProxy` in `middlewares.py` and add it to `DOWNLOADER_MIDDLEWARES` in `settings.py`. For long crawls, reload the pool now and then – the list changes every hour.
|
|
658
|
+
|
|
581
659
|
**proxychains, Clash / Mihomo** – ready-made configs with `--export`
|
|
582
660
|
|
|
583
661
|
```bash
|
|
@@ -538,6 +538,84 @@ for p in proxies:
|
|
|
538
538
|
continue # free proxies come and go – just take the next one
|
|
539
539
|
```
|
|
540
540
|
|
|
541
|
+
**httpx** (`pip install "httpx[socks]"`) – straight from the hourly list, no scan
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
import httpx
|
|
545
|
+
from proxyscraper import live_proxies
|
|
546
|
+
|
|
547
|
+
for p in live_proxies(types=["http", "socks5"], https=True, min_uptime=90, limit=10): # httpx can't do SOCKS4
|
|
548
|
+
try:
|
|
549
|
+
with httpx.Client(proxy=p.url, timeout=10) as client:
|
|
550
|
+
print(p.url, "→", client.get("https://api.ipify.org").text)
|
|
551
|
+
break
|
|
552
|
+
except httpx.HTTPError:
|
|
553
|
+
continue # next one
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**aiohttp** (`pip install aiohttp aiohttp-socks` – aiohttp alone can't do SOCKS)
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
import asyncio
|
|
560
|
+
|
|
561
|
+
import aiohttp
|
|
562
|
+
from aiohttp_socks import ProxyConnector, ProxyError
|
|
563
|
+
from proxyscraper import live_proxies_async
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
async def main():
|
|
567
|
+
for p in await live_proxies_async(https=True, min_uptime=90, limit=10):
|
|
568
|
+
try:
|
|
569
|
+
async with aiohttp.ClientSession(connector=ProxyConnector.from_url(p.url)) as session:
|
|
570
|
+
async with session.get("https://api.ipify.org", timeout=aiohttp.ClientTimeout(total=10)) as r:
|
|
571
|
+
print(p.url, "→", await r.text())
|
|
572
|
+
return
|
|
573
|
+
except (aiohttp.ClientError, ProxyError, asyncio.TimeoutError, OSError):
|
|
574
|
+
continue # free proxies come and go – just take the next one
|
|
575
|
+
|
|
576
|
+
asyncio.run(main())
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**Scrapy** – a downloader middleware that sends every request, retries included, through the next proxy. Scrapy only speaks HTTP proxies, `https=True` picks the ones that can tunnel `https://` pages
|
|
580
|
+
|
|
581
|
+
```python
|
|
582
|
+
import itertools
|
|
583
|
+
|
|
584
|
+
import scrapy
|
|
585
|
+
from scrapy.crawler import CrawlerProcess
|
|
586
|
+
from proxyscraper import live_proxies
|
|
587
|
+
|
|
588
|
+
PROXIES = [p.url for p in live_proxies(types="http", https=True, min_uptime=50)]
|
|
589
|
+
if not PROXIES:
|
|
590
|
+
raise SystemExit("No proxy matches right now – loosen the filters (e.g. min_uptime)")
|
|
591
|
+
POOL = itertools.cycle(PROXIES)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class RotatingProxy:
|
|
595
|
+
def process_request(self, request, spider=None): # newer Scrapy leaves out spider
|
|
596
|
+
request.meta["proxy"] = next(POOL)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class IpSpider(scrapy.Spider):
|
|
600
|
+
name = "ip"
|
|
601
|
+
start_urls = [f"https://api.ipify.org/?n={i}" for i in range(3)]
|
|
602
|
+
custom_settings = {
|
|
603
|
+
"DOWNLOADER_MIDDLEWARES": {f"{__name__}.RotatingProxy": 350},
|
|
604
|
+
"RETRY_TIMES": 5, "DOWNLOAD_TIMEOUT": 15,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def parse(self, response):
|
|
608
|
+
print(response.meta["proxy"], "→", response.text)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
if __name__ == "__main__":
|
|
612
|
+
process = CrawlerProcess()
|
|
613
|
+
process.crawl(IpSpider)
|
|
614
|
+
process.start()
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
In a Scrapy project, put `RotatingProxy` in `middlewares.py` and add it to `DOWNLOADER_MIDDLEWARES` in `settings.py`. For long crawls, reload the pool now and then – the list changes every hour.
|
|
618
|
+
|
|
541
619
|
**proxychains, Clash / Mihomo** – ready-made configs with `--export`
|
|
542
620
|
|
|
543
621
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.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
|
|
@@ -578,6 +578,84 @@ for p in proxies:
|
|
|
578
578
|
continue # free proxies come and go – just take the next one
|
|
579
579
|
```
|
|
580
580
|
|
|
581
|
+
**httpx** (`pip install "httpx[socks]"`) – straight from the hourly list, no scan
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
import httpx
|
|
585
|
+
from proxyscraper import live_proxies
|
|
586
|
+
|
|
587
|
+
for p in live_proxies(types=["http", "socks5"], https=True, min_uptime=90, limit=10): # httpx can't do SOCKS4
|
|
588
|
+
try:
|
|
589
|
+
with httpx.Client(proxy=p.url, timeout=10) as client:
|
|
590
|
+
print(p.url, "→", client.get("https://api.ipify.org").text)
|
|
591
|
+
break
|
|
592
|
+
except httpx.HTTPError:
|
|
593
|
+
continue # next one
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**aiohttp** (`pip install aiohttp aiohttp-socks` – aiohttp alone can't do SOCKS)
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
import asyncio
|
|
600
|
+
|
|
601
|
+
import aiohttp
|
|
602
|
+
from aiohttp_socks import ProxyConnector, ProxyError
|
|
603
|
+
from proxyscraper import live_proxies_async
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
async def main():
|
|
607
|
+
for p in await live_proxies_async(https=True, min_uptime=90, limit=10):
|
|
608
|
+
try:
|
|
609
|
+
async with aiohttp.ClientSession(connector=ProxyConnector.from_url(p.url)) as session:
|
|
610
|
+
async with session.get("https://api.ipify.org", timeout=aiohttp.ClientTimeout(total=10)) as r:
|
|
611
|
+
print(p.url, "→", await r.text())
|
|
612
|
+
return
|
|
613
|
+
except (aiohttp.ClientError, ProxyError, asyncio.TimeoutError, OSError):
|
|
614
|
+
continue # free proxies come and go – just take the next one
|
|
615
|
+
|
|
616
|
+
asyncio.run(main())
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Scrapy** – a downloader middleware that sends every request, retries included, through the next proxy. Scrapy only speaks HTTP proxies, `https=True` picks the ones that can tunnel `https://` pages
|
|
620
|
+
|
|
621
|
+
```python
|
|
622
|
+
import itertools
|
|
623
|
+
|
|
624
|
+
import scrapy
|
|
625
|
+
from scrapy.crawler import CrawlerProcess
|
|
626
|
+
from proxyscraper import live_proxies
|
|
627
|
+
|
|
628
|
+
PROXIES = [p.url for p in live_proxies(types="http", https=True, min_uptime=50)]
|
|
629
|
+
if not PROXIES:
|
|
630
|
+
raise SystemExit("No proxy matches right now – loosen the filters (e.g. min_uptime)")
|
|
631
|
+
POOL = itertools.cycle(PROXIES)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class RotatingProxy:
|
|
635
|
+
def process_request(self, request, spider=None): # newer Scrapy leaves out spider
|
|
636
|
+
request.meta["proxy"] = next(POOL)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class IpSpider(scrapy.Spider):
|
|
640
|
+
name = "ip"
|
|
641
|
+
start_urls = [f"https://api.ipify.org/?n={i}" for i in range(3)]
|
|
642
|
+
custom_settings = {
|
|
643
|
+
"DOWNLOADER_MIDDLEWARES": {f"{__name__}.RotatingProxy": 350},
|
|
644
|
+
"RETRY_TIMES": 5, "DOWNLOAD_TIMEOUT": 15,
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
def parse(self, response):
|
|
648
|
+
print(response.meta["proxy"], "→", response.text)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
if __name__ == "__main__":
|
|
652
|
+
process = CrawlerProcess()
|
|
653
|
+
process.crawl(IpSpider)
|
|
654
|
+
process.start()
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
In a Scrapy project, put `RotatingProxy` in `middlewares.py` and add it to `DOWNLOADER_MIDDLEWARES` in `settings.py`. For long crawls, reload the pool now and then – the list changes every hour.
|
|
658
|
+
|
|
581
659
|
**proxychains, Clash / Mihomo** – ready-made configs with `--export`
|
|
582
660
|
|
|
583
661
|
```bash
|
|
@@ -36,7 +36,9 @@ proxyscraper/parsing.py
|
|
|
36
36
|
proxyscraper/paths.py
|
|
37
37
|
proxyscraper/pipeline.py
|
|
38
38
|
proxyscraper/preferences.py
|
|
39
|
+
proxyscraper/proxypages.py
|
|
39
40
|
proxyscraper/publish.py
|
|
41
|
+
proxyscraper/report.py
|
|
40
42
|
proxyscraper/sites.py
|
|
41
43
|
proxyscraper/sources.json
|
|
42
44
|
proxyscraper/sources.py
|
|
@@ -86,8 +88,10 @@ tests/test_pages.py
|
|
|
86
88
|
tests/test_parsing.py
|
|
87
89
|
tests/test_paths.py
|
|
88
90
|
tests/test_pipeline.py
|
|
91
|
+
tests/test_proxy_pages.py
|
|
89
92
|
tests/test_proxy_sources.py
|
|
90
93
|
tests/test_publish.py
|
|
94
|
+
tests/test_report.py
|
|
91
95
|
tests/test_robustness.py
|
|
92
96
|
tests/test_serve_password.py
|
|
93
97
|
tests/test_serve_refill.py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""proxy-scraper: collects public HTTP/SOCKS4/SOCKS5 proxies from hundreds of sources, checks them
|
|
2
2
|
in parallel with its own protocol handshakes and learns which sources deliver good proxies."""
|
|
3
3
|
|
|
4
|
-
__version__ = "1.
|
|
4
|
+
__version__ = "1.9.0"
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def __getattr__(name):
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from html import escape
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Callable, Dict, List, Tuple
|
|
13
|
+
from typing import Callable, Dict, List, Sequence, Tuple
|
|
14
14
|
|
|
15
15
|
SITE_URL = "https://maximilianfeix.github.io/proxy-scraper/"
|
|
16
16
|
REPO_URL = "https://github.com/maximilianfeix/proxy-scraper"
|
|
@@ -121,7 +121,8 @@ def _render(path: str, title: str, what: str, rows: List[dict], updated: datetim
|
|
|
121
121
|
lede = (f"No {what} passed every check in the last run ({when}). The list is checked again every hour, "
|
|
122
122
|
"so look again later or try the full list.")
|
|
123
123
|
body_rows = "\n".join(
|
|
124
|
-
f"<tr><td class=\"mono\"
|
|
124
|
+
f"<tr><td class=\"mono\"><a href=\"{root}proxy/{escape(r['proxy'].replace(':', '-'))}/\">"
|
|
125
|
+
f"{escape(r['proxy'])}</a></td><td>{escape(r['ptype'].upper())}</td>"
|
|
125
126
|
f"<td>{escape(r.get('country') or '–')}</td><td>{r['latency']:,} ms</td>"
|
|
126
127
|
f"<td>{'yes' if r.get('https') else 'no'}</td><td>{escape(r.get('anonymity') or '–')}</td>"
|
|
127
128
|
f"<td>{escape((r.get('org') or '–')[:40])}</td></tr>"
|
|
@@ -196,8 +197,9 @@ def _nav(specs, counts: Dict[str, int], current: str, root: str) -> str:
|
|
|
196
197
|
f"<nav aria-label=\"By country\"><h2>By country</h2><ul>{links(countries)}</ul></nav>")
|
|
197
198
|
|
|
198
199
|
|
|
199
|
-
def write_pages(rows: List[dict], out: Path, updated: datetime) -> List[str]:
|
|
200
|
-
"""Writes every page with its proxies.txt and the sitemap
|
|
200
|
+
def write_pages(rows: List[dict], out: Path, updated: datetime, extra: Sequence[str] = ()) -> List[str]:
|
|
201
|
+
"""Writes every page with its proxies.txt and the sitemap (plus the `extra` paths, e.g. the proxy pages)
|
|
202
|
+
-> the paths listed in the sitemap."""
|
|
201
203
|
rows = sorted(rows, key=lambda r: r["latency"])
|
|
202
204
|
specs = page_specs()
|
|
203
205
|
selected = {p: [r for r in rows if keep(r)] for p, _, _, keep in specs}
|
|
@@ -212,6 +214,7 @@ def write_pages(rows: List[dict], out: Path, updated: datetime) -> List[str]:
|
|
|
212
214
|
(folder / "proxies.txt").write_text("".join(f"{r['url']}\n" for r in page_rows), encoding="utf-8")
|
|
213
215
|
if not path.startswith("country/") or counts[path] >= SITEMAP_MIN:
|
|
214
216
|
listed.append(path)
|
|
217
|
+
listed.extend(extra)
|
|
215
218
|
lastmod = updated.strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
|
216
219
|
urls = "".join(f"<url><loc>{SITE_URL}{p}</loc><lastmod>{lastmod}</lastmod><changefreq>hourly</changefreq></url>"
|
|
217
220
|
for p in listed)
|
|
@@ -44,8 +44,8 @@ _SPACE_SEPARATED_RE = re.compile(rb"\d\.\d{1,3}[ \t]+\d{2,5}(?![\d.])")
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
# Bump whenever parsing finds different proxies in the same bytes: the fetch cache stores parsed keys and
|
|
47
|
-
# reloads every list once when this changes (2: plain lists of "auto" sources are read as http)
|
|
48
|
-
PARSER_VERSION =
|
|
47
|
+
# reloads every list once when this changes (2: plain lists of "auto" sources are read, 3: as http and socks5)
|
|
48
|
+
PARSER_VERSION = 3
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def _needs_full_regex(data: bytes) -> bool:
|
|
@@ -59,8 +59,9 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
|
|
|
59
59
|
|
|
60
60
|
Lines with a type:// prefix keep their own type; everything else gets the type of the source.
|
|
61
61
|
For sources of type "auto" the plain lines only count when they are the majority: a generic proxies.txt of
|
|
62
|
-
plain ip:port lines
|
|
63
|
-
|
|
62
|
+
plain ip:port lines doesn't say its protocol, so they're tried as HTTP and as SOCKS5 (measured on 20,000 such
|
|
63
|
+
entries: 44 worked as http, 32 as socks5, none as both; an unreachable address fails fast for both). In a list
|
|
64
|
+
of mostly prefixed lines, the odd plain one has an unknown type and is left out.
|
|
64
65
|
"""
|
|
65
66
|
out: Set[RawCandidate] = set()
|
|
66
67
|
with_scheme = set()
|
|
@@ -71,8 +72,6 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
|
|
|
71
72
|
out.add((ptype, ip, port, auth))
|
|
72
73
|
with_scheme.add((ip, port))
|
|
73
74
|
auto = default_type == "auto"
|
|
74
|
-
if auto:
|
|
75
|
-
default_type = "http"
|
|
76
75
|
regex = PROXY_RE if _needs_full_regex(data) else PLAIN_RE
|
|
77
76
|
pairs = set(regex.findall(data))
|
|
78
77
|
if b'"ip"' in data:
|
|
@@ -81,7 +80,8 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
|
|
|
81
80
|
plain = pairs - with_scheme
|
|
82
81
|
if auto and len(plain) <= len(out):
|
|
83
82
|
return out
|
|
84
|
-
|
|
83
|
+
for ptype in ("http", "socks5") if auto else (default_type,):
|
|
84
|
+
out.update((ptype, ip, port, b"") for ip, port in plain)
|
|
85
85
|
return out
|
|
86
86
|
|
|
87
87
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""A page per proxy on the live list: what it is, how reliable it has been this week, which big sites let it
|
|
2
|
+
through, and the commands to try it yourself.
|
|
3
|
+
|
|
4
|
+
Only proxies that were on the list for at least half of the week's runs are indexed – thousands of pages that
|
|
5
|
+
come and go every hour would look like spam to search engines. The others get a page too, with noindex.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from html import escape
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
15
|
+
|
|
16
|
+
from .pages import COUNTRIES, CSS, SITE_URL, _short_name
|
|
17
|
+
|
|
18
|
+
INDEX_MIN_UPTIME = 50 # percent of the week's runs
|
|
19
|
+
SITE_NAMES = {"google": "Google", "reddit": "Reddit", "amazon": "Amazon"}
|
|
20
|
+
|
|
21
|
+
EXTRA_CSS = """
|
|
22
|
+
.wrap > * { min-width: 0; }
|
|
23
|
+
h1.mono { font-size: clamp(28px, 7vw, 64px); letter-spacing: -.03em; overflow-wrap: anywhere; }
|
|
24
|
+
.facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 160px), 1fr)); gap: 1px;
|
|
25
|
+
background: var(--line);
|
|
26
|
+
border: 1px solid var(--line); border-radius: 16px; overflow: hidden; margin: 0; }
|
|
27
|
+
.facts div { background: var(--panel); padding: 14px 16px; }
|
|
28
|
+
.facts dt { color: var(--muted); font-size: 13px; }
|
|
29
|
+
.facts dd { margin: 4px 0 0; font-size: 17px; }
|
|
30
|
+
h2 { font-size: 22px; letter-spacing: -.02em; margin: 0 0 12px; font-weight: 600; }
|
|
31
|
+
.timeline { display: flex; gap: 2px; align-items: stretch; height: 34px; }
|
|
32
|
+
.cell { flex: 1; min-width: 2px; border-radius: 2px; background: var(--line); }
|
|
33
|
+
.cell.on { background: var(--signal-ink); }
|
|
34
|
+
.axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 13px; margin-top: 6px; }
|
|
35
|
+
.cmd code { white-space: pre-wrap; word-break: break-all; }
|
|
36
|
+
.cmd { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
|
37
|
+
padding: 12px 56px 12px 14px; margin: 0 0 10px; overflow-x: auto; font-size: 14px; }
|
|
38
|
+
.cmd button { position: absolute; top: 8px; right: 8px; font: inherit; font-size: 13px; padding: 4px 10px;
|
|
39
|
+
border-radius: 99px; border: 1px solid var(--line); background: var(--bg); color: var(--text);
|
|
40
|
+
cursor: pointer; }
|
|
41
|
+
.lede { overflow-wrap: anywhere; }
|
|
42
|
+
.note { color: var(--muted); font-size: 14px; }
|
|
43
|
+
.status { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); }
|
|
44
|
+
.status::before { content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--signal-ink); }
|
|
45
|
+
.status.off::before { background: var(--muted); }
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
COPY_JS = """<script>
|
|
49
|
+
document.querySelectorAll(".cmd button").forEach(b => b.onclick = () => {
|
|
50
|
+
navigator.clipboard.writeText(b.previousElementSibling.textContent).then(() => {
|
|
51
|
+
b.textContent = "Copied"; setTimeout(() => { b.textContent = "Copy"; }, 1500);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
</script>"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def page_path(proxy: str) -> str:
|
|
58
|
+
"""'1.2.3.4:1080' -> 'proxy/1.2.3.4-1080/'"""
|
|
59
|
+
return "proxy/" + proxy.replace(":", "-") + "/"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _country(cc: str) -> str:
|
|
63
|
+
return _short_name(COUNTRIES[cc]) if cc in COUNTRIES else (cc or "unknown")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _commands(row: dict) -> List[str]:
|
|
67
|
+
target = "https://api.ipify.org" if row.get("https") else "http://api.ipify.org"
|
|
68
|
+
scheme = "socks5h" if row["ptype"] == "socks5" else row["ptype"] # socks5h: DNS through the proxy too
|
|
69
|
+
return [f"curl -x {scheme}://{row['proxy']} {target}",
|
|
70
|
+
f'python -c "import requests; print(requests.get(\'{target}\', '
|
|
71
|
+
f'proxies={{\'http\': \'{row["url"]}\', \'https\': \'{row["url"]}\'}}, timeout=10).text)"']
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cmd(command: str) -> str:
|
|
75
|
+
return f'<div class="cmd"><code class="mono">{escape(command)}</code><button type="button">Copy</button></div>'
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _timeline(bits: Optional[int], runs: Sequence[datetime]) -> str:
|
|
79
|
+
"""One cell per run of the week, oldest left – filled where the proxy was on the list."""
|
|
80
|
+
if bits is None or not runs:
|
|
81
|
+
return ""
|
|
82
|
+
bits &= (1 << len(runs)) - 1 # bits seeded from a streak can reach further back than the history
|
|
83
|
+
cells = "".join(f'<span class="cell{" on" if bits >> k & 1 else ""}"></span>'
|
|
84
|
+
for k in range(len(runs) - 1, -1, -1)) # bit 0 = the newest run
|
|
85
|
+
first = runs[0].strftime("%b %d, %H:%M UTC")
|
|
86
|
+
return (f'<section aria-label="Timeline"><h2>Every check this week</h2>'
|
|
87
|
+
f'<div class="timeline" role="img" aria-label="On the list in {bin(bits).count("1")} '
|
|
88
|
+
f'of the last {len(runs)} hourly checks">{cells}</div>'
|
|
89
|
+
f'<div class="axis"><span>{escape(first)}</span><span>now</span></div></section>')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _summary(entries: List[dict]) -> dict:
|
|
93
|
+
"""One address, several protocols: the best of each value (the page is indexed by the best uptime too)."""
|
|
94
|
+
def first(key):
|
|
95
|
+
return next((r[key] for r in entries if r.get(key) not in (None, "")), None)
|
|
96
|
+
uptimes = [r["uptime_7d"] for r in entries if r.get("uptime_7d") is not None]
|
|
97
|
+
https = [r.get("https") for r in entries]
|
|
98
|
+
rank = {"elite": 3, "anonymous": 2, "transparent": 1}
|
|
99
|
+
sites = {k for r in entries for k, ok in (r.get("sites") or {}).items() if ok}
|
|
100
|
+
return {
|
|
101
|
+
"uptime": max(uptimes) if uptimes else None,
|
|
102
|
+
"latency": min(r["latency"] for r in entries) if all("latency" in r for r in entries) else None,
|
|
103
|
+
"https": True if True in https else False if False in https else None,
|
|
104
|
+
"anonymity": max((r.get("anonymity") or "" for r in entries), key=lambda a: rank.get(a, 0)) or None,
|
|
105
|
+
"sites": [SITE_NAMES[k] for k in SITE_NAMES if k in sites],
|
|
106
|
+
"country": first("country") or "", "org": first("org"), "asn": first("asn"), "exit_ip": first("exit_ip"),
|
|
107
|
+
"hosting": first("hosting"), "blocklisted": first("blocklisted"),
|
|
108
|
+
"first_seen": min((r["first_seen"] for r in entries if r.get("first_seen")), default=None),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _yes_no(value, yes: str, no: str) -> str:
|
|
113
|
+
return yes if value else no if value is False else "unknown"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _render(proxy: str, entries: List[dict], updated: datetime, bits: Optional[int], runs: Sequence[datetime],
|
|
117
|
+
indexed: bool, gone: bool = False) -> str:
|
|
118
|
+
root = "../../"
|
|
119
|
+
info = _summary(entries)
|
|
120
|
+
urls = " · ".join(escape(r["url"]) for r in entries)
|
|
121
|
+
cc = info["country"]
|
|
122
|
+
uptime = info["uptime"]
|
|
123
|
+
first_seen = (info["first_seen"][:16].replace("T", " ") + " UTC") if info["first_seen"] else "unknown"
|
|
124
|
+
facts = [("Protocol", ", ".join(r["ptype"].upper() for r in entries)),
|
|
125
|
+
("Uptime this week", f"{uptime} %" if uptime is not None else "not measured yet")]
|
|
126
|
+
if not gone:
|
|
127
|
+
facts[1:1] = [
|
|
128
|
+
("Country", _country(cc)),
|
|
129
|
+
("Provider", f"{info['org'] or 'unknown'}" + (f" (AS{info['asn']})" if info["asn"] else "")),
|
|
130
|
+
("Latency", f"{info['latency']:,} ms" if info["latency"] is not None else "unknown"),
|
|
131
|
+
("HTTPS", _yes_no(info["https"], "yes, verified TLS", "no")),
|
|
132
|
+
("Anonymity", info["anonymity"] or "unknown"),
|
|
133
|
+
]
|
|
134
|
+
facts += [
|
|
135
|
+
("Gets through to", ", ".join(info["sites"]) if info["sites"] else "none of the checked sites"),
|
|
136
|
+
("Datacenter exit", _yes_no(info["hosting"], "yes", "no")),
|
|
137
|
+
("Spam blocklist", _yes_no(info["blocklisted"], "listed", "not listed")),
|
|
138
|
+
("First seen", first_seen),
|
|
139
|
+
("Exit IP", info["exit_ip"] or "unknown"),
|
|
140
|
+
]
|
|
141
|
+
else:
|
|
142
|
+
facts.append(("First seen", first_seen))
|
|
143
|
+
facts_html = "".join(f"<div><dt>{escape(k)}</dt><dd>{escape(v)}</dd></div>" for k, v in facts)
|
|
144
|
+
fastest = min(entries, key=lambda r: r.get("latency") or 0)
|
|
145
|
+
commands = "".join(_cmd(_commands(r)[0]) for r in entries)
|
|
146
|
+
python = _commands(fastest)[1]
|
|
147
|
+
where = f" ({_country(cc)})" if cc else ""
|
|
148
|
+
title = f"{'/'.join(r['ptype'].upper() for r in entries)} proxy {proxy}{where}"
|
|
149
|
+
reliability = f", on the list in {uptime} % of this week's hourly checks" if uptime is not None else ""
|
|
150
|
+
description = (f"Free {', '.join(r['ptype'].upper() for r in entries)} proxy {proxy}"
|
|
151
|
+
f"{' in ' + _country(cc) if cc else ''}{reliability}. "
|
|
152
|
+
"Latency, provider, HTTPS support and commands to test it.")
|
|
153
|
+
robots = "" if indexed else '<meta name="robots" content="noindex">\n'
|
|
154
|
+
checked = updated.strftime("%Y-%m-%d %H:%M UTC")
|
|
155
|
+
if gone:
|
|
156
|
+
status = (f'<span class="status off">Didn\'t pass the last check ({escape(checked)})</span><br>'
|
|
157
|
+
"It was on the list for most of the week, so it may well come back.")
|
|
158
|
+
back = f'<a href="{root}#list">All working proxies</a>'
|
|
159
|
+
else:
|
|
160
|
+
status = f'<span class="status">Worked in the check at {escape(checked)}</span>'
|
|
161
|
+
back = f'<a href="{root}?country={escape(cc)}#list">All proxies in {escape(_country(cc))}</a>'
|
|
162
|
+
return f"""<!doctype html>
|
|
163
|
+
<html lang="en">
|
|
164
|
+
<head>
|
|
165
|
+
<meta charset="utf-8">
|
|
166
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
167
|
+
<title>{escape(title)} | proxy-scraper</title>
|
|
168
|
+
<meta name="description" content="{escape(description)}">
|
|
169
|
+
{robots}<link rel="canonical" href="{SITE_URL}{page_path(proxy)}">
|
|
170
|
+
<link rel="icon" href="{root}logo.png">
|
|
171
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
172
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
173
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400;500&display=swap"
|
|
174
|
+
rel="stylesheet">
|
|
175
|
+
<link rel="stylesheet" href="../style.css">
|
|
176
|
+
</head>
|
|
177
|
+
<body>
|
|
178
|
+
<div class="wrap">
|
|
179
|
+
<header>
|
|
180
|
+
<a class="brand" href="{root}"><img src="{root}logo.png" alt="">proxy-scraper</a>
|
|
181
|
+
{back}
|
|
182
|
+
</header>
|
|
183
|
+
<main>
|
|
184
|
+
<h1 class="mono">{escape(proxy)}</h1>
|
|
185
|
+
<p class="lede">{status}<br>{urls}</p>
|
|
186
|
+
</main>
|
|
187
|
+
<dl class="facts">{facts_html}</dl>
|
|
188
|
+
{_timeline(bits, runs)}
|
|
189
|
+
<section aria-label="Test it"><h2>Test it from your machine</h2>
|
|
190
|
+
<p class="note">Free proxies come and go, and some behave differently from your network. This prints the IP
|
|
191
|
+
a website sees – the proxy's, if it works.</p>
|
|
192
|
+
{commands}
|
|
193
|
+
{_cmd(python)}
|
|
194
|
+
<p class="note">Never send passwords or personal data through a public proxy. Its operator can read everything
|
|
195
|
+
that isn't encrypted.</p>
|
|
196
|
+
</section>
|
|
197
|
+
<footer><span>Checked every hour by <a href="https://github.com/maximilianfeix/proxy-scraper">proxy-scraper</a>,
|
|
198
|
+
open source.</span></footer>
|
|
199
|
+
</div>
|
|
200
|
+
{COPY_JS}
|
|
201
|
+
</body>
|
|
202
|
+
</html>
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def write_proxy_pages(rows: List[dict], out: Path, updated: datetime, timelines: Dict[str, int],
|
|
207
|
+
runs: Sequence[datetime], gone: Optional[Dict[str, Tuple[int, int, str]]] = None) -> List[str]:
|
|
208
|
+
"""A page per ip:port -> the paths that should be in the sitemap (the reliable ones).
|
|
209
|
+
|
|
210
|
+
timelines: url -> bits (bit 0 = this run), runs: the times of the week's runs, oldest first.
|
|
211
|
+
gone: url -> (bits, uptime, first_seen) of reliable proxies that missed this run – their page stays, so a
|
|
212
|
+
link from the sitemap doesn't turn into a 404 the first time a proxy misses a check."""
|
|
213
|
+
(out / "proxy").mkdir(parents=True, exist_ok=True)
|
|
214
|
+
(out / "proxy" / "style.css").write_text(CSS + EXTRA_CSS, encoding="utf-8") # shared: ~1,500 pages
|
|
215
|
+
by_address: Dict[str, List[dict]] = defaultdict(list)
|
|
216
|
+
for r in rows:
|
|
217
|
+
by_address[r["proxy"]].append(r)
|
|
218
|
+
missing: Dict[str, List[dict]] = defaultdict(list)
|
|
219
|
+
for url, (bits, uptime, first_seen) in (gone or {}).items():
|
|
220
|
+
ptype, _, proxy = url.partition("://")
|
|
221
|
+
if proxy and proxy not in by_address and uptime >= INDEX_MIN_UPTIME:
|
|
222
|
+
missing[proxy].append({"url": url, "ptype": ptype, "proxy": proxy, "uptime_7d": uptime,
|
|
223
|
+
"first_seen": first_seen, "_bits": bits})
|
|
224
|
+
indexed = []
|
|
225
|
+
for proxy, entries, is_gone in [*((p, e, False) for p, e in by_address.items()),
|
|
226
|
+
*((p, e, True) for p, e in missing.items())]:
|
|
227
|
+
entries.sort(key=lambda r: r["ptype"])
|
|
228
|
+
uptime = _summary(entries)["uptime"]
|
|
229
|
+
is_indexed = uptime is not None and uptime >= INDEX_MIN_UPTIME
|
|
230
|
+
bits = None
|
|
231
|
+
for r in entries:
|
|
232
|
+
b = r.get("_bits", timelines.get(r["url"]))
|
|
233
|
+
if b is not None:
|
|
234
|
+
bits = (bits or 0) | b
|
|
235
|
+
folder = out / page_path(proxy)
|
|
236
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
(folder / "index.html").write_text(_render(proxy, entries, updated, bits, runs, is_indexed, is_gone),
|
|
238
|
+
encoding="utf-8")
|
|
239
|
+
if is_indexed:
|
|
240
|
+
indexed.append(page_path(proxy))
|
|
241
|
+
return sorted(indexed)
|
|
@@ -24,6 +24,8 @@ from typing import Dict, List, Optional
|
|
|
24
24
|
from .charts import THEMES, countries_svg, trend_svg
|
|
25
25
|
from .pages import write_pages
|
|
26
26
|
from .parsing import PROXY_TYPES
|
|
27
|
+
from .proxypages import write_proxy_pages
|
|
28
|
+
from .report import weekly_facts, write_report
|
|
27
29
|
from .sites import SITES
|
|
28
30
|
|
|
29
31
|
SKIP_EXIT_CODE = 78
|
|
@@ -294,8 +296,20 @@ def publish(run_dir: Path, out: Path, minimum: int = 20, now: Optional[datetime]
|
|
|
294
296
|
if asset.suffix in (".html", ".png"):
|
|
295
297
|
(out / asset.name).write_bytes(asset.read_bytes())
|
|
296
298
|
(out / ".nojekyll").write_text("", encoding="utf-8") # Pages should serve the files unchanged
|
|
297
|
-
|
|
299
|
+
# a page per proxy (with its week as a timeline), then the pages per protocol and country plus sitemap.xml
|
|
300
|
+
try:
|
|
301
|
+
run_times = [datetime.fromisoformat(run["updated"]) for run in runs[-UPTIME_RUNS:]]
|
|
302
|
+
except (KeyError, TypeError, ValueError):
|
|
303
|
+
run_times = [] # a broken entry: no timeline rather than one whose cells don't match the times
|
|
304
|
+
timelines = {url: e["bits"] for url, e in listed.items()} if run_times else {}
|
|
305
|
+
current = {r["url"] for r in rows}
|
|
306
|
+
gone = {url: (e["bits"], uptime(e["bits"], runs, now, 7 * 24), e["first_seen"])
|
|
307
|
+
for url, e in listed.items() if url not in current} if run_times else {}
|
|
308
|
+
proxy_pages = write_proxy_pages(rows, out, now, timelines, run_times, gone=gone)
|
|
309
|
+
write_pages(rows, out, now, extra=proxy_pages)
|
|
298
310
|
write_json(out / "history.json", runs)
|
|
311
|
+
# 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)
|
|
299
313
|
for theme in THEMES: # charts for the README, which embeds them from GitHub Pages
|
|
300
314
|
(out / f"chart-{theme}.svg").write_text(trend_svg(runs, now, theme), encoding="utf-8")
|
|
301
315
|
(out / f"countries-{theme}.svg").write_text(countries_svg(stats["countries"], theme), encoding="utf-8")
|