咱們嘗試維護過一個代理池。代理池能夠挑選出許多可用代理,可是經常其穩定性不高、響應速度慢,並且這些代理一般是公共代理,可能不止一人同時使用,其IP被封的機率很大。另外,這些代理可能有效時間比較短,雖然代理池一直在篩選,但若是沒有及時更新狀態,也有可能獲取到不可用的代理。
git
若是要追求更加穩定的代理,就須要購買專有代理或者本身搭建代理服務器。可是服務器通常都是固定的IP,咱們總不能搭建100個代理就用100臺服務器吧,這顯然是不現實的。github
因此,ADSL動態撥號主機就派上用場了。下面咱們來了解一下ADSL撥號代理服務器的相關設置。web
ADSL(Asymmetric Digital Subscriber Line,非對稱數字用戶環路),它的上行和下行帶寬不對稱,它採用頻分複用技術把普通的電話線分紅了電話、上行和下行三個相對獨立的信道,從而避免了相互之間的干擾。
redis
ADSL經過撥號的方式上網,須要輸入ADSL帳號和密碼,每次撥號就更換一個IP。IP分佈在多個A段,若是IP都能使用,則意味着IP量級可達千萬。若是咱們將ADSL主機做爲代理,每隔一段時間主機撥號就換一個IP,這樣能夠有效防止IP被封禁。另外,主機的穩定性很好,代理響應速度很快。數據庫
首先須要成功安裝Redis數據庫並啓動服務,另外還須要安裝requests、RedisPy、Tornado庫。
json
咱們先購買一臺動態撥號VPS主機,這樣的主機服務商至關多。在這裏使用了雲立方,官方網站:http://www.yunlifang.cn/dynamicvps.asp。
api
建議選擇電信線路。能夠自行選擇主機配置,主要考慮帶寬是否知足需求。服務器
而後進入撥號主機的後臺,預裝一個操做系統,以下圖所示。網絡
推薦安裝CentOS 7系統。架構
而後找到遠程管理面板-遠程鏈接的用戶名和密碼,也就是SSH遠程鏈接服務器的信息。好比我使用的IP和端口是153.36.65.214:20063,用戶名是root
。命令行下輸入以下代碼:
ssh root@153.36.65.214 -p 20063
輸入管理密碼,就能夠鏈接上遠程服務器了。
進入以後,咱們發現一個可用的腳本文件ppp.sh,這是撥號初始化的腳本。運行此腳本會提示輸入撥號的用戶名和密碼,而後它就開始各類撥號配置。一次配置成功,後面撥號就不須要重複輸入用戶名和密碼。
運行ppp.sh腳本,輸入用戶名密碼等待它的配置完成,以下圖所示。
提示成功以後就能夠進行撥號了。注意,在撥號以前測試ping任何網站都是不通的,由於當前網絡還沒聯通。輸入以下撥號命令:
adsl-start
撥號命令成功運行,沒有報錯信息,耗時約幾秒。接下來再去ping外網就能夠通了。
若是要中止撥號,能夠輸入以下指令:
adsl-stop
以後,能夠發現又連不通網絡了,以下圖所示。
斷線重播的命令就是兩者組合起來,先執行adsl-stop
,再執行adsl-start
。每次撥號,ifconfig
命令觀察主機的IP,發現主機的IP一直在變化,網卡名稱叫做ppp0,以下圖所示。
接下來,咱們要作兩件事:一是怎樣將主機設置爲代理服務器,二是怎樣實時獲取撥號主機的IP。
在Linux下搭建HTTP代理服務器,推薦TinyProxy和Squid,配置都很是簡單。在這裏咱們以TinyProxy爲例來說解一下怎樣搭建代理服務器。
第一步就是安裝TinyProxy軟件。在這裏我使用的系統是CentOS,因此使用yum來安裝。若是是其餘系統如Ubuntu,能夠選擇apt-get
等命令安裝。
命令行執行yum安裝指令:
yum install -y epel-release yum update -y yum install -y tinyproxy
TinyProxy安裝完成以後還要配置一下才能夠用做代理服務器。咱們須要編輯配置文件,此文件通常的路徑是/etc/tinyproxy/tinyproxy.conf。
能夠看到一行代碼:
Port 8888
在這裏能夠設置代理的端口,端口默認是8888。
繼續向下找到以下代碼:
Allow 127.0.0.1
這行代碼表示被容許鏈接的主機IP。若是但願鏈接任何主機,那就直接將這行代碼註釋便可。在這裏咱們選擇直接註釋,也就是任何主機均可以使用這臺主機做爲代理服務器。
修改成以下代碼:
# Allow 127.0.0.1
設置完成以後重啓TinyProxy便可:
systemctl enable tinyproxy.service systemctl restart tinyproxy.service
防火牆開放該端口:
iptables -I INPUT -p tcp --dport 8888 -j ACCEPT
固然若是想直接關閉防火牆也能夠:
systemctl stop firewalld.service
這樣咱們就完成了TinyProxy的配置。
首先,用ifconfig
查看當前主機的IP。好比,當前個人主機撥號IP爲112.84.118.216,在其餘的主機運行測試一下。
用curl
命令設置代理請求httpbin,檢測代理是否生效。
curl -x 112.84.118.216:8888 httpbin.org/get
運行結果以下圖所示。
若是有正常的結果輸出,而且origin
的值爲代理IP的地址,就證實TinyProxy配置成功了。
如今能夠執行命令讓主機動態切換IP,也在主機上搭建了代理服務器。咱們只須要知道撥號後的IP就可使用代理。
咱們考慮到,在一臺主機撥號切換IP的間隙代理是不可用的,在這撥號的幾秒時間內若是有第二臺主機頂替第一臺主機,那就能夠解決撥號間隙代理沒法使用的問題了。因此咱們要設計的架構必需要考慮支持多主機的問題。
假若有10臺撥號主機同時須要維護,而爬蟲須要使用這10臺主機的代理,那麼在爬蟲端維護的開銷是很是大的。若是爬蟲在不一樣的機器上運行,那麼每一個爬蟲必需要得到這10臺撥號主機的配置,這顯然是不理想的。
爲了更加方便地使用代理,咱們能夠像上文的代理池同樣定義一個統一的代理接口,爬蟲端只須要配置代理接口便可獲取可用代理。要搭建一個接口,就勢必須要一臺服務器,而接口的數據從哪裏得到呢,固然最理想的仍是選擇數據庫。
好比咱們須要同時維護10臺撥號主機,每臺撥號主機都會定時撥號,那這樣每臺主機在某個時刻可用的代理只有一個,因此咱們沒有必要存儲以前的撥號代理,由於從新撥號以後以前的代理已經不能用了,因此只須要將以前的代理更新其內容就行了。數據庫要作的就是定時對每臺主機的代理進行更新,而更新時又須要撥號主機的惟一標識,根據主機標識查出這條數據,而後將這條數據對應的代理更新。
因此數據庫端就須要存儲一個主機標識到代理的映射關係。那麼很天然地咱們就會想到關係型數據庫如MySQL或者Redis的Hash存儲,只需存儲一個映射關係,不須要不少字段,並且Redis比MySQL效率更高、使用更方便,因此最終選定的存儲方式就是Redis的Hash。
那麼接下來咱們要作可被遠程訪問的Redis數據庫,各個撥號機器只須要將各自的主機標識和當前IP和端口(也就是代理)發送給數據庫就行了。
先定義一個操做Redis數據庫的類,示例以下:
import redis
import random
# Redis數據庫IP
REDIS_HOST = 'remoteaddress'
# Redis數據庫密碼, 如無則填None
REDIS_PASSWORD = 'foobared'
# Redis數據庫端口
REDIS_PORT = 6379
# 代理池鍵名
PROXY_KEY = 'adsl'
class RedisClient(object):
def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, proxy_key=PROXY_KEY):
"""
初始化Redis鏈接
:param host: Redis 地址
:param port: Redis 端口
:param password: Redis 密碼
:param proxy_key: Redis 散列表名
"""
self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)
self.proxy_key = proxy_key
def set(self, name, proxy):
"""
設置代理
:param name: 主機名稱
:param proxy: 代理
:return: 設置結果
"""
return self.db.hset(self.proxy_key, name, proxy)
def get(self, name):
"""
獲取代理
:param name: 主機名稱
:return: 代理
"""
return self.db.hget(self.proxy_key, name)
def count(self):
"""
獲取代理總數
:return: 代理總數
"""
return self.db.hlen(self.proxy_key)
def remove(self, name):
"""
刪除代理
:param name: 主機名稱
:return: 刪除結果
"""
return self.db.hdel(self.proxy_key, name)
def names(self):
"""
獲取主機名稱列表
:return: 獲取主機名稱列表
"""
return self.db.hkeys(self.proxy_key)
def proxies(self):
"""
獲取代理列表
:return: 代理列表
"""
return self.db.hvals(self.proxy_key)
def random(self):
"""
隨機獲取代理
:return:
"""
proxies = self.proxies()
return random.choice(proxies)
def all(self):
"""
獲取字典
:return:
"""
return self.db.hgetall(self.proxy_key)
這裏定義了一個RedisClient
類,在__init__()
方法中初始化了Redis鏈接,其中REDIS_HOST
就是遠程Redis的地址,REDIS_PASSWORD
是密碼,REDIS_PORT
是端口,PROXY_KEY
是存儲代理的散列表的鍵名。
接下來定義了一個set()
方法,這個方法用來向散列表添加映射關係。映射是從主機標識到代理的映射,好比一臺主機的標識爲adsl1,當前的代理爲118.119.111.172:8888,那麼散列表中就會存儲一個key爲adsl一、value爲118.119.111.172:8888的映射,Hash結構以下圖所示。
若是有多臺主機,只須要向Hash中添加映射便可。
另外,get()
方法就是從散列表中取出某臺主機對應的代理。remove()
方法則是從散列表中移除對應的主機的代理。還有names()
、proxies()
、all()
方法則是分別獲取散列表中的主機列表、代理列表及全部主機代理映射。count()
方法則是返回當前散列表的大小,也就是可用代理的數目。
最後還有一個比較重要的方法random()
,它隨機從散列表中取出一個可用代理,相似前面代理池的思想,確保每一個代理都能被取到。
若是要對數據庫進行操做,只須要初始化RedisClient
對象,而後調用它的set()
或者remove()
方法,便可對散列表進行設置和刪除。
接下來要作的就是撥號,並把新的IP保存到Redis散列表裏。
首先是撥號定時,它分爲定時撥號和非定時撥號兩種選擇。
非定時撥號,最好的方法就是向該主機發送一個信號,而後主機就啓動撥號,但這樣作的話,咱們首先要搭建一個從新撥號的接口,如搭建一個Web接口,請求該接口即進行撥號,但開始撥號以後,此時主機的狀態就從在線轉爲離線,而此時的Web接口也就相應失效了,撥號過程沒法再鏈接,撥號以後接口的IP也變了,因此咱們沒法經過接口來方便地控制撥號過程和獲取撥號結果,下次撥號還得改變撥號請求接口,因此非定時撥號的開銷仍是比較大的。
定時撥號,咱們只須要在撥號主機上運行定時腳本便可,每隔一段時間撥號一次,更新IP,而後將IP在Redis散列表中更新便可,很是簡單易用,另外能夠適當將撥號頻率調高一點,減小短期內IP被封的可能性。
在這裏選擇定時撥號。
接下來就是獲取IP。獲取撥號後的IP很是簡單,只須要調用ifconfig
命令,而後解析出對應網卡的IP便可。
獲取了IP以後,咱們還須要進行有效性檢測。撥號主機能夠本身檢測,好比能夠利用requests設置自身的代理請求外網,若是成功,那麼證實代理可用,而後再修改Redis散列表,更新代理。
須要注意,因爲在撥號的間隙撥號主機是離線狀態,而此時Redis散列表中還存留了上次的代理,一旦這個代理被取用了,該代理是沒法使用的。爲了不這個狀況,每臺主機在撥號以前還須要將自身的代理從Redis散列表中移除。
這樣基本的流程就理順了,咱們用以下代碼實現:
import re
import time
import requests
from requests.exceptions import ConnectionError, ReadTimeout
from db import RedisClient
# 撥號網卡
ADSL_IFNAME = 'ppp0'
# 測試URL
TEST_URL = 'http://www.baidu.com'
# 測試超時時間
TEST_TIMEOUT = 20
# 撥號間隔
ADSL_CYCLE = 100
# 撥號出錯重試間隔
ADSL_ERROR_CYCLE = 5
# ADSL命令
ADSL_BASH = 'adsl-stop;adsl-start'
# 代理運行端口
PROXY_PORT = 8888
# 客戶端惟一標識
CLIENT_NAME = 'adsl1'
class Sender():
def get_ip(self, ifname=ADSL_IFNAME):
"""
獲取本機IP
:param ifname: 網卡名稱
:return:
"""
(status, output) = subprocess.getstatusoutput('ifconfig')
if status == 0:
pattern = re.compile(ifname + '.*?inet.*?(\d+\.\d+\.\d+\.\d+).*?netmask', re.S)
result = re.search(pattern, output)
if result:
ip = result.group(1)
return ip
def test_proxy(self, proxy):
"""
測試代理
:param proxy: 代理
:return: 測試結果
"""
try:
response = requests.get(TEST_URL, proxies={
'http': 'http://' + proxy,
'https': 'https://' + proxy
}, timeout=TEST_TIMEOUT)
if response.status_code == 200:
return True
except (ConnectionError, ReadTimeout):
return False
def remove_proxy(self):
"""
移除代理
:return: None
"""
self.redis = RedisClient()
self.redis.remove(CLIENT_NAME)
print('Successfully Removed Proxy')
def set_proxy(self, proxy):
"""
設置代理
:param proxy: 代理
:return: None
"""
self.redis = RedisClient()
if self.redis.set(CLIENT_NAME, proxy):
print('Successfully Set Proxy', proxy)
def adsl(self):
"""
撥號主進程
:return: None
"""
while True:
print('ADSL Start, Remove Proxy, Please wait')
self.remove_proxy()
(status, output) = subprocess.getstatusoutput(ADSL_BASH)
if status == 0:
print('ADSL Successfully')
ip = self.get_ip()
if ip:
print('Now IP', ip)
print('Testing Proxy, Please Wait')
proxy = '{ip}:{port}'.format(ip=ip, port=PROXY_PORT)
if self.test_proxy(proxy):
print('Valid Proxy')
self.set_proxy(proxy)
print('Sleeping')
time.sleep(ADSL_CYCLE)
else:
print('Invalid Proxy')
else:
print('Get IP Failed, Re Dialing')
time.sleep(ADSL_ERROR_CYCLE)
else:
print('ADSL Failed, Please Check')
time.sleep(ADSL_ERROR_CYCLE)
def run():
sender = Sender()
sender.adsl()
在這裏定義了一個Sender
類,它的主要做用是執行定時撥號,並將新的IP測試經過以後更新到遠程Redis散列表裏。
主方法是adsl()
方法,它首先是一個無限循環,循環體內就是撥號的邏輯。
adsl()
方法首先調用了remove_proxy()
方法,將遠程Redis散列表中本機對應的代理移除,避免撥號時本主機的殘留代理被取到。
接下來利用subprocess模塊來執行撥號腳本,撥號腳本很簡單,就是stop
以後再start
,這裏將撥號的命令直接定義成了ADSL_BASH
。
隨後程序又調用get_ip()
方法,經過subprocess模塊執行獲取IP的命令ifconfig
,而後根據網卡名稱獲取了當前撥號網卡的IP地址,即撥號後的IP。
再接下來就須要測試代理有效性了。程序首先調用了test_proxy()
方法,將自身的代理設置好,使用requests庫來用代理鏈接TEST_URL
。在此TEST_URL
設置爲百度,若是請求成功,則證實代理有效。
若是代理有效,再調用set_proxy()
方法將Redis散列表中本機對應的代理更新,設置時須要指定本機惟一標識和本機當前代理。本機惟一標識可隨意配置,其對應的變量爲CLIENT_NAME
,保證各臺撥號主機不衝突便可。本機當前代理則由撥號後的新IP加端口組合而成。經過調用RedisClient
的set()
方法,參數name
爲本機惟一標識,proxy
爲撥號後的新代理,執行以後即可以更新散列表中的本機代理了。
建議至少配置兩臺主機,這樣在一臺主機的撥號間隙還有另外一臺主機的代理可用。撥號主機的數量不限,越多越好。
在撥號主機上執行撥號腳本,示例輸出以下圖所示。
首先移除了代理,再進行撥號,撥號完成以後獲取新的IP,代理檢測成功以後就設置到Redis散列表中,而後等待一段時間再從新進行撥號。
咱們添加了多臺撥號主機,這樣就有多個穩定的定時更新的代理可用了。Redis散列表會實時更新各臺撥號主機的代理,以下圖所示。
圖中所示是四臺ADSL撥號主機配置並運行後的散列表的內容,表中的代理都是可用的。
目前爲止,咱們已經成功實時更新撥號主機的代理。不過還缺乏一個模塊,那就是接口模塊。像以前的代理池同樣,咱們也定義一些接口來獲取代理,如random
獲取隨機代理、count
獲取代理個數等。
咱們選用Tornado來實現,利用Tornado的Server模塊搭建Web接口服務,示例以下:
import json
import tornado.ioloop
import tornado.web
from tornado.web import RequestHandler, Application
# API端口
API_PORT = 8000
class MainHandler(RequestHandler):
def initialize(self, redis):
self.redis = redis
def get(self, api=''):
if not api:
links = ['random', 'proxies', 'names', 'all', 'count']
self.write('<h4>Welcome to ADSL Proxy API</h4>')
for link in links:
self.write('<a href=' + link + '>' + link + '</a><br>')
if api == 'random':
result = self.redis.random()
if result:
self.write(result)
if api == 'names':
result = self.redis.names()
if result:
self.write(json.dumps(result))
if api == 'proxies':
result = self.redis.proxies()
if result:
self.write(json.dumps(result))
if api == 'all':
result = self.redis.all()
if result:
self.write(json.dumps(result))
if api == 'count':
self.write(str(self.redis.count()))
def server(redis, port=API_PORT, address=''):
application = Application([
(r'/', MainHandler, dict(redis=redis)),
(r'/(.*)', MainHandler, dict(redis=redis)),
])
application.listen(port, address=address)
print('ADSL API Listening on', port)
tornado.ioloop.IOLoop.instance().start()
這裏定義了5個接口,random
獲取隨機代理,names
獲取主機列表,proxies
獲取代理列表,all
獲取代理映射,count
獲取代理數量。
程序啓動以後便會在API_PORT端口上運行Web服務,主頁面以下圖所示。
訪問proxies
接口能夠得到全部代理列表,以下圖所示。
訪問random
接口能夠獲取隨機可用代理,以下圖所示。
咱們只需將接口部署到服務器上,便可經過Web接口獲取可用代理,獲取方式和代理池相似。
本節代碼地址爲:https://github.com/Python3WebSpider/AdslProxy。
本節介紹了ADSL撥號代理的搭建過程。經過這種代理,咱們能夠無限次更換IP,並且線路很是穩定,抓取效果好不少。