5分鐘,自己做一個(gè) IP 代理隧道
什么是隧道代理?我們來(lái)看下面這張截圖:

所謂隧道代理,就是一個(gè)能幫你自動(dòng)更換代理 IP 的代理服務(wù)。
在你的代碼里面,你只需要把一個(gè)入口代理地址寫死,然后正常發(fā)起請(qǐng)求,而目標(biāo)服務(wù)器接收到的請(qǐng)求,每一次都是不同的代理地址。
在某代理網(wǎng)站上,隧道代理50并發(fā)每秒的價(jià)格是4000元/月:

而常規(guī)的,先請(qǐng)求接口拿到一批代理 IP,再選一個(gè)發(fā)起請(qǐng)求的原始代理服務(wù)器,一個(gè)月價(jià)格才600多元:

所以,如果我們能自己做一個(gè)隧道代理,將會(huì)省下很多錢!
隧道代理的原理,跟常規(guī)代理的不同之處,用下面這兩張圖就能說(shuō)清楚:
傳統(tǒng)代理服務(wù)
隧道代理要自己開(kāi)發(fā)一個(gè)這樣的隧道代理,我們需要做兩步:
- 構(gòu)建一個(gè)代理池
- 實(shí)現(xiàn)代理自動(dòng)轉(zhuǎn)發(fā)
構(gòu)建代理池
假設(shè)你從代理供應(yīng)商手上買到的便宜代理地址為:http://xxx.com/ips,直接在瀏覽器上面請(qǐng)求,頁(yè)面效果如下圖所示:

現(xiàn)在,你需要做的就是寫一個(gè)程序,周期性訪問(wèn)這個(gè)url,拉取當(dāng)前最新可用的IP地址,然后把它放到Redis中。
這里,我們使用Redis的Hash這個(gè)數(shù)據(jù)結(jié)構(gòu),其中Hash的字段名就是IP:端口,里面的值就是跟每個(gè)IP相關(guān)的一些信息。
你這個(gè)程序需要確保,當(dāng)前在Redis里面的代理地址,全部都是可用的。這里,我給出了一個(gè)示例程序:
"""
ProxyManager.py
~~~~~~~~~~~~~~~~~~~~~
簡(jiǎn)易代理池管理工具,直接從URL中讀取所有
最新的代理,并寫入Redis。
"""
import?yaml
import?time
import?json
import?redis
import?datetime
import?requests
class?ProxyManager:
????def?__init__(self):
????????self.config?=?self.read_config()
????????self.redis_config?=?self.config['redis']
????????self.client?=?redis.Redis(host=self.redis_config['host'],
??????????????????????????????????password=self.redis_config['password'],
??????????????????????????????????port=self.redis_config['port'])
????????self.instance_dict?=?{}
????def?read_config(self):
????????with?open('config.yaml')?as?f:
????????????config?=?yaml.safe_load(f.read())
????????????return?config
????def?read_ip(self):
????????resp?=?requests.get(self.config['proxy']).text
????????if?'{'?in?resp:
????????????return?[]
????????proxy_list?=?resp.split()
????????return?proxy_list
????def?delete_ip(self,?live_ips,?pool_ips):
????????ip_to_removed?=?set(pool_ips)?-?set(live_ips)
????????if?ip_to_removed:
????????????print('ip?to?be?removed:',?ip_to_removed)
????????????self.client.hdel(self.redis_config['key'],?*list(ip_to_removed))
????def?add_new_ips(self,?live_ips,?pool_ips):
????????ip_to_add?=?set(live_ips)?-?set(pool_ips)
????????if?ip_to_add:
????????????print('ip?to?add:',?ip_to_add)
????????????ips?=?{}
????????????for?ip?in?ip_to_add:
????????????????ips[ip]?=?json.dumps({'private_ip':?ip,
??????????????????????????????????????'ts':?datetime.datetime.now().strftime('%Y-%m-%d?%H:%M:%S')})
????????????self.client.hset(self.redis_config['key'],?mapping=ips)
????def?run(self):
????????while?True:
????????????live_ips?=?self.read_ip()
????????????pool_ips?=?[x.decode()?for?x?in?self.client.hgetall(self.redis_config['key'])]
????????????self.delete_ip(live_ips,?pool_ips)
????????????self.add_new_ips(live_ips,?pool_ips)
????????????time.sleep(40)
if?__name__?==?'__main__':
????manager?=?ProxyManager()
????manager.run()
其中,我把Redis相關(guān)的配置、代理供應(yīng)商的URL寫到了一個(gè)yaml配置文件中,防止被你們看到。配置文件的格式如下圖所示:
由于我這個(gè)代理供應(yīng)商提供的IP,有效期是1-5分鐘,所以保險(xiǎn)起見(jiàn),我每40秒更換一次IP。更換的時(shí)候,采用了增量更換的方式。
把當(dāng)前拉取的IP和Redis里面的已有IP進(jìn)行對(duì)比。不在這次拉取的IP全部從Redis移除,然后把新增的IP加到Redis中。
大家在實(shí)際過(guò)程中,還可以加一些代理校驗(yàn)的邏輯,確保從URL拉下來(lái)的代理也進(jìn)行有效性檢查,發(fā)現(xiàn)無(wú)效的立刻移除。
實(shí)現(xiàn)自動(dòng)轉(zhuǎn)發(fā)
要實(shí)現(xiàn)自動(dòng)轉(zhuǎn)發(fā),我們可以使用OpenResty[1]。這是一個(gè)基于Nginx和Lua實(shí)現(xiàn)的高性能Web平臺(tái)。通過(guò)它,我們可以使用Lua語(yǔ)言實(shí)現(xiàn)一些邏輯,例如從Redis讀取數(shù)據(jù),把來(lái)源請(qǐng)求轉(zhuǎn)發(fā)到上游代理服務(wù)器……
因此,我們使用OpenResty搭建一個(gè)轉(zhuǎn)發(fā)服務(wù)。并把這個(gè)轉(zhuǎn)發(fā)服務(wù)所在服務(wù)器的IP地址作為我們的入口IP地址。
在使用Requests等等網(wǎng)絡(luò)請(qǐng)求客戶端發(fā)送請(qǐng)求的時(shí)候,只需要把這個(gè)入口IP地址設(shè)置為代理。那么,當(dāng)客戶端發(fā)送請(qǐng)求的時(shí)候,請(qǐng)求首先到了OpenResty。
然后它從Redis中隨機(jī)選一個(gè)代理IP來(lái)作為上游代理,并把剛剛發(fā)來(lái)的請(qǐng)求轉(zhuǎn)發(fā)到上游代理。從而實(shí)現(xiàn)隧道代理的效果。
Lua是一門非常老的語(yǔ)言,它的語(yǔ)法不少地方跟Python不太一樣。不過(guò)你不用擔(dān)心,這個(gè)配置文件我已經(jīng)寫好了。大家拿過(guò)來(lái)改一改就能用。
對(duì)應(yīng)的配置文件如下圖所示:
worker_processes??16;????????#nginx?worker?數(shù)量
error_log?/usr/local/openresty/nginx/logs/error.log;???#指定錯(cuò)誤日志文件路徑
events?{
????worker_connections?1024;
}
stream?{
????##?TCP?代理日志格式定義
????log_format?tcp_proxy?'$remote_addr?[$time_local]?'
?????????????????????????'$protocol?$status?$bytes_sent?$bytes_received?'
?????????????????????????'$session_time?"$upstream_addr"?'
?????????????????????????'"$upstream_bytes_sent"?"$upstream_bytes_received"?"$upstream_connect_time"';
????##?TCP?代理日志配置
????access_log?/usr/local/openresty/nginx/logs/access.log?tcp_proxy;
????open_log_file_cache?off;
????##?TCP?代理配置
????upstream?backend{
????????server?127.0.0.2:1101;#?愛(ài)寫啥寫啥??反正下面的代碼也給你改了
????????balancer_by_lua_block?{
????????????--?初始化balancer
????????????local?balancer?=?require?"ngx.balancer"
????????????local?host?=?""
????????????local?port?=?0
????????????host?=?ngx.ctx.proxy_host
????????????port?=?ngx.ctx.proxy_port
????????????--?設(shè)置?balancer
????????????local?ok,?err?=?balancer.set_current_peer(host,?port)
????????????if?not?ok?then
????????????????ngx.log(ngx.ERR,?"failed?to?set?the?peer:?",?err)
????????????end
????????}
????}
????server?{
????????preread_by_lua_block{
????????????local?redis?=?require("resty.redis")
????????????--創(chuàng)建實(shí)例
????????????local?redis_instance?=?redis:new()
????????????--設(shè)置超時(shí)(毫秒)
????????????redis_instance:set_timeout(3000)
????????????--建立連接,請(qǐng)?jiān)谶@里配置Redis?的?IP?地址、端口號(hào)、密碼和用到的?Key
????????????local?rhost?=?"123.45.67.89"
????????????local?rport?=?6739
????????????local?rpass?=?"abcdefg"
????????????local?rkey?=?"proxy:pool"
????????????local?ok,?err?=?redis_instance:connect(rhost,?rport)
????????????ngx.log(ngx.ERR,?"1111111?",?ok,?"?",?err)
????????????--?如果沒(méi)有密碼,移除下面這一行
????????????local?res,?err?=?redis_instance:auth(rpass)
????????????local?res,?err?=?redis_instance:hkeys(rkey)
????????????if?not?res?then
????????????????ngx.log(ngx.ERR,"res?num?error?:?",?err)
????????????????return?redis_instance:close()
????????????end
????????????math.randomseed(tostring(ngx.now()):reverse():sub(1,?6))
????????????local?proxy?=?res[math.random(#res)]
????????????local?colon_index?=?string.find(proxy,?":")
????????????local?proxy_ip?=?string.sub(proxy,?1,?colon_index?-?1)
????????????local?proxy_port?=?string.sub(proxy,?colon_index?+?1)
????????????ngx.log(ngx.ERR,"redis?data?=?",?proxy_ip,?":",?proxy_port);
????????????ngx.ctx.proxy_host?=?proxy_ip
????????????ngx.ctx.proxy_port?=?proxy_port
????????????redis_instance:close()
????????}
????????#??下面是本機(jī)的端口,也就是爬蟲(chóng)固定寫死的端口
???????listen?0.0.0.0:9976;?#監(jiān)聽(tīng)本機(jī)地址和端口,當(dāng)使用keeplived的情況下使用keeplived?VIP
???????proxy_connect_timeout?3s;
???????proxy_timeout?10s;
???????proxy_pass?backend;?#這里填寫對(duì)端的地址
????}
}
需要修改的地方,我在配置文件里面已經(jīng)做好的注釋。具體而言,需要修改地方包含:
- Redis的地址、端口、密碼和 Key。如果你的Redis沒(méi)有密碼,可以把設(shè)置密碼的這一行刪掉

- 入口代理的端口

設(shè)置好了這些配置以后,我們就可以使用Docker來(lái)啟動(dòng)它。Docker的配置文件極其簡(jiǎn)單:
from?openresty/openresty:centos
copy?nginx_redis.conf?/usr/local/openresty/nginx/conf/nginx.conf
然后,執(zhí)行命令構(gòu)建和運(yùn)行:
docker?build?--network?host?-t?tunnel_proxy:0.01?.
docker?run?--name?tunnel_proxy?--network?host?-it?tunnel_proxy:0.01
運(yùn)行以后,你會(huì)看到Docker的命令行似乎卡住了。這是正常請(qǐng)求。因?yàn)樾枰阌辛苏?qǐng)求,它才會(huì)輸出內(nèi)容。
現(xiàn)在,你可以用Requests趕快寫一段代碼來(lái)進(jìn)行驗(yàn)證:
import?requests
import?time
proxies?=?{'http':?'http://13.88.220.207:9976'}
for?_?in?range(10):
????resp?=?requests.get('http://httpbin.org/ip',?proxies=proxies).text
????print(resp)
????time.sleep(1)
運(yùn)行效果如下圖所示。

說(shuō)明隧道代理搭建成功。目前隧道代理我已經(jīng)穩(wěn)定運(yùn)行了半年,從來(lái)沒(méi)有出過(guò)問(wèn)題,大家可以放心使用。
最后,本文受到 @萌木蓋 的文章:openresty正向代理搭建 - 簡(jiǎn)書[2]的啟發(fā),并在該文章基礎(chǔ)上進(jìn)行改進(jìn)。特別感謝原作者。
參考資料[1]OpenResty: https://openresty.org/cn/
[2]openresty正向代理搭建 - 簡(jiǎn)書: https://www.jianshu.com/p/7808ee6395ab
< END >爬蟲(chóng)|Js逆向某加速 cookie 加密分析爬蟲(chóng) | 找到了一個(gè)破解谷歌驗(yàn)證碼的新方案!