1. 程式人生 > >《零基礎入門學習Python》第055講:論一隻爬蟲的自我修養3:隱藏

《零基礎入門學習Python》第055講:論一隻爬蟲的自我修養3:隱藏

目錄

0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!

測試題

0. 伺服器是如何識訪問來自瀏覽器還是非瀏覽器的?

1. 明明程式碼跟視訊中的栗子一樣,一執行卻出錯了,但在不修改程式碼的情況下再次嘗試執行卻又變好了,這是為什麼呢?

2. Request 是由客戶端發出還是由服務端發出?

3. 請問如何為一個 Request 物件動態的新增 headers?

4. 簡單來說,代理伺服器是如何工作的?他有時為何不工作了?

5. HTTP 有好幾種方法(GET,POST,PUT,HEAD,DELETE,OPTIONS,CONNECT),請問你如何曉得 Python 是使用哪種方法訪問伺服器呢?

6. 上一節課後題中有涉及到登陸問題,辣麼,你還記得伺服器是通過什麼來確定你是登陸還是沒登陸的麼?他會持續到什麼時候呢?

動動手

0. 編寫一個爬蟲,爬百度百科“網路爬蟲”的詞條

1. 直接列印詞條名和連結不算什麼真本事兒,這題要求你的爬蟲允許使用者輸入搜尋的關鍵詞。

2. 嘩啦啦地丟一堆連結給使用者可不是什麼好的體驗,我們應該先列印 10 個連結,然後問下使用者“您還往下看嗎?


0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!

上節課我們說過了,有一些網站比較痛恨爬蟲程式,它們不喜歡被程式所訪問,所以它們會檢查連結的來源,如果說來源不是正常的途徑,那麼它就會把你給遮蔽掉,所以呢,要讓我們的程式可以持續的幹活,要可以投入生產,我們就需要對程式碼進行隱藏,讓它看起來更像是普通人瀏覽器的正常點選。

我們知道,伺服器檢查連結是通過檢查 連結中的 Headers 中的 User Agent 來判斷你是來自於程式碼還是來自於瀏覽器,像我們的Python,你用Python預設的Headers 中的 User Agent 是Python 加上版本號,伺服器一檢查是Python,就會把你遮蔽掉。我們可以修改 Headers 來模擬正常的瀏覽器訪問。

先看一下文件:

urllib.request.Request 有一個 headers 的引數,通過修改 headers 引數,你可以設定自己的 headers,這個引數是一個字典,你可以通過兩種途徑來設定:一種是你直接設定一個字典,然後作為引數傳給 Request,或者第二種,在Request 生成之後,呼叫 add_header() 將其加進去。我們使用上節課的例子來嘗試一下:

第一種 程式碼清單:

#translation.py
import urllib.request
import urllib.parse
import json
print("---------這是一個Python翻譯器---------")
        
content = input('請輸入需要翻譯的內容:')

url = "http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule"
#直接從審查元素中copy過來的url會報錯,必須把translate_o中的_o 刪除才可以
#url = "http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule"

head = {}
head['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'

data = {}            #這裡就是把 Form Data 中的內容貼過來
data['i'] = content
data['from'] = 'AUTO'
data['to'] = 'AUTO'
data['smartresult'] = 'dict'
data['client'] = 'fanyideskweb'
data['salt'] = '15445124815349'
data['sign'] = 'a824eba4c23c6f541ffadfee26b1e500'
data['ts'] = '1544512481534'
data['bv'] = 'bbb3ed55971873051bc2ff740579bb49'
data['doctype'] = 'json'
data['version'] = '2.1'
data['keyfrom'] = 'fanyi.web'
data['action'] = 'FY_BY_REALTIME'
data['typoResult'] = 'false'

#需要使用urllib.parse.urlencode() 把data轉換為需要的形式
data = urllib.parse.urlencode(data).encode('utf-8')

req = urllib.request.Request(url, data, head)  
response = urllib.request.urlopen(url, data)
html = response.read().decode('utf-8')

target = json.loads(html)

print('翻譯結果:%s' %(target['translateResult'][0][0]['tgt']))

第二種 程式碼清單:

#translation.py
import urllib.request
import urllib.parse
import json
print("---------這是一個Python翻譯器---------")
        
content = input('請輸入需要翻譯的內容:')

url = "http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule"
#直接從審查元素中copy過來的url會報錯,必須把translate_o中的_o 刪除才可以
#url = "http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule"

#head = {}
#head['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'

data = {}            #這裡就是把 Form Data 中的內容貼過來
data['i'] = content
data['from'] = 'AUTO'
data['to'] = 'AUTO'
data['smartresult'] = 'dict'
data['client'] = 'fanyideskweb'
data['salt'] = '15445124815349'
data['sign'] = 'a824eba4c23c6f541ffadfee26b1e500'
data['ts'] = '1544512481534'
data['bv'] = 'bbb3ed55971873051bc2ff740579bb49'
data['doctype'] = 'json'
data['version'] = '2.1'
data['keyfrom'] = 'fanyi.web'
data['action'] = 'FY_BY_REALTIME'
data['typoResult'] = 'false'

#需要使用urllib.parse.urlencode() 把data轉換為需要的形式
data = urllib.parse.urlencode(data).encode('utf-8')

req = urllib.request.Request(url, data)
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36')
response = urllib.request.urlopen(url, data)
html = response.read().decode('utf-8')

target = json.loads(html)

print('翻譯結果:%s' %(target['translateResult'][0][0]['tgt']))


執行結果:

=========== RESTART: C:\Users\XiangyangDai\Desktop\translation.py ===========
---------這是一個Python翻譯器---------
請輸入需要翻譯的內容:愛
翻譯結果:love
>>> req.headers
{'User-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}

我們來回顧一下,修改 headers 有兩個途徑:

  • 通過 Request 的 headers 引數修改
  • 通過 Request.add_header() 方法修改

修改 User-Agent 可以算是最簡單的隱藏方法了,也是切實可行的,不過呢,如果是用 Python 抓取網頁,例如批量下載 圖片,你一個 IP 地址短時間內連續的進行訪問,那麼這是不符合正常人類的標準的,而且對伺服器帶來的壓力不小,所以伺服器合情合理會把你遮蔽掉,遮蔽的做法也很簡單,只需要記錄每個IP的訪問頻率,在單位時間內,如果訪問頻率超過一個閾值,那麼伺服器就會認為這個IP很有可能是一個爬蟲,那麼伺服器就會把不管它的User-Agent是什麼了,伺服器就會返回一個驗證碼的介面,因為使用者會填寫驗證碼,但是爬蟲不會,這就會合理的把你的訪問給遮蔽掉。

就我們目前的學習水平來說,有兩種做法可以解決這種問題,一種就是延遲提交的時間,讓我們的爬蟲看起來更像是一個正常的人類在瀏覽(這是沒有辦法的辦法);還有一種就是使用代理。

首先說第一種方法,我們可以使用 time 模組來完成延遲操作:(這種方法的工作效率太慢了

#translation.py
import urllib.request
import urllib.parse
import json
import time

print("---------這是一個Python翻譯器---------")

while True:       
        content = input("請輸入需要翻譯的內容(輸入'Q'退出程式):")
        if content == 'Q':
                break

        url = "http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule"
        #直接從審查元素中copy過來的url會報錯,必須把translate_o中的_o 刪除才可以
        #url = "http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule"

        #head = {}
        #head['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'

        data = {}            #這裡就是把 Form Data 中的內容貼過來
        data['i'] = content
        data['from'] = 'AUTO'
        data['to'] = 'AUTO'
        data['smartresult'] = 'dict'
        data['client'] = 'fanyideskweb'
        data['salt'] = '15445124815349'
        data['sign'] = 'a824eba4c23c6f541ffadfee26b1e500'
        data['ts'] = '1544512481534'
        data['bv'] = 'bbb3ed55971873051bc2ff740579bb49'
        data['doctype'] = 'json'
        data['version'] = '2.1'
        data['keyfrom'] = 'fanyi.web'
        data['action'] = 'FY_BY_REALTIME'
        data['typoResult'] = 'false'

        #需要使用urllib.parse.urlencode() 把data轉換為需要的形式
        data = urllib.parse.urlencode(data).encode('utf-8')

        req = urllib.request.Request(url, data)
        req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36')
        response = urllib.request.urlopen(url, data)
        html = response.read().decode('utf-8')

        target = json.loads(html)

        print('翻譯結果:%s' %(target['translateResult'][0][0]['tgt']))
        time.sleep(5)  #暫停5秒鐘  

第二個方案:使用代理

首先需要知道,代理是什麼?

嘿,兄弟,哥們兒訪問這個網址有點困難,幫忙解決一下唄!

然後你就把需要訪問的網址告訴代理先生,代理幫你訪問,然後把他看到的所有內容原封不動的轉發給你。

這就是代理。

代理的工作原理就是這麼簡單,因此呢,伺服器看到的IP地址就是代理的IP地址,而不是你的IP地址,這樣子你用很多個代理同時發起訪問,伺服器也沒有辦法。使用代理的步驟如下:

  • 1.引數是一個字典 {‘型別’ :‘代理ip: 埠號’}

         proxy_support = urllib.request.ProxyHandler({})

         它這個引數就是一個字典,字典的鍵就是代理的型別(如:http,https...),值就是對應的 ip 或者 域名 +埠號

  • 2.定製、建立 一個 opener

(opener可以看做是私人訂製,當你使用 urlopen 開啟一個普通網頁的時候,你就是在使用預設的 opener 來工作,而這個opener 是可以有我們定製的,例如我們可以給它加上特殊的 headers,或者指定 代理,我們使用下面的語句定製、建立一個    opener)

opener = urllib.request.build_opener(proxy_support)

  • 3a.安裝opener

     我們使用 urllib.request.install_opener(opener),把它安裝到系統中,這是一勞永逸的做法,因為在此之後,你只要使用普通的 urlopen() 函式,就是使用定製好的 opener 進行工作,如果你不想替換掉預設的 opener,你可以使用下面的語句呼叫 opener。

  • 3b.呼叫opener

      opener.open(url)

我們來舉個例子:

我們需要代理ip,直接在網上搜索即可。

import urllib.request

url = 'http:/www.whatismyip.com.tw'  #查詢ip的網站

proxy_support = urllib.request.ProxyHandler({'http': '123.206.56.247:80'})

opener = urllib.request.build_opener(proxy_support)

urllib.request.install_opener(opener)

response = urllib.request.urlopen(url)
html = response.read().decode('utf-8')

print(html)

測試題

0. 伺服器是如何識訪問來自瀏覽器還是非瀏覽器的?

答:通過傳送的 HTTP 頭中的 User-Agent 來進行識別瀏覽器與非瀏覽器,伺服器還以 User-Agent 來區分各個瀏覽器。

1. 明明程式碼跟視訊中的栗子一樣,一執行卻出錯了,但在不修改程式碼的情況下再次嘗試執行卻又變好了,這是為什麼呢?

答: 在網路資訊的傳輸中會出現偶然的“丟包”現象,有可能是你傳送的請求伺服器沒收到,也有可能是伺服器響應的資訊不能完整送回來……尤其在網路阻塞的時候。所以,在設計一個“稱職”的爬蟲時,需要考慮到這偶爾的“丟包”現象。

2. Request 是由客戶端發出還是由服務端發出?

答:我們之前說 HTTP 是基於“請求-響應”模式,Request 即請求的意思,而 Response 則是響應的意思。由客戶端首先發出 Request,伺服器收到後返回 Response。

3. 請問如何為一個 Request 物件動態的新增 headers?

答:add_header() 方法往 Request 物件新增 headers。

4. 簡單來說,代理伺服器是如何工作的?他有時為何不工作了?

答: 將資訊傳給代理伺服器,代理伺服器替你向你要訪問的伺服器傳送請求,然後在將伺服器返回的內容返回給你。

因為有“丟包”現象發生,所以多了一箇中間人就意味著會多一層發生“丟包”的機率,且大多數代理並不只為一個人服務,尤其是免費代理。

PS:大家想做“壞壞”的事情時可以考慮多幾層代理,一般來說路由器日誌並不會儲存很長時間,幾層代理後,基本很難查到是誰請求的。

5. HTTP 有好幾種方法(GET,POST,PUT,HEAD,DELETE,OPTIONS,CONNECT),請問你如何曉得 Python 是使用哪種方法訪問伺服器呢?

答:使用 get_method() 方法獲取 Request 物件具體使用哪種方法訪問伺服器。最常用的無非就是 GET 和 POST 了,當 Request 的 data 引數被賦值的時候,get_method() 返回 'POST',否則一般情況下返回 'GET'。

6. 上一節課後題中有涉及到登陸問題,辣麼,你還記得伺服器是通過什麼來確定你是登陸還是沒登陸的麼?他會持續到什麼時候呢?

答: 是 cookie,伺服器通過判斷你提交的 cookie 來確定訪問是否來自”熟人“。

簡單來說 cookie 可以分成兩類:

  • 一類是即時過期的 cookies,稱為“會話” cookies,當瀏覽器關閉時(這裡是 python 的請求程式)自動清除
  • 另一類是有期限的 cookies,由瀏覽器進行儲存,並在下一次請求該網站時自動附帶(如果沒過期或清理的話)

動動手

小甲魚打算在這裡先給大家介紹一個壓箱底的模組 —— Beautiful Soup 4

翻譯過來名字有點詭異:漂亮的湯?美味的雞湯?呃……

好吧,只要你寫出一個普羅大眾都喜歡的模組,你管它叫“Beautiful Shit”大家也是能接受的…… 

Beautiful Soup 是一個可以從 HTML 或 XML 檔案中提取資料的 Python 庫。它能夠通過你喜歡的轉換器實現慣用的文件導航,查詢,修改文件的方式。Beautiful Soup 會幫你節省數小時甚至數天的工作時間。

這玩意兒到底怎麼用?

看這 -> 傳送門

上邊連結是官方的快速入門教程(不用懼怕,這次有中文版了),請大家花差不多半個小時的時間自學一下,然後完成下邊題目。

噢,對了,大家可以使用 pip 安裝(Python3.4 以上自帶的神一般的軟體包管理系統,有了它 Python 的模組安裝、解除安裝全部一鍵搞定!)

開啟命令列視窗(CMD) -> 輸入 pip install BeautifulSoup4 命令) -> 搞定。

0. 編寫一個爬蟲,爬百度百科“網路爬蟲”的詞條

(連結 -> http://baike.baidu.com/view/284853.htm),將所有包含“view”的連結按下邊格式打印出來:

提示:題目中需要使用到簡單的正則表示式(在官方的快速入門教程有演示),如果你希望馬上就深入學習正則表示式,當然可以給你預支一下後邊的知識 -> Python3 如何優雅地使用正則表示式

程式碼清單:

import urllib.request
import re
from bs4 import BeautifulSoup

def main():
    url = "http://baike.baidu.com/view/284853.htm"
    response = urllib.request.urlopen(url)
    html = response.read()
    soup = BeautifulSoup(html, "html.parser") # 使用 Python 預設的解析器
    
    for each in soup.find_all(href=re.compile("view")):
        print(each.text, "->", ''.join(["http://baike.baidu.com", each["href"]]))
        # 上邊用 join() 不用 + 直接拼接,是因為 join() 被證明執行效率要高很多

if __name__ == "__main__":
    main()

1. 直接列印詞條名和連結不算什麼真本事兒,這題要求你的爬蟲允許使用者輸入搜尋的關鍵詞。

然後爬蟲進入每一個詞條,然後檢測該詞條是否具有副標題(比如搜尋“豬八戒”,副標題就是“(中國神話小說《西遊記》的角色)”),如果有,請將副標題一併打印出來:

程式實現效果如下:

程式碼清單:

import urllib.request
import urllib.parse
import re 
from bs4 import BeautifulSoup

def main():
    keyword = input("請輸入關鍵詞:")
    keyword = urllib.parse.urlencode({"word":keyword})
    response = urllib.request.urlopen("http://baike.baidu.com/search/word?%s" % keyword)
    html = response.read()
    soup = BeautifulSoup(html, "html.parser")

    for each in soup.find_all(href=re.compile("view")):
        content = ''.join([each.text])
        url2 = ''.join(["http://baike.baidu.com", each["href"]])
        response2 = urllib.request.urlopen(url2)
        html2 = response2.read()
        soup2 = BeautifulSoup(html2, "html.parser")
        if soup2.h2:
            content = ''.join([content, soup2.h2.text])
        content = ''.join([content, " -> ", url2])
        print(content)

if __name__ == "__main__":
    main()

2. 嘩啦啦地丟一堆連結給使用者可不是什麼好的體驗,我們應該先列印 10 個連結,然後問下使用者“您還往下看嗎?

來,我給大家演示下:

然後為了增加使用者體驗,程式碼需要捕獲未收錄的詞條,並提示:

提示:希望你還記得有生成器這麼個東東

程式碼清單:

import urllib.request
import urllib.parse
import re 
from bs4 import BeautifulSoup

def test_url(soup):
    result = soup.find(text=re.compile("百度百科尚未收錄詞條"))
    if result:
        print(result[0:-1]) # 百度這個碧池在最後加了個“符號,給它去掉
        return False
    else:
        return True

def summary(soup):
    word = soup.h1.text
    # 如果存在副標題,一起列印
    if soup.h2:
        word += soup.h2.text
    # 列印標題
    print(word)
    # 列印簡介
    if soup.find(class_="lemma-summary"):
        print(soup.find(class_="lemma-summary").text)   

def get_urls(soup):
    for each in soup.find_all(href=re.compile("view")):
        content = ''.join([each.text])
        url2 = ''.join(["http://baike.baidu.com", each["href"]])
        response2 = urllib.request.urlopen(url2)
        html2 = response2.read()
        soup2 = BeautifulSoup(html2, "html.parser")
        if soup2.h2:
            content = ''.join([content, soup2.h2.text])
        content = ''.join([content, " -> ", url2])
        yield content
        

def main():
    word = input("請輸入關鍵詞:")
    keyword = urllib.parse.urlencode({"word":word})
    response = urllib.request.urlopen("http://baike.baidu.com/search/word?%s" % keyword)
    html = response.read()
    soup = BeautifulSoup(html, "html.parser")

    if test_url(soup):
        summary(soup)
        
        print("下邊列印相關連結:")
        each = get_urls(soup)
        while True:
            try:
                for i in range(10):
                    print(next(each))
            except StopIteration:
                break
            
            command = input("輸入任意字元將繼續列印,q退出程式:")
            if command == 'q':
                break
            else:
                continue
    
if __name__ == "__main__":
    main()