原文連接: Jack-Cui,blog.csdn.net/c406495762html
運行平臺: Windows Python版本: Python3.x IDE: Sublime text3python
近期,有些朋友問我一些關於如何應對反爬蟲的問題。因爲好多朋友都在問,所以決定寫一篇此類的博客。把我知道的一些方法,分享給你們。博主屬於小菜級別,玩爬蟲也徹底是處於興趣愛好,若有不足之處,還望指正。linux
在互聯網上進行自動數據採集(抓取)這件事和互聯網存在的時間差很少同樣長。今天大衆好像更傾向於用「網絡數據採集」,有時會把網絡數據採集程序稱爲網絡機器人(bots)。最經常使用的方法是寫一個自動化程序向網絡服務器請求數據(一般是用 HTML 表單或其餘網頁文件),而後對數據進行解析,提取須要的信息。git
說句實在話,若是個人網站老是讓人爬來爬取的,常常被虛擬訪問者騷擾,我也是蠻煩的,並且若是遇到「霸道」一點的爬蟲,都能直接把服務器卡死。所以,咱們在爬取別人網站的時候,也多爲對方考慮考慮。不過話說回來,我卻沒有這個煩惱,爲何呢?由於我根本就沒有本身的網站。=.=github
網站防採集的前提就是要正確地區分人類訪問用戶和網絡機器人。如今網站有不少技術來防止爬蟲,好比驗證碼,對於一些簡單的數字驗證碼,可使用訓練好的caffemodel諸如此類的模型去識別,準確率仍是能夠的。固然,也能夠在Github搜一搜關於驗證碼識別的東西,看一看大牛們是怎麼玩的。除了這些高大上的,還有一些十分簡單的方法可讓你的網絡機器人看起來更像人類訪問用戶。web
除了處理網站表單,requests 模塊仍是一個設置請求頭的利器。HTTP 的請求頭是在你每次向網絡服務器發送請求時,傳遞的一組屬性和配置信息。HTTP 定義了十幾種古怪的請求頭類型,不過大多數都不經常使用。正則表達式
每一個網站都有不一樣的請求頭,如何獲取這個請求頭呢?能夠用我從前提到過的Fiddler或者審查元素的方法,咱們能夠根據實際狀況進行配置。例如,GET百度根目錄的時候,須要添加的請求頭信息以下:shell
部分參數說明:數據庫
Upgrade-Insecure-Requests:參數爲1。該指令用於讓瀏覽器自動升級請求從http到https,用於大量包含http資源的http網頁直接升級到https而不會報錯。簡潔的來說,就至關於在http和https之間起的一個過渡做用。就是瀏覽器告訴服務器,本身支持這種操做,我能讀懂你服務器發過來的上面這條信息,而且在之後發請求的時候不用http而用https;windows
User-Agent:有一些網站不喜歡被爬蟲程序訪問,因此會檢測鏈接對象,若是是爬蟲程序,也就是非人點擊訪問,它就會不讓你繼續訪問,因此爲了要讓程序能夠正常運行,咱們須要設置一個瀏覽器的User-Agent;
Accept:瀏覽器可接受的MIME類型,能夠根據實際狀況進行設置;
Accept-Encoding:瀏覽器可以進行解碼的數據編碼方式,好比gzip。Servlet可以向支持gzip的瀏覽器返回經gzip編碼的HTML頁面。許多情形下這能夠減小5到10倍的下載時間;
Accept-Language:瀏覽器所但願的語言種類,當服務器可以提供一種以上的語言版本時要用到;
Cookie:這是最重要的請求頭信息之一。中文名稱爲「小型文本文件」或「小甜餅「,指某些網站爲了辨別用戶身份而儲存在用戶本地終端(Client Side)上的數據(一般通過加密)。定義於RFC2109。是網景公司的前僱員盧·蒙特利在1993年3月的發明。
雖然 cookie 是一把雙刃劍,但正確地處理 cookie 能夠避免許多采集問題。網站會用 cookie 跟蹤你的訪問過程,若是發現了爬蟲異常行爲就會中斷你的訪問,好比特別快速地填寫表單,或者瀏覽大量頁面。雖然這些行爲能夠經過關閉並從新鏈接或者改變 IP 地址來假裝,可是若是 cookie 暴露了你的身份,再多努力也是白費。
在採集一些網站時 cookie 是不可或缺的。要在一個網站上持續保持登陸狀態,須要在多個頁面中保存一個 cookie。有些網站不要求在每次登陸時都得到一個新 cookie,只要保存一箇舊的「已登陸」的 cookie 就能夠訪問。
若是你在採集一個或者幾個目標網站,建議你檢查這些網站生成的 cookie,而後想一想哪個 cookie 是爬蟲須要處理的。有一些瀏覽器插件能夠爲你顯示訪問網站和離開網站時 cookie 是如何設置的。例如:EditThisCookie,該插件能夠谷歌商店進行下載。URL:www.editthiscookie.com/
Cookie信息,也能夠根據實際狀況填寫。不過requests已經封裝好了不少操做,自動管理cookie,session保持鏈接。咱們能夠先訪問某個目標網站,創建一個session鏈接以後,獲取cookie。代碼以下:
# -*- coding:UTF-8 -*-
import requests
if __name__ == '__main__':
url = 'https://www.baidu.com/'
headers = {'Upgrade-Insecure-Requests':'1',
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding':'gzip, deflate, sdch, br',
'Accept-Language':'zh-CN,zh;q=0.8',
}
s = requests.Session()
req = s.get(url=url,headers=headers)
print(s.cookies)
複製代碼
運行結果以下:
使用 requests.Session 會話對象讓你可以跨請求保持某些參數,它也會在同一個 Session 實例發出的全部請求之間保持 cookie, 期間使用 urllib3 的 connection pooling 功能。詳細內容參見requests高級用法:docs.python-requests.org/zh_CN/lates…
由於 requests 模塊不能執行 JavaScript,因此它不能處理不少新式的跟蹤軟件生成的 cookie,好比 Google Analytics,只有當客戶端腳本執行後才設置 cookie(或者在用戶瀏覽頁面時基於網頁事件產生 cookie,好比點擊按鈕)。要處理這些動做,須要用 Selenium 和 PhantomJS 包。
Selenium的安裝已經在以前的文章中講到,今天就說下PhantomJS吧。URL:phantomjs.org/ PhantomJS 是一個「無頭」(headless)瀏覽器。它會把網站加載到內存並執行頁面上的 JavaScript,但不會向用戶展現網頁的圖形界面。將 Selenium 和 PhantomJS 結合在一塊兒,就能夠運行一個很是強大的網絡爬蟲了,能夠處理 cookie、JavaScript、headers,以及任何你須要作的事情。
PhantomJS能夠依據本身的開發平臺選擇不一樣的包進行下載:phantomjs.org/download.ht… 解壓即用,很方便。
接下來呢,仍是以實例出發,對 pythonscraping.com 網站調用 webdriver 的 get_cookie()方法來查看 cookie(D:/phantomjs-2.1.1-windows/bin/phantomjs.exe是個人PhantomJS路徑,這裏須要更改爲你本身的):
# -*- coding:UTF-8 -*-
from selenium import webdriver
if __name__ == '__main__':
url = 'http://pythonscraping.com'
driver = webdriver.PhantomJS(executable_path='D:/phantomjs-2.1.1-windows/bin/phantomjs.exe')
driver.get(url)
driver.implicitly_wait(1)
print(driver.get_cookies())
複製代碼
這樣就能夠得到一個很是典型的 Google Analytics 的 cookie 列表:
還能夠調用 delete_cookie()、add_cookie() 和 delete_all_cookies() 方法來處理 cookie。另外,還能夠保存 cookie 以備其餘網絡爬蟲使用。
經過Selenium和PhantomJS,咱們能夠很好的處理一些須要事件執行後才能得到的cookie。
有一些防禦措施完備的網站可能會阻止你快速地提交表單,或者快速地與網站進行交互。即便沒有這些安全措施,用一個比普通人快不少的速度從一個網站下載大量信息也可能讓本身被網站封殺。
所以,雖然多進程程序多是一個快速加載頁面的好辦法——在一個進程中處理數據,另外一個進程中加載頁面——可是這對編寫好的爬蟲來講是恐怖的策略。仍是應該儘可能保證一次加載頁面加載且數據請求最小化。若是條件容許,儘可能爲每一個頁面訪問增長一點兒時間間隔,即便你要增長兩行代碼:
import time
time.sleep(1)
複製代碼
合理控制速度是你不該該破壞的規則。過分消耗別人的服務器資源會讓你置身於非法境地,更嚴重的是這麼作可能會把一個小型網站拖垮甚至下線。拖垮網站是不道德的,是徹頭徹尾的錯誤。因此請控制採集速度!
在 HTML 表單中,「隱含」字段可讓字段的值對瀏覽器可見,可是對用戶不可見(除非看網頁源代碼)。隨着愈來愈多的網站開始用 cookie 存儲狀態變量來管理用戶狀態,在找到另外一個最佳用途以前,隱含字段主要用於阻止爬蟲自動提交表單。
下圖顯示的例子就是 Facebook 登陸頁面上的隱含字段。雖然表單裏只有三個可見字段(username、password 和一個確認按鈕),可是在源代碼裏表單會向服務器傳送大量的信息。
用隱含字段阻止網絡數據採集的方式主要有兩種。第一種是表單頁面上的一個字段能夠用服務器生成的隨機變量表示。若是提交時這個值不在表單處理頁面上,服務器就有理由認爲這個提交不是從原始表單頁面上提交的,而是由一個網絡機器人直接提交到表單處理頁面的。繞開這個問題的最佳方法就是,首先採集表單所在頁面上生成的隨機變量,而後再提交到表單處理頁面。
第二種方式是「蜜罐」(honey pot)。若是表單裏包含一個具備普通名稱的隱含字段(設置蜜罐圈套),好比「用戶名」(username)或「郵箱地址」(email address),設計不太好的網絡機器人每每無論這個字段是否是對用戶可見,直接填寫這個字段並向服務器提交,這樣就會中服務器的蜜罐圈套。服務器會把全部隱含字段的真實值(或者與表單提交頁面的默認值不一樣的值)都忽略,並且填寫隱含字段的訪問用戶也可能被網站封殺。
總之,有時檢查表單所在的頁面十分必要,看看有沒有遺漏或弄錯一些服務器預先設定好的隱含字段(蜜罐圈套)。若是你看到一些隱含字段,一般帶有較大的隨機字符串變量,那麼極可能網絡服務器會在表單提交的時候檢查它們。另外,還有其餘一些檢查,用來保證這些當前生成的表單變量只被使用一次或是最近生成的(這樣能夠避免變量被簡單地存儲到一個程序中反覆使用)。
雖然在進行網絡數據採集時用 CSS 屬性區分有用信息和無用信息會很容易(好比,經過讀取 id和 class 標籤獲取信息),但這麼作有時也會出問題。若是網絡表單的一個字段經過 CSS 設置成對用戶不可見,那麼能夠認爲普通用戶訪問網站的時候不能填寫這個字段,由於它沒有顯示在瀏覽器上。若是這個字段被填寫了,就多是機器人乾的,所以這個提交會失效。
這種手段不只能夠應用在網站的表單上,還能夠應用在連接、圖片、文件,以及一些能夠被機器人讀取,但普通用戶在瀏覽器上卻看不到的任何內容上面。訪問者若是訪問了網站上的一個「隱含」內容,就會觸發服務器腳本封殺這個用戶的 IP 地址,把這個用戶踢出網站,或者採起其餘措施禁止這個用戶接入網站。實際上,許多商業模式就是在幹這些事情。
下面的例子所用的網頁在 pythonscraping.com/pages/itsat… CSS 隱含了,另外一個是可見的。另外,頁面上還包括兩個隱含字段:
這三個元素經過三種不一樣的方式對用戶隱藏:
由於 Selenium 能夠獲取訪問頁面的內容,因此它能夠區分頁面上的可見元素與隱含元素。經過 is_displayed() 能夠判斷元素在頁面上是否可見。
例如,下面的代碼示例就是獲取前面那個頁面的內容,而後查找隱含連接和隱含輸入字段(一樣,須要更改下PhantomJS路徑):
# -*- coding:UTF-8 -*-
from selenium import webdriver
if __name__ == '__main__':
url = 'http://pythonscraping.com/pages/itsatrap.html'
driver = webdriver.PhantomJS(executable_path='D:/phantomjs-2.1.1-windows/bin/phantomjs.exe')
driver.get(url)
links = driver.find_elements_by_tag_name('a')
for link in links:
if not link.is_displayed():
print('鏈接:' + link.get_attribute('href') + ',是一個蜜罐圈套.')
fields = driver.find_elements_by_tag_name('input')
for field in fields:
if not field.is_displayed():
print('不要改變' + field.get_attribute('name') + '的值.')
複製代碼
Selenium 抓取出了每一個隱含的連接和字段,結果以下所示:
啓用遠程平臺的人一般有兩個目的:對更大計算能力和靈活性的需求,以及對可變 IP 地址的需求。
有一些網站會設置訪問閾值,也就是說,若是一個IP訪問速度超過這個閾值,那麼網站就會認爲,這是一個爬蟲程序,而不是用戶行爲。爲了不遠程服務器封鎖IP,或者想加快爬取速度,一個可行的方法就是使用代理IP,咱們須要作的就是建立一個本身的代理IP池。
思路:經過免費IP代理網站爬取IP,構建一個容量爲100的代理IP池。從代理IP池中隨機選取IP,在使用IP以前,檢查IP是否可用。若是可用,使用該IP訪問目標頁面,若是不可用,捨棄該IP。當代理IP池中IP的數量小於20的時候,更新整個代理IP池,即從新從免費IP代理網站爬取IP,構建一個新的容量爲100的代理IP池。
仍是使用在以前筆記中提到過的西刺代理,URL:www.xicidaili.com/,若是想方便一些,能夠…
咱們能夠本身爬取IP。可是,注意一點,千萬不要爬太快!很容易被服務器Block哦!
好比,我想爬取國內高匿代理,第一頁的URL爲:www.xicidaili.com/nn/1,第二頁的UR…
經過審查元素可知,這些ip都存放在了id屬性爲ip_list的table中。
咱們可使用lxml的xpath和Beutifulsoup結合的方法,爬取全部的IP。固然,也可使用正則表達式,方法不少。代碼以下:
# -*- coding:UTF-8 -*-
import requests
from bs4 import BeautifulSoup
from lxml import etree
if __name__ == '__main__':
#requests的Session能夠自動保持cookie,不須要本身維護cookie內容
page = 1
S = requests.Session()
target_url = 'http://www.xicidaili.com/nn/%d' % page
target_headers = {'Upgrade-Insecure-Requests':'1',
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Referer':'http://www.xicidaili.com/nn/',
'Accept-Encoding':'gzip, deflate, sdch',
'Accept-Language':'zh-CN,zh;q=0.8',
}
target_response = S.get(url = target_url, headers = target_headers)
target_response.encoding = 'utf-8'
target_html = target_response.text
bf1_ip_list = BeautifulSoup(target_html, 'lxml')
bf2_ip_list = BeautifulSoup(str(bf1_ip_list.find_all(id = 'ip_list')), 'lxml')
ip_list_info = bf2_ip_list.table.contents
proxys_list = []
for index in range(len(ip_list_info)):
if index % 2 == 1 and index != 1:
dom = etree.HTML(str(ip_list_info[index]))
ip = dom.xpath('//td[2]')
port = dom.xpath('//td[3]')
protocol = dom.xpath('//td[6]')
proxys_list.append(protocol[0].text.lower() + '#' + ip[0].text + '#' + port[0].text)
print(proxys_list)
複製代碼
能夠看到,經過這種方法,很容易的就得到了這100個IP,包括他們的協議、IP和端口號。這裏我是用」#」符號隔開,使用以前,只須要spilt()方法,就能夠提取出信息。
已經獲取了IP,如何驗證這個IP是否可用呢?一種方案是GET請求一個網頁,設置timeout超市時間,若是超時服務器沒有反應,說明IP不可用。這裏的實現,能夠參見Requests的高級用法:docs.python-requests.org/zh_CN/lates…
這種設置timeout的驗證方法是一種常見的方法,不少人都這樣驗證。因此博主就想了一個問題,有沒有其餘的方法呢?通過思考,想出了一個方法,測試了一個,驗證一個IP大約須要3秒左右。呃..固然這種方法是我本身琢磨出來的,沒有參考,因此,若是有錯誤之處,或者更好的方法,還望指正!
在Windows下,能夠在CMD中輸入以下指令查看IP的連通性(mac和linux能夠在中斷查看):
從免費代理網站得到的代理IP很不穩定,過幾分鐘再測試這個代理IP你可能會發現,這個IP已經不能用了。因此再使用代理IP以前,咱們須要測試下代理IP是否可用。
從上文可知,經過測試本機和代理IP地址的連通性,咱們可以大體知道這個代理 IP的健康狀況。若是,本機可以ping通這個代理 IP,那麼咱們也就可使用這個代理 IP去訪問其餘網站。這個過程是在cmd中執行的,那麼python有沒有提供一個方法,經過程序來實現這樣的操做呢?答案是確定的,有!Subprocess.Popen()能夠建立一個進程,當shell參數爲true時,程序經過shell來執行:# -*- coding:UTF-8 -*-
import subprocess as sp
if __name__ == '__main__':
cmd = "ping -n 3 -w 3 127.0.0.1"
#執行命令
p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE, shell=True)
#得到返回結果並解碼
out = p.stdout.read().decode("gbk")
print(out)
複製代碼
運行結果以下:
能都獲得返回結果,跟cmd中相似,接下來,咱們就能夠制定相應的規則,根據返回信息來剔除不知足要求的ip。
總體代碼以下:
# -*- coding:UTF-8 -*-
from bs4 import BeautifulSoup
import subprocess as sp
from lxml import etree
import requests
import random
import re
""" 函數說明:獲取IP代理 Parameters: page - 高匿代理頁數,默認獲取第一頁 Returns: proxys_list - 代理列表 Modify: 2017-05-27 """
def get_proxys(page = 1):
#requests的Session能夠自動保持cookie,不須要本身維護cookie內容
S = requests.Session()
#西祠代理高匿IP地址
target_url = 'http://www.xicidaili.com/nn/%d' % page
#完善的headers
target_headers = {'Upgrade-Insecure-Requests':'1',
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Referer':'http://www.xicidaili.com/nn/',
'Accept-Encoding':'gzip, deflate, sdch',
'Accept-Language':'zh-CN,zh;q=0.8',
}
#get請求
target_response = S.get(url = target_url, headers = target_headers)
#utf-8編碼
target_response.encoding = 'utf-8'
#獲取網頁信息
target_html = target_response.text
#獲取id爲ip_list的table
bf1_ip_list = BeautifulSoup(target_html, 'lxml')
bf2_ip_list = BeautifulSoup(str(bf1_ip_list.find_all(id = 'ip_list')), 'lxml')
ip_list_info = bf2_ip_list.table.contents
#存儲代理的列表
proxys_list = []
#爬取每一個代理信息
for index in range(len(ip_list_info)):
if index % 2 == 1 and index != 1:
dom = etree.HTML(str(ip_list_info[index]))
ip = dom.xpath('//td[2]')
port = dom.xpath('//td[3]')
protocol = dom.xpath('//td[6]')
proxys_list.append(protocol[0].text.lower() + '#' + ip[0].text + '#' + port[0].text)
#返回代理列表
return proxys_list
""" 函數說明:檢查代理IP的連通性 Parameters: ip - 代理的ip地址 lose_time - 匹配丟包數 waste_time - 匹配平均時間 Returns: average_time - 代理ip平均耗時 Modify: 2017-05-27 """
def check_ip(ip, lose_time, waste_time):
#命令 -n 要發送的回顯請求數 -w 等待每次回覆的超時時間(毫秒)
cmd = "ping -n 3 -w 3 %s"
#執行命令
p = sp.Popen(cmd % ip, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE, shell=True)
#得到返回結果並解碼
out = p.stdout.read().decode("gbk")
#丟包數
lose_time = lose_time.findall(out)
#當匹配到丟失包信息失敗,默認爲三次請求所有丟包,丟包數lose賦值爲3
if len(lose_time) == 0:
lose = 3
else:
lose = int(lose_time[0])
#若是丟包數目大於2個,則認爲鏈接超時,返回平均耗時1000ms
if lose > 2:
#返回False
return 1000
#若是丟包數目小於等於2個,獲取平均耗時的時間
else:
#平均時間
average = waste_time.findall(out)
#當匹配耗時時間信息失敗,默認三次請求嚴重超時,返回平均好使1000ms
if len(average) == 0:
return 1000
else:
#
average_time = int(average[0])
#返回平均耗時
return average_time
""" 函數說明:初始化正則表達式 Parameters: 無 Returns: lose_time - 匹配丟包數 waste_time - 匹配平均時間 Modify: 2017-05-27 """
def initpattern():
#匹配丟包數
lose_time = re.compile(u"丟失 = (\d+)", re.IGNORECASE)
#匹配平均時間
waste_time = re.compile(u"平均 = (\d+)ms", re.IGNORECASE)
return lose_time, waste_time
if __name__ == '__main__':
#初始化正則表達式
lose_time, waste_time = initpattern()
#獲取IP代理
proxys_list = get_proxys(1)
#若是平均時間超過200ms從新選取ip
while True:
#從100個IP中隨機選取一個IP做爲代理進行訪問
proxy = random.choice(proxys_list)
split_proxy = proxy.split('#')
#獲取IP
ip = split_proxy[1]
#檢查ip
average_time = check_ip(ip, lose_time, waste_time)
if average_time > 200:
#去掉不能使用的IP
proxys_list.remove(proxy)
print("ip鏈接超時, 從新獲取中!")
if average_time < 200:
break
#去掉已經使用的IP
proxys_list.remove(proxy)
proxy_dict = {split_proxy[0]:split_proxy[1] + ':' + split_proxy[2]}
print("使用代理:", proxy_dict)
複製代碼
從上面代碼能夠看出,我制定的規則是,若是丟包數大於2個,則認爲ip不能用。ping通的平均時間大於200ms也拋棄。固然,我這個要求有點嚴格,能夠視狀況放寬規則:
從打印結果中能夠看出,第一個隨機選取的IP被拋棄了,第二個隨機選取的IP能用。
我只是實現了,構建代理IP池和檢查IP是否可用,若是你感興趣也能夠將獲取的IP放入到數據庫中,不過我沒這樣作,由於感受免費獲取的代理IP,失效很快,隨用隨取就行。固然,也能夠本身寫代碼試試reqeusts的GET請求,經過設置timeout參數來驗證代理IP是否可用,由於方法簡單,因此在此再也不累述。
除此以外,咱們也能夠個建立一個User-Agent的列表,多羅列點。也是跟代理IP同樣,每次訪問隨機選取一個。這樣在必定程度上,也能避免被服務器封殺。
若是你一直被網站封殺卻找不到緣由,那麼這裏有個檢查列表,能夠幫你診斷一下問題出在哪裏。
使用免費的代理IP也是有侷限的,就是不穩定。更好的方法是,花錢買一個能夠動態切換IP的阿里雲服務器,這樣IP就能夠無限動態變化了!
以上內容整理自《Python網絡數據採集》,以及本身的一點當心得。重要的事情再說一遍:咱們在爬取別人網站的時候,也爲對方考慮考慮!
代碼獲取:Python3爬蟲的程序,能夠在個人Github上查看。URL:github.com/Jack-Cheris…
圓方圓學院聚集 Python + AI 名師,打造精品的 Python + AI 技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。 公開課地址:ke.qq.com/course/3627…
加入python學習討論羣 78486745 ,獲取資料,和廣大羣友一塊兒學習。