熟悉爬蟲的,必定會熟悉各種反爬機制。今天就講一下自己如何建立ip代理池的。
一個合格的代理池必須擁有一個爬取代理IP的爬取器、一個驗證IP可否使用的校驗器、一個存儲IP的數據庫、調用這些的調度器以及可以供獲取IP的接口(這裏推薦flask,比較簡單)。
先來說說爬取器,首先要爬取的代理IP網站儘量是無需登錄的,其次是對代理IP更新較快的,前者加快代理池的效率,後者增加代理池的質量。這裏我對市面上部分代理網站進行爬取,當然一些常用的代理IP網站提供IP質量不高,比如西刺無憂66這些經常被爬取(西刺偶爾還會崩潰,估計是爬取的人有些多,網站保護性503)
def crawl_xici():
"""
西刺代理:http://www.xicidaili.com
"""
url = "http://www.xicidaili.com/{}"
items = []
for page in range(1, 21):
items.append(("wt/{}".format(page), "http://{}:{}"))
items.append(("wn/{}".format(page), "https://{}:{}"))
for item in items:
proxy_type, host = item
html = requests(url.format(proxy_type))
if html:
doc = pyquery.PyQuery(html)
for proxy in doc("table tr").items():
ip = proxy("td:nth-child(2)").text()
port = proxy("td:nth-child(3)").text()
if ip and port:
yield host.format(ip, port)
def crawl_zhandaye():
"""
站大爺代理:http://ip.zdaye.com/dayProxy.html
"""
url = 'http://ip.zdaye.com/dayProxy.html'
html = requests(url)
sttrs = re.findall('<H3 class="title"><a href="(.*?)">', html, re.S)
for sttr in sttrs:
new_url = url[:28] + sttr[9:]
new_html = requests_other(new_url)
get_div = re.search("<div class=\"cont\">(.*?)</div>", new_html, re.S).group(1)
print(get_div)
results = re.findall("<br>(.*?)@(.*?)#\[(.*?)\]", get_div, re.S)
for result in results:
yield "{}://{}".format(result[1].lower(), result[0])
def crawl_66ip():
"""
66ip 代理:http://www.66ip.cn
19-04-30可用
"""
url = (
"http://www.66ip.cn/nmtq.php?getnum=100&isp=0"
"&anonymoustype=0&area=0&proxytype={}&api=66ip"
)
pattern = "\d+\.\d+.\d+\.\d+:\d+"
items = [(0, "http://{}"), (1, "https://{}")]
for item in items:
proxy_type, host = item
html = requests(url.format(proxy_type))
if html:
for proxy in re.findall(pattern, html):
yield host.format(proxy)
def crawl_kuaidaili():
"""
快代理:https://www.kuaidaili.com
每次30個
19-04-13可用
"""
url = "https://www.kuaidaili.com/free/inha/{}/"
items = [p for p in range(1, 3)]
for page in items:
html = requests(url.format(page))
if html:
doc = pyquery.PyQuery(html)
for proxy in doc(".table-bordered tr").items():
ip = proxy("[data-title=IP]").text()
port = proxy("[data-title=PORT]").text()
if ip and port:
yield "http://{}:{}".format(ip, port)
def crawl_ip3366():
"""
雲代理:http://www.ip3366.net
每頁10個,驗證較快
19-04-30可用
"""
url = "http://www.ip3366.net/?stype=1&page={}"
items = [p for p in range(1, 8)]
for page in items:
html = requests(url.format(page))
if html:
doc = pyquery.PyQuery(html)
for proxy in doc(".table-bordered tr").items():
ip = proxy("td:nth-child(1)").text()
port = proxy("td:nth-child(2)").text()
schema = proxy("td:nth-child(4)").text()
if ip and port and schema:
yield "{}://{}:{}".format(schema.lower(), ip, port)
def crawl_data5u():
"""
無憂代理:http://www.data5u.com/
每次14個,驗證時間比較新
19-04-30可用
"""
url = "http://www.data5u.com/free/index.html"
html = requests(url)
if html:
doc = pyquery.PyQuery(html)
for index, item in enumerate(doc(".wlist li .l2").items()):
if index > 0:
ip = item("span:nth-child(1)").text()
port = item("span:nth-child(2)").text()
schema = item("span:nth-child(4)").text()
if ip and port and schema:
yield "{}://{}:{}".format(schema, ip, port)
def crawl_iphai():
"""
ip 海代理:http://www.iphai.com
爬取國內高匿、國外高匿、國外普通各10個
19-04-30可用
"""
url = "http://www.iphai.com/free/{}"
items = ["ng", "np", "wg", "wp"]
for proxy_type in items:
html = requests(url.format(proxy_type))
if html:
doc = pyquery.PyQuery(html)
for item in doc(".table-bordered tr").items():
ip = item("td:nth-child(1)").text()
port = item("td:nth-child(2)").text()
schema = item("td:nth-child(4)").text().split(",")[0]
if ip and port and schema:
yield "{}://{}:{}".format(schema.lower(), ip, port)
解釋一下代碼,返回的代理一般都是(http/https)://ip:port格式的代理。這裏的requests是使用 asyncio、aiohttp做了個方法,來實現異步爬取。對於asyncio的介紹可以查看廖雪峯的教程。這個是自己寫的異步爬取,替換了之前的request.get方法,加快爬蟲效率。
import asyncio
import aiohttp
from settings import HEADERS, REQUEST_TIMEOUT, REQUEST_DELAY
LOOP = asyncio.get_event_loop()
async def _get_page(url, sleep):
"""
獲取並返回網頁內容
"""
async with aiohttp.ClientSession() as session:
try:
await asyncio.sleep(sleep)
async with session.get(
url, headers=HEADERS, timeout=REQUEST_TIMEOUT
) as resp:
return await resp.text()
except:
return ""
def requests(url, sleep=REQUEST_DELAY):
"""
請求方法,用於獲取網頁內容
:param url: 請求鏈接
:param sleep: 延遲時間(秒)
"""
html = LOOP.run_until_complete(asyncio.gather(_get_page(url, sleep)))
if html:
return "".join(html)
做好異步爬取工作就應該完成代理池搭建的30%了,之後我們就要嘗試保存進數據庫了,這裏推薦使用redis數據庫,畢竟其中的有序集合類型(sorted set)非常適合代理池cookies池的搭建,因爲其中是有score的,也就是我們存入一個代理的同時也要給它一個分數,這方便我們之後對其校驗以及取代理IP的優先級。redis有序集合類型
# redis 地址
REDIS_HOST = "localhost"
# redis 端口
REDIS_PORT = 6379
# redis 密碼
REDIS_PASSWORD = None
# redis set key
REDIS_KEY = "myproxies"
# redis 連接池最大連接量
REDIS_MAX_CONNECTION = 20
# REDIS SCORE 最大分數
MAX_SCORE = 10
# REDIS SCORE 最小分數
MIN_SCORE = 0
# REDIS SCORE 初始分數
INIT_SCORE = 5
class RedisClient:
"""
代理池依賴了 Redis 數據庫,使用了其`有序集合`的數據結構
(可按分數排序,key 值不能重複)
"""
def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
conn_pool = redis.ConnectionPool(
host=host,
port=port,
password=password,
max_connections=REDIS_MAX_CONNECTION,
)
self.redis = redis.Redis(connection_pool=conn_pool)
def add_proxy(self, proxy, score=INIT_SCORE):
"""
新增一個代理,初始化分數 INIT_SCORE < MAX_SCORE,確保在
運行完收集器後還沒運行校驗器就獲取代理,導致獲取到分數雖爲 MAX_SCORE,
但實際上確是未經驗證,不可用的代理
:param proxy: 新增代理
:param score: 初始化分數
"""
if not self.redis.zscore(REDIS_KEY, proxy):
self.redis.zadd(REDIS_KEY, proxy, score)
def reduce_proxy_score(self, proxy):
"""
驗證未通過,分數減一
:param proxy: 驗證代理
"""
score = self.redis.zscore(REDIS_KEY, proxy)
if score and score > MIN_SCORE:
self.redis.zincrby(REDIS_KEY, proxy, -1)
else:
self.redis.zrem(REDIS_KEY, proxy)
def increase_proxy_score(self, proxy):
"""
驗證通過,分數加一
:param proxy: 驗證代理
"""
score = self.redis.zscore(REDIS_KEY, proxy)
if score and score < MAX_SCORE:
self.redis.zincrby(REDIS_KEY, proxy, 1)
def pop_proxy(self):
"""
返回一個代理
"""
# 第一次嘗試取分數最高,也就是最新可用的代理
first_chance = self.redis.zrangebyscore(REDIS_KEY, MAX_SCORE, MAX_SCORE)
if first_chance:
return random.choice(first_chance)
else:
# 第二次嘗試取 7-10 分數的任意一個代理
second_chance = self.redis.zrangebyscore(
REDIS_KEY, MAX_SCORE - 3, MAX_SCORE
)
if second_chance:
return random.choice(second_chance)
# 最後一次就隨便取咯
else:
last_chance = self.redis.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)
if last_chance:
return random.choice(last_chance)
def get_proxies(self, count=1):
"""
返回指定數量代理,分數由高到低排序
:param count: 代理數量
"""
proxies = self.redis.zrevrange(REDIS_KEY, 0, count - 1)
for proxy in proxies:
yield proxy.decode("utf-8")
def count_all_proxies(self):
"""
返回所有代理總數
"""
return self.redis.zcard(REDIS_KEY)
def count_score_proxies(self, score):
"""
返回指定分數代理總數
:param score: 代理分數
"""
if 0 <= score <= 10:
proxies = self.redis.zrangebyscore(REDIS_KEY, score, score)
return len(proxies)
return -1
def clear_proxies(self, score):
"""
刪除分數小於等於 score 的代理
"""
if 0 <= score <= 10:
proxies = self.redis.zrangebyscore(REDIS_KEY, 0, score)
for proxy in proxies:
self.redis.zrem(REDIS_KEY, proxy)
return True
return False
def all_proxies(self):
"""
返回全部代理
"""
return self.redis.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)
這就是寫的對redis數據庫的操作,至於我們如何把爬取的IP放入,就需要改一下我們的爬取器了,封裝成一個類會比我寫這樣的方法好得多。
redis_conn = RedisClient()
all_funcs = []
def collect_funcs(func):
"""
裝飾器,用於收集爬蟲函數
"""
all_funcs.append(func)
return func
class Crawler:
"""
返回格式: http://host:port
"""
@staticmethod
def run():
"""
啓動收集器
"""
for func in all_funcs:
for proxy in func():
redis_conn.add_proxy(proxy)
#添加之前寫的爬取ip方法,在每個方法之前聲明裝飾器
@collect_funcs
#然後實例個對象
crawl = Crawl()
完成了數據庫和爬取器的搭建,就差不多完成七七八八了,現在就加驗證器,驗證器是爲了驗證代理IP是否可用,如何可以使用的話就給它加一分,如果不可以使用的話就減一分
import os
import asyncio
import aiohttp
from db import RedisClient
#驗證url
VALIDATOR_BASE_URL = "http://baidu.com"
#批量測試數量
VALIDATOR_BATCH_COUNT = 250
class Validator:
def __init__(self):
self.redis = RedisClient()
async def test_proxy(self, proxy):
"""
測試代理
:param proxy: 指定代理
"""
async with aiohttp.ClientSession() as session:
try:
if isinstance(proxy, bytes):
proxy = proxy.decode("utf8")
async with session.get(
VALIDATOR_BASE_URL, proxy=proxy, timeout=REQUEST_TIMEOUT
) as resp:
if resp.status == 200:
self.redis.increase_proxy_score(proxy)
else:
self.redis.reduce_proxy_score(proxy)
except:
self.redis.reduce_proxy_score(proxy)
def run(self):
"""
啓動校驗器
"""
proxies = self.redis.all_proxies()
loop = asyncio.get_event_loop()
for i in range(0, len(proxies), VALIDATOR_BATCH_COUNT):
_proxies = proxies[i : i + VALIDATOR_BATCH_COUNT]
tasks = [self.test_proxy(proxy) for proxy in _proxies]
if tasks:
loop.run_until_complete(asyncio.wait(tasks))
validator = Validator()
網上有許多檢驗IP的方法,諸如requests.get telnet之類的,這裏利用的是aiohttp的session,其實這個檢驗都差不多的,只是在爬取IP那邊利用的異步,乾脆檢驗這裏也用異步吧。
之後寫個調度器,調度器就是運行項目後,在每隔一個時間段後運行某個方法,這裏我們設置循環時間,爬取是30分鐘,檢驗是15分鐘,然後啓動爬取器和校驗器的run方法
import time
import schedule
from crawler import crawler
from validator import validator
#爬取ip檢查時間(分)
CRAWLER_RUN_CYCLE = 30
#驗證ip檢查時間(分)
VALIDATOR_RUN_CYCLE = 15
def run_schedule():
"""
啓動客戶端
"""
# 啓動收集器
schedule.every(CRAWLER_RUN_CYCLE).minutes.do(crawler.run).run()
# 啓動驗證器
schedule.every(VALIDATOR_RUN_CYCLE).minutes.do(validator.run).run()
while True:
try:
schedule.run_pending()
time.sleep(1)
except KeyboardInterrupt:
return
最後就是整個項目如何運行了,我們單純跑調度器肯定是不合適的,因爲這樣redis中有代理了,但是我們要在爬蟲項目中連接redis來獲取代理,這一點是比較麻煩的,而且每次運行都要在本地跑一次代理池,這樣肯定不符合程序員偷懶的初衷。正確的做法是做一個api接口,然後部署到雲端,讓雲服務器爬取代理IP,這樣我們就每次訪問api接口就能得到我們的數據了,下面來寫寫接口,這裏用的是Flask,因爲簡單
from flask import Flask, jsonify
from db import RedisClient
from scheduler import run_schedule
myapp = Flask(__name__)
redis_conn = RedisClient()
@myapp.route("/")
def index():
return jsonify({"Welcome": "This is a proxy pool system."},
{"if there has problem": "Please communicate with QQ:976264593"})
@myapp.route("/pop")
def pop_proxy():
proxy = redis_conn.pop_proxy().decode("utf8")
if proxy[:5] == "https":
return jsonify({"https": proxy})
else:
return jsonify({"http": proxy})
@myapp.route("/get/<int:count>")
def get_proxy(count):
res = []
for proxy in redis_conn.get_proxies(count):
if proxy[:5] == "https":
res.append({"https": proxy})
else:
res.append({"http": proxy})
return jsonify(res)
@myapp.route("/count")
def count_all_proxies():
count = redis_conn.count_all_proxies()
return jsonify({"count": str(count)})
@myapp.route("/count/<int:score>")
def count_score_proxies(score):
count = redis_conn.count_score_proxies(score)
return jsonify({"count": str(count)})
@myapp.route("/clear/<int:score>")
def clear_proxies(score):
if redis_conn.clear_proxies(score):
return jsonify({"Clear": "Successful"})
return jsonify({"Clear": "Score should >= 0 and <= 10"})
if __name__ == "__main__":
# 啓動服務端 Flask app
myapp.run(host='localhost', port=5000, debug=True)
run_schedule()
這個接口比較簡單,實現了查看代理池數量,獲取代理IP(推薦pop因爲使用後就可以將代理IP刪除)當然有許多低分的代理我們也可以通過接口調用將其刪除。出於安全性,其實這個api應該加個校驗的,不然別人一直用你的代理池就白費功夫了。
好了,這就是python代理池搭建,如果有不懂可以提出來,博主也是小白一個