1. 程式人生 > >《零基礎入門學習Python》第054講:論一隻爬蟲的自我修養2:實戰

《零基礎入門學習Python》第054講:論一隻爬蟲的自我修養2:實戰

目錄

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

測試題

0. urlopen() 方法的 timeout 引數用於設定什麼?

1. 如何從 urlopen() 返回的物件中獲取 HTTP 狀態碼?

2. 在客戶端和伺服器之間進行請求-響應時,最常用的是哪兩種方法?

3. HTTP 是基於請求-響應的模式,那是客戶端發出請求,服務端做出響應;還是服務端發出請求,客戶端做出響應呢?

4. User-Agent 屬性通常是記錄什麼資訊?

5. 如何通過 urlopen() 使用 POST 方法像服務端發出請求?

6. 使用字串的什麼方法將其它編碼轉換為 Unicode 編碼?

7. JSON 是什麼鬼?

動動手

0. 配合 EasyGui,給“下載一隻貓“的程式碼增加互動:

1. 寫一個登入豆瓣的客戶端。


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

今天我們決定在實戰中來進行學習,會舉兩個例子,第一個例子是我們會下載一隻貓,第二個例子是我們用Python來模擬瀏覽器通過線上的谷歌翻譯進行文字的翻譯。

如果你認為上節課我只是簡單介紹了一下 urlopen() 函式的用法,那你就錯了,上節課我已經說了,相關的文件在哪裡,要教你的東西在文件裡都有,OK,我們來第一個例子吧。

(一)使用Python下載一隻貓

我們常說,林子大了,什麼鳥都有。網際網路這麼大,那當然不管什麼樣的奇葩網站都會有。我們今天舉的例子就是要訪問一個 http://placekitten.com/,這個網站是為貓農量身定製的一個站點,網站後面你只需要加上 /寬度/高度,就可以得到一隻相應寬度和高度的貓的圖片。這些圖片都是JPG格式的,你可以通過右鍵將其簡單儲存到桌面上。

我們第一個例子就是使用Python實現剛才的操作,事實上我們上節課教過的內容也是完全足夠的,我們新建一個 download_cat.py 檔案。

首先,我們需要 import urllib.request,然後使用urlopen() 函式得到 response,得到的 cat_img 可以用一個檔案儲存,我們命名這個檔案為 cat_500_600.jpg,我們說過,圖片也是檔案,它也是二進位制資料組成的

,我們這裡用 ‘wb’ 將收到的二進位制資料寫入 jpg 格式的檔案就可以了。

#download_cat.py
import urllib.request
response = urllib.request.urlopen("http://placekitten.com/500/600")
cat_img = response.read()

with open('cat_500_600.jpg', 'wb') as f:
        f.write(cat_img)

執行之後,就在桌面上有了一張 名為 cat_500_600.jpg 的圖片。我們接著繼續解釋一下上面的程式碼:

上節課,我們說過,urlopen() 函式中的 url 引數可以是字串,也可以是 Request object,其實,在上面的程式中,我們傳入的是地址字串,它也是將地址字串轉換為 Request 物件,然後再將物件傳入 urlopen() 函式。因此,

response = urllib.request.urlopen("http://placekitten.com/500/600")

等價於

req = urllib.request.Resquest("http://placekitten.com/500/600")
response = urllib.request.urlopen(req)

另外,urlopen() 函式返回的 response 其實是一個物件(object),看下圖文件解釋,因此你可以使用 read() 方法來讀取內容,

文件還告訴我們,除了可以使用 read() 方法之外,還可以是使用 geturl() 、info()  和 getcode() 方法,我們試一下這三個函式分別返回什麼:

我們執行 download_cat.py 之後,呼叫這幾個方法:

>>> 
=========== RESTART: C:\Users\XiangyangDai\Desktop\download_cat.py ===========
>>> response.geturl()
'http://placekitten.com/500/600'
>>> response.info()
<http.client.HTTPMessage object at 0x00000150F729AB70>
>>> print(response.info())
Date: Tue, 11 Dec 2018 06:57:33 GMT
Content-Type: image/jpeg
Content-Length: 20921
Connection: close
Set-Cookie: __cfduid=d2f9e8e46b6e9940463cf24baf0b7f0fb1544511453; expires=Wed, 11-Dec-19 06:57:33 GMT; path=/; domain=.placekitten.com; HttpOnly
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400
Expires: Wed, 12 Dec 2018 06:57:33 GMT
CF-Cache-Status: HIT
Accept-Ranges: bytes
Vary: Accept-Encoding
Server: cloudflare
CF-RAY: 48760ec5d6fc99c1-LAX


>>> response.getcode()
200

geturl() 得到的就是你訪問的具體的地址;

info() 得到的是一個 HTTPMessage 的物件,你可以將它打印出來,包含了 遠端伺服器返回的 Head 資訊;

getcode() 返回的是 Http 的狀態碼,200 表示 OK,就是正常響應。

(二)利用線上有道翻譯來翻譯文字

我們怎樣編寫Python 程式模擬瀏覽器,讓它翻譯呢?我們首先要介紹的是審查元素這個功能。基本上現在所有的瀏覽器都會自帶這樣這個除錯外掛,以360瀏覽器為例,右鍵選擇-審查元素,或者直接按 F12,就會顯示審查元素視窗。

我們要看的是 Network 這一塊,當我們點下自動翻譯按鈕時,在下面會看到有很多 Method,其中有 Get , 有Post ,這些內容都是瀏覽器與客戶端的通訊內容,在客服端與伺服器之間進行請求的時候,兩種最常用的方法:一種就是Get,一種就是 Post,在定義上來說,Get是指從伺服器請求獲得資料,而Post是向指定伺服器提交被處理的資料,當然在現實情況中,Get也常常用作提交資料。但是我們這裡有 Post,剛剛我們是提交資料,提交 I love you!這個語句讓它翻譯,我們點進去:

我們看到有 Headers 和 Preview 等,我們先看一下 Preview

我們看到這裡有我們所需要的結果,說明我們就找對地方了,但是在編寫程式之前,我們還是有必要講解一下 Headers 中的內容:

Request URL:http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule,有人會認為 urlopen()函式開啟的應該是 http://fanyi.youdao.com/ 這個地址,其實在內部嵌入的是前面的這個地址,你要實現翻譯的機制是在這。

Request Method:POST,請求的方法是 Post 的形式。

Status Code:200 OK,狀態碼 200 表示正常響應。如果是 404 就是頁面不見了。更多關於HTTP狀態碼的資訊請查閱:

HTTP狀態碼大全

Remote Address 是伺服器的 IP 地址加上開啟的埠號。

Resquest Headers:是客服端(這裡就是瀏覽器,用 Python程式碼的時候就是我們的程式碼)傳送請求的Headers,這個常常用於服務端來判斷是否非人類訪問,什麼意思呢?假設我們寫一個 Python 程式碼,然後用這個程式碼批量的訪問網站的資料,這樣子,伺服器的壓力就很大了,所以呢,伺服器一般是不歡迎非人類的訪問的。一般我們就是使用Resquest Headers裡面的User-Agent來識別是瀏覽器訪問還是程式碼訪問,大家可以看到,這裡的User-Agent顯示的系統的架構是(Windows NT 10.0; WOW64),後面你還包括瀏覽器的核心及其版本號等資訊。如果你使用Python 訪問的話,這個User-Agent預設就是 Python URL 3.5,這樣就可能被遮蔽掉。(不過呢,如果伺服器君以為這樣就可以阻擋我們前進的腳步的話,他就太天真了,這個User-Agent是可以進行自定義的,嘻嘻,後面會給大家介紹)

Form Data:其實就是我們這個Post提交的主要內容,在 i 這裡看到了提交的待翻譯的內容。

介紹到這裡就已經夠用了,接下來看看文件,瞭解Python如何提交Post呢?

urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)

Open the URL url, which can be either a string or a Request object.

data must be a bytes object specifying additional data to be sent to the server, or None if no such data is needed. data may also be an iterable object and in that case Content-Length value must be specified in the headers. Currently HTTP requests are the only ones that use data; the HTTP request will be a POST instead of a GET when the data parameter is provided.

data should be a buffer in the standard application/x-www-form-urlencoded format. The urllib.parse.urlencode() function takes a mapping or sequence of 2-tuples and returns an ASCII text string in this format. It should be encoded to bytes before being used as the data parameter.

上面藍色文字已經寫得很清楚了(這些內容來自urllib的Python文件的urllib.request部分),urlopen有一個data引數,如果這個引數被賦值,那麼它就是以POST的形式取代GET的形式,也就是說,如果data = None的話,就預設是以GET的形式。這裡還說了,data引數必須是基於application/x-www-form-urlencoded的格式,它還很貼心的告訴我們,你可以使用urllib.parse.urlencode()函式將字串轉換為所需要的形式。

事實上,我們有了這兩段話的描述,我們就可以來寫程式碼了:(命名為:translation.py)

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

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"
data = {}            #這裡就是把 Form Data 中的內容貼過來
data['i'] = '我愛你'
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')

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

print(html)

執行結果為:

=========== RESTART: C:\Users\XiangyangDai\Desktop\translation.py ===========
                          {"type":"ZH_CN2EN","errorCode":0,"elapsedTime":0,"translateResult":[[{"src":"我愛你","tgt":"I love you"}]]}

結果倒是可以了,只是這樣的結果是給程式設計師看的,如果是給使用者看,那也太不友好了。(另外,如果大家對於編碼還有什麼困惑的,可以檢視:Python編碼問題的解決方案總結),我們打印出來的是一個字串,有人就說,我們可以通過字串查詢的形式把 tgt 找出來,但這樣太被動了。

其實,這是一個 json 結構,json 是一種輕量級的資料交換結構,說白了,這裡就是用字串的形式把 Python 的輸出結果給封裝起來,這個字串裡面包含的其實是一個字典,"translateResult" 裡面的值是一個列表的列表的字典,我們可以使用下面的方法來解決:

=========== RESTART: C:\Users\XiangyangDai\Desktop\translation.py ===========
                          {"type":"ZH_CN2EN","errorCode":0,"elapsedTime":0,"translateResult":[[{"src":"我愛你","tgt":"I love you"}]]}

>>> import json
>>> json.loads(html)
{'errorCode': 0, 'type': 'ZH_CN2EN', 'elapsedTime': 0, 'translateResult': [[{'tgt': 'I love you', 'src': '我愛你'}]]}
>>> target = json.loads(html)
>>> type(target)
<class 'dict'>
>>> target['translateResult'][0][0]['tgt']
'I love you'

綜上,我們就可以把我們的翻譯程式美化一下:

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

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"
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')

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
翻譯結果:Life is too short, I learn Python

我們的要求實現了,但是這樣的程式碼還不能應用到我們的生產實踐中,因為你這樣搞多了,伺服器就會發現非人類的 User Agent 頻繁訪問,就會把你遮蔽掉了。還有就是發現這個IP怎麼訪問的這麼頻繁,就把你拉黑了。其實這些問題,Python都是有解決方法的,欲知詳情如何,請聽下回分解。


測試題

0. urlopen() 方法的 timeout 引數用於設定什麼?

答:timeout 引數用於設定連線的超時時間,單位是秒。

1. 如何從 urlopen() 返回的物件中獲取 HTTP 狀態碼?

答:

…
response = urllib.request.urlopen(url)
code = response.getcode()
…

2. 在客戶端和伺服器之間進行請求-響應時,最常用的是哪兩種方法?

答:GET 和 POST。

3. HTTP 是基於請求-響應的模式,那是客戶端發出請求,服務端做出響應;還是服務端發出請求,客戶端做出響應呢?

答:發出請求的永遠是客戶端,做出響應的永遠是服務端。

4. User-Agent 屬性通常是記錄什麼資訊?

答:普通瀏覽器會通過該內容向訪問網站提供你所使用的瀏覽器型別、作業系統、瀏覽器核心等資訊的標識。

5. 如何通過 urlopen() 使用 POST 方法像服務端發出請求?

答:urlopen 函式有一個 data 引數,如果給這個引數賦值,那麼 HTTP 的請求就是使用 POST 方式;如果 data 的值是 None,也就是預設值,那麼 HTTP 的請求就是使用 GET 方式。

6. 使用字串的什麼方法將其它編碼轉換為 Unicode 編碼?

答:decode。decode 的作用是將其他編碼的字串轉換成 unicode 編碼,相反,encode 的作用是將 unicode 編碼轉換成其他編碼的字串。

7. JSON 是什麼鬼?

答:JSON 是一種輕量級的資料交換格式,說白了這裡就是用字串把 Python 的資料結構封裝起來,便與儲存和使用。


動動手

0. 配合 EasyGui,給“下載一隻貓“的程式碼增加互動:

  • 讓使用者輸入尺寸;
  • 如果使用者不輸入尺寸,那麼按預設寬400,高600下載喵;
  • 讓使用者指定儲存位置。

程式實現如下圖:

程式碼清單:

import easygui as g
import urllib.request

def main():
    msg = "請填寫喵的尺寸"
    title = "下載一隻喵"
    fieldNames = ["寬:", "高:"]
    fieldValues = []
    size = width, height = 400, 600
    fieldValues = g.multenterbox(msg, title, fieldNames, size)

    while 1:
        if fieldValues == None:
            break
        errmsg = ""

        try:
            width = int(fieldValues[0].strip())
        except:
            errmsg += "寬度必須為整數!"

        try:
            height = int(fieldValues[1].strip())
        except:
            errmsg += "高度必須為整數!"    

        if errmsg == "":
            break
        
        fieldValues = g.multenterbox(errmsg, title, fieldNames, fieldValues)

    url = "http://placekitten.com/g/%d/%d" % (width, height)

    response = urllib.request.urlopen(url)
    cat_img = response.read()

    filepath = g.diropenbox("請選擇存放喵的資料夾")

    if filepath:
        filename = '%s/cat_%d_%d.jpg' % (filepath, width, height)
    else:
        filename = 'cat_%d_%d.jpg' % (width, height)

    with open(filename, 'wb') as f:
        f.write(cat_img)

if __name__ == "__main__":
    main()

1. 寫一個登入豆瓣的客戶端。

這道題可能要難為大家了,因為需要 N 多你沒學過的知識!

不過我也不打算讓你斷送希望,下邊是一個可行的 Python 2 的程式碼片段,請修改為 Python 3 版本。其中一些庫和知識點你可能還沒學過,但憑藉著過人的自學能力,你可以在不看答案的情況下完成任務的,對嗎?

程式實現如下圖:

Python2 實現的程式碼:

# -- coding:gbk --
import re
import urllib, urllib2, cookielib

loginurl = 'https://www.douban.com/accounts/login'
cookie = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))

params = {
"form_email":"your email",
"form_password":"your password",
"source":"index_nav" #沒有的話登入不成功
}

#從首頁提交登入
response=opener.open(loginurl, urllib.urlencode(params))

#驗證成功跳轉至登入頁
if response.geturl() == "https://www.douban.com/accounts/login":
    html=response.read()

    #驗證碼圖片地址
    imgurl=re.search('<img id="captcha_image" src="(.+?)" alt="captcha" class="captcha_image"/>', html)
    if imgurl:
        url=imgurl.group(1)
        #將圖片儲存至同目錄下
        res=urllib.urlretrieve(url, 'v.jpg')
        #獲取captcha-id引數
        captcha=re.search('<input type="hidden" name="captcha-id" value="(.+?)"/>' ,html)
        if captcha:
            vcode=raw_input('請輸入圖片上的驗證碼:')
            params["captcha-solution"] = vcode
            params["captcha-id"] = captcha.group(1)
            params["user_login"] = "登入"
            #提交驗證碼驗證
            response=opener.open(loginurl, urllib.urlencode(params))
            ''' 登入成功跳轉至首頁 '''
            if response.geturl() == "http://www.douban.com/":
                print 'login success ! '

答:Python 3 對比 Python 2 有不少的改變。

在本題中:

  • urllib 和 urllib2 合併,大多數功能放入了 urllib.request 模組;
  • 原來的 urllib.urlencode() 變為 urllib.parse.urlencode().encode(),由於編碼的關係,你還需要在後邊加上 encode('utf-8');
  • cookielib 被改名為 http.cookiejar;

課堂中我們還沒講,所以這裡藉機會給大家簡單科普一下 cookie 是什麼東西:

我們說 HTTP 協議是基於請求響應模式,就是客戶端發一個請求,服務端回覆一個響應醬紫……

但 HTTP 協議是無狀態的,也就是說客戶端這會兒給服務端提交了賬號密碼,服務端回覆驗證通過,但下一秒客戶端說我要訪問 XXOO 資源,服務端回覆:“啊??你是誰?!”

為了解決這個尷尬的困境,有人就發明出了 cookie。cookie 相當於服務端(網站)用於驗證你的身份的密文。於是客戶端每次提交請求的時候,服務端通過驗證 cookie 即可知道你的身份資訊。那麼正如你所猜測的,CookieJar 是 Python 用於存放 cookie 的物件。

當然,這裡已經給你提供了 Python 2 的程式碼,你不懂上邊這些,也不影響完成作業。

程式碼清單:

import re
import urllib.request
from http.cookiejar import CookieJar

# 豆瓣的登入url 
loginurl = 'https://www.douban.com/accounts/login'
cookie = CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor)

data = {
    "form_email":"your email",
    "form_password":"your password",
    "source":"index_nav"
}
data = {}
data['form_email'] = '你的賬號'
data['form_password'] = '你的密碼'
data['source'] = 'index_nav'

response = opener.open(loginurl, urllib.parse.urlencode(data).encode('utf-8'))

#驗證成功跳轉至登入頁
if response.geturl() == "https://www.douban.com/accounts/login":
    html = response.read().decode()
    
    #驗證碼圖片地址
    imgurl = re.search('<img id="captcha_image" src="(.+?)" alt="captcha" class="captcha_image"/>', html)
    if imgurl:
        url = imgurl.group(1)
        # 將驗證碼圖片儲存至同目錄下
        res = urllib.request.urlretrieve(url, 'v.jpg')

        # 獲取captcha-id引數
        captcha = re.search('<input type="hidden" name="captcha-id" value="(.+?)"/>' ,html)

        if captcha:
            vcode = input('請輸入圖片上的驗證碼:')
            data["captcha-solution"] = vcode
            data["captcha-id"] = captcha.group(1)
            data["user_login"] = "登入"

            # 提交驗證碼驗證
            response = opener.open(loginurl, urllib.parse.urlencode(data).encode('utf-8'))

            # 登入成功跳轉至首頁 '''
            if response.geturl() == "http://www.douban.com/":
                print('登入成功!')