Huge update
- Drop support for Python 3.7. - Use offline geolocation database instead of ip-api.com. - Add support for proxy authentication. - Add pre-compiled binaries. - Refactor the code by a lot. - Simplify config. - Add automatic text encoding detection. - Use TOML instead of INI for configuration. - Add support for using httpbin-compatible services and services which return plain IP address for getting proxy's exit node. - Add support for reading proxy lists from local files. - Add support for saving to json. - Remove support for saving geolocation to txt, use json instead. - Improve parser. - Improve config validation. - Use poetry instead of requirements.txt. - Use venv in start scripts.
This commit is contained in:
@@ -6,6 +6,17 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
auto-merge-dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- id: dependabot-metadata
|
||||
uses: dependabot/fetch-metadata@v1
|
||||
- if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}
|
||||
run: gh pr merge --auto --delete-branch --squash "${PR_URL}"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-merge-updates:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'monosans' && startsWith(github.head_ref, 'update/') }}
|
||||
|
||||
@@ -14,3 +14,30 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: pipx run pre-commit run --all-files --show-diff-on-failure
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu
|
||||
- macos
|
||||
- windows
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: pipx install poetry
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: poetry
|
||||
check-latest: true
|
||||
- run: poetry install --only main,nuitka --sync --no-root --no-interaction
|
||||
- run: poetry run --no-interaction python -m nuitka --onefile --python-flag='-m' --prefer-source-code --assume-yes-for-downloads --lto=yes proxy_scraper_checker
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact-${{ matrix.os }}
|
||||
path: |
|
||||
config.toml
|
||||
proxy_scraper_checker.bin
|
||||
proxy_scraper_checker.exe
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -12,6 +12,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- cmd: pipx run poetry lock --no-interaction
|
||||
commit-msg: Update poetry.lock
|
||||
branch: update/poetry-lock
|
||||
- cmd: pipx run pre-commit autoupdate
|
||||
commit-msg: Update .pre-commit-config.yaml
|
||||
branch: update/pre-commit-config
|
||||
|
||||
+1
-4
@@ -376,7 +376,4 @@ $RECYCLE.BIN/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,linux,macos,python,vim,visualstudiocode,windows
|
||||
|
||||
proxies/
|
||||
proxies_anonymous/
|
||||
proxies_geolocation/
|
||||
proxies_geolocation_anonymous/
|
||||
out/
|
||||
|
||||
@@ -35,6 +35,13 @@ repos:
|
||||
- --scripts-are-modules
|
||||
additional_dependencies:
|
||||
- aiohttp<4
|
||||
- attrs
|
||||
- certifi
|
||||
- charset-normalizer<4
|
||||
- maxminddb<3
|
||||
- platformdirs<5
|
||||
- rich<14
|
||||
- typing-extensions<5
|
||||
- uvloop<0.20; implementation_name == "cpython" and (sys_platform == "darwin" or sys_platform == "linux")
|
||||
- types-aiofiles
|
||||
- typing-extensions<5; python_version < "3.11"
|
||||
- tomli<3; python_version < "3.11"
|
||||
- uvloop<0.20; platform_python_implementation == "CPython" and (sys_platform == "darwin" or sys_platform == "linux")
|
||||
|
||||
@@ -6,27 +6,37 @@
|
||||
|
||||
HTTP, SOCKS4, SOCKS5 proxies scraper and checker.
|
||||
|
||||
- Asynchronous.
|
||||
- Uses regex to search for proxies (ip:port format) on a web page, allowing proxies to be extracted even from json without making changes to the code.
|
||||
- It is possible to specify the URL to which to send a request to check the proxy.
|
||||
- Can sort proxies by speed.
|
||||
- Supports determining the geolocation of the proxy exit node.
|
||||
- Can determine if the proxy is anonymous.
|
||||
- Supports determining the geolocation of the proxy exit node.
|
||||
- Can sort proxies by speed.
|
||||
- Uses regex to find proxies of format `protocol://username:password@ip:port` on a web page or in a local file, allowing proxies to be extracted even from json without code changes.
|
||||
- Supports proxies with authentication.
|
||||
- It is possible to specify the URL to which to send a request to check the proxy.
|
||||
- Supports saving to plain text and json.
|
||||
- Asynchronous.
|
||||
|
||||
You can get proxies obtained using this script in [monosans/proxy-list](https://github.com/monosans/proxy-list).
|
||||
You can get proxies obtained using this project in [monosans/proxy-list](https://github.com/monosans/proxy-list).
|
||||
|
||||
## Installation and usage
|
||||
|
||||
### Desktop
|
||||
### Pre-compiled binary
|
||||
|
||||
This is the easiest way, but it is only available for x64 Windows, macOS and Linux. Just download the archive for your OS from <https://nightly.link/monosans/proxy-scraper-checker/workflows/ci/main?preview>, unzip it, edit `config.toml` and run the executable.
|
||||
|
||||
If Windows Defender detects an executable file as a virus, please read [this](https://github.com/Nuitka/Nuitka/issues/2495#issuecomment-1762836583).
|
||||
|
||||
### Running from source code
|
||||
|
||||
#### Desktop
|
||||
|
||||
- Install [Python](https://python.org/downloads). The minimum version required is 3.8. The recommended version is 3.11, because 3.12 may not install some libraries in the absence of a C compiler.
|
||||
- Download and unpack [the archive with the program](https://github.com/monosans/proxy-scraper-checker/archive/refs/heads/main.zip).
|
||||
- Edit `config.ini` to your preference.
|
||||
- Install [Python](https://python.org/downloads) (minimum required version is 3.7).
|
||||
- Edit `config.toml` to your preference.
|
||||
- Run the script that installs dependencies and starts `proxy-scraper-checker`:
|
||||
- On Windows run `start.cmd`
|
||||
- On Unix-like operating systems run `start.sh`
|
||||
|
||||
### Termux
|
||||
#### Termux
|
||||
|
||||
To use `proxy-scraper-checker` in Termux, knowledge of the Unix command-line interface is required.
|
||||
|
||||
@@ -35,27 +45,18 @@ To use `proxy-scraper-checker` in Termux, knowledge of the Unix command-line int
|
||||
```bash
|
||||
bash <(curl -fsSL 'https://raw.githubusercontent.com/monosans/proxy-scraper-checker/main/install-termux.sh')
|
||||
```
|
||||
- Edit `~/proxy-scraper-checker/config.ini` to your preference using a text editor (vim/nano).
|
||||
- Edit `~/proxy-scraper-checker/config.toml` to your preference using a text editor (vim/nano).
|
||||
- To run `proxy-scraper-checker` use the following command:
|
||||
```bash
|
||||
cd ~/proxy-scraper-checker && sh start-termux.sh
|
||||
```
|
||||
|
||||
## Checking local proxy lists
|
||||
## Something else?
|
||||
|
||||
To check the local proxy lists, start the Python HTTP server on your local machine by running the `python -m http.server --bind localhost` command in the folder with the proxy lists. After that, add links to the appropriate files in `config.ini`.
|
||||
|
||||
## Folders description
|
||||
|
||||
When the script finishes running, the following folders will be created (this behavior can be changed in the config):
|
||||
|
||||
- `proxies` - proxies with any anonymity level.
|
||||
- `proxies_anonymous` - anonymous proxies.
|
||||
- `proxies_geolocation` - same as `proxies`, but includes exit-node's geolocation.
|
||||
- `proxies_geolocation_anonymous` - same as `proxies_anonymous`, but includes exit-node's geolocation.
|
||||
|
||||
Geolocation format is `ip:port|Country|Region|City`.
|
||||
All other info is available in `config.toml` file.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
This product includes GeoLite2 Data created by MaxMind, available from <https://www.maxmind.com>.
|
||||
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
[General]
|
||||
; The number of seconds to wait for a proxied request.
|
||||
; The higher the number, the longer the check will take
|
||||
; and the more proxies you get.
|
||||
Timeout = 5
|
||||
|
||||
; The number of seconds to wait for the proxies to be downloaded from the source.
|
||||
SourceTimeout = 15
|
||||
|
||||
; Maximum concurrent connections.
|
||||
; Windows supports maximum of 512.
|
||||
; On *nix operating systems, this restriction is much looser.
|
||||
; The limit on *nix can be seen with the command `ulimit -Hn`.
|
||||
; Don't be in a hurry to set high values.
|
||||
; Make sure you have enough RAM first, gradually increasing the default value.
|
||||
; If set to 0, the maximum value available for your OS will be used.
|
||||
MaxConnections = 512
|
||||
|
||||
; URL to which to send a request to check the proxy.
|
||||
; If not equal to 'default', it will not be possible
|
||||
; to determine the anonymity and geolocation of the proxies.
|
||||
; Example:
|
||||
; CheckWebsite = https://google.com
|
||||
CheckWebsite = default
|
||||
|
||||
; Set to no to sort proxies alphabetically.
|
||||
SortBySpeed = yes
|
||||
|
||||
; Path to the folder where the proxy folders will be saved.
|
||||
; Leave empty to save the proxies to the current directory.
|
||||
SavePath =
|
||||
|
||||
; Enable output of all errors
|
||||
Debug = no
|
||||
|
||||
[Folders]
|
||||
; This section is ignored if CheckWebsite is not 'default'.
|
||||
|
||||
; Proxies with any anonymity level.
|
||||
proxies = yes
|
||||
; Anonymous proxies.
|
||||
proxies_anonymous = yes
|
||||
; Same as proxies, but includes exit-node's geolocation.
|
||||
; Geolocation format is ip:port|Country|Region|City
|
||||
proxies_geolocation = yes
|
||||
; Same as proxies_geolocation, but includes exit-node's geolocation.
|
||||
proxies_geolocation_anonymous = yes
|
||||
|
||||
[HTTP]
|
||||
Enabled = yes
|
||||
Sources =
|
||||
https://api.proxyscrape.com/v2/?request=getproxies&protocol=http
|
||||
; https://openproxy.space/list/http
|
||||
https://openproxylist.xyz/http.txt
|
||||
https://proxyspace.pro/http.txt
|
||||
https://proxyspace.pro/https.txt
|
||||
https://raw.githubusercontent.com/ALIILAPRO/Proxy/main/http.txt
|
||||
https://raw.githubusercontent.com/andigwandi/free-proxy/main/proxy_list.txt
|
||||
https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/http_proxies.txt
|
||||
https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/https_proxies.txt
|
||||
https://raw.githubusercontent.com/aslisk/proxyhttps/main/https.txt
|
||||
https://raw.githubusercontent.com/B4RC0DE-TM/proxy-list/main/HTTP.txt
|
||||
https://raw.githubusercontent.com/clarketm/proxy-list/master/proxy-list-raw.txt
|
||||
https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt
|
||||
https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt
|
||||
https://raw.githubusercontent.com/hendrikbgr/Free-Proxy-Repo/master/proxy_list.txt
|
||||
https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-http.txt
|
||||
https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-https.txt
|
||||
https://raw.githubusercontent.com/mmpx12/proxy-list/master/http.txt
|
||||
https://raw.githubusercontent.com/mmpx12/proxy-list/master/https.txt
|
||||
https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt
|
||||
https://raw.githubusercontent.com/NotUnko/autoproxies/main/http.txt
|
||||
https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/http/http.txt
|
||||
https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/https/https.txt
|
||||
https://raw.githubusercontent.com/proxy4parsing/proxy-list/main/http.txt
|
||||
https://raw.githubusercontent.com/proxylist-to/proxy-list/main/http.txt
|
||||
https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt
|
||||
https://raw.githubusercontent.com/roosterkid/openproxylist/main/HTTPS_RAW.txt
|
||||
https://raw.githubusercontent.com/saisuiu/Lionkings-Http-Proxys-Proxies/main/free.txt
|
||||
https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/http.txt
|
||||
https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/https.txt
|
||||
https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/generated/http_proxies.txt
|
||||
https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt
|
||||
https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/http_proxies%20(2).txt
|
||||
https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/http_proxies%20(3).txt
|
||||
https://raw.githubusercontent.com/yemixzy/proxy-list/main/proxies/http.txt
|
||||
https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/http.txt
|
||||
https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/https.txt
|
||||
https://raw.githubusercontent.com/zevtyardt/proxy-list/main/http.txt
|
||||
https://raw.githubusercontent.com/zloi-user/hideip.me/main/http.txt
|
||||
https://raw.githubusercontent.com/zloi-user/hideip.me/main/https.txt
|
||||
https://rootjazz.com/proxies/proxies.txt
|
||||
https://www.proxy-list.download/api/v1/get?type=http
|
||||
https://www.proxy-list.download/api/v1/get?type=https
|
||||
|
||||
[SOCKS4]
|
||||
Enabled = yes
|
||||
Sources =
|
||||
https://api.proxyscrape.com/v2/?request=getproxies&protocol=socks4
|
||||
; https://openproxy.space/list/socks4
|
||||
https://openproxylist.xyz/socks4.txt
|
||||
https://proxyspace.pro/socks4.txt
|
||||
https://raw.githubusercontent.com/ALIILAPRO/Proxy/main/socks4.txt
|
||||
https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/socks4_proxies.txt
|
||||
https://raw.githubusercontent.com/B4RC0DE-TM/proxy-list/main/SOCKS4.txt
|
||||
https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/socks4.txt
|
||||
https://raw.githubusercontent.com/iptotal/free-proxy-list/master/socks4.txt
|
||||
https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-socks4.txt
|
||||
https://raw.githubusercontent.com/mmpx12/proxy-list/master/socks4.txt
|
||||
https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/socks4.txt
|
||||
https://raw.githubusercontent.com/NotUnko/autoproxies/main/socks4.txt
|
||||
https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/socks4/socks4.txt
|
||||
https://raw.githubusercontent.com/proxylist-to/proxy-list/main/socks4.txt
|
||||
https://raw.githubusercontent.com/prxchk/proxy-list/main/socks4.txt
|
||||
https://raw.githubusercontent.com/roosterkid/openproxylist/main/SOCKS4_RAW.txt
|
||||
https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/socks4.txt
|
||||
https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/generated/socks4_proxies.txt
|
||||
https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt
|
||||
https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/socks4_proxies%20(1).txt
|
||||
https://raw.githubusercontent.com/yemixzy/proxy-list/main/proxies/socks4.txt
|
||||
https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/socks4.txt
|
||||
https://raw.githubusercontent.com/zevtyardt/proxy-list/main/socks4.txt
|
||||
https://raw.githubusercontent.com/zloi-user/hideip.me/main/socks4.txt
|
||||
https://www.proxy-list.download/api/v1/get?type=socks4
|
||||
|
||||
[SOCKS5]
|
||||
Enabled = yes
|
||||
Sources =
|
||||
https://api.proxyscrape.com/v2/?request=getproxies&protocol=socks5
|
||||
; https://openproxy.space/list/socks5
|
||||
https://openproxylist.xyz/socks5.txt
|
||||
https://proxyspace.pro/socks5.txt
|
||||
https://raw.githubusercontent.com/ALIILAPRO/Proxy/main/socks5.txt
|
||||
https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/socks5_proxies.txt
|
||||
https://raw.githubusercontent.com/B4RC0DE-TM/proxy-list/main/SOCKS5.txt
|
||||
https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/socks5.txt
|
||||
https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt
|
||||
https://raw.githubusercontent.com/iptotal/free-proxy-list/master/socks5.txt
|
||||
https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-socks5.txt
|
||||
https://raw.githubusercontent.com/manuGMG/proxy-365/main/SOCKS5.txt
|
||||
https://raw.githubusercontent.com/mmpx12/proxy-list/master/socks5.txt
|
||||
https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/socks5.txt
|
||||
https://raw.githubusercontent.com/NotUnko/autoproxies/main/socks5.txt
|
||||
https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/socks5/socks5.txt
|
||||
https://raw.githubusercontent.com/proxylist-to/proxy-list/main/socks5.txt
|
||||
https://raw.githubusercontent.com/prxchk/proxy-list/main/socks5.txt
|
||||
https://raw.githubusercontent.com/roosterkid/openproxylist/main/SOCKS5_RAW.txt
|
||||
https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/socks5.txt
|
||||
https://raw.githubusercontent.com/steamboatid/proxyout/main/proxy-socks5.txt
|
||||
https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/generated/socks5_proxies.txt
|
||||
https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt
|
||||
https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/socks5_proxies.txt
|
||||
https://raw.githubusercontent.com/yemixzy/proxy-list/main/proxies/socks5.txt
|
||||
https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/socks5.txt
|
||||
https://raw.githubusercontent.com/zevtyardt/proxy-list/main/socks5.txt
|
||||
https://raw.githubusercontent.com/zloi-user/hideip.me/main/socks5.txt
|
||||
https://www.proxy-list.download/api/v1/get?type=socks5
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
# The number of seconds to wait for a proxied request.
|
||||
# The higher the number, the longer the check will take
|
||||
# and the more proxies you get.
|
||||
timeout = 5
|
||||
|
||||
# The number of seconds to wait for the proxies to be downloaded from the source.
|
||||
source_timeout = 15
|
||||
|
||||
# Maximum concurrent connections.
|
||||
# Windows supports maximum of 512.
|
||||
# On *nix operating systems, this restriction is much looser.
|
||||
# The limit on *nix can be seen with the command `ulimit -Hn`.
|
||||
# Don't be in a hurry to set high values.
|
||||
# Make sure you have enough RAM first, gradually increasing the default value.
|
||||
# If set to 0, the maximum value available for your OS will be used.
|
||||
max_connections = 512
|
||||
|
||||
# URL to which to send a request to check the proxy.
|
||||
# If it is not httpbin or a service that returns an IP address,
|
||||
# it will be impossible to determine the anonymity and geolocation of the proxy.
|
||||
# Example:
|
||||
# check_website = "https://google.com"
|
||||
# check_website = "https://checkip.amazonaws.com"
|
||||
check_website = "https://httpbin.smp.io/ip"
|
||||
|
||||
# Set to false to sort proxies alphabetically.
|
||||
sort_by_speed = true
|
||||
|
||||
# Save the geolocation of the proxy exit-node.
|
||||
# Uses offline geolocation database.
|
||||
# Only works for json output.
|
||||
enable_geolocation = true
|
||||
|
||||
# Enable output of all errors. Significantly slows down proxy checking.
|
||||
debug = false
|
||||
|
||||
[output]
|
||||
# Path to the folder where the proxies will be saved.
|
||||
# Leave blank to save the proxies to the current directory.
|
||||
path = "./results"
|
||||
|
||||
json = true
|
||||
txt = true
|
||||
|
||||
# If you have sources with a protocol prefix, such as socks5://127.0.0.1:1050, you can add them to any of these sections.
|
||||
[http]
|
||||
enabled = true
|
||||
sources = [
|
||||
# Local proxy list example
|
||||
# "./http_proxies.txt",
|
||||
"https://api.proxyscrape.com/v2/?request=getproxies&protocol=http",
|
||||
"https://openproxy.space/list/http",
|
||||
"https://openproxylist.xyz/http.txt",
|
||||
"https://proxyspace.pro/http.txt",
|
||||
"https://proxyspace.pro/https.txt",
|
||||
"https://raw.githubusercontent.com/ALIILAPRO/Proxy/main/http.txt",
|
||||
"https://raw.githubusercontent.com/andigwandi/free-proxy/main/proxy_list.txt",
|
||||
"https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/http_proxies.txt",
|
||||
"https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/https_proxies.txt",
|
||||
"https://raw.githubusercontent.com/aslisk/proxyhttps/main/https.txt",
|
||||
"https://raw.githubusercontent.com/B4RC0DE-TM/proxy-list/main/HTTP.txt",
|
||||
"https://raw.githubusercontent.com/clarketm/proxy-list/master/proxy-list-raw.txt",
|
||||
"https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt",
|
||||
"https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt",
|
||||
"https://raw.githubusercontent.com/hendrikbgr/Free-Proxy-Repo/master/proxy_list.txt",
|
||||
"https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-http.txt",
|
||||
"https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-https.txt",
|
||||
"https://raw.githubusercontent.com/mmpx12/proxy-list/master/http.txt",
|
||||
"https://raw.githubusercontent.com/mmpx12/proxy-list/master/https.txt",
|
||||
"https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt",
|
||||
"https://raw.githubusercontent.com/NotUnko/autoproxies/main/http.txt",
|
||||
"https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/http/http.txt",
|
||||
"https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/https/https.txt",
|
||||
"https://raw.githubusercontent.com/proxy4parsing/proxy-list/main/http.txt",
|
||||
"https://raw.githubusercontent.com/proxylist-to/proxy-list/main/http.txt",
|
||||
"https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt",
|
||||
"https://raw.githubusercontent.com/roosterkid/openproxylist/main/HTTPS_RAW.txt",
|
||||
"https://raw.githubusercontent.com/saisuiu/Lionkings-Http-Proxys-Proxies/main/free.txt",
|
||||
"https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/http.txt",
|
||||
"https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/https.txt",
|
||||
"https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/generated/http_proxies.txt",
|
||||
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt",
|
||||
"https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/http_proxies%20(2).txt",
|
||||
"https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/http_proxies%20(3).txt",
|
||||
"https://raw.githubusercontent.com/yemixzy/proxy-list/main/proxies/http.txt",
|
||||
"https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/http.txt",
|
||||
"https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/https.txt",
|
||||
"https://raw.githubusercontent.com/zevtyardt/proxy-list/main/http.txt",
|
||||
"https://raw.githubusercontent.com/zloi-user/hideip.me/main/http.txt",
|
||||
"https://raw.githubusercontent.com/zloi-user/hideip.me/main/https.txt",
|
||||
"https://rootjazz.com/proxies/proxies.txt",
|
||||
"https://www.proxy-list.download/api/v1/get?type=http",
|
||||
"https://www.proxy-list.download/api/v1/get?type=https",
|
||||
]
|
||||
|
||||
[socks4]
|
||||
enabled = true
|
||||
sources = [
|
||||
# Local proxy list example
|
||||
# "./socks4_proxies.txt",
|
||||
"https://api.proxyscrape.com/v2/?request=getproxies&protocol=socks4",
|
||||
"https://openproxy.space/list/socks4",
|
||||
"https://openproxylist.xyz/socks4.txt",
|
||||
"https://proxyspace.pro/socks4.txt",
|
||||
"https://raw.githubusercontent.com/ALIILAPRO/Proxy/main/socks4.txt",
|
||||
"https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/socks4_proxies.txt",
|
||||
"https://raw.githubusercontent.com/B4RC0DE-TM/proxy-list/main/SOCKS4.txt",
|
||||
"https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/socks4.txt",
|
||||
"https://raw.githubusercontent.com/iptotal/free-proxy-list/master/socks4.txt",
|
||||
"https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-socks4.txt",
|
||||
"https://raw.githubusercontent.com/mmpx12/proxy-list/master/socks4.txt",
|
||||
"https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/socks4.txt",
|
||||
"https://raw.githubusercontent.com/NotUnko/autoproxies/main/socks4.txt",
|
||||
"https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/socks4/socks4.txt",
|
||||
"https://raw.githubusercontent.com/proxylist-to/proxy-list/main/socks4.txt",
|
||||
"https://raw.githubusercontent.com/prxchk/proxy-list/main/socks4.txt",
|
||||
"https://raw.githubusercontent.com/roosterkid/openproxylist/main/SOCKS4_RAW.txt",
|
||||
"https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/socks4.txt",
|
||||
"https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/generated/socks4_proxies.txt",
|
||||
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt",
|
||||
"https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/socks4_proxies%20(1).txt",
|
||||
"https://raw.githubusercontent.com/yemixzy/proxy-list/main/proxies/socks4.txt",
|
||||
"https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/socks4.txt",
|
||||
"https://raw.githubusercontent.com/zevtyardt/proxy-list/main/socks4.txt",
|
||||
"https://raw.githubusercontent.com/zloi-user/hideip.me/main/socks4.txt",
|
||||
"https://www.proxy-list.download/api/v1/get?type=socks4",
|
||||
]
|
||||
|
||||
[socks5]
|
||||
enabled = true
|
||||
sources = [
|
||||
# Local proxy list example
|
||||
# "./socks5_proxies.txt",
|
||||
"https://api.proxyscrape.com/v2/?request=getproxies&protocol=socks5",
|
||||
"https://openproxy.space/list/socks5",
|
||||
"https://openproxylist.xyz/socks5.txt",
|
||||
"https://proxyspace.pro/socks5.txt",
|
||||
"https://raw.githubusercontent.com/ALIILAPRO/Proxy/main/socks5.txt",
|
||||
"https://raw.githubusercontent.com/Anonym0usWork1221/Free-Proxies/main/proxy_files/socks5_proxies.txt",
|
||||
"https://raw.githubusercontent.com/B4RC0DE-TM/proxy-list/main/SOCKS5.txt",
|
||||
"https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/socks5.txt",
|
||||
"https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt",
|
||||
"https://raw.githubusercontent.com/iptotal/free-proxy-list/master/socks5.txt",
|
||||
"https://raw.githubusercontent.com/jetkai/proxy-list/main/online-proxies/txt/proxies-socks5.txt",
|
||||
"https://raw.githubusercontent.com/manuGMG/proxy-365/main/SOCKS5.txt",
|
||||
"https://raw.githubusercontent.com/mmpx12/proxy-list/master/socks5.txt",
|
||||
"https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/socks5.txt",
|
||||
"https://raw.githubusercontent.com/NotUnko/autoproxies/main/socks5.txt",
|
||||
"https://raw.githubusercontent.com/officialputuid/KangProxy/KangProxy/socks5/socks5.txt",
|
||||
"https://raw.githubusercontent.com/proxylist-to/proxy-list/main/socks5.txt",
|
||||
"https://raw.githubusercontent.com/prxchk/proxy-list/main/socks5.txt",
|
||||
"https://raw.githubusercontent.com/roosterkid/openproxylist/main/SOCKS5_RAW.txt",
|
||||
"https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/socks5.txt",
|
||||
"https://raw.githubusercontent.com/steamboatid/proxyout/main/proxy-socks5.txt",
|
||||
"https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/generated/socks5_proxies.txt",
|
||||
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt",
|
||||
"https://raw.githubusercontent.com/TylerAmesIsGay/proxy-list/main/socks5_proxies.txt",
|
||||
"https://raw.githubusercontent.com/yemixzy/proxy-list/main/proxies/socks5.txt",
|
||||
"https://raw.githubusercontent.com/Zaeem20/FREE_PROXIES_LIST/master/socks5.txt",
|
||||
"https://raw.githubusercontent.com/zevtyardt/proxy-list/main/socks5.txt",
|
||||
"https://raw.githubusercontent.com/zloi-user/hideip.me/main/socks5.txt",
|
||||
"https://www.proxy-list.download/api/v1/get?type=socks5",
|
||||
]
|
||||
@@ -11,6 +11,4 @@ fi &&
|
||||
curl -fsSLo "${download_path}" 'https://github.com/monosans/proxy-scraper-checker/archive/refs/heads/main.zip' &&
|
||||
unzip -d "${base_path}" "${download_path}" &&
|
||||
mv "${path}-main" "${path}" &&
|
||||
python3 -m pip install -U --no-cache-dir --disable-pip-version-check setuptools wheel &&
|
||||
python3 -m pip install -U --no-cache-dir --disable-pip-version-check -r "${path}/requirements-termux.txt" &&
|
||||
printf "proxy-scraper-checker installed successfully.\nRun 'cd %s && sh start-termux.sh'.\n" "${path}"
|
||||
|
||||
Generated
+1284
File diff suppressed because it is too large
Load Diff
@@ -2,20 +2,42 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING, Dict, Mapping
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
import rich.traceback
|
||||
from aiohttp import ClientSession, TCPConnector
|
||||
from aiohttp_socks import ProxyType
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn
|
||||
from rich.table import Table
|
||||
|
||||
from .proxy_scraper_checker import ProxyScraperChecker
|
||||
from . import cache, checker, geodb, http, output, scraper, sort, utils
|
||||
from .settings import Settings
|
||||
from .storage import ProxyStorage
|
||||
from .typing_compat import Any
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
# Help users on older alphas
|
||||
if not TYPE_CHECKING:
|
||||
import tomli as tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_event_loop_policy() -> None:
|
||||
if sys.platform == "win32":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
elif sys.implementation.name == "cpython" and sys.platform in {
|
||||
elif platform.python_implementation() == "CPython" and sys.platform in {
|
||||
"darwin",
|
||||
"linux",
|
||||
}:
|
||||
@@ -27,7 +49,13 @@ def set_event_loop_policy() -> None:
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
|
||||
|
||||
def configure_logging(console: Console, *, debug: bool) -> None:
|
||||
async def read_config(file: str, /) -> Dict[str, Any]:
|
||||
async with aiofiles.open(file, "rb") as f:
|
||||
content = await f.read()
|
||||
return tomllib.loads(utils.bytes_decode(content))
|
||||
|
||||
|
||||
def configure_logging(*, console: Console, debug: bool) -> None:
|
||||
rich.traceback.install(
|
||||
console=console, width=None, extra_lines=0, word_wrap=True
|
||||
)
|
||||
@@ -47,19 +75,80 @@ def configure_logging(console: Console, *, debug: bool) -> None:
|
||||
)
|
||||
|
||||
|
||||
def get_config(file: str) -> ConfigParser:
|
||||
cfg = ConfigParser(interpolation=None)
|
||||
cfg.read(file, encoding="utf-8")
|
||||
return cfg
|
||||
def get_summary_table(
|
||||
*, before: Mapping[ProxyType, int], after: Mapping[ProxyType, int]
|
||||
) -> Table:
|
||||
table = Table()
|
||||
table.add_column("Protocol", style="cyan")
|
||||
table.add_column("Working", style="magenta")
|
||||
table.add_column("Total", style="green")
|
||||
for proto in sort.PROTOCOL_ORDER:
|
||||
if total := before.get(proto):
|
||||
working = after.get(proto, 0)
|
||||
percentage = working / total
|
||||
table.add_row(
|
||||
proto.name, f"{working} ({percentage:.1%})", str(total)
|
||||
)
|
||||
return table
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
cfg = get_config("config.ini")
|
||||
|
||||
cfg = await read_config("config.toml")
|
||||
console = Console()
|
||||
configure_logging(console, debug=cfg["General"].getboolean("Debug", False))
|
||||
configure_logging(console=console, debug=cfg["debug"])
|
||||
|
||||
await ProxyScraperChecker.from_configparser(cfg, console=console).run()
|
||||
storage = ProxyStorage()
|
||||
|
||||
async with ClientSession(
|
||||
connector=TCPConnector(ssl=http.SSL_CONTEXT),
|
||||
headers=http.HEADERS,
|
||||
cookie_jar=http.get_cookie_jar(),
|
||||
fallback_charset_resolver=http.fallback_charset_resolver,
|
||||
) as session:
|
||||
settings = await Settings.from_mapping(cfg, session=session)
|
||||
with Progress(
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
MofNCompleteColumn(),
|
||||
console=console,
|
||||
transient=True,
|
||||
) as progress:
|
||||
scrape = scraper.scrape_all(
|
||||
progress=progress,
|
||||
session=session,
|
||||
settings=settings,
|
||||
storage=storage,
|
||||
)
|
||||
await (
|
||||
asyncio.gather(
|
||||
cache.create(),
|
||||
geodb.download_geodb(progress=progress, session=session),
|
||||
scrape,
|
||||
)
|
||||
if settings.enable_geolocation
|
||||
else scrape
|
||||
)
|
||||
await session.close()
|
||||
count_before_checking = storage.get_count()
|
||||
await checker.check_all(
|
||||
settings=settings,
|
||||
storage=storage,
|
||||
progress=progress,
|
||||
proxies_count=count_before_checking,
|
||||
)
|
||||
|
||||
count_after_checking = storage.get_count()
|
||||
console.print(
|
||||
get_summary_table(
|
||||
before=count_before_checking, after=count_after_checking
|
||||
)
|
||||
)
|
||||
|
||||
await output.save_proxies(storage=storage, settings=settings)
|
||||
|
||||
logger.info(
|
||||
"Thank you for using https://github.com/monosans/proxy-scraper-checker"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import aiofiles.os
|
||||
import platformdirs
|
||||
|
||||
DIR = platformdirs.user_cache_dir("proxy_scraper_checker")
|
||||
READY_EVENT = asyncio.Event()
|
||||
|
||||
|
||||
async def create() -> None:
|
||||
await aiofiles.os.makedirs(DIR, exist_ok=True)
|
||||
READY_EVENT.set()
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from random import shuffle
|
||||
from typing import Mapping
|
||||
|
||||
from aiohttp_socks import ProxyType
|
||||
from rich.progress import Progress, TaskID
|
||||
|
||||
from . import sort
|
||||
from .proxy import Proxy
|
||||
from .settings import Settings
|
||||
from .storage import ProxyStorage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def check_one(
|
||||
*,
|
||||
progress: Progress,
|
||||
proxy: Proxy,
|
||||
settings: Settings,
|
||||
storage: ProxyStorage,
|
||||
task: TaskID,
|
||||
) -> None:
|
||||
try:
|
||||
await proxy.check(settings=settings)
|
||||
except Exception as e:
|
||||
# Too many open files
|
||||
if isinstance(e, OSError) and e.errno == 24: # noqa: PLR2004
|
||||
logger.error("Please, set max_connections to lower value")
|
||||
|
||||
logger.debug(
|
||||
"%s.%s: %s", e.__class__.__module__, e.__class__.__qualname__, e
|
||||
)
|
||||
storage.remove(proxy)
|
||||
progress.update(task, advance=1)
|
||||
|
||||
|
||||
async def check_all(
|
||||
*,
|
||||
settings: Settings,
|
||||
storage: ProxyStorage,
|
||||
progress: Progress,
|
||||
proxies_count: Mapping[ProxyType, int],
|
||||
) -> None:
|
||||
tasks = {
|
||||
proto: progress.add_task(
|
||||
f"[yellow]Checker [red]:: [green]{proto.name}",
|
||||
total=proxies_count[proto],
|
||||
)
|
||||
for proto in sort.PROTOCOL_ORDER
|
||||
if proto in storage.enabled_protocols
|
||||
}
|
||||
coroutines = [
|
||||
check_one(
|
||||
progress=progress,
|
||||
proxy=proxy,
|
||||
settings=settings,
|
||||
storage=storage,
|
||||
task=tasks[proxy.protocol],
|
||||
)
|
||||
for proxy in storage
|
||||
]
|
||||
shuffle(coroutines)
|
||||
await asyncio.gather(*coroutines)
|
||||
@@ -1,12 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import MappingProxyType
|
||||
|
||||
from aiohttp import hdrs
|
||||
|
||||
DEFAULT_CHECK_WEBSITE = "http://ip-api.com/json/?fields=8217"
|
||||
HEADERS: MappingProxyType[str, str] = MappingProxyType({
|
||||
hdrs.USER_AGENT: (
|
||||
"Mozilla/5.0 (Windows NT 10.0; rv:121.0) Gecko/20100101 Firefox/121.0"
|
||||
)
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
|
||||
|
||||
@dataclass(repr=False, eq=False)
|
||||
class Folder:
|
||||
__slots__ = ("path", "is_enabled", "for_anonymous", "for_geolocation")
|
||||
|
||||
path: Path
|
||||
is_enabled: bool
|
||||
for_anonymous: bool
|
||||
for_geolocation: bool
|
||||
|
||||
def remove(self) -> None:
|
||||
try:
|
||||
rmtree(self.path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def create(self) -> None:
|
||||
self.path.mkdir(parents=True, exist_ok=True)
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.ospath
|
||||
from aiohttp import ClientResponse, ClientSession, hdrs
|
||||
from rich.progress import Progress, TaskID
|
||||
|
||||
from . import cache
|
||||
from .utils import bytes_decode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GEODB_URL = "https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/download/GeoLite2-City.mmdb"
|
||||
GEODB_PATH = Path(cache.DIR, "geolocation_database.mmdb")
|
||||
GEODB_ETAG_PATH = GEODB_PATH.with_suffix(".mmdb.etag")
|
||||
|
||||
|
||||
async def _read_etag() -> Optional[str]:
|
||||
try:
|
||||
async with aiofiles.open(GEODB_ETAG_PATH, "rb") as etag_file:
|
||||
content = await etag_file.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return bytes_decode(content)
|
||||
|
||||
|
||||
async def _save_etag(etag: str, /) -> None:
|
||||
async with aiofiles.open(
|
||||
GEODB_ETAG_PATH, "w", encoding="utf-8"
|
||||
) as etag_file:
|
||||
await etag_file.write(etag)
|
||||
|
||||
|
||||
async def _save_geodb(
|
||||
*, progress: Progress, response: ClientResponse, task: TaskID
|
||||
) -> None:
|
||||
async with aiofiles.open(GEODB_PATH, "wb") as geodb:
|
||||
async for chunk in response.content.iter_any():
|
||||
await geodb.write(chunk)
|
||||
progress.update(task, advance=len(chunk))
|
||||
|
||||
|
||||
async def download_geodb(*, progress: Progress, session: ClientSession) -> None:
|
||||
headers = (
|
||||
{hdrs.IF_NONE_MATCH: current_etag}
|
||||
if await aiofiles.ospath.exists(GEODB_PATH)
|
||||
and (current_etag := await _read_etag())
|
||||
else None
|
||||
)
|
||||
|
||||
async with session.get(
|
||||
GEODB_URL, headers=headers, raise_for_status=True
|
||||
) as response:
|
||||
if response.status == 304: # noqa: PLR2004
|
||||
logger.info(
|
||||
"Latest geolocation database is already cached at %s",
|
||||
GEODB_PATH,
|
||||
)
|
||||
return
|
||||
await cache.READY_EVENT.wait()
|
||||
await _save_geodb(
|
||||
progress=progress,
|
||||
response=response,
|
||||
task=progress.add_task(
|
||||
"[yellow]Downloader[red] :: [green]GeoDB",
|
||||
total=response.content_length,
|
||||
),
|
||||
)
|
||||
logger.info("Downloaded geolocation database to %s", GEODB_PATH)
|
||||
|
||||
if etag := response.headers.get(hdrs.ETAG):
|
||||
await _save_etag(etag)
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
from functools import lru_cache
|
||||
from types import MappingProxyType
|
||||
|
||||
import certifi
|
||||
import charset_normalizer
|
||||
from aiohttp import ClientResponse, DummyCookieJar, hdrs
|
||||
|
||||
SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
|
||||
HEADERS: MappingProxyType[str, str] = MappingProxyType({
|
||||
hdrs.USER_AGENT: (
|
||||
"Mozilla/5.0 (Windows NT 10.0; rv:121.0) Gecko/20100101 Firefox/121.0"
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@lru_cache(None)
|
||||
def get_cookie_jar() -> DummyCookieJar:
|
||||
return DummyCookieJar()
|
||||
|
||||
|
||||
def fallback_charset_resolver(r: ClientResponse, b: bytes) -> str: # noqa: ARG001
|
||||
return charset_normalizer.from_bytes(b)[0].encoding
|
||||
@@ -1,9 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class AsyncNullContext:
|
||||
class NullContext:
|
||||
__slots__ = ()
|
||||
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
|
||||
def __exit__(self, *_: object) -> None:
|
||||
pass
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from shutil import rmtree
|
||||
from typing import Sequence, Union
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
import aiofiles.ospath
|
||||
import maxminddb
|
||||
|
||||
from . import sort
|
||||
from .geodb import GEODB_PATH
|
||||
from .null_context import NullContext
|
||||
from .proxy import Proxy
|
||||
from .settings import Settings
|
||||
from .storage import ProxyStorage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _create_proxy_list_str(
|
||||
*, anonymous_only: bool, include_protocol: bool, proxies: Sequence[Proxy]
|
||||
) -> str:
|
||||
return "\n".join(
|
||||
proxy.as_str(include_protocol=include_protocol)
|
||||
for proxy in proxies
|
||||
if not anonymous_only or proxy.host != proxy.exit_ip
|
||||
)
|
||||
|
||||
|
||||
@aiofiles.ospath.wrap
|
||||
def save_proxies(*, settings: Settings, storage: ProxyStorage) -> None:
|
||||
if settings.output_json:
|
||||
mmdb: Union[maxminddb.Reader, NullContext] = (
|
||||
maxminddb.open_database(GEODB_PATH)
|
||||
if settings.enable_geolocation
|
||||
else NullContext()
|
||||
)
|
||||
settings.output_path.mkdir(parents=True, exist_ok=True)
|
||||
with mmdb as mmdb_reader:
|
||||
proxy_dicts = [
|
||||
{
|
||||
"protocol": proxy.protocol.name.lower(),
|
||||
"username": proxy.username,
|
||||
"password": proxy.password,
|
||||
"host": proxy.host,
|
||||
"port": proxy.port,
|
||||
"exit_ip": proxy.exit_ip,
|
||||
"timeout": round(proxy.timeout, 2),
|
||||
"geolocation": mmdb_reader.get(proxy.exit_ip)
|
||||
if mmdb_reader is not None
|
||||
else None,
|
||||
}
|
||||
for proxy in sorted(storage, key=sort.timeout_sort_key)
|
||||
]
|
||||
with (settings.output_path / "proxies.json").open(
|
||||
"w", encoding="utf-8"
|
||||
) as f:
|
||||
json.dump(
|
||||
proxy_dicts, f, ensure_ascii=False, separators=(",", ":")
|
||||
)
|
||||
with (settings.output_path / "proxies_pretty.json").open(
|
||||
"w", encoding="utf-8"
|
||||
) as f:
|
||||
json.dump(proxy_dicts, f, ensure_ascii=False, indent="\t")
|
||||
if settings.output_txt:
|
||||
sorted_proxies = storage.get_sorted(key=settings.sorting_key)
|
||||
grouped_proxies = tuple(
|
||||
(k, sorted(v, key=settings.sorting_key))
|
||||
for k, v in storage.get_grouped().items()
|
||||
)
|
||||
for folder, anonymous_only in (
|
||||
(settings.output_path / "proxies", False),
|
||||
(settings.output_path / "proxies_anonymous", True),
|
||||
):
|
||||
try:
|
||||
rmtree(folder)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
text = _create_proxy_list_str(
|
||||
proxies=sorted_proxies,
|
||||
anonymous_only=anonymous_only,
|
||||
include_protocol=True,
|
||||
)
|
||||
(folder / "all.txt").write_text(text, encoding="utf-8")
|
||||
for proto, proxies in grouped_proxies:
|
||||
text = _create_proxy_list_str(
|
||||
proxies=proxies,
|
||||
anonymous_only=anonymous_only,
|
||||
include_protocol=False,
|
||||
)
|
||||
(folder / f"{proto.name.lower()}.txt").write_text(
|
||||
text, encoding="utf-8"
|
||||
)
|
||||
logger.info(
|
||||
"Proxies have been saved to %s", settings.output_path.absolute()
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
PROXY_REGEX = re.compile(
|
||||
r"(?:^|[^\.\/\d:@A-Za-z])(?:(?P<protocol>https?|socks[45]):\/\/)?(?:(?P<username>[\dA-Za-z]*):(?P<password>[\dA-Za-z]*)@)?(?P<host>(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])(?:\.(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])){3}):(?P<port>\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])(?=[^\.\/\d:@A-Za-z]|$)",
|
||||
flags=re.MULTILINE | re.IGNORECASE,
|
||||
)
|
||||
IPV4_REGEX = re.compile(
|
||||
r"^\s*(?P<host>(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])(?:\.(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])){3})(?::(?:\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]))?\s*$"
|
||||
)
|
||||
|
||||
|
||||
def parse_ipv4(s: str, /) -> str:
|
||||
"""Parse ip from ip:port."""
|
||||
match = IPV4_REGEX.match(s)
|
||||
if not match:
|
||||
raise ValueError
|
||||
return match.group("host")
|
||||
@@ -1,59 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from time import perf_counter
|
||||
from typing import Union
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from aiohttp.abc import AbstractCookieJar
|
||||
import attrs
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp_socks import ProxyConnector, ProxyType
|
||||
|
||||
from .constants import HEADERS
|
||||
from .null_context import AsyncNullContext
|
||||
from .http import (
|
||||
HEADERS,
|
||||
SSL_CONTEXT,
|
||||
fallback_charset_resolver,
|
||||
get_cookie_jar,
|
||||
)
|
||||
from .parsers import parse_ipv4
|
||||
from .settings import CheckWebsiteType, Settings
|
||||
|
||||
|
||||
@dataclass(repr=False, unsafe_hash=True)
|
||||
@attrs.define(
|
||||
repr=False,
|
||||
unsafe_hash=True,
|
||||
weakref_slot=False,
|
||||
kw_only=True,
|
||||
eq=False,
|
||||
getstate_setstate=False,
|
||||
match_args=False,
|
||||
)
|
||||
class Proxy:
|
||||
__slots__ = ("geolocation", "host", "is_anonymous", "port", "timeout")
|
||||
|
||||
protocol: ProxyType
|
||||
host: str
|
||||
port: int
|
||||
username: Optional[str]
|
||||
password: Optional[str]
|
||||
timeout: float = attrs.field(hash=False, init=False)
|
||||
exit_ip: str = attrs.field(hash=False, init=False)
|
||||
|
||||
async def check(
|
||||
self,
|
||||
*,
|
||||
website: str,
|
||||
sem: Union[asyncio.Semaphore, AsyncNullContext],
|
||||
cookie_jar: AbstractCookieJar,
|
||||
proto: ProxyType,
|
||||
timeout: ClientTimeout,
|
||||
set_geolocation: bool,
|
||||
) -> None:
|
||||
async with sem:
|
||||
async def check(self, *, settings: Settings) -> None:
|
||||
async with settings.semaphore:
|
||||
start = perf_counter()
|
||||
connector = ProxyConnector(
|
||||
proxy_type=proto, host=self.host, port=self.port
|
||||
proxy_type=self.protocol,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
ssl=SSL_CONTEXT,
|
||||
)
|
||||
async with ClientSession(
|
||||
connector=connector,
|
||||
cookie_jar=cookie_jar,
|
||||
timeout=timeout,
|
||||
headers=HEADERS,
|
||||
cookie_jar=get_cookie_jar(),
|
||||
timeout=settings.timeout,
|
||||
fallback_charset_resolver=fallback_charset_resolver,
|
||||
) as session, session.get(
|
||||
website, raise_for_status=True
|
||||
settings.check_website, raise_for_status=True
|
||||
) as response:
|
||||
if set_geolocation:
|
||||
await response.read()
|
||||
await response.read()
|
||||
self.timeout = perf_counter() - start
|
||||
if set_geolocation:
|
||||
data = await response.json(content_type=None)
|
||||
self.is_anonymous = self.host != data["query"]
|
||||
self.geolocation = (
|
||||
f"|{data['country']}|{data['regionName']}|{data['city']}"
|
||||
)
|
||||
if settings.check_website_type == CheckWebsiteType.HTTPBIN_IP:
|
||||
r = await response.json(content_type=None)
|
||||
self.exit_ip = r["origin"]
|
||||
elif settings.check_website_type == CheckWebsiteType.PLAIN_IP:
|
||||
self.exit_ip = parse_ipv4(await response.text())
|
||||
|
||||
def as_str(self, *, include_geolocation: bool) -> str:
|
||||
if include_geolocation:
|
||||
return f"{self.host}:{self.port}{self.geolocation}"
|
||||
return f"{self.host}:{self.port}"
|
||||
def as_str(self, *, include_protocol: bool) -> str:
|
||||
with StringIO() as buf:
|
||||
if include_protocol:
|
||||
buf.write(f"{self.protocol.name.lower()}://")
|
||||
if self.username is not None and self.password is not None:
|
||||
buf.write(f"{self.username}:{self.password}@")
|
||||
buf.write(f"{self.host}:{self.port}")
|
||||
return buf.getvalue()
|
||||
|
||||
@@ -1,441 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
from random import shuffle
|
||||
from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
FrozenSet,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout, DummyCookieJar
|
||||
from aiohttp_socks import ProxyType
|
||||
from rich.console import Console
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
MofNCompleteColumn,
|
||||
Progress,
|
||||
TaskID,
|
||||
TextColumn,
|
||||
)
|
||||
from rich.table import Table
|
||||
from typing_extensions import Self
|
||||
|
||||
from . import sort, validators
|
||||
from .constants import DEFAULT_CHECK_WEBSITE, HEADERS
|
||||
from .folder import Folder
|
||||
from .null_context import AsyncNullContext
|
||||
from .proxy import Proxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProxyScraperChecker:
|
||||
"""HTTP, SOCKS4, SOCKS5 proxies scraper and checker."""
|
||||
|
||||
__slots__ = (
|
||||
"check_website",
|
||||
"console",
|
||||
"cookie_jar",
|
||||
"folders",
|
||||
"geolocation_enabled",
|
||||
"path",
|
||||
"proxies_count",
|
||||
"proxies",
|
||||
"regex",
|
||||
"sem",
|
||||
"sort_by_speed",
|
||||
"source_timeout",
|
||||
"sources",
|
||||
"timeout",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
timeout: float,
|
||||
source_timeout: float,
|
||||
max_connections: int,
|
||||
check_website: str,
|
||||
sort_by_speed: bool,
|
||||
save_path: Path,
|
||||
folders: Iterable[Folder],
|
||||
sources: Dict[ProxyType, Optional[str]],
|
||||
console: Optional[Console] = None,
|
||||
) -> None:
|
||||
"""HTTP, SOCKS4, SOCKS5 proxies scraper and checker.
|
||||
|
||||
Args:
|
||||
timeout: The number of seconds to wait for a proxied request.
|
||||
The higher the number, the longer the check will take and the
|
||||
more proxies you get.
|
||||
source_timeout: The number of seconds to wait for the proxies to be
|
||||
downloaded from the source.
|
||||
max_connections: Maximum concurrent connections.
|
||||
Windows supports maximum of 512.
|
||||
On *nix operating systems, this restriction is much looser.
|
||||
The limit on *nix can be seen with the command `ulimit -Hn`.
|
||||
Don't be in a hurry to set high values.
|
||||
Make sure you have enough RAM first, gradually increasing the
|
||||
default value.
|
||||
If set to 0, the maximum value available for your OS will be
|
||||
used.
|
||||
check_website: URL to which to send a request to check the proxy.
|
||||
If not equal to 'default', it will not be possible to determine
|
||||
the anonymity and geolocation of the proxies.
|
||||
sort_by_speed: Set to False to sort proxies alphabetically.
|
||||
save_path: Path to the folder where the proxy folders will be
|
||||
saved.
|
||||
Leave empty to save the proxies to the current directory.
|
||||
"""
|
||||
validators.timeout(timeout)
|
||||
self.timeout = ClientTimeout(total=timeout, sock_connect=float("inf"))
|
||||
|
||||
validators.source_timeout(source_timeout)
|
||||
self.source_timeout = source_timeout
|
||||
|
||||
max_conn = validators.max_connections(max_connections)
|
||||
self.sem: Union[asyncio.Semaphore, AsyncNullContext] = (
|
||||
asyncio.Semaphore(max_conn) if max_conn else AsyncNullContext()
|
||||
)
|
||||
|
||||
self.check_website = check_website
|
||||
self.sort_by_speed = sort_by_speed
|
||||
self.path = save_path
|
||||
self.folders = folders
|
||||
|
||||
if self.check_website == "default":
|
||||
self.check_website = DEFAULT_CHECK_WEBSITE
|
||||
|
||||
if self.check_website == DEFAULT_CHECK_WEBSITE:
|
||||
validators.folders(self.folders)
|
||||
else:
|
||||
validators.check_website(check_website)
|
||||
logger.info(
|
||||
"CheckWebsite is not 'default', "
|
||||
"so it will not be possible to determine "
|
||||
"the anonymity and geolocation of the proxies"
|
||||
)
|
||||
for folder in self.folders:
|
||||
folder.is_enabled = (
|
||||
not folder.for_anonymous and not folder.for_geolocation
|
||||
)
|
||||
|
||||
self.geolocation_enabled = any(
|
||||
self.check_website == DEFAULT_CHECK_WEBSITE
|
||||
and folder.is_enabled
|
||||
and folder.for_geolocation
|
||||
for folder in self.folders
|
||||
)
|
||||
|
||||
self.sources: Dict[ProxyType, FrozenSet[str]] = {
|
||||
proto: frozenset(filter(None, sources.splitlines()))
|
||||
for proto, sources in sources.items()
|
||||
if sources
|
||||
}
|
||||
validators.sources(self.sources)
|
||||
self.proxies: Dict[ProxyType, Set[Proxy]] = {
|
||||
proto: set() for proto in self.sources
|
||||
}
|
||||
|
||||
self.console = console or Console()
|
||||
self.cookie_jar = DummyCookieJar()
|
||||
self.regex = re.compile(
|
||||
r"(?:^|\D)?("
|
||||
r"(?:[1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])" # 1-255
|
||||
+ r"\.(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])" * 3 # 0-255
|
||||
+ r"):"
|
||||
+ (
|
||||
r"(\d|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}"
|
||||
r"|65[0-4]\d{2}|655[0-2]\d|6553[0-5])"
|
||||
) # 0-65535
|
||||
+ r"(?:\D|$)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_configparser(
|
||||
cls, cfg: ConfigParser, *, console: Optional[Console] = None
|
||||
) -> Self:
|
||||
general = cfg["General"]
|
||||
folders = cfg["Folders"]
|
||||
http = cfg["HTTP"]
|
||||
socks4 = cfg["SOCKS4"]
|
||||
socks5 = cfg["SOCKS5"]
|
||||
save_path = Path(general.get("SavePath", ""))
|
||||
return cls(
|
||||
timeout=general.getfloat("Timeout", 5),
|
||||
source_timeout=general.getfloat("SourceTimeout", 15),
|
||||
max_connections=general.getint("MaxConnections", 512),
|
||||
check_website=general.get("CheckWebsite", "default"),
|
||||
sort_by_speed=general.getboolean("SortBySpeed", True),
|
||||
save_path=save_path,
|
||||
folders=(
|
||||
Folder(
|
||||
path=save_path / "proxies",
|
||||
is_enabled=folders.getboolean("proxies", True),
|
||||
for_anonymous=False,
|
||||
for_geolocation=False,
|
||||
),
|
||||
Folder(
|
||||
path=save_path / "proxies_anonymous",
|
||||
is_enabled=folders.getboolean("proxies_anonymous", True),
|
||||
for_anonymous=True,
|
||||
for_geolocation=False,
|
||||
),
|
||||
Folder(
|
||||
path=save_path / "proxies_geolocation",
|
||||
is_enabled=folders.getboolean("proxies_geolocation", True),
|
||||
for_anonymous=False,
|
||||
for_geolocation=True,
|
||||
),
|
||||
Folder(
|
||||
path=save_path / "proxies_geolocation_anonymous",
|
||||
is_enabled=folders.getboolean(
|
||||
"proxies_geolocation_anonymous", True
|
||||
),
|
||||
for_anonymous=True,
|
||||
for_geolocation=True,
|
||||
),
|
||||
),
|
||||
sources={
|
||||
ProxyType.HTTP: (
|
||||
http.get("Sources")
|
||||
if http.getboolean("Enabled", True)
|
||||
else None
|
||||
),
|
||||
ProxyType.SOCKS4: (
|
||||
socks4.get("Sources")
|
||||
if socks4.getboolean("Enabled", True)
|
||||
else None
|
||||
),
|
||||
ProxyType.SOCKS5: (
|
||||
socks5.get("Sources")
|
||||
if socks5.getboolean("Enabled", True)
|
||||
else None
|
||||
),
|
||||
},
|
||||
console=console,
|
||||
)
|
||||
|
||||
async def fetch_source(
|
||||
self,
|
||||
*,
|
||||
session: ClientSession,
|
||||
source: str,
|
||||
proto: ProxyType,
|
||||
progress: Progress,
|
||||
task: TaskID,
|
||||
) -> None:
|
||||
"""Get proxies from source.
|
||||
|
||||
Args:
|
||||
source: Proxy list URL.
|
||||
"""
|
||||
try:
|
||||
async with session.get(source) as response:
|
||||
await response.read()
|
||||
text = await response.text()
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("%s | Timed out", source)
|
||||
except Exception as e:
|
||||
e_str = str(e)
|
||||
args: Tuple[object, ...] = (
|
||||
(
|
||||
"%s | %s.%s (%s)",
|
||||
source,
|
||||
e.__class__.__module__,
|
||||
e.__class__.__qualname__,
|
||||
e_str,
|
||||
)
|
||||
if e_str
|
||||
else (
|
||||
"%s | %s.%s",
|
||||
source,
|
||||
e.__class__.__module__,
|
||||
e.__class__.__qualname__,
|
||||
)
|
||||
)
|
||||
logger.error(*args)
|
||||
else:
|
||||
proxies = self.regex.finditer(text)
|
||||
try:
|
||||
proxy = next(proxies)
|
||||
except StopIteration:
|
||||
args = (
|
||||
("%s | No proxies found", source)
|
||||
if response.status == 200 # noqa: PLR2004
|
||||
else ("%s | HTTP status code %d", source, response.status)
|
||||
)
|
||||
logger.warning(*args)
|
||||
else:
|
||||
proxies_set = self.proxies[proto]
|
||||
proxies_set.add(
|
||||
Proxy(host=proxy.group(1), port=int(proxy.group(2)))
|
||||
)
|
||||
for proxy in proxies:
|
||||
proxies_set.add(
|
||||
Proxy(host=proxy.group(1), port=int(proxy.group(2)))
|
||||
)
|
||||
progress.update(task, advance=1)
|
||||
|
||||
async def check_proxy(
|
||||
self,
|
||||
*,
|
||||
proxy: Proxy,
|
||||
proto: ProxyType,
|
||||
progress: Progress,
|
||||
task: TaskID,
|
||||
) -> None:
|
||||
"""Check if proxy is alive."""
|
||||
try:
|
||||
await proxy.check(
|
||||
website=self.check_website,
|
||||
sem=self.sem,
|
||||
cookie_jar=self.cookie_jar,
|
||||
proto=proto,
|
||||
timeout=self.timeout,
|
||||
set_geolocation=self.geolocation_enabled,
|
||||
)
|
||||
except Exception as e:
|
||||
# Too many open files
|
||||
if isinstance(e, OSError) and e.errno == 24: # noqa: PLR2004
|
||||
logger.error("Please, set MaxConnections to lower value.")
|
||||
|
||||
logger.debug(
|
||||
"%s.%s | %s",
|
||||
e.__class__.__module__,
|
||||
e.__class__.__qualname__,
|
||||
e,
|
||||
)
|
||||
self.proxies[proto].remove(proxy)
|
||||
progress.update(task, advance=1)
|
||||
|
||||
async def fetch_all_sources(self, progress: Progress) -> None:
|
||||
tasks = {
|
||||
proto: progress.add_task(
|
||||
f"[yellow]Scraper [red]:: [green]{proto.name}",
|
||||
total=len(sources),
|
||||
)
|
||||
for proto, sources in self.sources.items()
|
||||
}
|
||||
async with ClientSession(
|
||||
headers=HEADERS,
|
||||
cookie_jar=self.cookie_jar,
|
||||
timeout=ClientTimeout(total=self.source_timeout),
|
||||
) as session:
|
||||
coroutines = (
|
||||
self.fetch_source(
|
||||
session=session,
|
||||
source=source,
|
||||
proto=proto,
|
||||
progress=progress,
|
||||
task=tasks[proto],
|
||||
)
|
||||
for proto, sources in self.sources.items()
|
||||
for source in sources
|
||||
)
|
||||
await asyncio.gather(*coroutines)
|
||||
|
||||
self.proxies_count = {
|
||||
proto: len(proxies) for proto, proxies in self.proxies.items()
|
||||
}
|
||||
|
||||
async def check_all_proxies(self, progress: Progress) -> None:
|
||||
tasks = {
|
||||
proto: progress.add_task(
|
||||
f"[yellow]Checker [red]:: [green]{proto.name}",
|
||||
total=len(proxies),
|
||||
)
|
||||
for proto, proxies in self.proxies.items()
|
||||
}
|
||||
coroutines = [
|
||||
self.check_proxy(
|
||||
proxy=proxy, proto=proto, progress=progress, task=tasks[proto]
|
||||
)
|
||||
for proto, proxies in self.proxies.items()
|
||||
for proxy in proxies
|
||||
]
|
||||
shuffle(coroutines)
|
||||
await asyncio.gather(*coroutines)
|
||||
|
||||
def save_proxies(self) -> None:
|
||||
"""Delete old proxies and save new ones."""
|
||||
sorted_proxies = self.get_sorted_proxies().items()
|
||||
for folder in self.folders:
|
||||
folder.remove()
|
||||
for folder in self.folders:
|
||||
if not folder.is_enabled:
|
||||
continue
|
||||
folder.create()
|
||||
for proto, proxies in sorted_proxies:
|
||||
text = "\n".join(
|
||||
proxy.as_str(include_geolocation=folder.for_geolocation)
|
||||
for proxy in proxies
|
||||
if (proxy.is_anonymous if folder.for_anonymous else True)
|
||||
)
|
||||
file = folder.path / f"{proto.name.lower()}.txt"
|
||||
file.write_text(text, encoding="utf-8")
|
||||
logger.info(
|
||||
"Proxy folders have been created in the %s folder.",
|
||||
self.path.resolve(),
|
||||
)
|
||||
|
||||
async def run(self) -> None:
|
||||
with self._get_progress_bar() as progress:
|
||||
await self.fetch_all_sources(progress)
|
||||
await self.check_all_proxies(progress)
|
||||
|
||||
table = self._get_results_table()
|
||||
self.console.print(table)
|
||||
|
||||
self.save_proxies()
|
||||
|
||||
logger.info(
|
||||
"Thank you for using "
|
||||
"https://github.com/monosans/proxy-scraper-checker :)"
|
||||
)
|
||||
|
||||
def get_sorted_proxies(self) -> Dict[ProxyType, List[Proxy]]:
|
||||
key: Union[
|
||||
Callable[[Proxy], float], Callable[[Proxy], Tuple[int, ...]]
|
||||
] = (
|
||||
sort.timeout_sort_key
|
||||
if self.sort_by_speed
|
||||
else sort.natural_sort_key
|
||||
)
|
||||
return {
|
||||
proto: sorted(proxies, key=key)
|
||||
for proto, proxies in self.proxies.items()
|
||||
}
|
||||
|
||||
def _get_results_table(self) -> Table:
|
||||
table = Table()
|
||||
table.add_column("Protocol", style="cyan")
|
||||
table.add_column("Working", style="magenta")
|
||||
table.add_column("Total", style="green")
|
||||
for proto, proxies in self.proxies.items():
|
||||
working = len(proxies)
|
||||
total = self.proxies_count[proto]
|
||||
percentage = working / total if total else 0
|
||||
table.add_row(
|
||||
proto.name, f"{working} ({percentage:.1%})", str(total)
|
||||
)
|
||||
return table
|
||||
|
||||
def _get_progress_bar(self) -> Progress:
|
||||
return Progress(
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
MofNCompleteColumn(),
|
||||
console=self.console,
|
||||
)
|
||||
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
import aiofiles.ospath
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from aiohttp_socks import ProxyType
|
||||
from rich.progress import Progress, TaskID
|
||||
|
||||
from .parsers import PROXY_REGEX
|
||||
from .proxy import Proxy
|
||||
from .settings import Settings
|
||||
from .storage import ProxyStorage
|
||||
from .utils import bytes_decode, is_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def scrape_one(
|
||||
*,
|
||||
progress: Progress,
|
||||
proto: ProxyType,
|
||||
session: ClientSession,
|
||||
source: str,
|
||||
storage: ProxyStorage,
|
||||
task: TaskID,
|
||||
timeout: ClientTimeout,
|
||||
) -> None:
|
||||
try:
|
||||
if is_url(source):
|
||||
async with session.get(source, timeout=timeout) as response:
|
||||
await response.read()
|
||||
text = await response.text()
|
||||
else:
|
||||
response = None
|
||||
async with aiofiles.open(source, "rb") as f:
|
||||
content = await f.read()
|
||||
text = bytes_decode(content)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"%s | %s.%s: %s",
|
||||
source,
|
||||
e.__class__.__module__,
|
||||
e.__class__.__qualname__,
|
||||
e,
|
||||
)
|
||||
else:
|
||||
proxies = PROXY_REGEX.finditer(text)
|
||||
try:
|
||||
proxy = next(proxies)
|
||||
except StopIteration:
|
||||
if response and response.status != 200: # noqa: PLR2004
|
||||
logger.warning(
|
||||
"%s | HTTP status code %d", source, response.status
|
||||
)
|
||||
else:
|
||||
logger.warning("%s | No proxies found", source)
|
||||
else:
|
||||
for proxy in itertools.chain((proxy,), proxies): # noqa: B020
|
||||
try:
|
||||
protocol = ProxyType[
|
||||
"HTTP"
|
||||
if (p := proxy.group("protocol").upper()) == "HTTPS"
|
||||
else p
|
||||
]
|
||||
except AttributeError:
|
||||
protocol = proto
|
||||
storage.add(
|
||||
Proxy(
|
||||
protocol=protocol,
|
||||
host=proxy.group("host"),
|
||||
port=int(proxy.group("port")),
|
||||
username=proxy.group("username"),
|
||||
password=proxy.group("password"),
|
||||
)
|
||||
)
|
||||
progress.update(task, advance=1)
|
||||
|
||||
|
||||
async def scrape_all(
|
||||
*,
|
||||
progress: Progress,
|
||||
session: ClientSession,
|
||||
settings: Settings,
|
||||
storage: ProxyStorage,
|
||||
) -> None:
|
||||
tasks = {
|
||||
proto: progress.add_task(
|
||||
f"[yellow]Scraper [red]:: [green]{proto.name}", total=len(sources)
|
||||
)
|
||||
for proto, sources in settings.sources.items()
|
||||
}
|
||||
timeout = ClientTimeout(total=settings.source_timeout)
|
||||
coroutines = (
|
||||
scrape_one(
|
||||
progress=progress,
|
||||
proto=proto,
|
||||
session=session,
|
||||
source=source,
|
||||
storage=storage,
|
||||
task=tasks[proto],
|
||||
timeout=timeout,
|
||||
)
|
||||
for proto, sources in settings.sources.items()
|
||||
for source in sources
|
||||
)
|
||||
await asyncio.gather(*coroutines)
|
||||
@@ -0,0 +1,295 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
import math
|
||||
import sys
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Dict,
|
||||
FrozenSet,
|
||||
Iterable,
|
||||
Mapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import attrs
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from aiohttp_socks import ProxyType
|
||||
|
||||
from . import sort
|
||||
from .null_context import NullContext
|
||||
from .parsers import parse_ipv4
|
||||
from .typing_compat import Any, Literal, Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .proxy import Proxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_supported_max_connections() -> Optional[int]:
|
||||
if sys.platform == "win32":
|
||||
if isinstance(
|
||||
asyncio.get_event_loop_policy(),
|
||||
asyncio.WindowsSelectorEventLoopPolicy,
|
||||
):
|
||||
return 512
|
||||
return None
|
||||
import resource # noqa: PLC0415
|
||||
|
||||
soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||
logger.debug(
|
||||
"max_connections: soft limit = %d, hard limit = %d, infinity = %d",
|
||||
soft_limit,
|
||||
hard_limit,
|
||||
resource.RLIM_INFINITY,
|
||||
)
|
||||
if soft_limit != hard_limit:
|
||||
try:
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (hard_limit, hard_limit))
|
||||
except ValueError as e:
|
||||
logger.warning("Failed setting max_connections: %s", e)
|
||||
else:
|
||||
soft_limit = hard_limit
|
||||
if soft_limit == resource.RLIM_INFINITY:
|
||||
return None
|
||||
return soft_limit
|
||||
|
||||
|
||||
def _get_max_connections(value: int, /) -> Optional[int]:
|
||||
if value < 0:
|
||||
msg = "max_connections must be non-negative"
|
||||
raise ValueError(msg)
|
||||
max_supported = _get_supported_max_connections()
|
||||
if not value:
|
||||
logger.info("Using %d as max_connections value", max_supported or 0)
|
||||
return max_supported
|
||||
if not max_supported or value <= max_supported:
|
||||
return value
|
||||
logger.warning(
|
||||
"max_connections value is too high. "
|
||||
"Your OS supports a maximum of %d. "
|
||||
"The config value will be ignored and %d will be used.",
|
||||
max_supported,
|
||||
max_supported,
|
||||
)
|
||||
return max_supported
|
||||
|
||||
|
||||
def _semaphore_converter(
|
||||
value: int, /
|
||||
) -> Union[asyncio.Semaphore, NullContext]:
|
||||
v = _get_max_connections(value)
|
||||
return asyncio.Semaphore(v) if v else NullContext()
|
||||
|
||||
|
||||
def _timeout_converter(value: float, /) -> ClientTimeout:
|
||||
return ClientTimeout(total=value, sock_connect=math.inf)
|
||||
|
||||
|
||||
def _sources_converter(
|
||||
value: Mapping[ProxyType, Optional[Iterable[str]]], /
|
||||
) -> Dict[ProxyType, FrozenSet[str]]:
|
||||
return {
|
||||
proxy_type: frozenset(sources)
|
||||
for proxy_type, sources in value.items()
|
||||
if sources is not None
|
||||
}
|
||||
|
||||
|
||||
class CheckWebsiteType(enum.Enum):
|
||||
UNKNOWN = enum.auto()
|
||||
PLAIN_IP = enum.auto()
|
||||
"""https://checkip.amazonaws.com"""
|
||||
HTTPBIN_IP = enum.auto()
|
||||
"""https://httpbin.org/ip"""
|
||||
|
||||
@property
|
||||
def supports_geolocation(self) -> bool:
|
||||
return self != CheckWebsiteType.UNKNOWN
|
||||
|
||||
@property
|
||||
def supports_anonymity(self) -> bool:
|
||||
return self != CheckWebsiteType.UNKNOWN
|
||||
|
||||
|
||||
async def _get_check_website_type_and_real_ip(
|
||||
*, check_website: str, session: ClientSession
|
||||
) -> Union[
|
||||
Tuple[Literal[CheckWebsiteType.UNKNOWN], None],
|
||||
Tuple[Literal[CheckWebsiteType.PLAIN_IP, CheckWebsiteType.HTTPBIN_IP], str],
|
||||
]:
|
||||
try:
|
||||
async with session.get(check_website, raise_for_status=True) as r:
|
||||
await r.read()
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Error when opening check_website without proxy, it will be "
|
||||
"impossible to determine anonymity and geolocation of proxies"
|
||||
)
|
||||
return CheckWebsiteType.UNKNOWN, None
|
||||
try:
|
||||
content = await r.json(content_type=None)
|
||||
except JSONDecodeError:
|
||||
try:
|
||||
return CheckWebsiteType.PLAIN_IP, parse_ipv4(await r.text())
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
return CheckWebsiteType.HTTPBIN_IP, parse_ipv4(content["origin"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
pass
|
||||
logger.warning(
|
||||
"Check_website is not httpbin and does not return plain ip, so it will"
|
||||
" be impossible to determine the anonymity and geolocation of proxies"
|
||||
)
|
||||
return CheckWebsiteType.UNKNOWN, None
|
||||
|
||||
|
||||
@attrs.define(
|
||||
repr=False,
|
||||
weakref_slot=False,
|
||||
kw_only=True,
|
||||
eq=False,
|
||||
getstate_setstate=False,
|
||||
match_args=False,
|
||||
)
|
||||
class Settings:
|
||||
check_website: str = attrs.field(
|
||||
validator=attrs.validators.instance_of(str)
|
||||
)
|
||||
check_website_type: CheckWebsiteType = attrs.field(
|
||||
validator=attrs.validators.instance_of(CheckWebsiteType)
|
||||
)
|
||||
enable_geolocation: bool = attrs.field(
|
||||
validator=attrs.validators.instance_of(bool)
|
||||
)
|
||||
output_json: bool = attrs.field(
|
||||
validator=attrs.validators.instance_of(bool)
|
||||
)
|
||||
output_path: Path = attrs.field(converter=Path)
|
||||
output_txt: bool = attrs.field(validator=attrs.validators.instance_of(bool))
|
||||
real_ip: Optional[str] = attrs.field(
|
||||
validator=attrs.validators.optional(attrs.validators.instance_of(str))
|
||||
)
|
||||
semaphore: Union[asyncio.Semaphore, NullContext] = attrs.field(
|
||||
converter=_semaphore_converter
|
||||
)
|
||||
sort_by_speed: bool = attrs.field(
|
||||
validator=attrs.validators.instance_of(bool)
|
||||
)
|
||||
source_timeout: float = attrs.field(validator=attrs.validators.gt(0))
|
||||
sources: Dict[ProxyType, FrozenSet[str]] = attrs.field(
|
||||
validator=attrs.validators.and_(
|
||||
attrs.validators.instance_of(dict),
|
||||
attrs.validators.min_len(1),
|
||||
attrs.validators.deep_mapping(
|
||||
attrs.validators.instance_of(ProxyType),
|
||||
attrs.validators.and_(
|
||||
attrs.validators.min_len(1),
|
||||
attrs.validators.deep_iterable(
|
||||
attrs.validators.and_(
|
||||
attrs.validators.instance_of(str),
|
||||
attrs.validators.min_len(1),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
converter=_sources_converter,
|
||||
)
|
||||
timeout: ClientTimeout = attrs.field(converter=_timeout_converter)
|
||||
|
||||
@property
|
||||
def sorting_key(
|
||||
self,
|
||||
) -> Union[Callable[[Proxy], float], Callable[[Proxy], Tuple[int, ...]]]:
|
||||
return (
|
||||
sort.timeout_sort_key
|
||||
if self.sort_by_speed
|
||||
else sort.natural_sort_key
|
||||
)
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
if not self.output_json and not self.output_txt:
|
||||
msg = "both json and txt outputs are disabled"
|
||||
raise ValueError(msg)
|
||||
|
||||
if not self.output_json and self.enable_geolocation:
|
||||
msg = "geolocation can not be enabled if json output is disabled"
|
||||
raise ValueError(msg)
|
||||
|
||||
@check_website.validator
|
||||
def _validate_check_website( # noqa: PLR6301
|
||||
self,
|
||||
attribute: attrs.Attribute[str], # noqa: ARG002
|
||||
value: str,
|
||||
/,
|
||||
) -> None:
|
||||
parsed_url = urlparse(value)
|
||||
if not parsed_url.scheme or not parsed_url.netloc:
|
||||
msg = f"invalid URL: {value}"
|
||||
raise ValueError(msg)
|
||||
|
||||
if parsed_url.scheme == "http":
|
||||
logger.warning(
|
||||
"check_website uses the http protocol. "
|
||||
"It is recommended to use https for correct checking."
|
||||
)
|
||||
|
||||
@timeout.validator
|
||||
def _validate_timeout(
|
||||
self,
|
||||
attribute: attrs.Attribute[str], # noqa: ARG002
|
||||
value: float, # noqa: ARG002
|
||||
/,
|
||||
) -> None:
|
||||
if self.timeout.total is None or self.timeout.total <= 0:
|
||||
msg = "timeout must be positive"
|
||||
raise ValueError(msg)
|
||||
|
||||
@classmethod
|
||||
async def from_mapping(
|
||||
cls, cfg: Mapping[str, Any], /, *, session: ClientSession
|
||||
) -> Self:
|
||||
check_website_type, real_ip = await _get_check_website_type_and_real_ip(
|
||||
check_website=cfg["check_website"], session=session
|
||||
)
|
||||
return cls(
|
||||
check_website=cfg["check_website"],
|
||||
check_website_type=check_website_type,
|
||||
enable_geolocation=cfg["enable_geolocation"]
|
||||
and check_website_type.supports_geolocation,
|
||||
output_json=cfg["output"]["json"],
|
||||
output_path=cfg["output"]["path"],
|
||||
output_txt=cfg["output"]["txt"],
|
||||
real_ip=real_ip,
|
||||
semaphore=cfg["max_connections"],
|
||||
sort_by_speed=cfg["sort_by_speed"],
|
||||
source_timeout=cfg["source_timeout"],
|
||||
sources={
|
||||
ProxyType.HTTP: (
|
||||
cfg["http"]["sources"] if cfg["http"]["enabled"] else None
|
||||
),
|
||||
ProxyType.SOCKS4: (
|
||||
cfg["socks4"]["sources"]
|
||||
if cfg["socks4"]["enabled"]
|
||||
else None
|
||||
),
|
||||
ProxyType.SOCKS5: (
|
||||
cfg["socks5"]["sources"]
|
||||
if cfg["socks5"]["enabled"]
|
||||
else None
|
||||
),
|
||||
},
|
||||
timeout=cfg["timeout"],
|
||||
)
|
||||
@@ -2,12 +2,20 @@ from __future__ import annotations
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
from aiohttp_socks import ProxyType
|
||||
|
||||
from .proxy import Proxy
|
||||
|
||||
PROTOCOL_ORDER = (ProxyType.HTTP, ProxyType.SOCKS4, ProxyType.SOCKS5)
|
||||
|
||||
def timeout_sort_key(proxy: Proxy) -> float:
|
||||
|
||||
def protocol_sort_key(proxy: Proxy, /) -> Tuple[int, ProxyType]:
|
||||
return (PROTOCOL_ORDER.index(proxy.protocol), proxy.protocol)
|
||||
|
||||
|
||||
def natural_sort_key(proxy: Proxy, /) -> Tuple[int, ...]:
|
||||
return (proxy.protocol.value, *map(int, proxy.host.split(".")), proxy.port)
|
||||
|
||||
|
||||
def timeout_sort_key(proxy: Proxy, /) -> float:
|
||||
return proxy.timeout
|
||||
|
||||
|
||||
def natural_sort_key(proxy: Proxy) -> Tuple[int, ...]:
|
||||
return (*map(int, proxy.host.split(".")), proxy.port)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from collections import Counter
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Iterator, List, Set, Tuple
|
||||
|
||||
from aiohttp_socks import ProxyType
|
||||
|
||||
from . import sort
|
||||
from .proxy import Proxy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import SupportsRichComparison
|
||||
|
||||
|
||||
class ProxyStorage:
|
||||
__slots__ = ("_proxies", "enabled_protocols")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._proxies: Set[Proxy] = set()
|
||||
self.enabled_protocols: Set[ProxyType] = set()
|
||||
|
||||
def add(self, proxy: Proxy, /) -> None:
|
||||
self.enabled_protocols.add(proxy.protocol)
|
||||
self._proxies.add(proxy)
|
||||
|
||||
def remove(self, proxy: Proxy, /) -> None:
|
||||
self._proxies.remove(proxy)
|
||||
|
||||
def get_grouped(self) -> Dict[ProxyType, Tuple[Proxy, ...]]:
|
||||
key = sort.protocol_sort_key
|
||||
d: Dict[ProxyType, Tuple[Proxy, ...]] = {
|
||||
proto: ()
|
||||
for proto in sort.PROTOCOL_ORDER
|
||||
if proto in self.enabled_protocols
|
||||
}
|
||||
for (_, proto), v in itertools.groupby(
|
||||
sorted(self._proxies, key=key), key=key
|
||||
):
|
||||
d[proto] = tuple(v)
|
||||
return d
|
||||
|
||||
def get_sorted(
|
||||
self,
|
||||
*,
|
||||
key: Callable[[Proxy], SupportsRichComparison] = sort.protocol_sort_key,
|
||||
) -> List[Proxy]:
|
||||
return sorted(self._proxies, key=key)
|
||||
|
||||
def get_count(self) -> Dict[ProxyType, int]:
|
||||
return dict(Counter(proxy.protocol for proxy in self._proxies))
|
||||
|
||||
def __iter__(self) -> Iterator[Proxy]:
|
||||
return iter(self._proxies)
|
||||
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from typing_extensions import Any, Literal, Self
|
||||
else:
|
||||
from typing import Any, Literal, Self
|
||||
|
||||
__all__ = ("Any", "Literal", "Self")
|
||||
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import charset_normalizer
|
||||
|
||||
|
||||
def is_url(value: str, /) -> bool:
|
||||
parsed_url = urlparse(value)
|
||||
return bool(parsed_url.scheme and parsed_url.netloc)
|
||||
|
||||
|
||||
def bytes_decode(value: bytes, /) -> str:
|
||||
return str(charset_normalizer.from_bytes(value)[0])
|
||||
@@ -1,93 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Iterable, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from typing_extensions import Any
|
||||
|
||||
from .folder import Folder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def timeout(value: float) -> None:
|
||||
if value <= 0:
|
||||
msg = "Timeout must be positive"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def source_timeout(value: float) -> None:
|
||||
if value <= 0:
|
||||
msg = "SourceTimeout must be positive"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def max_connections(value: int) -> Optional[int]:
|
||||
if value < 0:
|
||||
msg = "MaxConnections must be non-negative"
|
||||
raise ValueError(msg)
|
||||
max_supported = _get_supported_max_connections()
|
||||
if not value:
|
||||
logger.info("Using %d as MaxConnections value", max_supported or 0)
|
||||
return max_supported
|
||||
if not max_supported or value <= max_supported:
|
||||
return value
|
||||
logger.warning(
|
||||
"MaxConnections value is too high. "
|
||||
"Your OS supports a maximum of %d. "
|
||||
"The config value will be ignored and %d will be used.",
|
||||
max_supported,
|
||||
max_supported,
|
||||
)
|
||||
return max_supported
|
||||
|
||||
|
||||
def _get_supported_max_connections() -> Optional[int]:
|
||||
if sys.platform == "win32":
|
||||
if isinstance(
|
||||
asyncio.get_event_loop_policy(),
|
||||
asyncio.WindowsSelectorEventLoopPolicy,
|
||||
):
|
||||
return 512
|
||||
return None
|
||||
import resource # noqa: PLC0415
|
||||
|
||||
soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||
logger.debug(
|
||||
"MaxConnections soft limit = %d, hard limit = %d, infinity = %d",
|
||||
soft_limit,
|
||||
hard_limit,
|
||||
resource.RLIM_INFINITY,
|
||||
)
|
||||
if soft_limit != hard_limit:
|
||||
try:
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (hard_limit, hard_limit))
|
||||
except ValueError as e:
|
||||
logger.warning("Failed setting MaxConnections: %s", e)
|
||||
else:
|
||||
soft_limit = hard_limit
|
||||
if soft_limit == resource.RLIM_INFINITY:
|
||||
return None
|
||||
return soft_limit
|
||||
|
||||
|
||||
def check_website(value: str) -> None:
|
||||
parsed_url = urlparse(value)
|
||||
if not parsed_url.scheme or not parsed_url.netloc:
|
||||
msg = f"invalid CheckWebsite URL: {value}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def folders(value: Iterable[Folder]) -> None:
|
||||
if not any(folder for folder in value if folder.is_enabled):
|
||||
msg = "all folders are disabled in the config"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def sources(value: Any) -> None:
|
||||
if not value:
|
||||
msg = "proxy sources list is empty"
|
||||
raise ValueError(msg)
|
||||
+47
-1
@@ -1,3 +1,45 @@
|
||||
[tool.poetry]
|
||||
name = "proxy-scraper-checker"
|
||||
version = "0"
|
||||
description = ""
|
||||
license = "MIT"
|
||||
authors = ["monosans <hsyqixco@protonmail.com>"]
|
||||
repository = "https://github.com/monosans/proxy-scraper-checker"
|
||||
classifiers = ["Private :: Do Not Upload"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
aiodns = { version = ">=1.1.1,<4", optional = true }
|
||||
aiofiles = ">=0.8"
|
||||
aiohttp = "^3.8.6"
|
||||
aiohttp-socks = ">=0.7,<0.9"
|
||||
attrs = ">=22.2"
|
||||
brotli = { version = "^1", markers = "platform_python_implementation == 'CPython'" }
|
||||
brotlicffi = { version = "<2", markers = "platform_python_implementation != 'CPython'" }
|
||||
certifi = "*"
|
||||
charset-normalizer = ">=2,<4"
|
||||
maxminddb = ">=1.3,<3"
|
||||
platformdirs = "<5"
|
||||
rich = ">=12.3,<14"
|
||||
tomli = { version = "<3", python = "<3.11" }
|
||||
typing-extensions = { version = "^4.4", python = "<3.11" }
|
||||
uvloop = { version = ">=0.14,<0.20", optional = true, markers = "platform_python_implementation == 'CPython' and (sys_platform == 'darwin' or sys_platform == 'linux')" }
|
||||
|
||||
[tool.poetry.extras]
|
||||
full = ["aiodns", "uvloop"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = "1.8.0"
|
||||
ruff = "0.1.14"
|
||||
tomli = "2.0.1"
|
||||
types-aiofiles = "23.2.0.20240106"
|
||||
|
||||
[tool.poetry.group.nuitka]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.nuitka.dependencies]
|
||||
nuitka = "1.9.7"
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
platform = "linux"
|
||||
@@ -75,7 +117,7 @@ ignore-init-module-imports = true
|
||||
line-length = 80
|
||||
preview = true
|
||||
select = ["ALL"]
|
||||
target-version = "py37"
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.flake8-self]
|
||||
ignore-names = []
|
||||
@@ -97,3 +139,7 @@ convention = "google"
|
||||
|
||||
[tool.ruff.pyupgrade]
|
||||
keep-runtime-typing = true
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
aiohttp-socks>=0.7,<0.9
|
||||
aiohttp>=3.8,<4
|
||||
rich>=12.3,<14
|
||||
typing-extensions>=4.4,<5
|
||||
@@ -1,6 +0,0 @@
|
||||
aiodns>=3.0,<4
|
||||
aiohttp-socks>=0.7,<0.9
|
||||
aiohttp>=3.8,<4
|
||||
rich>=12.3,<14
|
||||
typing-extensions>=4.4,<5
|
||||
uvloop>=0.16,<0.20; implementation_name == "cpython" and (sys_platform == "darwin" or sys_platform == "linux")
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
python3 -m pip install -U --no-cache-dir --disable-pip-version-check setuptools wheel &&
|
||||
python3 -m pip install -U --no-cache-dir --disable-pip-version-check -r requirements-termux.txt &&
|
||||
python3 -m proxy_scraper_checker
|
||||
python3 -m venv --upgrade-deps .venv &&
|
||||
.venv/bin/python3 -m pip install -U --disable-pip-version-check --editable . &&
|
||||
.venv/bin/python3 -m proxy_scraper_checker
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
py -m pip install -U --no-cache-dir --disable-pip-version-check pip setuptools wheel
|
||||
py -m pip install -U --no-cache-dir --disable-pip-version-check -r requirements.txt
|
||||
py -m proxy_scraper_checker
|
||||
Pause
|
||||
py -m venv --upgrade-deps .venv
|
||||
.venv\Scripts\python.exe -m pip install -U --disable-pip-version-check --editable .[full]
|
||||
.venv\Scripts\python.exe -m proxy_scraper_checker
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
python3 -m pip install -U --no-cache-dir --disable-pip-version-check pip setuptools wheel &&
|
||||
python3 -m pip install -U --no-cache-dir --disable-pip-version-check -r requirements.txt &&
|
||||
python3 -m proxy_scraper_checker
|
||||
python3 -m venv --upgrade-deps .venv &&
|
||||
.venv/bin/python3 -m pip install -U --disable-pip-version-check --editable .[full] &&
|
||||
.venv/bin/python3 -m proxy_scraper_checker
|
||||
|
||||
Reference in New Issue
Block a user