proxy-scraper-cli 1.8.0__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.0 → proxy_scraper_cli-1.9.0}/PKG-INFO +79 -1
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/README.md +78 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/PKG-INFO +79 -1
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/SOURCES.txt +5 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/__init__.py +1 -1
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/fetchcache.py +7 -3
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/pages.py +17 -5
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/parsing.py +15 -4
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/pipeline.py +1 -0
- proxy_scraper_cli-1.9.0/proxyscraper/proxypages.py +241 -0
- {proxy_scraper_cli-1.8.0 → 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.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/index.html +66 -7
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.py +119 -6
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_fetchcache.py +13 -0
- proxy_scraper_cli-1.9.0/tests/test_mine_sources.py +110 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_pages.py +8 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_parsing.py +13 -0
- proxy_scraper_cli-1.9.0/tests/test_proxy_pages.py +93 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_proxy_sources.py +14 -0
- proxy_scraper_cli-1.9.0/tests/test_report.py +113 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_uptime.py +12 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/LICENSE +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/__main__.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/agent.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/api.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/app.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/asndb.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/blocklist.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/charts.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/checker.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/cli.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/compat.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/completion.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/exporters.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/geo.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/geodb.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/handshake.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/history.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/judges.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_entry.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_server.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/netio.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/options.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/output.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/paths.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/preferences.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/__init__.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/core.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/http.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/pool.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/socks.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/status.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/upstream.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/apple-touch-icon.png +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/logo.png +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/og.png +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/sites.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.json +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/targets.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/__init__.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/dashboard.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/keys.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/report.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/serve.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/widgets.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/wizard.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/pyproject.toml +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/setup.cfg +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_agent.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_api.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_app.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_asndb.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_auth.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_blocklist.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_charts.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_checker.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_compat.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_completion.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_discovery_growth.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_exporters.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_geodb.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_judges.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_learning.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_live_api.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_mcp_server.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_options.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_output_stdout.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_packaging.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_paths.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_pipeline.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_publish.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_robustness.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_serve_password.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_serve_refill.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_serve_v2.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_server.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_server_fixes.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_sites.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_tamper.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_targets.py +0 -0
- {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_ui.py +0 -0
- {proxy_scraper_cli-1.8.0 → 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
|
|
@@ -78,6 +80,7 @@ tests/test_judges.py
|
|
|
78
80
|
tests/test_learning.py
|
|
79
81
|
tests/test_live_api.py
|
|
80
82
|
tests/test_mcp_server.py
|
|
83
|
+
tests/test_mine_sources.py
|
|
81
84
|
tests/test_options.py
|
|
82
85
|
tests/test_output_stdout.py
|
|
83
86
|
tests/test_packaging.py
|
|
@@ -85,8 +88,10 @@ tests/test_pages.py
|
|
|
85
88
|
tests/test_parsing.py
|
|
86
89
|
tests/test_paths.py
|
|
87
90
|
tests/test_pipeline.py
|
|
91
|
+
tests/test_proxy_pages.py
|
|
88
92
|
tests/test_proxy_sources.py
|
|
89
93
|
tests/test_publish.py
|
|
94
|
+
tests/test_report.py
|
|
90
95
|
tests/test_robustness.py
|
|
91
96
|
tests/test_serve_password.py
|
|
92
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):
|
|
@@ -5,7 +5,7 @@ On the next run the download asks with If-None-Match / If-Modified-Since; if 304
|
|
|
5
5
|
the keys come from the cache. They are stored unfiltered (all types), because --types can
|
|
6
6
|
change between two runs.
|
|
7
7
|
|
|
8
|
-
data/fetch-cache/index.json url -> {etag, modified, type, file, used}
|
|
8
|
+
data/fetch-cache/index.json url -> {etag, modified, type, file, used, parser}
|
|
9
9
|
data/fetch-cache/<sha1>.txt.gz the keys, one per line
|
|
10
10
|
"""
|
|
11
11
|
|
|
@@ -19,6 +19,7 @@ import zlib
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Dict, Optional
|
|
21
21
|
|
|
22
|
+
from . import parsing
|
|
22
23
|
from .paths import DATA_DIR, atomic_write
|
|
23
24
|
|
|
24
25
|
CACHE_DIR = DATA_DIR / "fetch-cache"
|
|
@@ -53,7 +54,9 @@ class FetchCache:
|
|
|
53
54
|
ptype is the type the list is parsed with: if it changes (e.g. http -> auto),
|
|
54
55
|
the stored keys no longer fit and the list is loaded again."""
|
|
55
56
|
entry = self.entries.get(url) if self.enabled else None
|
|
56
|
-
|
|
57
|
+
# keys from an older parser may miss proxies the current one finds -> load the list again
|
|
58
|
+
if (not entry or entry.get("type") != ptype or entry.get("parser") != parsing.PARSER_VERSION
|
|
59
|
+
or not (self.dir / entry["file"]).is_file()):
|
|
57
60
|
return {}
|
|
58
61
|
headers = {}
|
|
59
62
|
if entry.get("etag"):
|
|
@@ -91,7 +94,8 @@ class FetchCache:
|
|
|
91
94
|
tmp = self.dir / (name + ".tmp")
|
|
92
95
|
tmp.write_bytes(gzip.compress(keys.encode("utf-8"), compresslevel=5))
|
|
93
96
|
tmp.replace(self.dir / name)
|
|
94
|
-
self.entries[url] = {"etag": etag, "modified": modified, "type": ptype, "file": name, "used": time.time()
|
|
97
|
+
self.entries[url] = {"etag": etag, "modified": modified, "type": ptype, "file": name, "used": time.time(),
|
|
98
|
+
"parser": parsing.PARSER_VERSION}
|
|
95
99
|
|
|
96
100
|
def save(self, now: Optional[float] = None) -> None:
|
|
97
101
|
index = self.dir / "index.json"
|
|
@@ -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"
|
|
@@ -100,6 +100,15 @@ footer { color: var(--muted); font-size: 14px; display: grid; gap: 6px; }
|
|
|
100
100
|
"""
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def _list_query(path: str) -> str:
|
|
104
|
+
"""The same filter on the full list: the website reads its filters from the address."""
|
|
105
|
+
kind = path.strip("/")
|
|
106
|
+
if kind.startswith("country/"):
|
|
107
|
+
return f"?country={kind.split('/')[1].upper()}"
|
|
108
|
+
return {"http": "?types=http", "socks4": "?types=socks4", "socks5": "?types=socks5", "https": "?https=1",
|
|
109
|
+
"elite": "?elite=1"}.get(kind, "")
|
|
110
|
+
|
|
111
|
+
|
|
103
112
|
def _render(path: str, title: str, what: str, rows: List[dict], updated: datetime, nav: str) -> str:
|
|
104
113
|
depth = path.count("/")
|
|
105
114
|
root = "../" * depth
|
|
@@ -112,7 +121,8 @@ def _render(path: str, title: str, what: str, rows: List[dict], updated: datetim
|
|
|
112
121
|
lede = (f"No {what} passed every check in the last run ({when}). The list is checked again every hour, "
|
|
113
122
|
"so look again later or try the full list.")
|
|
114
123
|
body_rows = "\n".join(
|
|
115
|
-
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>"
|
|
116
126
|
f"<td>{escape(r.get('country') or '–')}</td><td>{r['latency']:,} ms</td>"
|
|
117
127
|
f"<td>{'yes' if r.get('https') else 'no'}</td><td>{escape(r.get('anonymity') or '–')}</td>"
|
|
118
128
|
f"<td>{escape((r.get('org') or '–')[:40])}</td></tr>"
|
|
@@ -157,7 +167,7 @@ def _render(path: str, title: str, what: str, rows: List[dict], updated: datetim
|
|
|
157
167
|
<p class="lede">{escape(lede)}</p>
|
|
158
168
|
<div class="actions">
|
|
159
169
|
<a class="btn primary" href="proxies.txt" download>Download {total:,} as .txt</a>
|
|
160
|
-
<a class="btn" href="{root}#list">Filter the full list</a>
|
|
170
|
+
<a class="btn" href="{root}{_list_query(path)}#list">Filter the full list</a>
|
|
161
171
|
<a class="btn" href="{REPO_URL}">Check them from your network</a>
|
|
162
172
|
</div>
|
|
163
173
|
</main>
|
|
@@ -187,8 +197,9 @@ def _nav(specs, counts: Dict[str, int], current: str, root: str) -> str:
|
|
|
187
197
|
f"<nav aria-label=\"By country\"><h2>By country</h2><ul>{links(countries)}</ul></nav>")
|
|
188
198
|
|
|
189
199
|
|
|
190
|
-
def write_pages(rows: List[dict], out: Path, updated: datetime) -> List[str]:
|
|
191
|
-
"""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."""
|
|
192
203
|
rows = sorted(rows, key=lambda r: r["latency"])
|
|
193
204
|
specs = page_specs()
|
|
194
205
|
selected = {p: [r for r in rows if keep(r)] for p, _, _, keep in specs}
|
|
@@ -203,6 +214,7 @@ def write_pages(rows: List[dict], out: Path, updated: datetime) -> List[str]:
|
|
|
203
214
|
(folder / "proxies.txt").write_text("".join(f"{r['url']}\n" for r in page_rows), encoding="utf-8")
|
|
204
215
|
if not path.startswith("country/") or counts[path] >= SITEMAP_MIN:
|
|
205
216
|
listed.append(path)
|
|
217
|
+
listed.extend(extra)
|
|
206
218
|
lastmod = updated.strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
|
207
219
|
urls = "".join(f"<url><loc>{SITE_URL}{p}</loc><lastmod>{lastmod}</lastmod><changefreq>hourly</changefreq></url>"
|
|
208
220
|
for p in listed)
|
|
@@ -43,6 +43,11 @@ RawCandidate = Tuple[str, bytes, bytes, bytes] # type, ip, port, credentials (b
|
|
|
43
43
|
_SPACE_SEPARATED_RE = re.compile(rb"\d\.\d{1,3}[ \t]+\d{2,5}(?![\d.])")
|
|
44
44
|
|
|
45
45
|
|
|
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, 3: as http and socks5)
|
|
48
|
+
PARSER_VERSION = 3
|
|
49
|
+
|
|
50
|
+
|
|
46
51
|
def _needs_full_regex(data: bytes) -> bool:
|
|
47
52
|
"""Only space/HTML lists need the slower regex (sample from the start of the file)."""
|
|
48
53
|
sample = data[:8192]
|
|
@@ -53,7 +58,10 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
|
|
|
53
58
|
"""Finds proxies in text, HTML tables and JSON.
|
|
54
59
|
|
|
55
60
|
Lines with a type:// prefix keep their own type; everything else gets the type of the source.
|
|
56
|
-
For sources of type "auto"
|
|
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 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.
|
|
57
65
|
"""
|
|
58
66
|
out: Set[RawCandidate] = set()
|
|
59
67
|
with_scheme = set()
|
|
@@ -63,14 +71,17 @@ def extract_candidates(data: bytes, default_type: str) -> Set[RawCandidate]:
|
|
|
63
71
|
if ptype:
|
|
64
72
|
out.add((ptype, ip, port, auth))
|
|
65
73
|
with_scheme.add((ip, port))
|
|
66
|
-
|
|
67
|
-
return out
|
|
74
|
+
auto = default_type == "auto"
|
|
68
75
|
regex = PROXY_RE if _needs_full_regex(data) else PLAIN_RE
|
|
69
76
|
pairs = set(regex.findall(data))
|
|
70
77
|
if b'"ip"' in data:
|
|
71
78
|
pairs.update(JSON_IP_PORT_RE.findall(data))
|
|
72
79
|
pairs.update((ip, port) for port, ip in JSON_PORT_IP_RE.findall(data))
|
|
73
|
-
|
|
80
|
+
plain = pairs - with_scheme
|
|
81
|
+
if auto and len(plain) <= len(out):
|
|
82
|
+
return out
|
|
83
|
+
for ptype in ("http", "socks5") if auto else (default_type,):
|
|
84
|
+
out.update((ptype, ip, port, b"") for ip, port in plain)
|
|
74
85
|
return out
|
|
75
86
|
|
|
76
87
|
|
|
@@ -65,6 +65,7 @@ async def collect_sources(opts: RunOptions, quality: srcs.SourceStats) -> Source
|
|
|
65
65
|
found = await srcs.discover_github(
|
|
66
66
|
http_get, token, max_repos,
|
|
67
67
|
on_progress=lambda msg: status.update(f"[bold {ACCENT}]GitHub-Discovery:[/] {msg}"),
|
|
68
|
+
known=sources, # curated ones needn't be tried; found ones are, so they stay fresh
|
|
68
69
|
)
|
|
69
70
|
if found:
|
|
70
71
|
srcs.save_discovered(found)
|