最近發(fā)現(xiàn)自己之前爬的某個(gè)網(wǎng)站更換了新的網(wǎng)頁設(shè)計(jì),于是重寫了爬蟲,在測試的時(shí)候突然被封了 IP寻仗,雖然說一般網(wǎng)站都不是永久封 IP,但是等不了的我還是嘗試用 IP 池來突破該網(wǎng)站的反爬凡壤。
而就在我測試爬下來的 IP 能不能使用的時(shí)候署尤,某提供 IP 池的網(wǎng)站也把我的 IP 封了!想不到現(xiàn)在的反爬策略已經(jīng)如此激進(jìn)亚侠。
開始之前
首先要清楚一些基本的網(wǎng)絡(luò)狀態(tài)號(hào)曹体。
- 1XX消息 這一類型的狀態(tài)碼,代表請求已被接受硝烂,需要繼續(xù)處理箕别。(一般很少用)
-
2XX成功 這一類型的狀態(tài)碼,代表請求已成功被服務(wù)器接收、理解串稀、并接受除抛。(但是未必能按請求返回結(jié)果)
200 OK 請求成功
201 Created 請求已經(jīng)被實(shí)現(xiàn),而且有一個(gè)新的資源已經(jīng)依據(jù)請求的需要而建立
202 Accepted 服務(wù)器已接受請求母截,但尚未處理 -
3XX重定向 這類狀態(tài)碼代表需要客戶端采取進(jìn)一步的操作才能完成請求到忽。通常,重定向目標(biāo)在本次響應(yīng)的Location域中指明清寇。
301 Moved Permanently 被請求的資源已永久移動(dòng)到新位置
302 Found 要求客戶端執(zhí)行臨時(shí)重定向, 原始描述短語為“Moved Temporarily” -
4xx客戶端錯(cuò)誤 這類的狀態(tài)碼代表了客戶端看起來可能發(fā)生了錯(cuò)誤喘漏,妨礙了服務(wù)器的處理.
401 Unauthorized 該狀態(tài)碼表示當(dāng)前請求需要用戶驗(yàn)證
403 Forbidden 服務(wù)器已經(jīng)理解請求,但是拒絕執(zhí)行它(爬蟲被禁的標(biāo)志)
404 Not Found 請求失敗华烟,請求所希望得到的資源未被在服務(wù)器上發(fā)現(xiàn) -
5xx服務(wù)器錯(cuò)誤 這類狀態(tài)碼代表了服務(wù)器在處理請求的過程中有錯(cuò)誤或者異常狀態(tài)發(fā)生陷遮,也有可能是服務(wù)器意識(shí)到以當(dāng)前的軟硬件資源無法完成對請求的處理.
500 Internal Server Error 通用錯(cuò)誤消息,服務(wù)器遇到了一個(gè)未曾預(yù)料的狀況垦江,導(dǎo)致了它無法完成對請求的處理帽馋。沒有給出具體錯(cuò)誤信息。
502 Bad Gateway 作為網(wǎng)關(guān)或 "代理服務(wù)器" 工作的服務(wù)器嘗試執(zhí)行請求時(shí)比吭,從上游服務(wù)器接收到無效的響應(yīng)绽族。
503 Service Unavailable 由于臨時(shí)的服務(wù)器維護(hù)或者過載,服務(wù)器當(dāng)前無法處理請求衩藤。
在爬蟲過程中吧慢,我們最想看到的狀態(tài)碼是 200,最不想看到的是 403赏表,當(dāng)你看到 403检诗,有相當(dāng)大可能是你的爬蟲被封了。
常見的反爬和反反爬策略
基于 Headers 和 UserAgent 的反爬
這應(yīng)該是最基本的反爬瓢剿,之前的文章提到過一些網(wǎng)站的 robots.txt 會(huì)明確指明哪些 header 名不能訪問網(wǎng)站(比如一些國內(nèi)的網(wǎng)站不會(huì)讓國外某些搜索網(wǎng)站收錄逢慌,因?yàn)檫@只會(huì)增加網(wǎng)站負(fù)載,但是無法帶來真正有用的流量)
-
應(yīng)對方式 隨機(jī)更換
UserAgent
间狂」テ茫可以自己寫一個(gè)UserAgent
列表,然后隨機(jī)挑一條作為當(dāng)前爬蟲請求的UserAgent
鉴象,也可以使用已經(jīng)寫好的庫fake_useragent
安裝使用非常簡單:
# 安裝
pip install fake_useragent
>>> from fake_useragent import UserAgent
>>> ua = UserAgent(verify_ssl=False)
>>> ua.random
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36'
基于用戶行為的反爬
爬蟲除了有英文 Spider 外忙菠,還有一個(gè)英文是 bot,也就是機(jī)器人纺弊,機(jī)器人固定的模式是比較容易識(shí)別的牛欢。爬蟲這個(gè)機(jī)器人最明顯的行為特征就是短期內(nèi)密集多次請求網(wǎng)站數(shù)據(jù)。
- 應(yīng)對方式1 減少請求數(shù)量淆游,減少請求密度 在 Scrapy 中可以設(shè)置并發(fā)請求的數(shù)量傍睹,也可以設(shè)置下載延遲隔盛。前面提到我爬取的 IP 池網(wǎng)站,就是沒有設(shè)置下載延遲焰望,很快就被網(wǎng)站封了 IP骚亿。
- 應(yīng)對方式2 變換 IP 通過多個(gè) IP 代理你的請求進(jìn)行爬蟲已亥,繞過同一個(gè) IP 多次請求的反爬熊赖。
多說一句,基于用戶行為能做的除了反爬虑椎,還能精準(zhǔn)推送震鹉,精準(zhǔn)拉黑。精準(zhǔn)推送比如你多次搜索某些關(guān)鍵詞捆姜,在網(wǎng)頁中你會(huì)收到相關(guān)的廣告传趾;精準(zhǔn)拉黑比如你使用百度云的破解插件或者修改版多次后,你會(huì)被限制下載等泥技。
隱藏真實(shí)地址的動(dòng)態(tài)網(wǎng)頁反爬
之前筆者的文章寫過 JS動(dòng)態(tài)加載以及JavaScript void(0)的爬蟲解決方案浆兰,實(shí)際上是動(dòng)態(tài)網(wǎng)頁中最基本的反爬。更高級(jí)的反爬珊豹,會(huì)把請求過程中的 XHR 對象的真實(shí)地址進(jìn)一步隱藏簸呈,如果直接打開該XHR地址,你收到的內(nèi)容可能是一樣的店茶,也可能什么內(nèi)容都沒收到蜕便。
應(yīng)對方式1 下圖中的網(wǎng)址就隱藏了真實(shí)網(wǎng)址,你可能需要去查看請求的頭部信息猜測請求參數(shù)贩幻,或者直接通過發(fā)送相同的頭部信息繞過反爬轿腺。
應(yīng)對方式2 使用 selenium+phantomJS 框架調(diào)用瀏覽器內(nèi)核模擬人瀏覽網(wǎng)站的行為,比如滾動(dòng)鼠標(biāo)丛楚,滑動(dòng)驗(yàn)證碼等來繞過反爬族壳,這種應(yīng)該是比較高級(jí)的反反爬策略了。
IP 池突破反爬策略
平時(shí)為了隱藏自己的網(wǎng)絡(luò)行為趣些,有些人會(huì)使用 VPN 來代理自己的流量决侈,隱藏真實(shí)的IP地址。IP 池也是這個(gè)道理喧务,通過不斷變換請求的 IP 地址赖歌,偽裝出低頻訪問的假象繞過反爬策略。
在 Scrapy 中你需要做的有:
- 爬取并存儲(chǔ)可用 IP(當(dāng)然功茴,RMB玩家可以直接購買接口使用)
- 編輯并啟用 IP 池中間件
提供 IP 池的網(wǎng)站有不少庐冯,并且大部分會(huì)提供免費(fèi)易黃版和RMB玩家穩(wěn)定版,我使用的是免費(fèi)版坎穿,這里介紹兩個(gè)
https://www.kuaidaili.com
http://www.xicidaili.com/
在爬取中務(wù)必設(shè)置合適的速度展父,否則還沒爬到 IP 自己的先被封了返劲。
IP 池是一個(gè)動(dòng)態(tài)構(gòu)建的倉庫,無論是插入還是取出都必須驗(yàn)證該 IP 的有效性栖茉。如何驗(yàn)證篮绿?Python3 中有一個(gè)輕量的 requests 庫(非標(biāo)準(zhǔn)庫),你可以使用該IP地址請求某個(gè)網(wǎng)站看看返回的狀態(tài)碼是否是 200(有時(shí)候也可能是 3XX 這樣的重定向狀態(tài)碼)吕漂,是則證明 IP 可用亲配,可用來爬取信息,否則直接移除惶凝,不保存吼虎。
示例
最好使用 try-except
避免因?yàn)閳?bào)錯(cuò)退出
import requests
request_url = 'http://wwwbaidu.com'
proxy = {'http':'218.28.58.150:53281'}
try:
requests.get(url=request_url, proxies=proxy, timeout=5)
except Exception as e:
print(e)
整體的流程大概是
- 爬取 IP 網(wǎng)站
驗(yàn)證 IP
>status == 200 ? 入庫:下一條
- 爬取數(shù)據(jù)
取出 IP
驗(yàn)證 IP
>status == 200 ? 出庫, 執(zhí)行爬蟲:下一條
未找到可用 IP, 數(shù)據(jù)庫為空 -> 爬取 IP 網(wǎng)站
按照下面的步驟,就大功告成啦苍鲜。
- 建立
ipProxy.py
的文件(需要新建數(shù)據(jù)庫表) - 在
middlewares.py
中創(chuàng)建中間件 -
settings.py
中啟用中間件
ipProxy.py
# 此類用于爬取和存儲(chǔ)IP
import requests
from scrapy.selector import Selector
import pymysql
import time
# 鏈接數(shù)據(jù)庫
conn = pymysql.connect(host="127.0.0.1", user="feson", passwd="feson", db="Spider", charset="utf8")
cursor = conn.cursor()
# UserAgent,這里也可以使用隨機(jī)的
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
}
class GetRandomIp(object):
# 用于解析網(wǎng)頁
def parse(self, next_url='/inha/1'):
"""
Parse Ip List From Site, Transfer to parse_detail
:param next_url:
:return: None
"""
print("Begin Parsing...")
response = requests.get(url='https://www.kuaidaili.com/free/intr'.format(next_url), headers=headers)
response = Selector(text=response.text)
tr_list = response.xpath('//*[@id="list"]/table/tbody/tr/td')
if tr_list:
self.parse_detail(tr_list)
for i in range(20):
time.sleep(5)
next_url = 'https://www.kuaidaili.com/free/intr/%d' % i
if next_url:
self.parse(next_url)
def parse_detail(self, tr_list):
"""
Parse Ip detail from list, transfer to insert into database
:param tr_list:
:return: None
"""
ip = tr_list.xpath('//td[@data-title="IP"]/text()').extract()
port = tr_list.xpath('//td[@data-title="PORT"]/text()').extract()
type = tr_list.xpath('//td[@data-title="類型"]/text()').extract()
speed = tr_list.xpath('//td[@data-title="響應(yīng)速度"]/text()').extract()
for i in range(len(ip)):
self.insert_sql(ip[i], port[i], type[i])
def insert_sql(self, ip, port, type):
type = type.lower()
proxy_url = '{0}://{1}:{2}'.format(type, ip, port)
res = self.check_ip(type, proxy_url)
print(proxy_url)
if res:
cursor.execute(
"insert proxy_ip(ip, port, type) VALUES('{0}', '{1}', '{2}')".format(
ip, port, type
)
)
conn.commit()
def get_ip(self):
# 獲取和檢查IP
sql = "select * from proxy_ip ORDER BY RAND() LIMIT 1"
cursor.execute(sql)
ip, port, type = cursor.fetchone()
conn.commit()
type = type.lower()
proxy_url = '{0}://{1}:{2}'.format(type, ip, port)
res = self.check_ip(type, proxy_url)
if res:
return proxy_url
else:
self.delete_ip(ip)
return self.get_ip()
def check_ip(self, type, proxy_url):
request_url = 'http://hf.58.com/ershoufang/0'
try:
proxy = {type: proxy_url}
response = requests.get(url=request_url, proxies=proxy, timeout=5)
except Exception as e:
print(e)
return False
else:
code = response.status_code
if code == 200 or code == 302:
return True
else:
print('invalid ip and port')
return False
def delete_ip(self, ip):
sql = """delete from proxy_ip where ip='%s'""" % ip
cursor.execute(sql)
conn.commit()
ip = GetRandomIp()
if __name__ == '__main__':
ip = GetRandomIp()
ip.parse()
middlewares.py
import ipProxy
class RandomIpMiddleware(object):
def process_request(self, request, spider):
ip_proxy = ipProxy.ip.get_ip()
request.meta['proxy'] = ip_proxy
settings.py
# 添加以下參數(shù), 沒有就新建條目
...
# Retry many times since proxies often fail
RETRY_TIMES = 3
# Retry on most error codes since proxies fail for different reasons
RETRY_HTTP_CODES = [500, 503, 504, 400, 403, 404, 408]
# Enable or disable downloader middlewares
DOWNLOADER_MIDDLEWARES = {
'middleware.customUserAgent.RandomUserAgent': 543,
'finvest.middlewares.RandomIpMiddleware': 520,
}
歡迎關(guān)注公眾號(hào): 程序員的碎碎念