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.
Files changed (108) hide show
  1. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/PKG-INFO +79 -1
  2. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/README.md +78 -0
  3. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/PKG-INFO +79 -1
  4. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/SOURCES.txt +5 -0
  5. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/__init__.py +1 -1
  6. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/fetchcache.py +7 -3
  7. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/pages.py +17 -5
  8. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/parsing.py +15 -4
  9. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/pipeline.py +1 -0
  10. proxy_scraper_cli-1.9.0/proxyscraper/proxypages.py +241 -0
  11. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/publish.py +15 -1
  12. proxy_scraper_cli-1.9.0/proxyscraper/report.py +264 -0
  13. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/index.html +66 -7
  14. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.py +119 -6
  15. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_fetchcache.py +13 -0
  16. proxy_scraper_cli-1.9.0/tests/test_mine_sources.py +110 -0
  17. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_pages.py +8 -0
  18. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_parsing.py +13 -0
  19. proxy_scraper_cli-1.9.0/tests/test_proxy_pages.py +93 -0
  20. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_proxy_sources.py +14 -0
  21. proxy_scraper_cli-1.9.0/tests/test_report.py +113 -0
  22. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_uptime.py +12 -0
  23. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/LICENSE +0 -0
  24. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
  25. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
  26. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
  27. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
  28. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/__main__.py +0 -0
  29. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/agent.py +0 -0
  30. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/api.py +0 -0
  31. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/app.py +0 -0
  32. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/asndb.py +0 -0
  33. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/blocklist.py +0 -0
  34. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/charts.py +0 -0
  35. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/checker.py +0 -0
  36. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/cli.py +0 -0
  37. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/compat.py +0 -0
  38. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/completion.py +0 -0
  39. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/exporters.py +0 -0
  40. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/geo.py +0 -0
  41. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/geodb.py +0 -0
  42. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/handshake.py +0 -0
  43. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/history.py +0 -0
  44. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/judges.py +0 -0
  45. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_entry.py +0 -0
  46. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/mcp_server.py +0 -0
  47. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/netio.py +0 -0
  48. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/options.py +0 -0
  49. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/output.py +0 -0
  50. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/paths.py +0 -0
  51. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/preferences.py +0 -0
  52. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/__init__.py +0 -0
  53. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/core.py +0 -0
  54. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/http.py +0 -0
  55. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/pool.py +0 -0
  56. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/socks.py +0 -0
  57. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/status.py +0 -0
  58. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/server/upstream.py +0 -0
  59. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/apple-touch-icon.png +0 -0
  60. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
  61. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/logo.png +0 -0
  62. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/site/og.png +0 -0
  63. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/sites.py +0 -0
  64. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/sources.json +0 -0
  65. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/targets.py +0 -0
  66. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/__init__.py +0 -0
  67. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/dashboard.py +0 -0
  68. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/keys.py +0 -0
  69. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/report.py +0 -0
  70. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/serve.py +0 -0
  71. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/widgets.py +0 -0
  72. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/proxyscraper/ui/wizard.py +0 -0
  73. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/pyproject.toml +0 -0
  74. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/setup.cfg +0 -0
  75. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_agent.py +0 -0
  76. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_api.py +0 -0
  77. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_app.py +0 -0
  78. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_asndb.py +0 -0
  79. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_auth.py +0 -0
  80. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_blocklist.py +0 -0
  81. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_charts.py +0 -0
  82. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_checker.py +0 -0
  83. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_compat.py +0 -0
  84. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_completion.py +0 -0
  85. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_discovery_growth.py +0 -0
  86. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_exporters.py +0 -0
  87. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_geodb.py +0 -0
  88. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_judges.py +0 -0
  89. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_learning.py +0 -0
  90. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_live_api.py +0 -0
  91. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_mcp_server.py +0 -0
  92. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_options.py +0 -0
  93. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_output_stdout.py +0 -0
  94. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_packaging.py +0 -0
  95. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_paths.py +0 -0
  96. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_pipeline.py +0 -0
  97. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_publish.py +0 -0
  98. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_robustness.py +0 -0
  99. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_serve_password.py +0 -0
  100. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_serve_refill.py +0 -0
  101. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_serve_v2.py +0 -0
  102. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_server.py +0 -0
  103. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_server_fixes.py +0 -0
  104. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_sites.py +0 -0
  105. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_tamper.py +0 -0
  106. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_targets.py +0 -0
  107. {proxy_scraper_cli-1.8.0 → proxy_scraper_cli-1.9.0}/tests/test_ui.py +0 -0
  108. {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.8.0
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.8.0
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.8.0"
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
- if not entry or entry.get("type") != ptype or not (self.dir / entry["file"]).is_file():
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\">{escape(r['proxy'])}</td><td>{escape(r['ptype'].upper())}</td>"
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. -> the paths listed in 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" only lines with a prefix count.
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
- if default_type == "auto":
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
- out.update((default_type, ip, port, b"") for ip, port in pairs - with_scheme)
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)