Skip to content

Commit b915026

Browse files
authored
feat: filter proxies by country via ?area=CN (#84, #177) (#233)
* feat: support fetching multiple proxies via /random?count=N - redis: add RedisClient.randoms(count, ...) which prefers max-score proxies and falls back to rank, returning a de-duplicated random sample - server: /random accepts a count parameter and returns N random proxies (one per line); count<=1 keeps the original single-proxy behavior; works with key too - README: document the new count parameter Closes #194 * feat: filter proxies by country via ?area=CN on /random and /all - utils/geo: add get_country_iso() backed by the bundled GeoLite2 db (previously an unused dependency); fails safe to None if unavailable - server: /random and /all accept an area parameter (country iso code) to return only proxies from that country; combines with count and key - README: document the area parameter Closes #84, Closes #177
1 parent 40a326a commit b915026

4 files changed

Lines changed: 124 additions & 3 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,27 @@ get random proxy 116.196.115.209:8080
222222

223223
可以看到成功获取了代理,并请求 httpbin.org 验证了代理的可用性。
224224

225+
### 获取多个代理
226+
227+
如果一次需要多个代理,可以给 `/random` 接口传入 `count` 参数,一次返回多个随机代理(每行一个):
228+
229+
```
230+
GET http://localhost:5555/random?count=5
231+
```
232+
233+
`count` 不传或为 1 时行为不变,仍返回单个代理;`count` 大于可用数量时返回全部可用代理。也可与 `key` 参数组合使用。
234+
235+
### 按地区(国家)筛选代理
236+
237+
可以给 `/random``/all` 接口传入 `area` 参数,按代理 IP 所属国家筛选(ISO 国家码,大小写不敏感),例如只获取国内(中国)代理:
238+
239+
```
240+
GET http://localhost:5555/random?area=CN
241+
GET http://localhost:5555/all?area=CN
242+
```
243+
244+
国家信息由内置的 GeoLite2 离线库解析,无法解析归属地的代理会被排除。`area` 可与 `count``key` 参数组合使用。
245+
225246
## 可配置项
226247

227248
代理池可以通过设置环境变量来配置一些参数。

proxypool/processors/server.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from proxypool.storages.redis import RedisClient
66
from proxypool.setting import API_HOST, API_PORT, API_THREADED, API_KEY, IS_DEV, PROXY_RAND_KEY_DEGRADED
77
import functools
8+
from random import choice, sample
9+
from proxypool.utils.geo import get_country_iso
810

911
__all__ = ['app']
1012

@@ -58,6 +60,20 @@ def get_request_key():
5860
return key
5961

6062

63+
def filter_proxies_by_area(proxies, area):
64+
"""
65+
filter proxies by country iso code (e.g. 'CN', 'US'), case-insensitive;
66+
proxies whose country cannot be resolved are excluded
67+
:param proxies: list of Proxy
68+
:param area: country iso code, or falsy to skip filtering
69+
:return: filtered list of Proxy
70+
"""
71+
if not area:
72+
return proxies
73+
area = area.upper()
74+
return [proxy for proxy in proxies if get_country_iso(proxy.host) == area]
75+
76+
6177
@app.route('/')
6278
@auth_required
6379
def index():
@@ -74,11 +90,37 @@ def get_proxy():
7490
"""
7591
get a random proxy, can query the specific sub-pool according the (redis) key
7692
if PROXY_RAND_KEY_DEGRADED is set to True, will get a universal random proxy if no proxy found in the sub-pool
93+
can pass a `count` parameter to get multiple random proxies at once
94+
can pass an `area` parameter to only get proxies from a country (iso code, e.g. CN)
7795
:return: get a random proxy
7896
"""
7997
key = get_request_key()
98+
count = request.args.get('count', type=int)
99+
area = request.args.get('area')
80100
conn = get_conn()
81101
# return conn.random(key).string() if key else conn.random().string()
102+
if area:
103+
# area filtering needs the candidate set first, then filter by country
104+
candidates = conn.all(key) if key else conn.all()
105+
candidates = filter_proxies_by_area(candidates, area)
106+
if not candidates and key and PROXY_RAND_KEY_DEGRADED:
107+
candidates = filter_proxies_by_area(conn.all(), area)
108+
if not candidates:
109+
raise PoolEmptyException
110+
if count and count > 1:
111+
count = min(count, len(candidates))
112+
return '\n'.join(proxy.string() for proxy in sample(candidates, count))
113+
return choice(candidates).string()
114+
if count and count > 1:
115+
# return multiple random proxies, one per line
116+
try:
117+
proxies = conn.randoms(count, key) if key else conn.randoms(count)
118+
except PoolEmptyException:
119+
if key and PROXY_RAND_KEY_DEGRADED:
120+
proxies = conn.randoms(count)
121+
else:
122+
raise
123+
return '\n'.join(proxy.string() for proxy in proxies)
82124
if key:
83125
try:
84126
return conn.random(key).string()
@@ -92,13 +134,15 @@ def get_proxy():
92134
@auth_required
93135
def get_proxy_all():
94136
"""
95-
get a random proxy
96-
:return: get a random proxy
137+
get all proxies, optionally filtered by `area` (country iso code, e.g. CN)
138+
:return: all proxies
97139
"""
98140
key = get_request_key()
141+
area = request.args.get('area')
99142

100143
conn = get_conn()
101144
proxies = conn.all(key) if key else conn.all()
145+
proxies = filter_proxies_by_area(proxies, area)
102146
proxies_string = ''
103147
if proxies:
104148
for proxy in proxies:

proxypool/storages/redis.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from proxypool.schemas.proxy import Proxy
44
from proxypool.setting import REDIS_CONNECTION_STRING, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_DB, REDIS_KEY, PROXY_SCORE_MAX, PROXY_SCORE_MIN, \
55
PROXY_SCORE_INIT
6-
from random import choice
6+
from random import choice, sample
77
from typing import List
88
from loguru import logger
99
from proxypool.utils.proxy import is_valid_proxy, convert_proxy_or_proxies
@@ -70,6 +70,27 @@ def random(self, redis_key=REDIS_KEY, proxy_score_min=PROXY_SCORE_MIN, proxy_sco
7070
# else raise error
7171
raise PoolEmptyException
7272

73+
def randoms(self, count, redis_key=REDIS_KEY, proxy_score_min=PROXY_SCORE_MIN, proxy_score_max=PROXY_SCORE_MAX) -> List[Proxy]:
74+
"""
75+
get a batch of random proxies
76+
firstly try to get proxies with max score,
77+
if not enough, get proxies by rank (score from high to low)
78+
if none exists, raise error
79+
:param count: number of proxies to return
80+
:return: list of proxies
81+
"""
82+
# try to get proxies with max score first
83+
proxies = self.db.zrangebyscore(
84+
redis_key, proxy_score_max, proxy_score_max)
85+
if len(proxies) < count:
86+
# not enough max-score proxies, fall back to all proxies by rank
87+
proxies = self.db.zrevrangebyscore(
88+
redis_key, proxy_score_max, proxy_score_min)
89+
if not proxies:
90+
raise PoolEmptyException
91+
count = min(count, len(proxies))
92+
return convert_proxy_or_proxies(sample(proxies, count))
93+
7394
def decrease(self, proxy: Proxy, redis_key=REDIS_KEY, proxy_score_min=PROXY_SCORE_MIN) -> int:
7495
"""
7596
decrease score of proxy, if small than PROXY_SCORE_MIN, delete it

proxypool/utils/geo.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from loguru import logger
2+
3+
# geolite2 provides an offline IP -> country database (bundled with the
4+
# maxminddb_geolite2 dependency). loading it can fail if the optional
5+
# dependency is missing, so degrade gracefully and disable area filtering.
6+
try:
7+
from geolite2 import geolite2
8+
9+
_reader = geolite2.reader()
10+
except Exception as e: # pragma: no cover
11+
_reader = None
12+
logger.warning(f'geolite2 is unavailable, area filtering disabled: {e}')
13+
14+
15+
def get_country_iso(ip):
16+
"""
17+
look up the ISO country code (e.g. 'CN', 'US') for an ip address
18+
:param ip: ip address string
19+
:return: uppercase iso code, or None if unknown/unavailable
20+
"""
21+
if _reader is None:
22+
return None
23+
try:
24+
record = _reader.get(ip)
25+
except Exception:
26+
return None
27+
if not record:
28+
return None
29+
country = record.get('country') or record.get('registered_country') or {}
30+
return country.get('iso_code')
31+
32+
33+
if __name__ == '__main__':
34+
print('8.8.8.8', get_country_iso('8.8.8.8'))
35+
print('114.114.114.114', get_country_iso('114.114.114.114'))

0 commit comments

Comments
 (0)