為你的爬蟲添加 IP 池反反爬策略

最近發現自己之前爬的某個網站更換了新的網頁設計,于是重寫了爬蟲,在測試的時候突然被封了 IP,雖然說一般網站都不是永久封 IP,但是等不了的我還是嘗試用 IP 池來突破該網站的反爬。

封面

而就在我測試爬下來的 IP 能不能使用的時候,某提供 IP 池的網站也把我的 IP 封了!想不到現在的反爬策略已經如此激進。

開始之前

首先要清楚一些基本的網絡狀態號。

  • 1XX消息 這一類型的狀態碼,代表請求已被接受,需要繼續處理。(一般很少用)
  • 2XX成功 這一類型的狀態碼,代表請求已成功被服務器接收、理解、并接受。(但是未必能按請求返回結果)
    200 OK 請求成功
    201 Created 請求已經被實現,而且有一個新的資源已經依據請求的需要而建立
    202 Accepted 服務器已接受請求,但尚未處理
  • 3XX重定向 這類狀態碼代表需要客戶端采取進一步的操作才能完成請求。通常,重定向目標在本次響應的Location域中指明。
    301 Moved Permanently 被請求的資源已永久移動到新位置
    302 Found 要求客戶端執行臨時重定向, 原始描述短語為“Moved Temporarily”
  • 4xx客戶端錯誤 這類的狀態碼代表了客戶端看起來可能發生了錯誤,妨礙了服務器的處理.
    401 Unauthorized 該狀態碼表示當前請求需要用戶驗證
    403 Forbidden 服務器已經理解請求,但是拒絕執行它(爬蟲被禁的標志)
    404 Not Found 請求失敗,請求所希望得到的資源未被在服務器上發現
  • 5xx服務器錯誤 這類狀態碼代表了服務器在處理請求的過程中有錯誤或者異常狀態發生,也有可能是服務器意識到以當前的軟硬件資源無法完成對請求的處理.
    500 Internal Server Error 通用錯誤消息,服務器遇到了一個未曾預料的狀況,導致了它無法完成對請求的處理。沒有給出具體錯誤信息。
    502 Bad Gateway 作為網關或 "代理服務器" 工作的服務器嘗試執行請求時,從上游服務器接收到無效的響應。
    503 Service Unavailable 由于臨時的服務器維護或者過載,服務器當前無法處理請求。

在爬蟲過程中,我們最想看到的狀態碼是 200,最不想看到的是 403,當你看到 403,有相當大可能是你的爬蟲被封了。

常見的反爬和反反爬策略

基于 Headers 和 UserAgent 的反爬

這應該是最基本的反爬,之前的文章提到過一些網站的 robots.txt 會明確指明哪些 header 名不能訪問網站(比如一些國內的網站不會讓國外某些搜索網站收錄,因為這只會增加網站負載,但是無法帶來真正有用的流量

  • 應對方式 隨機更換 UserAgent。可以自己寫一個 UserAgent 列表,然后隨機挑一條作為當前爬蟲請求的 UserAgent,也可以使用已經寫好的庫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 外,還有一個英文是 bot,也就是機器人,機器人固定的模式是比較容易識別的。爬蟲這個機器人最明顯的行為特征就是短期內密集多次請求網站數據。

  • 應對方式1 減少請求數量,減少請求密度 在 Scrapy 中可以設置并發請求的數量,也可以設置下載延遲。前面提到我爬取的 IP 池網站,就是沒有設置下載延遲,很快就被網站封了 IP。
  • 應對方式2 變換 IP 通過多個 IP 代理你的請求進行爬蟲,繞過同一個 IP 多次請求的反爬。

多說一句,基于用戶行為能做的除了反爬,還能精準推送,精準拉黑。精準推送比如你多次搜索某些關鍵詞,在網頁中你會收到相關的廣告;精準拉黑比如你使用百度云的破解插件或者修改版多次后,你會被限制下載等。

隱藏真實地址的動態網頁反爬

之前筆者的文章寫過 JS動態加載以及JavaScript void(0)的爬蟲解決方案,實際上是動態網頁中最基本的反爬。更高級的反爬,會把請求過程中的 XHR 對象的真實地址進一步隱藏,如果直接打開該XHR地址,你收到的內容可能是一樣的,也可能什么內容都沒收到。

應對方式1 下圖中的網址就隱藏了真實網址,你可能需要去查看請求的頭部信息猜測請求參數,或者直接通過發送相同的頭部信息繞過反爬。

示例

應對方式2 使用 selenium+phantomJS 框架調用瀏覽器內核模擬人瀏覽網站的行為,比如滾動鼠標,滑動驗證碼等來繞過反爬,這種應該是比較高級的反反爬策略了。


IP 池突破反爬策略

平時為了隱藏自己的網絡行為,有些人會使用 VPN 來代理自己的流量,隱藏真實的IP地址。IP 池也是這個道理,通過不斷變換請求的 IP 地址,偽裝出低頻訪問的假象繞過反爬策略。

在 Scrapy 中你需要做的有:

  • 爬取并存儲可用 IP(當然,RMB玩家可以直接購買接口使用)
  • 編輯并啟用 IP 池中間件

提供 IP 池的網站有不少,并且大部分會提供免費易黃版RMB玩家穩定版,我使用的是免費版,這里介紹兩個

https://www.kuaidaili.com
http://www.xicidaili.com/

在爬取中務必設置合適的速度,否則還沒爬到 IP 自己的先被封了。

IP 池是一個動態構建的倉庫,無論是插入還是取出都必須驗證該 IP 的有效性。如何驗證?Python3 中有一個輕量的 requests 庫(非標準庫),你可以使用該IP地址請求某個網站看看返回的狀態碼是否是 200(有時候也可能是 3XX 這樣的重定向狀態碼),是則證明 IP 可用,可用來爬取信息,否則直接移除,不保存。

示例

最好使用 try-except 避免因為報錯退出

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 網站
  驗證 IP
  >status == 200 ? 入庫:下一條

- 爬取數據
  取出 IP
  驗證 IP
  >status == 200 ? 出庫, 執行爬蟲:下一條
  未找到可用 IP, 數據庫為空 -> 爬取 IP 網站

按照下面的步驟,就大功告成啦。

  1. 建立 ipProxy.py 的文件(需要新建數據庫表)
  2. middlewares.py 中創建中間件
  3. settings.py 中啟用中間件

ipProxy.py

# 此類用于爬取和存儲IP
import requests
from scrapy.selector import Selector
import pymysql
import time

# 鏈接數據庫
conn = pymysql.connect(host="127.0.0.1", user="feson", passwd="feson", db="Spider", charset="utf8")
cursor = conn.cursor()

# UserAgent,這里也可以使用隨機的
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):
    # 用于解析網頁
    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="響應速度"]/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

# 添加以下參數, 沒有就新建條目
...
# 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,
}

歡迎關注公眾號: 程序員的碎碎念

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373