Python 爬蟲系列

雲崖先生發表於2021-01-01

爬蟲簡介

網路爬蟲

   爬蟲指在使用程式模擬瀏覽器向服務端發出網路請求,以便獲取服務端返回的內容。

   但這些內容可能涉及到一些機密資訊,所以爬蟲領域目前來講是屬於灰色領域,切勿違法犯罪。

   爬蟲本身作為一門技術沒有任何問題,關鍵是看人們怎麼去使用它

   《中華人民共和國刑法》第二百八十五條規定:非法獲取計算機資訊系統資料、非法控制計算機資訊系統罪,是指違反國家規定,侵入國家事務、國防建設、尖端科學技術領域以外的計算機資訊系統或者採用其他技術手段,獲取該計算機資訊系統中儲存、處理或者傳輸的資料,情節嚴重的行為。刑法第285條第2款明確規定,犯本罪的,處三年以下有期徒刑或者拘役,並處或者單處罰金;情節特別嚴重的,處三年以上七年以下有期徒刑,並處罰金。

   《反不正當競爭法》第九條規定:以不正當手段獲取他人商業祕密的行為即已經構成侵犯商業祕密。而後續如果進一步利用,或者公開該等資訊,則構成對他人商業祕密的披露和使用,同樣構成對權利人的商業祕密的侵犯。

   《刑法》第二百八十六條規定:違反國家規定,對計算機資訊系統功能進行刪除、修改、增加、干擾,造成計算機資訊系統不能正常執行,後果嚴重的,構成犯罪,處五年以下有期徒刑或者拘役;後果特別嚴重的,處五年以上有期徒刑。而違反國家規定,對計算機資訊系統中儲存、處理或者傳輸的資料和應用程式進行刪除、修改、增加的操作,後果嚴重的,也構成犯罪,依照前款的規定處罰。

   《網路安全法》第四十四條規定:任何個人和組織不得竊取或者以其他非法方式獲取個人資訊。因此,如果爬蟲在未經使用者同意的情況下大量抓取使用者的個人資訊,則有可能構成非法收集個人資訊的違法行為。

   《民法總則》第111條規定:任何組織和個人需要獲取他人個人資訊的,應當依法取得並確保資訊保安。不得非法收集、使用、加工、傳輸他人個人資訊

爬蟲分類

   根據爬蟲的應用範疇,可有一些三種區分:

   通用爬蟲

   搜尋引擎本質就是一個巨大的爬蟲,首先該爬蟲會爬取整張頁面,並且對該頁面做備份,之後對其進行資料內容處理如抓取關鍵字等,然後向使用者提供檢索介面。

   聚焦式爬蟲

   只關注於頁面上某一部分內容,如只關注圖片、連結等。

   增量式爬蟲

   用於檢索內容是否更新,如開發了一個增量式爬蟲每天檢視一下雲崖部落格有沒有更新,有更新就爬下來等等...

robots協議

   robots協議是爬蟲領域非常出名的一種協議,由入口網站提供。

   它規定了該站點哪些內容允許爬取,哪些內容不允許爬取。

   如果爬取不允許的內容,可對其追究法律責任。

requests模組

   requests模組是Python中傳送網路請求的一款非常簡潔、高效的模組。

pip install requests

傳送請求

   支援所有的請求方式:

import requests

requests.get("https://www.python.org/")
requests.post("https://www.python.org/")
requests.put("https://www.python.org/")
requests.patch("https://www.python.org/")
requests.delete("https://www.python.org/")
requests.head("https://www.python.org/")
requests.options("https://www.python.org/")

# 指定請求方式
requests.request("get","https://www.python.org/")

   當請求傳送成功後,會返回一個response物件。

get請求

   基本的get請求引數如下:

引數描述
params 字典,get請求的引數,value支援字串、字典、位元組(ASCII編碼內)
headers 字典,本次請求攜帶的請求頭
cookies 字典,本次請求攜帶的cookies

   演示如下:

import requests

res = requests.get(
    url="http://127.0.0.1:5000/index",
    params={"key": "value"},
    cookies={"key": "value"},
)

print(res.content)

post請求

   基本的post請求引數如下:

引數描述
data 字典,post請求的引數,value支援檔案物件、字串、字典、位元組(ASCII編碼內)
headers 字典,本次請求攜帶的請求頭
cookies 字典,本次請求攜帶的cookies

   演示如下:

import requests

res = requests.post(
    url="http://127.0.0.1:5000/index",
    # 依舊可以攜帶 params
    data={"key": "value"},
    cookies={"key": "value"},
)

print(res.content)

高階引數

   更多引數:

引數描述
json 字典,傳入json資料,將自動進行序列化,支援get/post,請求體傳遞
files 字典,傳入檔案物件,支援post
auth 認證,傳入HTTPDigestAuth物件,一般場景是路由器彈出的兩個輸入框,爬蟲獲取不到,將使用者名稱和密碼輸入後會base64加密然後放入請求頭中進行交給服務端,base64("名字:密碼"),請求頭名字:authorization
timeout 超時時間,傳入float/int/tuple型別。如果傳入的是tuple,則是 (連結超時、返回超時)
allow_redirects 是否允許重定向,傳入bool值
proxies 開啟代理,傳入一個字典
stream 是否返回檔案流,傳入bool值
cert 證照地址,這玩意兒來自於HTTPS請求,需要傳入該網站的認證證照地址,通常來講如果是大公司的網站不會要求這玩意兒

   演示:

def param_method_url():
    # requests.request(method='get', url='http://127.0.0.1:8000/test/')
    # requests.request(method='post', url='http://127.0.0.1:8000/test/')
    pass


def param_param():
    # - 可以是字典
    # - 可以是字串
    # - 可以是位元組(ascii編碼以內)

    # requests.request(method='get',
    # url='http://127.0.0.1:8000/test/',
    # params={'k1': 'v1', 'k2': '水電費'})

    # requests.request(method='get',
    # url='http://127.0.0.1:8000/test/',
    # params="k1=v1&k2=水電費&k3=v3&k3=vv3")

    # requests.request(method='get',
    # url='http://127.0.0.1:8000/test/',
    # params=bytes("k1=v1&k2=k2&k3=v3&k3=vv3", encoding='utf8'))

    # 錯誤
    # requests.request(method='get',
    # url='http://127.0.0.1:8000/test/',
    # params=bytes("k1=v1&k2=水電費&k3=v3&k3=vv3", encoding='utf8'))
    pass


def param_data():
    # 可以是字典
    # 可以是字串
    # 可以是位元組
    # 可以是檔案物件

    # requests.request(method='POST',
    # url='http://127.0.0.1:8000/test/',
    # data={'k1': 'v1', 'k2': '水電費'})

    # requests.request(method='POST',
    # url='http://127.0.0.1:8000/test/',
    # data="k1=v1; k2=v2; k3=v3; k3=v4"
    # )

    # requests.request(method='POST',
    # url='http://127.0.0.1:8000/test/',
    # data="k1=v1;k2=v2;k3=v3;k3=v4",
    # headers={'Content-Type': 'application/x-www-form-urlencoded'}
    # )

    # requests.request(method='POST',
    # url='http://127.0.0.1:8000/test/',
    # data=open('data_file.py', mode='r', encoding='utf-8'), # 檔案內容是:k1=v1;k2=v2;k3=v3;k3=v4
    # headers={'Content-Type': 'application/x-www-form-urlencoded'}
    # )
    pass


def param_json():
    # 將json中對應的資料進行序列化成一個字串,json.dumps(...)
    # 然後傳送到伺服器端的body中,並且Content-Type是 {'Content-Type': 'application/json'}
    requests.request(method='POST',
                     url='http://127.0.0.1:8000/test/',
                     json={'k1': 'v1', 'k2': '水電費'})


def param_headers():
    # 傳送請求頭到伺服器端
    requests.request(method='POST',
                     url='http://127.0.0.1:8000/test/',
                     json={'k1': 'v1', 'k2': '水電費'},
                     headers={'Content-Type': 'application/x-www-form-urlencoded'}
                     )


def param_cookies():
    # 傳送Cookie到伺服器端
    requests.request(method='POST',
                     url='http://127.0.0.1:8000/test/',
                     data={'k1': 'v1', 'k2': 'v2'},
                     cookies={'cook1': 'value1'},
                     )
    # 也可以使用CookieJar(字典形式就是在此基礎上封裝)
    from http.cookiejar import CookieJar
    from http.cookiejar import Cookie

    obj = CookieJar()
    obj.set_cookie(Cookie(version=0, name='c1', value='v1', port=None, domain='', path='/', secure=False, expires=None,
                          discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False,
                          port_specified=False, domain_specified=False, domain_initial_dot=False, path_specified=False)
                   )
    requests.request(method='POST',
                     url='http://127.0.0.1:8000/test/',
                     data={'k1': 'v1', 'k2': 'v2'},
                     cookies=obj)


def param_files():
    # 傳送檔案
    # file_dict = {
    # 'f1': open('readme', 'rb')
    # }
    # requests.request(method='POST',
    # url='http://127.0.0.1:8000/test/',
    # files=file_dict)

    # 傳送檔案,定製檔名
    # file_dict = {
    # 'f1': ('test.txt', open('readme', 'rb'))
    # }
    # requests.request(method='POST',
    # url='http://127.0.0.1:8000/test/',
    # files=file_dict)

    # 傳送檔案,定製檔名
    # file_dict = {
    # 'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf")
    # }
    # requests.request(method='POST',
    # url='http://127.0.0.1:8000/test/',
    # files=file_dict)

    # 傳送檔案,定製檔名
    # file_dict = {
    #     'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf", 'application/text', {'k1': '0'})
    # }
    # requests.request(method='POST',
    #                  url='http://127.0.0.1:8000/test/',
    #                  files=file_dict)

    pass


def param_auth():
	# 認證,瀏覽器BOM物件彈出對話方塊
	# 在HTML文件中是找不到該標籤的,所以需要用這個對其進行傳入,一般來說常見於路由器登入頁面
    from requests.auth import HTTPBasicAuth, HTTPDigestAuth

    ret = requests.get('https://api.github.com/user', auth=HTTPBasicAuth('wupeiqi', 'sdfasdfasdf'))
    print(ret.text)

    # ret = requests.get('http://192.168.1.1',
    # auth=HTTPBasicAuth('admin', 'admin'))
    # ret.encoding = 'gbk'
    # print(ret.text)

    # ret = requests.get('http://httpbin.org/digest-auth/auth/user/pass', auth=HTTPDigestAuth('user', 'pass'))
    # print(ret)
    #


def param_timeout():
	# 超時時間,如果連結時間大於1秒就返回
    # ret = requests.get('http://google.com/', timeout=1)
    # print(ret)
	# 如果連結時間大於5秒就返回,或者響應時間大於1秒就返回
    # ret = requests.get('http://google.com/', timeout=(5, 1))
    # print(ret)
    pass


def param_allow_redirects():
    ret = requests.get('http://127.0.0.1:8000/test/', allow_redirects=False)
    print(ret.text)


def param_proxies():
	# 配置代理
    # proxies = {
    # "http": "61.172.249.96:80",
    # "https": "http://61.185.219.126:3128",
    # }

    # proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}

    # ret = requests.get("http://www.proxy360.cn/Proxy", proxies=proxies)
    # print(ret.headers)


    # from requests.auth import HTTPProxyAuth
    #
    # proxyDict = {
    # 'http': '77.75.105.165',
    # 'https': '77.75.105.165'
    # }
    # auth = HTTPProxyAuth('username', 'mypassword')
    #
    # r = requests.get("http://www.google.com", proxies=proxyDict, auth=auth)
    # print(r.text)

    pass


def param_stream():
	# 檔案流,直接寫入檔案即可
    ret = requests.get('http://127.0.0.1:8000/test/', stream=True)
    print(ret.content)
    ret.close()

    # from contextlib import closing
    # with closing(requests.get('http://httpbin.org/get', stream=True)) as r:
    # # 在此處理響應。
    # for i in r.iter_content():
    # print(i)

session物件

   如果爬取一個網站,該網站可能會返回給你一些cookies,對這個網站後續的請求每次都要帶上這些cookies比較麻煩。

   所以可以直接使用session物件(自動儲存cookies)傳送請求,它會攜帶當前物件中所有的cookies

def requests_session():
    import requests
	# 使用session時,會攜帶該網站中所返回的所有cookies傳送下一次請求。
	
	# 生成session物件
    session = requests.Session()

    ### 1、首先登陸任何頁面,獲取cookie

    i1 = session.get(url="http://dig.chouti.com/help/service")

    ### 2、使用者登陸,攜帶上一次的cookie,後臺對cookie中的 gpsd 進行授權
    i2 = session.post(
        url="http://dig.chouti.com/login",
        data={
            'phone': "8615131255089",
            'password': "xxxxxx",
            'oneMonth': ""
        }
    )

    i3 = session.post(
        url="http://dig.chouti.com/link/vote?linksId=8589623",
    )
    print(i3.text)

response物件

   以下是response物件的所有引數:

引數描述
response.text 返回文字響應內容
response.content 返回二進位制響應內容
response.json 如果返回內容是json格式,則進行序列化
response.encoding 返回響應內容的編碼格式
response.status_code 狀態碼
response.headers 返回頭
response.cookies 返回的cookies物件
response.cookies.get_dict() 以字典形式展示返回的cookies物件
response.cookies.items() 以元組形式展示返回的cookies物件
response.url 返回的url地址
response.history 這是一個列表,如果請求被重定向,則將上一次被重定向的response物件新增到該列表中

   編碼問題

   並非所有網頁都是utf8編碼,有的網頁是gbk編碼。

   此時如果使用txt檢視響應內容就要指定編碼格式:

import requests
response=requests.get('http://www.autohome.com/news')
response.encoding='gbk'
print(response.text)

   下載檔案

   使用response.context時,會將所有內容存放至記憶體中。

   如果訪問的資源是一個大檔案,而需要對其進行下載時,可使用如下方式生成迭代器下載:

import requests

response=requests.get('http://bangimg1.dahe.cn/forum/201612/10/200447p36yk96im76vatyk.jpg')
with open("res.png","wb") as f:
    for line in response.iter_content():
        f.write(line)

   json返回內容

   如果確定返回內容是json資料,則可以通過response.json進行檢視:

import requests
response = requests.get("http://127.0.0.1:5000/index")
print(response.json())

   歷史記錄

   如果訪問一個地址卻被重定向了,被重定向的地址會被存放到response.history這個列表中:

import requests
r = requests.get('http://127.0.0.1:5000/index')  # 被重定向了
print(r.status_code)  # 200
print(r.url)  # http://127.0.0.1:5000/new  # 重定向的地址
print(r.history)
# [<Response [302]>]

   如果在請求時,指定allow_redirects引數為False,則禁止重定向:

import requests
r = requests.get('http://127.0.0.1:5000/index',allow_redirects=False)  # 禁止重定向
print(r.status_code)  # 302
print(r.url)  # http://127.0.0.1:5000/index
print(r.history)
# []

bs4模組

   request模組可以傳送請求,獲取HTML文件內容。

   而bs4模組可以解析出HTMLXML文件的內容,如快速查詢標籤等等。

pip3 install bs4

   bs4模組只能在Python中使用

   bs4依賴解析器,雖然有自帶的解析器,但是目前使用最多的還是lxml

pip3 install lxml

   img

基本使用

   將request模組請求回來的HTML文件內容轉換為bs4物件,使用其下的方法進行查詢:

   如下示例,解析出蝦米音樂中的歌曲,歌手,歌曲時長:

import requests
from bs4 import BeautifulSoup
from prettytable import PrettyTable

# 例項化表格
table = PrettyTable(['編號', '歌曲名稱', '歌手', '歌曲時長'])

url = r"https://www.xiami.com/list?page=1&query=%7B%22genreType%22%3A1%2C%22genreId%22%3A%2220%22%7D&scene=genre&type=song"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
}
response = requests.get(url=url, headers=headers)

# step01: 將文字內容例項化出bs物件
soup_obj = BeautifulSoup(response.text, "lxml")

# step02: 查詢標籤
main = soup_obj.find("div", attrs={"class": "table idle song-table list-song"})

# step03: 查詢存放歌曲資訊的tbody標籤
tbody = main.select(".table-container>table>tbody")[0]

# step04: tbody標籤中的每個tr都是一首歌曲
tr = tbody.find_all("tr")

# step04: 每個tr裡都存放有歌曲資訊,所以直接迴圈即可
for music in tr:
    name = music.select(".song-name>a")[0].text
    singer = music.select(".COMPACT>a")[0].text
    time_len = music.select(".duration")[0].text
    table.add_row([tr.index(music) + 1, name, singer, time_len])

# step05: 列印資訊
print(table)

   結果如下:

+------+--------------------------------------------------+--------------------+----------+
| 編號 |                     歌曲名稱                     |        歌手        | 歌曲時長 |
+------+--------------------------------------------------+--------------------+----------+
|  1   | Love Story (Live from BBC 1's Radio Live Lounge) |    Taylor Swift    |  04:25   |
|  2   |                Five Hundred Miles                |        Jove        |  03:27   |
|  3   |    I'm Gonna Getcha Good! (Red Album Version)    |    Shania Twain    |  04:30   |
|  4   |                     Your Man                     |    Josh Turner     |  03:45   |
|  5   |             Am I That Easy To Forget             |     Jim Reeves     |  02:22   |
|  6   |                   Set for Life                   |    Trent Dabbs     |  04:23   |
|  7   |                    Blue Jeans                    |  Justin Rutledge   |  04:25   |
|  8   |                    Blind Tom                     | Grant-Lee Phillips |  02:59   |
|  9   |                      Dreams                      |   Slaid Cleaves    |  04:14   |
|  10  |                  Remember When                   |    Alan Jackson    |  04:31   |
|  11  |                Crying in the Rain                |    Don Williams    |  03:04   |
|  12  |                    Only Worse                    |    Randy Travis    |  02:53   |
|  13  |                     Vincent                      | The Sunny Cowgirls |  04:22   |
|  14  |           When Your Lips Are so Close            |    Gord Bamford    |  03:02   |
|  15  |                  Let It Be You                   |    Ricky Skaggs    |  02:42   |
|  16  |                  Steal a Heart                   |    Tenille Arts    |  03:09   |
|  17  |                      Rylynn                      |     Andy McKee     |  05:13   |
|  18  |        Rockin' Around The Christmas Tree         |     Brenda Lee     |  02:06   |
|  19  |            Love You Like a Love Song             |    Megan & Liz     |  03:17   |
|  20  |               Tonight I Wanna Cry                |    Keith Urban     |  04:18   |
|  21  |           If a Song Could Be President           |   Over the Rhine   |  03:09   |
|  22  |                   Shut'er Down                   |   Doug Supernaw    |  04:12   |
|  23  |                     Falling                      |  Jamestown Story   |  03:08   |
|  24  |                     Jim Cain                     |   Bill Callahan    |  04:40   |
|  25  |                  Parallel Line                   |    Keith Urban     |  04:14   |
|  26  |                 Jingle Bell Rock                 |    Bobby Helms     |  04:06   |
|  27  |                    Unsettled                     |  Justin Rutledge   |  04:01   |
|  28  |                Bummin' Cigarettes                |    Maren Morris    |  03:07   |
|  29  |              Cheatin' on Her Heart               |    Jeff Carson     |  03:18   |
|  30  |             If My Heart Had a Heart              |   Cassadee Pope    |  03:21   |
+------+--------------------------------------------------+--------------------+----------+

Process finished with exit code 0

HTML文件

   準備一個HTML文件,對他進行解析:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<form action="#" method="post" enctype="multipart/form-data">
    <fieldset>
        <legend><h1>歡迎註冊</h1></legend>

        <p>頭像:&nbsp;&nbsp;<input type="file" name="avatar"/></p>
        <p>使用者名稱:&nbsp; <input type="text" name="username" placeholder="請輸入使用者名稱"/></p>
        <p>密碼:&nbsp;&nbsp;&nbsp;&nbsp;<input type="text" name="pwd" placeholder="請輸入密碼"/></p>
        <p>性別: 男<input type="radio" name="gender" value="male"/>女<input type="radio" name="gender" value="female"/></p>
        <p>愛好: 籃球<input type="checkbox" name="hobby" value="basketball" checked/>足球<input type="checkbox" name="hobby"
                                                                                          value="football"/></p>

        居住地
        <select name="addr">
            <optgroup label="中國">
                <option value="bejing" selected>北京</option>
                <option value="shanghai">上海</option>
                <option value="guangzhou">廣州</option>
                <option value="shenzhen">深圳</option>
                <option value="other">其他</option>
            </optgroup>
            <optgroup label="海外">
                <option value="America">美國</option>
                <option value="Japanese">日本</option>
                <option value="England">英國</option>
                <option value="Germany">德國</option>
                <option value="Canada">加拿大</option>
            </optgroup>
        </select>
    </fieldset>

    <fieldset>
        <legend>請填寫註冊理由</legend>
        <p><textarea name="register_reason" cols="30" rows="10" placeholder="請填寫充分理由"></textarea></p>
    </fieldset>
    <p><input type="reset" value="重新填寫資訊"/>&nbsp;<input type="submit" value="提交註冊資訊">&nbsp;<input type="butoon"
                                                                                                  value="聯絡客服" disabled>
    </p>

</form>
</body>
</html>

基本選擇器

   基本選擇器如下 :

選擇器方法描述
TagName 唯一選擇器,根據標籤名來選擇
find() 唯一選擇器,可根據標籤名、屬性來做選擇
select_one() 唯一選擇器,可根據CSS選擇器語法做選擇
find_all() 集合選擇器,可根據標籤名、屬性來做選擇
select() 集合選擇器,可根據CSS選擇器語法做選擇

   .TagName選擇器只會拿出第一個匹配的內容,必須根據標籤名選擇:

input = soup.input
print(input)
# <input name="avatar" type="file"/>

   .find()選擇器只會拿出第一個匹配的內容,可根據標籤名、屬性來做選擇

input= soup.find("input",attrs={"name":"username","type":"text"})  # attrs指定屬性
print(input)

# <input name="username" placeholder="請輸入使用者名稱" type="text"/>

   .select_one()根據css選擇器來查詢標籤,只獲取第一個:

input = soup.select_one("input[type=text]")
print(input)
# <input name="username" placeholder="請輸入使用者名稱" type="text"/>

   .find_all()可獲取所有匹配的標籤,返回一個list,可根據標籤名、屬性來做選擇

input_list = soup.find_all("input",attrs={"type":"text"})
print(input_list)

# [<input name="username" placeholder="請輸入使用者名稱" type="text"/>, <input name="pwd" placeholder="請輸入密碼" type="text"/>]

   .select()根據css選擇器獲取所有匹配的標籤,返回一個list

input_list = soup.select("input[type=text]")
print(input_list)

# [<input name="username" placeholder="請輸入使用者名稱" type="text"/>, <input name="pwd" placeholder="請輸入密碼" type="text"/>]

  

關係與操作

   使用較少,選讀:

屬性/方法描述
children 獲取所有的後代標籤,返回迭代器
descendants 獲取所有的後代標籤,返回生成器
index() 檢查某個標籤在當前標籤中的索引值
clear() 刪除後代標籤,保留本標籤,相當於清空
decompose() 刪除標籤本身(包括所有後代標籤)
extract() 同.decomponse()效果相同,但會返回被刪除的標籤
decode() 將當前標籤與後代標籤轉換字串
decode_contents() 將當前標籤的後代標籤轉換為字串
encode() 將當前標籤與後代標籤轉換位元組串
encode_contents() 將當前標籤的後代標籤轉換為位元組串
append() 在當前標籤內部追加一個標籤(無示例)
insert() 在當前標籤內部指定位置插入一個標籤(無示例)
insert_before() 在當前標籤前面插入一個標籤(無示例)
insert_after() 在當前標籤後面插入一個標籤(無示例)
replace_with() 將當前標籤替換為指定標籤(無示例)

   .children獲取所有的後代標籤,返回迭代器

form = soup.find("form")
print(form.children)

# <list_iterator object at 0x0000025665D5BDD8>

  

   .descendants獲取所有的後代標籤,返回生成器

form = soup.find("form")
print(form.descendants)

# <generator object descendants at 0x00000271C8F0ACA8>

   .index()檢查某個標籤在當前標籤中的索引值

body = soup.find("body")
form = soup.find("form")
print(body.index(form)) 
# 3

   .clear()刪除後代標籤,保留本標籤,相當於清空

form = soup.find("form")
form.clear()
print(form)  # None
print(soup)
# 清空了form

   .decompose()刪除標籤本身(包括所有後代標籤)

form = soup.find("form")
form..decompose()
print(form)  # None
print(soup)
# 刪除了form

   .extract().decomponse()效果相同,但會返回被刪除的標籤

form = soup.find("form")
form..extract()
print(form)  # 被刪除的內容
print(soup)
# 被刪除了form

   .decode()將當前標籤與後代標籤轉換字串,.decode_contents()將當前標籤的後代標籤轉換為字串

form = soup.find("form")
print(form.decode())  # 包含form
print(form.decode_contents())  # 不包含form

   .encode()將當前標籤與後代標籤轉換位元組串,.encode_contents()將當前標籤的後代標籤轉換為位元組串

form = soup.find("form")
print(form.encode())  # 包含form
print(form.encode_contents())  # 不包含form

標籤內容

   以下方法都比較常用:

屬性/方法描述
name 獲取標籤名稱
attrs 獲取標籤屬性
text 獲取該標籤下的所有文字內容(包括後代)
string 獲取該標籤下的直系文字內容
is_empty_element 判斷是否是空標籤或者自閉合標籤
get_text() 獲取該標籤下的所有文字內容(包括後代)
has_attr() 檢查標籤是否具有該屬性

   .name獲取標籤名稱

form = soup.find("form")
print(form.name)

# form

   .attrs獲取標籤屬性

form = soup.find("form")
print(form.attrs)

# {'action': '#', 'method': 'post', 'enctype': 'multipart/form-data'}

   .is_empty_element判斷是否是空標籤或者自閉合標籤

input = soup.find("input")
print(input.is_empty_element)
# True

   .get_text()text獲取該標籤下的所有文字內容(包括後代)

form = soup.find("form")
print(form.get_text())
print(form.text)

   string獲取該標籤下的直系文字內容

form = soup.find("form")
print(form.get_text())
print(form.string)

   .has_attr()檢查標籤是否具有該屬性

form = soup.find("form")
print(form.has_attr("action"))
# True

xPath模組

   xPath模組的作用與bs4相同,都是查詢標籤。

   但是xPath模組的通用性更強,它的語法規則並不限於僅在Python中使用。

   作為一門小型的專業化查詢語言,xPathPython中被整合在了lxml模組中,所以直接下載安裝就可以開始使用了。

pip3 install lxml

   載入文件:

from lxml import etree

# 解析網路爬取的html原始碼
root = etree.HTML(response.text,,etree.HTMLParser())  # 載入整個HTML文件,並且返回根節點<html>

# 解析本地的html檔案
root = etree.parse(fileName,etree.HTMLParser())

基本選取符

   基本選取符:

符號描述
/ 從根節點開始選取
// 不考慮層級關係的選取節點
. 選取當前節點
.. 選取當前節點的父節點
@ 屬性檢測
[num] 選取第n個標籤元素,從1開始
/@attrName 選取當前元素的某一屬性
* 萬用字元
/text() 選取當前節點下的直系文字內容
//text() 選取當前文字下的所有文字內容
| 返回符號兩側所匹配的全部標籤

   以下是示例:

   注意:xPath選擇完成後,返回的始終是一個list,與jQuery類似,可以通過Index取出Element物件

from lxml import etree

root = etree.parse("./testDataDocument.html",etree.HTMLParser())

# 從根節點開始找 /
form_list = root.xpath("/html/body/form")
print(form_list)  # [<Element form at 0x203bd29c188>]

# 不考慮層級關係的選擇節點 //
input_list = root.xpath("//input")
print(input_list)

# 從當前的節點開始選擇 即第一個form表單 ./
select_list = form_list[0].xpath("./fieldset/select")
print(select_list)

# 選擇當前節點的父節點 ..
form_parent_list = form_list[0].xpath("..")
print(form_parent_list)  # [<Element body at 0x1c946e4c548>]

# 屬性檢測 @ 選取具有name屬性的input框
input_username_list = root.xpath("//input[@name='username']")
print(input_username_list)

# 屬性選取 @ 獲取元素的屬性
attrs_list = root.xpath("//p/@title")
print(attrs_list)

# 選取第n個元素,從1開始
p_text_list = root.xpath("//p[2]/text()")
print(p_text_list)

# 萬用字元 * 選取所有帶有屬性的標籤
have_attrs_ele_list = root.xpath("//*[@*]")
print(have_attrs_ele_list)

# 獲取文字內容-直系
print(root.xpath("//form/text()"))
# 結果:一堆\r\n

# 獲取文字內容-非直系
print(root.xpath("//form//text()"))
# 結果:本身和後代的text

# 返回所有input與p標籤
ele_list = root.xpath("//input|//p")
print(ele_list)

表示式形式

   你可以指定邏輯運算子,大於小於等。

from lxml import etree

root = etree.parse("./testDataDocument.html",etree.HTMLParser())

# 返回屬性值price大於或等於20的標籤
price_ele_list = root.xpath("//*[@price>=20]")
print(price_ele_list)

xPath軸關係

   xPath中擁有軸這一概念,不過相對來說使用較少,它就是做關係用的。瞭解即可:

示例說明
ancestor xpath(‘./ancestor::*’) 選取當前節點的所有先輩節點(父、祖父)
ancestor-or-self xpath(‘./ancestor-or-self::*’) 選取當前節點的所有先輩節點以及節點本身
attribute xpath(‘./attribute::*’) 選取當前節點的所有屬性
child xpath(‘./child::*’) 返回當前節點的所有子節點
descendant xpath(‘./descendant::*’) 返回當前節點的所有後代節點(子節點、孫節點)
following xpath(‘./following::*’) 選取文件中當前節點結束標籤後的所有節點
following-sibing xpath(‘./following-sibing::*’) 選取當前節點之後的兄弟節點
parent xpath(‘./parent::*’) 選取當前節點的父節點
preceding xpath(‘./preceding::*’) 選取文件中當前節點開始標籤前的所有節點
preceding-sibling xpath(‘./preceding-sibling::*’) 選取當前節點之前的兄弟節點
self xpath(‘./self::*’) 選取當前節點

功能函式

   功能函式更多的是做模糊搜尋,這裡舉幾個常見的例子,一般使用也不多:

函式示例描述
starts-with xpath(‘//div[starts-with(@id,”ma”)]‘) 選取id值以ma開頭的div節點
contains xpath(‘//div[contains(@id,”ma”)]‘) 選取id值包含ma的div節點
and xpath(‘//div[contains(@id,”ma”) and contains(@id,”in”)]‘) 選取id值包含ma和in的div節點
text() xpath(‘//div[contains(text(),”ma”)]‘) 選取節點文字包含ma的div節點

element物件

   上面說過,使用xPath進行篩選後得到的結果都是一個list,其中的成員就是element標籤物件。

   以下方法都是操縱element標籤物件的,比較常用。

   首先是針對自身標籤的操作:

屬性描述
tag 返回元素的標籤型別
text 返回元素的直系文字
tail 返回元素的尾行
attrib 返回元素的屬性(字典形式)

   演示如下:

from lxml import etree

root = etree.parse("./testDataDocument.html",etree.HTMLParser())

list(map(lambda ele:print(ele.tag),root.xpath("//option")))

list(map(lambda ele:print(ele.text),root.xpath("//option")))  # 常用

list(map(lambda ele:print(ele.tail),root.xpath("//option")))

list(map(lambda ele:print(ele.attrib),root.xpath("//option")))  # 常用

   針對當前element物件屬性的操作,用的不多:

方法描述
clear() 清空元素的後代、屬性、text和tail也設定為None
get() 獲取key對應的屬性值,如該屬性不存在則返回default值
items() 根據屬性字典返回一個列表,列表元素為(key, value)
keys() 返回包含所有元素屬性鍵的列表
set() 設定新的屬性鍵與值

   針對當前element物件後代的操作,用的更少:

方法描述
append() 新增直系子元素
extend() 增加一串元素物件作為子元素
find() 尋找第一個匹配子元素,匹配物件可以為tag或path
findall() 尋找所有匹配子元素,匹配物件可以為tag或path
findtext() 尋找第一個匹配子元素,返回其text值。匹配物件可以為tag或path
insert() 在指定位置插入子元素
iter() 生成遍歷當前元素所有後代或者給定tag的後代的迭代器
iterfind() 根據tag或path查詢所有的後代
itertext() 遍歷所有後代並返回text值
remove() 刪除子元素

高效能爬蟲

後端準備

   Flask作為後端伺服器:

from flask import Flask
import time

app = Flask(__name__,template_folder="./")

@app.route('/index',methods=["GET","POST"])
def index():
    time.sleep(2)
    return "index...ok!!!"

@app.route('/news')
def news():
    time.sleep(2)
    return "news...ok!!!"

@app.route('/hot')
def hot():
    time.sleep(2)
    return "hot...ok!!!"

if __name__ == '__main__':
    app.run()

同步爬蟲

   如果使用同步爬蟲對上述伺服器的三個url進行爬取,花費的結果是六秒:

import time

from requests import Session

headers = {
    "user-agent": "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}

urls = [
    "http://127.0.0.1:5000/index",
    "http://127.0.0.1:5000/news",
    "http://127.0.0.1:5000/hot",
]

start = time.time()


def func(url):
    session = Session()
    response = session.get(url)
    return response.text


# 回撥函式,處理後續任務
def callback(result):  # 獲取結果
    print(result)


for url in urls:
    res = func(url)
    callback(res)

end = time.time()
print("總用時:%s秒" % (end - start))

ThreadPoolExecutor

   使用多執行緒則基本兩秒左右即可完成:

import time
from concurrent.futures import ThreadPoolExecutor

from requests import Session

headers = {
    "user-agent": "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}

urls = [
    "http://127.0.0.1:5000/index",
    "http://127.0.0.1:5000/news",
    "http://127.0.0.1:5000/hot",
]

start = time.time()


def func(url):
    session = Session()
    response = session.get(url)
    return response.text


# 回撥函式
def callback(obj):  # 期程物件
    print(obj.result())


pool = ThreadPoolExecutor(max_workers=4)

for url in urls:
    res = pool.submit(func, url)
    # 為期程物件繫結回撥
    res.add_done_callback(callback)

pool.shutdown(wait=True)

end = time.time()
print("總用時:%s秒" % (end - start))

asyncio&aiohttp

   執行緒的切換開銷較大,可使用切換代價更小的協程進行實現。

   由於協程中不允許同步方法的出現,requests模組下的請求方法都是同步請求方法,所以需要使用aiohttp模組下的非同步請求方法完成網路請求。

   現今的所謂非同步,其實都是用I/O多路複用技術來完成,即在一個執行緒下進行where迴圈,監聽描述符,即eventLoop

import asyncio
import time

import aiohttp

headers = {
    "user-agent": "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}

urls = [
    "http://127.0.0.1:5000/index",
    "http://127.0.0.1:5000/news",
    "http://127.0.0.1:5000/hot",
]

start = time.time()


async def func(url):
    # 在async協程中,所有的阻塞方法都需要通過await手動掛起
    # 並且,如果存在同步方法,則還是同步執行,必須是非同步方法,所以這裡使用aiohttp模組傳送請求
    async with aiohttp.ClientSession() as session:
        async with await session.get(url) as response:
            # text():返回字串形式的響應資料
            # read(): 返回二進位制格式響應資料
            # json(): json格式反序列化
            result = await response.text()  # aiohttp中是一個方法
            return result


# 回撥函式
def callback(obj):  # 期程物件
    print(obj.result())


# 建立協程任務列表
tasks = []
for url in urls:
    g = func(url)  # 建立協程任務g
    task = asyncio.ensure_future(g)  # 註冊協程任務
    task.add_done_callback(callback)  # 繫結回撥,傳入期程物件
    tasks.append(task)  # 新增協程任務到任務列表

# 建立事件迴圈
loop = asyncio.get_event_loop()
# 執行任務,並且主執行緒會等待協程任務列表中的所有任務處理完畢後再執行
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print("總用時:%s秒" % (end - start))

selenium模組

   seleniumPython實現瀏覽器自動化操作的一款模組。

   通過它我們可以讓程式碼控制瀏覽器,從而進行資料爬取,尤其在以下兩個地方該模組的作用更加強大:

  1. 獲取整張頁面的資料,對有的頁面來說前後端分離的API介面太難找了,使用requests模組根本找不到傳送載入資料的介面
  2. 進行自動登入

   官方文件

   下載安裝:

pip3 install selenium

   由於要操縱瀏覽器,所以要下載對應的驅動檔案,需要注意的是驅動版本需要與瀏覽器版本一一對應:

   下載驅動

   如果是MAC平臺,解壓到如下路徑,win平臺解壓到任意位置皆可:

/usr/local/bin

   由於我們使用的是chorme瀏覽器,所以只需要例項化出其操縱物件即可:

from selenium import webdriver

driver = webdriver.Chrome()

   以後的操縱都是操縱該例項物件,如果你使用其他版本瀏覽器,請自行下載驅動,支援的瀏覽器如下:

driver = webdriver.Firefox()
driver = webdriver.Edge()
driver = webdriver.PhantomJS()
driver = webdriver.Safari()

基本使用

   以下是基本操縱例項,例項將展示如何搜尋部落格園:

from selenium import webdriver
import time

# 載入驅動
driver = webdriver.Chrome(r"./chromedriver.exe")

# 開啟百度頁面
driver.get("https://www.baidu.com")

# 找到搜尋框,輸入部落格園
driver.find_element_by_id("kw").send_keys("部落格園")
time.sleep(2)
driver.find_element_by_id('su').click()
time.sleep(2)

# 關閉瀏覽器
driver.quit()

元素定位

   webdriver提供了很多元素定位方法,常用的如下:

driver.find_element_by_id()
driver.find_element_by_name()
driver.find_element_by_class_name()
driver.find_element_by_tag_name()
driver.find_element_by_link_text()
driver.find_element_by_partial_link_text()
driver.find_element_by_xpath()
driver.find_element_by_css_selector()

ifarme定位

   對於webdriver來說,它擁有一層作用域。

   預設是在頂級作用域中,如果出現了ifarme標籤,則必須切換到ifarme標籤的作用域才能查詢其裡面的元素。

   如下,想查詢其中的button

<div id="modal">
  <iframe id="buttonframe"name="myframe"src="https://seleniumhq.github.io">
   <button>Click here</button>
 </iframe>
</div>

   如果直接獲取button則不會生效,因為目前作用域是外部的html標籤中,不能獲取內部iframe的作用域:

# 這不會工作
driver.find_element(By.TAG_NAME, 'button').click()

   正確的方法是找到ifarme標籤,對其進行切換作用域的操作:

# 儲存網頁元素
iframe = driver.find_element(By.CSS_SELECTOR, "#modal > iframe")

# 切換到選擇的 iframe
driver.switch_to.frame(iframe)

# 單擊按鈕
driver.find_element(By.TAG_NAME, 'button').click()

   如果您的frameiframe具有idname屬性,則可以使用該屬性。如果名稱或 id 在頁面上不是唯一的, 那麼將切換到找到的第一個。

# 通過 id 切換框架
driver.switch_to.frame('buttonframe')

# 單擊按鈕
driver.find_element(By.TAG_NAME, 'button').click()

   還可以通過索引值進行切換:

# 切換到第 2 個框架
driver.switch_to.frame(1)

   退出當前iframe的作用域,使用以下程式碼:

# 切回到預設內容
driver.switch_to.default_content()

互動相關

   我們可以與瀏覽器BOM或者element進行互動。

   如找到搜尋框,使用send_keys()即可輸入內容,clear()即可清空內容。

   再比如找到button使用click()即可觸發單擊事件。

   更多方法請參照官方文件,截圖也在其中:

   點我跳轉

動作鏈

   如果碰到滑動驗證的操作,則需要使用動作鏈進行。

   上述的互動中,如send_keys()click()都是一次性完成的,如果是非一次性的操作如拖拽,滑動的就可以通過動作鏈完成。

   動作鏈的官方文件,包括獲取當前元素的大小,配合截圖使用有奇效,舉個例子,截圖到當前的驗證碼頁面,然後使用第三方打碼工具進行解析驗證碼:

   點我跳轉

from selenium import webdriver
from time import sleep
#匯入動作鏈對應的類
from selenium.webdriver import ActionChains
bro = webdriver.Chrome(executable_path='./chromedriver')

bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')

#如果定位的標籤是存在於iframe標籤之中的則必須通過如下操作在進行標籤定位
bro.switch_to.frame('iframeResult')#切換瀏覽器標籤定位的作用域
div = bro.find_element_by_id('draggable')

#動作鏈
action = ActionChains(bro)
#點選長按指定的標籤
action.click_and_hold(div)

for i in range(5):
    #perform()立即執行動作鏈操作
    #move_by_offset(x,y):x水平方向 y豎直方向
    action.move_by_offset(17,0).perform()
    sleep(0.5)

#釋放動作鏈
action.release()

bro.quit()

執行指令碼

   如果webdriver例項中沒有實現某些方法,則可以通過執行Js程式碼來完成,比如下拉滑動條:

from selenium import webdriver
 
driver = webdriver.Chrome(r"./chromedriver.exe")
driver.get('https://www.jd.com/')
# 執行指令碼:滑動整個頁面
driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')

原始碼資料

   上面提到過,如果使用requets模組訪問某一url卻沒有拿到想要的資料,那麼很可能是前後端分離通過RESTful APIs進行資料互動。

   這個時候我們可以使用selenium模組來對同一url發起請求,由於是瀏覽器開啟,所有的RESTFUL API都會進行請求,然後直接通過屬性page_source解析返回的原始碼資料:

from selenium import webdriver
from lxml import etree

driver=webdriver.Chrome(r"./chromedriver.exe",)

driver.get('https://www.baidu.com/')
source_code = driver.page_source  # 獲取網頁原始碼

# 直接獲取百度的圖片地址
root = etree.HTML(source_code,parser=etree.HTMLParser())
driver.close()
img_src = "http:" + root.xpath(r"//*[@id='s_lg_img_new']")[0].attrib.get("src")
print(img_src)

節點操作

   上面我們通過使用lxml模組來解析原始碼中的百度圖片地址,其實可以不用這麼麻煩。

   Selenium也提供了節點操作,選取節點、獲取屬性等:

from selenium import webdriver
from selenium.webdriver.common.by import By  # 按照什麼方式查詢,By.ID,By.CSS_SELECTOR
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait  # 等待頁面載入某些元素

driver = webdriver.Chrome(r"./chromedriver.exe",)

driver.get('https://www.amazon.cn/')

wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.ID, 'cc-lm-tcgShowImgContainer')))

tag = driver.find_element(By.CSS_SELECTOR, '#cc-lm-tcgShowImgContainer img')

# 獲取標籤屬性,
print(tag.get_attribute('src'))
# 獲取標籤ID,位置,名稱,大小(瞭解)
print(tag.id)
print(tag.location)
print(tag.tag_name)
print(tag.size)

driver.close()

延時等待

   在Selenium中,get()方法會在網頁框架載入結束後結束執行,此時如果獲取page_source,可能並不是瀏覽器完全載入完成的頁面,如果某些頁面有額外的Ajax請求,我們在網頁原始碼中也不一定能成功獲取到。所以,這裡需要延時等待一定時間,確保節點已經載入出來。這裡等待的方式有兩種:一種是隱式等待,一種是顯式等待。

   隱式等待:

   當使用隱式等待執行測試的時候,如果Selenium沒有在DOM中找到節點,將繼續等待,超出設定時間後,則丟擲找不到節點的異常。換句話說,當查詢節點而節點並沒有立即出現的時候,隱式等待將等待一段時間再查詢DOM,預設的時間是0。示例如下:

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By #按照什麼方式查詢,By.ID,By.CSS_SELECTOR
from selenium.webdriver.common.keys import Keys #鍵盤按鍵操作
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait #等待頁面載入某些元素

driver=webdriver.Chrome(r"./chromedriver.exe",)

#隱式等待:在查詢所有元素時,如果尚未被載入,則等10秒
driver.implicitly_wait(10)

driver.get('https://www.baidu.com')
input_tag=driver.find_element_by_id('kw')
input_tag.send_keys('美女')
input_tag.send_keys(Keys.ENTER)

contents=driver.find_element_by_id('content_left') #沒有等待環節而直接查詢,找不到則會報錯
print(contents)

driver.close()

   顯示等待:

   隱式等待的效果其實並沒有那麼好,因為我們只規定了一個固定時間,而頁面的載入時間會受到網路條件的影響。這裡還有一種更合適的顯式等待方法,它指定要查詢的節點,然後指定一個最長等待時間。如果在規定時間內載入出來了這個節點,就返回查詢的節點;如果到了規定時間依然沒有載入出該節點,則丟擲超時異常。

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By #按照什麼方式查詢,By.ID,By.CSS_SELECTOR
from selenium.webdriver.common.keys import Keys #鍵盤按鍵操作
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait #等待頁面載入某些元素

driver=webdriver.Chrome(r"./chromedriver.exe",)
driver.get('https://www.baidu.com')


input_tag=driver.find_element_by_id('kw')
input_tag.send_keys('美女')
input_tag.send_keys(Keys.ENTER)


#顯式等待:顯式地等待某個元素被載入
wait=WebDriverWait(driver,10)
wait.until(EC.presence_of_element_located((By.ID,'content_left')))

contents=driver.find_element(By.CSS_SELECTOR,'#content_left')
print(contents)


driver.close()

   關於等待條件,其實還有很多,比如判斷標題內容,判斷某個節點內是否出現了某文字等。more

cookie操作

   使用Selenium,還可以方便地對Cookies進行操作,例如獲取、新增、刪除Cookies等。示例如下:

from selenium import webdriver
 
driver = webdriver.Chrome(r"./chromedriver.exe",)
driver.get('https://www.zhihu.com/explore')
print(driver.get_cookies())
driver.add_cookie({'name': 'name', 'domain': 'www.zhihu.com', 'value': 'germey'})
print(driver.get_cookies())
driver.delete_all_cookies()
print(driver.get_cookies())

異常處理

   遮蔽掉所有可能出現的異常:

from selenium import webdriver
from selenium.common.exceptions import TimeoutException,NoSuchElementException,NoSuchFrameException

try:
    driver=webdriver.Chrome()
    driver.get('http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
    driver.switch_to.frame('iframssseResult')

except TimeoutException as e:
    print(e)
except NoSuchFrameException as e:
    print(e)
finally:
    driver.close()

無頭操作

   每次使用selenium時都會開啟一個瀏覽器,能不能有什麼辦法讓他隱藏介面呢?

   指定引數即可,這種沒有介面的瀏覽也可以稱其為無頭瀏覽器:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# 設定配置項
chrome_options = Options()
chrome_options.add_argument('--headless')

# 指定配置
driver = webdriver.Chrome(r"./chromedriver.exe",chrome_options=chrome_options)
driver.get("http://www.baidu.com")
driver.close()

規避檢測

   可能有的入口網站已經對selenium做出了檢測,如果檢測到是該指令碼執行可能不允許你訪問API,此時就可以通過偽造資訊達到潛行的效果。

   將selenium偽裝成人為操作:

#實現規避檢測
from selenium.webdriver import ChromeOptions

#實現規避檢測
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])

# 指定配置
driver = webdriver.Chrome(executable_path='./chromedriver',options=option)

driver.get("http://www.baidu.com")
driver.close()

Scrapy框架基礎

基本介紹

   Scrapy框架是Python中最出名的一款爬蟲框架,本身基於twisted非同步框架封裝完成。

   它有著基本的五大元件,整個框架架構如下圖所示:

   img

   Scrapy 元件介紹

  1.    引擎(EGINE)

       引擎負責控制系統所有元件之間的資料流,並在某些動作發生時觸發事件。有關詳細資訊,請參見下面的資料流部分。

  2.    排程器(SCHEDULER) 用來接受引擎發過來的請求, 壓入佇列中, 並在引擎再次請求的時候返回. 可以想像成一個URL的優先順序佇列, 由它來決定下一個要抓取的網址是什麼, 同時去除重複的網址

  3.    下載器(DOWLOADER) 用於下載網頁內容, 並將網頁內容返回給EGINE,下載器是建立在twisted這個高效的非同步模型上的

  4.    爬蟲(SPIDERS) SPIDERS是開發人員自定義的類,用來解析responses,並且提取items,或者傳送新的請求

  5.    專案管道(ITEM PIPLINES) 在items被提取後負責處理它們,主要包括清理、驗證、持久化(比如存到資料庫)等操作

  6.    下載器中介軟體(Downloader Middlewares)

       位於Scrapy引擎和下載器之間,主要用來處理從EGINE傳到DOWLOADER的請求request,已經從DOWNLOADER傳到EGINE的響應response,你可用該中介軟體做以下幾件事

    1. 在將請求傳送到下載器之前處理請求(即,在Scrapy將請求傳送到網站之前);
    2. 在傳遞給SPIDERS之前更改收到的響應;
    3. 傳送新的請求,而不是將收到的響應傳遞給SPIDERS;
    4. 將響應傳遞給SPIDERS,而無需獲取網頁;
    5. 默默地丟棄一些請求。
  7.    爬蟲中介軟體(Spider Middlewares) 位於EGINE和SPIDERS之間,主要工作是處理SPIDERS的輸入(即responses)和輸出(即requests)

   整個爬取的資料流:

  1. 引擎開啟一個網站(open a domain),找到處理該網站的Spider並向該Spider請求第一個要爬取的URL(s)。
  2. 引擎從Spider中獲取到第一個要爬取的URL並在排程器(Scheduler)以Request排程。
  3. 引擎向排程器請求下一個要爬取的URL。
  4. 排程器返回下一個要爬取的URL給引擎,引擎將URL通過下載中介軟體(請求(request)方向)轉發給下載器(Downloader)。
  5. 一旦頁面下載完畢,下載器生成一個該頁面的Response,並將其通過下載中介軟體(返回(response)方向)傳送給引擎。
  6. 引擎從下載器中接收到Response並通過Spider中介軟體(輸入方向)傳送給Spider處理。
  7. Spider處理Response並返回爬取到的Item及(跟進的)新的Request給引擎。
  8. 引擎將(Spider返回的)爬取到的Item給Item Pipeline,將(Spider返回的)Request給排程器。
  9. (從第二步)重複直到排程器中沒有更多地request,引擎關閉該網站。

下載安裝

   在MAC/LINUX下安裝該框架十分簡單:

pip3 install scrapy

   如果是Windows平臺,則稍微有些麻煩,因為你需要安裝很多依賴庫:

pip3 install wheel # 安裝後,便支援通過wheel檔案安裝軟體,wheel檔案官網:https://www.lfd.uci.edu/~gohlke/pythonlibs
pip3 install lxml
pip3 install pyopenssl

   下載並安裝pywin32

pip3 install pywin32

   下載並安裝twistedwheel檔案,CP對應Python版本:

# 下載whell檔案:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
# pip3 install 下載目錄\Twisted-17.9.0-cp36-cp36m-win_amd64.whl

   安裝Scrapy

pip3 install scrapy

   安裝完成後,在終端輸入scrapy,如果有反應則代表安裝成功。

   如果沒有反應,重新安裝scrapy

pip uninstall scrapy
pip3 install scrapy

   它會給你一個提示:

Installing collected packages: scrapy
  WARNING: The script scrapy.exe is installed in 'C:\Users\yunya\AppData\Roaming\Python\Python36\Scripts' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed scrapy-2.4.1

   你需要將提示中的路徑加入環境變數即可。

命令列工具

   以下是常用的命令列,首先是全域性命令,即意味著你在終端中任何目錄下都能夠執行:

命令描述
scrapy -h 檢視幫助
scrapy [command] -h 檢視某條命令的幫助
scrapy startproject [ProjectName] 建立專案
scrapy genspider [SpiderName] <ur> 建立爬蟲程式
scrapy settings [options] [command] 檢視爬蟲的程式配置資訊,如果是在專案下,則獲取到專案的部署配置資訊
scrapy runspider [options] <spider_file> 單獨的執行某一個py檔案
scrapy fetch [options] <url> 獨立爬取一個頁面,可以拿到請求頭,如 scrapy fetch --headers http://www.baidu.com
scrapy shell [options] <url> 開啟shell除錯,直接向某一地址傳送請求
scrapy view [options] <url> 開啟瀏覽器,傳送本次請求
scrapy version [-v] 檢視scrapy的版本,新增-v檢視scrapy依賴庫的版本

   其次是區域性命令,指只有在Scrapy專案下執行才能生效的命令:

命令描述
scrapy crawl [options] <spider> 執行爬蟲程式,必須建立專案才行,確保配置檔案中ROBOTSTXT_OBEY = False
scrapy check [options] <spider> 檢測爬蟲程式中語法是否有錯誤
scrapy list 獲取該專案下所有爬蟲程式的名稱
scrapy parse [options] <url> scrapy parse url地址 --callback 回撥函式以此可以驗證我們的回撥函式是否正確
scrapy bench 壓力測試

   一些常用的全域性options

options描述
--help, -h 獲取幫助資訊
--logfile=FILE 日誌檔案,如果省略,將丟擲stderr
--loglevel=LEVEL, -L LEVEL 日誌級別,預設為info
--nolog 禁止顯示日誌資訊
--profile=FILE 將python cProfile統計資訊寫入FILE
--pidfile=FILE 將程式ID寫入FILE
--set=NAME=VALUE, -s NAME=VALUE 設定/替代設定(可以重複)
--pdb 在失敗時啟用pdb

   預設的命令只能在CMD中執行,如果向在IDE中執行,則需要新建一個py檔案,使用execute函式進行命令的執行。

# 在專案目錄下新建:entrypoint.py
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', 'xiaohua'])

目錄介紹

   以下是一個Scrapy專案的目錄:

-- ScrapyProject/		# 專案資料夾
   -- scrapy.cfg		# 專案的主配置資訊,用來部署scrapy時使用,爬蟲相關的配置資訊在settings.py檔案中。
   -- project_name/		# 專案全域性資料夾
          __init__.py
          items.py		# 設定資料儲存模板,用於結構化資料,如:Django的Model
          pipelines.py  # 資料處理行為,如:一般結構化的資料持久化
          settings.py   # 配置檔案,如:遞迴的層數、併發數,延遲下載等。配置變數名必須大寫
          
       -- spiders/		# 爬蟲資料夾,如:建立檔案,編寫爬蟲規則
           __init__.py
           爬蟲1.py
           爬蟲2.py
           爬蟲3.py

Scrapy-爬蟲

基本介紹

   Spiders的主要工作、進行資料爬取和資料解析。

   以下是一個爬蟲程式的初始程式碼:

import scrapy

class CnblogsSpider(scrapy.Spider):  # 基礎的爬蟲類
    name = 'cnblogs'  # 爬蟲程式名稱,非空且唯一
    allowed_domains = ['www.cnblogs.com']  # 允許網路請求的域名,一般來說直接註釋即可
    start_urls = ['http://www.cnblogs.com/']  # 初始的網路請求

    def parse(self, response):  # 資料解析函式
        pass

   預設情況下,當執行該爬蟲程式,會從start_urls中自動發生網路請求,並將返回的資訊傳入parse()方法,response是一個物件,可從中進行xpath解析等工作。

   parse()方法的返回值非常有趣,一般來說當我們解析工作完成後就進行持久化儲存,但是也可以再次的傳送網路請求,所以parse()方法的返回值是多種多樣的:

  • 包含解析資料的字典
  • Item物件,專案管道,用於持久化儲存,臨時儲存資料的地方
  • yield新的Request物件(新的Requests也需要指定一個回撥函式)
  • 或者是可迭代物件(物件中只包含Items或Request)

   一般來說,我們都是這麼做的,但是某些情況下你可能會發現我們需要爬取多個url並且會指定不同的回撥函式(預設start_urls列表中的url回撥函式都是parse()方法),那麼該怎麼做呢?你可以書寫一個名為start_requests()的方法,並且自己使用Request物件來傳送請求與繫結回撥函式,當有start_request()方法後,start_urls列表中的url不會被自動傳送請求:

import scrapy
from scrapy.http import Request

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'
    # allowed_domains = ['www.cnblogs.com']
    start_urls = ['http://www.cnblogs.com/']  # 具有start_request()方法,start_urls列表中的urls不會自動發起請求

    def start_requests(self):
        yield Request(url="http://www.baidu.com",callback=self.baidu)
        yield Request(url="http://www.biying.com",callback=self.biying)

    def baidu(self,response):
        print("baidu爬取完成...")

    def biying(self,response):
        print("biying爬取完成...")

    def parse(self, response):  # 失效
        print(response)

   如果你在爬蟲程式中遇到編碼問題無法正常解析response的內容,則更改編碼格式:

import sys,os
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')

Spiders自定製

   在Spiders類中,你可以進行各種各樣的自定義:

屬性/方法描述
name = "spiderName" 定義爬蟲名,scrapy會根據該值定位爬蟲程式,非空且唯一
allowed_domains = ['www.cnblogs.com'] 定義允許爬取的域名,如果OffsiteMiddleware啟動(預設就啟動), 那麼不屬於該列表的域名及其子域名都不允許爬取
start_urls = ['http://www.cnblogs.cn/'] 如果沒有指定url,就從該列表中讀取url來生成第一個請求
custom_settings 值為一個字典,定義一些配置資訊,在執行爬蟲程式時,這些配置會覆蓋專案級別的配置 所以custom_settings必須被定義成一個類屬性,由於settings會在類例項化前被載入
settings 通過self.settings['配置項的名字']可以訪問settings.py中的配置,如果自己定義了custom_settings還是以自己的為準
logger 日誌名預設為spider的名字,可通過self.settings['BOT_NAME']進行指定
start_requests() 該方法用來發起第一個Requests請求,且必須返回一個可迭代的物件。它在爬蟲程式開啟時就被Scrapy呼叫,Scrapy只呼叫它一次。 預設從start_urls裡取出每個url來生成Request(url, dont_filter=True)
closed(reason) 爬蟲程式結束時自動觸發的方法

Request請求

   傳送請求時,如何指定cookies或這請求頭呢?其實在Request物件中擁有很多引數:

引數描述
url str或者bytes型別,傳送請求的地址
callback 回撥函式,必須是一個可呼叫物件
method str型別,傳送請求的方式
header dict型別,本次請求所攜帶的請求頭
body str型別或者bytes型別,傳送的請求體
cookies dict型別,本次請求所攜帶的cookies
meta dict型別,如當前的request物件指定meta是{"name":"test"},則後面的response物件可通過response.meta.get("name")獲得該值,主要用於不同元件之間的資料傳遞
encoding str型別,編碼方式,預設為utf8
priority int型別,請求優先順序,優先順序高的先執行
dont_filter bool型別,取消過濾?預設是false,當多次請求的地址、引數均相同時,預設後面的請求將取消
errback 請求出現異常時的回撥函式

   meta是一個值得注意的地方:

import scrapy
from scrapy.http import Request

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'
    # allowed_domains = ['www.cnblogs.com']
    def start_requests(self):
        yield Request(url="https://www.cnblogs.com",meta={"name":"部落格園"},callback=self.parse,dont_filter=True)
        yield Request(url="https://www.baidu.com",meta={"name":"百度"},callback=self.parse,dont_filter=True)

    def parse(self, response):
        print(response.meta.get("name"))
        # 部落格園
        # 百度

    # meta通常傳遞跨元件資料

Response返回

   來看一下response物件中的一些基本方法/屬性:

屬性描述
url 獲取本次request請求的url地址
status 獲取本次request請求的狀態碼
body 獲取HTML響應正文,返回的是bytes格式內容,因此如果請求的是圖片,可直接拿到它進行寫入
text 獲取HTML響應正文,返回的是str格式內容
encoding 獲取本次請求的編碼格式,你也可以對本次請求的編碼格式進行設定
request 獲取傳送本次請求的request物件,如:response.request.method進行獲取本次的請求方式
meta 獲取本次request請求中傳遞的一些引數

資料解析

   在response物件中,會包含xpath()方法與css()方法。他們本身都是屬於response.selector中的方法,完整寫法與簡寫形式如下:

response.selector.css()
response.css()

response.selector.xpath()
response.xpath()

   注意這裡的xpath()方法返回的不是一個單純的List,而是selectorList:

def parse(self, response):
	print(response.xpath("//title"))

# [<Selector xpath='//title' data='<title>部落格園 - 開發者的網上家園</title>'>]

   下面是一些xpath返回列表的常用方法:

  

方法描述
extract() 從返回的selector列表中拿到全部的元素的xpath選取內容
extract_first() 從返回的selector列表中拿到第一個元素的xpath選取內容

   如果是css語法進行選擇,則更多的是在選擇器中拿到想要的東西:

選取符描述
::text 拿到文字
::attr(attrName) 獲取屬性
extract() 從返回的selector列表中拿到全部的元素的xpath選取內容
extract_first() 從返回的selector列表中拿到第一個元素的xpath選取內容

   示例如下:

 print(response.css("a::text"))
 print(response.css("a::attr(href)"))

去重規則

   去重規則的意思就是說如果一個爬蟲程式已經爬取過該URL,則其他的爬蟲程式就不要繼續爬取了。

   預設為指定去重:

import scrapy
from scrapy.http import Request

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'

    def start_requests(self):
        # dont_filter=False為開啟去重
        yield Request("http://www.cnblogs.com/",callback=self.parse,dont_filter=False)
        yield Request("http://www.cnblogs.com/",callback=self.parse,dont_filter=False)

    def parse(self, response):
        print("爬取...")
		# 只執行一次

   如果想要修改去重規則,如第一次訪問被拒絕後嘗試更換代理繼續訪問,就可以進行自定製:

DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter' # 預設的去重規則幫我們去重,去重規則在記憶體中維護了一個set,當請求成功Response後就會將URL進行記錄,如果再次爬取該URL就直接跳過
DUPEFILTER_DEBUG = False
JOBDIR = "儲存範文記錄的日誌路徑,如:/root/"  # 最終路徑為 /root/requests.seen,去重規則放檔案中

   自己寫一個類:

class MyDupeFilter:

    @classmethod
    def from_settings(cls, settings):
        return cls()

    def request_seen(self, request):
    	# 書寫去重規則,如果返回False則代表沒有重複,如果返回True則代表有重複,取消本次請求
        return False

    def open(self):  # can return deferred
        pass

    def close(self, reason):  # can return a deferred
        pass

    def log(self, request, spider):  # log that a request has been filtered
        pass

   最後記得在settings.py中修改配置項為自己的類。

headers&cookies

   在scrapy中,cookies都是預設攜帶的,就像requests模組的session一樣。

   在settings.py中可以將其幹掉。

# Disable cookies (enabled by default)
# COOKIES_ENABLED = False

   處了在傳送Request物件時指定headers,也可以在settings.py中進行,配置完成後所有的Request都會攜帶該請求頭字典:

# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
}

Scrapy-持久化

Item物件

   在資料解析paser()方法完成後,可以返回一個Item物件。

   Item物件你可以將它理解為在記憶體中臨時儲存一組資料的地方,因為每次爬取的欄位都是有限的,如歌曲名與歌手。

   所以我們可以將每一次的資料解析出的歌曲名和歌手返回給Item物件,由Item物件交給PIPE物件進行持久化儲存。

   可以這麼認為,一共分為三部分:

   image-20201231230751247

   一般來講,前兩步都很簡單,以下以爬取網易雲音樂TOP100為例:

   第一步,書寫Spider內容:

import scrapy
from ..items import WangyiMusic

class WangyimusicSpider(scrapy.Spider):
    name = 'wangyiMusic'
    # allowed_domains = ['music.163.com']
    start_urls = ['https://music.163.com/discover/toplist']

    def parse(self, response):
        message = response.xpath("//textarea[@id='song-list-pre-data']/text()").extract()[0]
        import json
        result = json.loads(message)
        for row in result:
            item = WangyiMusic()  # 例項化Item物件
            item["name"] = row.get("album").get("name")  # 解析出的歌曲名字
            item["singer"] = row.get("artists")[0].get("name")  # 解析出歌手的名字
            yield item

   第二步,書寫Item.py,新建一個類:

import scrapy

class WangyiMusic(scrapy.Item):
    name = scrapy.Field()
    singer = scrapy.Field()

   現在,當我們執行爬蟲程式,它就會將每一次迴圈到的歌曲和歌手資訊放入Item物件中做臨時儲存了。

PIPE物件

   光有臨時儲存還不夠,我們需要指定永久儲存,而PIPE則是從Item中取出臨時資料進行永久儲存的。

   當我們開啟pipelines.py後,會發現它給定了一個類:

class ProjectNamePipeline:
    def process_item(self, item, spider):
    	# spider是爬蟲物件,可通過settings拿到配置檔案,將是一個字典
    	# 如 spider.settings.get("xxx")等等...
        return item

   其實,該類可以指定很多鉤子函式:

class ProjectNamePipeline(object):
    def __init__(self,v):
    	# 正常例項化執行,一般不會走,如果走只執行一次,在美喲㐉form_crawler方法是才會走它
        self.value = v 

    @classmethod
    def from_crawler(cls, crawler):
        """
   		# 通過配置檔案進行例項化的過程,一般都是走這個方法,只執行一次
        """
        val = crawler.settings.getint('MMMM')
        return cls(val)

    def open_spider(self,spider):
        """
        # 爬蟲剛啟動時執行一次
        """
        print('start')

    def close_spider(self,spider):
        """
        # 爬蟲關閉時執行一次
        """
        print('close')


    def process_item(self, item, spider):
        # 操作並進行持久化邏輯函式
        # return item表示會被後續的pipeline繼續處理。可進行多方儲存,MySQL、Redis等地方
        
        return item  

        # 如果丟擲異常,則表示將item丟棄,
        # from scrapy.exceptions import DropItem
        # raise DropItem()

   這裡的process_item()方法和open_spider()以及close_spider()方法比較常用。

   注意,持久化儲存可以存入多個地方,如MySQL/Redis/Files中,前提是上一個類的process_item()方法必須將item物件返回。

   光看了這些還不夠,你需要在配置檔案中配置預設的持久化儲存方案類:

ITEM_PIPELINES = {
   'scrapyProject01.pipelines.FilesPipeline': 300,
   'scrapyProject01.pipelines.RedisPipeline': 200,  # 優先順序小的先進行儲存
}

   嘗試一下,將爬取到的歌手資訊和歌曲名稱存放到Redis/Fiels中:

   注意:持久化儲存對應的文字檔案的型別只可以為:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle'

from itemadapter import ItemAdapter

# 從Item中提取資料儲存到檔案 優先順序300 後
class FilesPipeline:
    def open_spider(self,spider):
        self.f = open(file="./MusicTop100.cvs",mode="a+",encoding="utf-8")

    def process_item(self, item, spider):
        name = item["name"]
        singer = item["singer"]
        self.f.write("歌曲名:%s     歌手:%s\n"%(name,singer))
        return item

    def close_spider(self,spider):
        self.f.close()

# 從Item中提取資料儲存到Redis 優先順序200 先
class RedisPipeline:
    def open_spider(self,spider):
        import redis
        self.conn = redis.Redis(host="localhost", port=6379)

    def process_item(self, item, spider):
        name = item["name"]
        singer = item["singer"]
        self.conn.lpush(singer,name)
        return item

    def close_spider(self,spider):
        self.conn.close()

圖片儲存

   如果是爬取的圖片,則資料直接處理出imgsrc屬性,交給Item,再由Item交由一個繼承於ImagesPipline的類直接儲存即可。

   依賴於pillow模組:

pip3 install pillow

   如下所示,爬取B站的封面圖,首先第一步是要確定爬取下來的圖片存放路徑:

# settings.py

# 圖片儲存的路徑
IMAGES_STORE = './BiliBiliimages'

   接下來就要書寫spider爬蟲程式:

import scrapy
from scrapy.http import Request


class BilibiliSpider(scrapy.Spider):
    name = 'bilibili'

    def start_requests(self):
        # 取消去重規則,每次爬取到的圖片都不一樣
        yield Request(url="https://manga.bilibili.com/twirp/comic.v1.Comic/GetRecommendComics", method="POST",
                      callback=self.parse, dont_filter=True)

    def parse(self, response):
        import json
        result = json.loads(response.text).get("data").get("comics")
        for img_message in result:
            img_title = img_message.get("title")
            img_src = img_message.get("vertical_cover")

            # 將圖片名字和src傳入item物件
            from ..items import BiliBiliImageItem
            item = BiliBiliImageItem()
            item["title"] = img_title
            item["src"] = img_src

            yield item

   Item十分簡單:

import scrapy

class BiliBiliImageItem(scrapy.Item):
    title = scrapy.Field()
    src = scrapy.Field()

   最後是pipelines的書寫,取出src並進行下載:

from scrapy.pipelines.images import ImagesPipeline
import scrapy

class DownloadImagesPipeline(ImagesPipeline):

    def get_media_requests(self, item, info):
        # 下載圖片
        yield scrapy.Request(url=item["src"],method="GET",meta={"filename":item["title"]})

    def file_path(self, request, response=None, info=None):
        # 設定儲存圖片的名稱
        filename = request.meta.get('filename')
        return filename + ".jpg"

    def item_completed(self, results, item, info):
        # 請求傳送後執行的函式,用於執行後續操作,如返回Item物件等
        
        """
        :returns :
        [
            (True,
                {
                    'url': 'http://i0.hdslb.com/bfs/manga-static/9351bbb71a9726af47e3abce3ce8f3cecbed5b08.jpg',
                    'path': '新世紀福音戰士.jpg', 'checksum': '95ce0e970b7198f23c4d67687bd56ba6',
                    'status': 'downloaded'
                  }
            )
        ]

        """
        
        if results[0][0] == True:
            print("下載圖片並儲存成功...")
            return item
        else:
            print("下載圖片並儲存失敗...")
            from scrapy.exceptions import DropItem
            raise DropItem("download img fail,url\n%s"%results[0][1].get("url"))

   別忘記在settings.py中指定PIPE:

ITEM_PIPELINES = {
   'scrapyProject01.pipelines.DownloadImagesPipeline': 200,
}

Scrapy-中介軟體

配置中介軟體

   settings.py中進行配置即可,優先順序越小執行越靠前:

# 爬蟲中介軟體
SPIDER_MIDDLEWARES = {
   'spider1.middlewares.Spider1SpiderMiddleware': 543,
}

# 下載中介軟體
DOWNLOADER_MIDDLEWARES = {
   'spider1.middlewares.Spider1DownloaderMiddleware': 543,
}

   如果要進行自定製,就將自定製的類按照字串的形式進行新增。

   多箇中介軟體的攔截方式同Falsk相同,並非同級返回。

   如,下載中介軟體A/B/C,在執行Aprocess_start_request()時候丟擲了錯誤,此時就執行C/B/Aprocess_spider_exception()方法。

爬蟲中介軟體

   以下是爬蟲中介軟體的鉤子函式,是Spiders和引擎的中介軟體,一般來講不會涉及到網路:

from scrapy import signals

class Spider1SpiderMiddleware:

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        # 建立spider(爬蟲物件)的時候,註冊一個訊號
        # 訊號: 當爬蟲的開啟的時候 執行 spider_opened 這個方法
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_spider_input(self, response, spider):
        # 下載完成後,執行,然後交給parse處理
        return None

    def process_spider_output(self, response, result, spider):
        """
        經歷過parse函式之後執行
        :param response: 上一次請求返回的結果
        :param result: yield的物件 包含 [item/Request] 物件的可迭代物件
        :param spider: 當前爬蟲物件
        :return: 返回Request物件 或 Item物件
        """
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
    
        """如果執行parse丟擲異常的話 會執行這個函式 預設不對異常處理交給下一個中介軟體處理"""
        pass

    def process_start_requests(self, start_requests, spider):
     
        """
        爬蟲啟動時呼叫
        :param start_requests: 包含 Request 物件的可迭代物件
        :param spider:
        :return: Request 物件
        """
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        # 生成爬蟲日誌
        spider.logger.info('Spider opened: %s' % spider.name)

下載中介軟體

   下面是下載中間的鉤子函式,下載中介軟體是Download與引擎中的中介軟體,涉及網路,因此代理等相關配置應該在下載中介軟體中進行:

class Spider1DownloaderMiddleware:


    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        # 這個方法同上,和爬蟲中介軟體一樣的功能
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        """
        # 可進行UA偽裝,user-agent
        請求需要被下載時,經過所有下載中介軟體的process_request呼叫
        spider處理完成,返回時呼叫
        :param request:
        :param spider:
        :return:
            None,繼續往下執行,去下載
            Response物件,停止process_request的執行,開始執行process_response
            Request物件,停止中介軟體的執行,將Request重新放到排程器中
            raise IgnoreRequest異常,停止process_request的執行,開始執行process_exception
        """
        return None

    def process_response(self, request, response, spider):

        """
        下載得到響應後,執行
        :param request: 請求物件
        :param response: 響應物件
        :param spider: 爬蟲物件
        :return:
            返回request物件,停止中介軟體,將Request物件重新放到排程器中
            返回response物件,轉交給其他中介軟體process_response
            raise IgnoreRequest 異常: 呼叫Request.errback
        """
        return response

    def process_exception(self, request, exception, spider):
        
        """當下載處理器(download handler)或process_request() (下載中介軟體)丟擲異常
            :return
                None: 繼續交給後續中介軟體處理異常
                Response物件: 停止後續process_exception方法
                Request物件: 停止中介軟體,request將會被重新呼叫下載
        """
        pass

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

操縱cookie

   可能有的頁面需要你手動攜帶一個cookie,比如token驗證等,此時就可以在下載中介軟體的process_request()方法中手動攜帶,

   如下所示:

    def process_request(self, request, spider):
        # 先獲取token token = ....
        request.cookies.update({"token":"xxx"})
        print(request.cookies)
        return None

代理設定

   為下載中介軟體中新增代理:

def get_proxy():
    """獲取代理的函式"""
    response = requests.get('http://134.175.188.27:5010/get/')
    data = response.json()
    return data["proxy"]

class ProxyDownloaderMiddleware(object):
    """下載中介軟體中的代理中介軟體"""
    def process_request(self, request, spider):
        request.meta['proxy'] = get_proxy()
        return None

   如果代理不可用,配置檔案中設定重試:

RETRY_ENABLED = True  # 是否開啟超時重試
RETRY_TIMES = 2       # initial response + 2 retries = 3 requests 重試次數
RETRY_HTTP_CODES =  [500, 502, 503, 504, 522, 524, 408, 429] # 重試的狀態碼
DOWNLOAD_TIMEOUT = 1  # 1秒沒有請求到資料,主動放棄

Scrapy-settings.py

基本配置

   配置檔案中的配置項:

# -*- coding: utf-8 -*-

# Scrapy settings for step8_king project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#     http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#     http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html

# 1. 爬蟲名稱
BOT_NAME = 'step8_king'
# 1.2 日誌級別,強烈建議
LOG_LEVEL = "ERROR"

# 2. 爬蟲應用路徑
SPIDER_MODULES = ['step8_king.spiders']
NEWSPIDER_MODULE = 'step8_king.spiders'

# Crawl responsibly by identifying yourself (and your website) on the user-agent
# 3. 客戶端 user-agent請求頭
# USER_AGENT = 'step8_king (+http://www.yourdomain.com)'

# Obey robots.txt rules
# 4. 禁止爬蟲配置
# ROBOTSTXT_OBEY = False

# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 5. 併發請求數
# CONCURRENT_REQUESTS = 4

# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# 6. 延遲下載秒數
# DOWNLOAD_DELAY = 2


# The download delay setting will honor only one of:
# 7. 單域名訪問併發數,並且延遲下次秒數也應用在每個域名
# CONCURRENT_REQUESTS_PER_DOMAIN = 2
# 單IP訪問併發數,如果有值則忽略:CONCURRENT_REQUESTS_PER_DOMAIN,並且延遲下次秒數也應用在每個IP
# CONCURRENT_REQUESTS_PER_IP = 3


# Disable cookies (enabled by default)
# 8. 是否支援cookie,cookiejar進行操作cookie
# COOKIES_ENABLED = True
# COOKIES_DEBUG = True


# Disable Telnet Console (enabled by default)
# 9. Telnet用於檢視當前爬蟲的資訊,操作爬蟲等...
#    使用telnet ip port ,然後通過命令操作
# TELNETCONSOLE_ENABLED = True
# TELNETCONSOLE_HOST = '127.0.0.1'
# TELNETCONSOLE_PORT = [6023,]
# 命令est()


# 10. 預設請求頭(優先順序低於request物件中的請求頭)
# Override the default request headers:
# DEFAULT_REQUEST_HEADERS = {
#     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#     'Accept-Language': 'en',
# }



# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
# 11. 定義pipeline處理請求 值越小優先順序越高 0-1000
# ITEM_PIPELINES = {
#    'step8_king.pipelines.JsonPipeline': 700,
#    'step8_king.pipelines.FilePipeline': 500,
# }




# 12. 自定義擴充套件,基於訊號進行呼叫
# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
# EXTENSIONS = {
#     # 'step8_king.extensions.MyExtension': 500,
# }




# 13. 爬蟲允許的最大深度,可以通過meta檢視當前深度;0表示無深度
# DEPTH_LIMIT = 3



# 14. 爬取時,0表示深度優先Lifo(預設);1表示廣度優先FiFo



# 後進先出,深度優先
# DEPTH_PRIORITY = 0
# 基於硬碟的 DISK
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleLifoDiskQueue'
# 基於記憶體的 MEMORY
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.LifoMemoryQueue'


# 先進先出,廣度優先
# DEPTH_PRIORITY = 1
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleFifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.FifoMemoryQueue'

# 15. 排程器佇列
# SCHEDULER = 'scrapy.core.scheduler.Scheduler'  這是一個類
# from scrapy.core.scheduler import Scheduler


# 16. 訪問URL去重
# DUPEFILTER_CLASS = 'step8_king.duplication.RepeatUrl'


# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html

"""
18. 啟用快取  一般不太用
    目的用於將已經傳送的請求或相應快取下來,以便以後使用,
    
    from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
    from scrapy.extensions.httpcache import DummyPolicy
    from scrapy.extensions.httpcache import FilesystemCacheStorage
"""
# 是否啟用快取策略
# HTTPCACHE_ENABLED = True

# 快取策略:所有請求均快取,下次在請求直接訪問原來的快取即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 快取策略:根據Http響應頭:Cache-Control、Last-Modified 等進行快取的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"

# 快取超時時間
# HTTPCACHE_EXPIRATION_SECS = 0

# 快取儲存路徑
# HTTPCACHE_DIR = 'httpcache'

# 快取忽略的Http狀態碼
# HTTPCACHE_IGNORE_HTTP_CODES = []

# 快取儲存的外掛
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'


爬蟲速率

   如果請求過於頻繁,可能會遭遇封禁,因此可以設定爬蟲的頻次:

"""
17. 自動限速演算法
    from scrapy.contrib.throttle import AutoThrottle
    自動限速設定
    1. 獲取最小延遲 DOWNLOAD_DELAY
    2. 獲取最大延遲 AUTOTHROTTLE_MAX_DELAY
    3. 設定初始下載延遲 AUTOTHROTTLE_START_DELAY
    4. 當請求下載完成後,獲取其"連線"時間 latency,即:請求連線到接受到響應頭之間的時間
    5. 用於計算的... AUTOTHROTTLE_TARGET_CONCURRENCY
    target_delay = latency / self.target_concurrency
    new_delay = (slot.delay + target_delay) / 2.0 # 表示上一次的延遲時間
    new_delay = max(target_delay, new_delay)
    new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
    slot.delay = new_delay
"""



# 開始自動限速
# AUTOTHROTTLE_ENABLED = True
# The initial download delay
# 初始下載延遲
# AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
# 最大下載延遲
# AUTOTHROTTLE_MAX_DELAY = 10
# The average number of requests Scrapy should be sending in parallel to each remote server
# 平均每秒併發數
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

# Enable showing throttling stats for every response received:
# 是否顯示
# AUTOTHROTTLE_DEBUG = True

# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings

"""

代理相關

   預設代理,一般放在環境變數中,即os.environ裡,用的時候取就好了:

# 一般不用,取代理費事

from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware

方式一:使用預設
os.environ = 
{
    http_proxy:http://root:woshiniba@192.168.11.11:9999/
    https_proxy:http://192.168.11.11:9999/
}

   自定義代理配置:

    def to_bytes(text, encoding=None, errors='strict'):
        if isinstance(text, bytes):
            return text
        if not isinstance(text, six.string_types):
            raise TypeError('to_bytes must receive a unicode, str or bytes '
                            'object, got %s' % type(text).__name__)
        if encoding is None:
            encoding = 'utf-8'
        return text.encode(encoding, errors)
        
    class ProxyMiddleware(object):
        def process_request(self, request, spider):
            # 這裡是寫死的代理,可以通過一個函式獲取
            PROXIES = [
                {'ip_port': '111.11.228.75:80', 'user_pass': ''},
                {'ip_port': '120.198.243.22:80', 'user_pass': ''},
                {'ip_port': '111.8.60.9:8123', 'user_pass': ''},
                {'ip_port': '101.71.27.120:80', 'user_pass': ''},
                {'ip_port': '122.96.59.104:80', 'user_pass': ''},
                {'ip_port': '122.224.249.122:8088', 'user_pass': ''},
            ]
            # 隨機取出一組代理
            proxy = random.choice(PROXIES)
            if proxy['user_pass'] is not None:
                request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
                encoded_user_pass = base64.encodestring(to_bytes(proxy['user_pass']))
                request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)
                print "**************ProxyMiddleware have pass************" + proxy['ip_port']
            else:
                print "**************ProxyMiddleware no pass************" + proxy['ip_port']
                request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
    
    # 在配置檔案中註冊中介軟體
    DOWNLOADER_MIDDLEWARES = {
       'step8_king.middlewares.ProxyMiddleware': 500,
    }

Scrapy高階

全站爬取

   上面的Scrapy都是基於spiders這個類,而全站爬取則是基於CrawlSpider這個類。

   全站爬取的意思就是說將該網站所有的資料爬取下來,如下例項,爬取蝦米音樂的目前所有動漫遊戲相關曲目,共十條:

from prettytable import PrettyTable
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

table = PrettyTable(['歌曲名稱', "專輯"])


# 全站爬取
class XiamiSpider(CrawlSpider):
    name = 'xiami'
    start_urls = [
        'https://www.xiami.com/list?page=1&query=%7B%22genreType%22%3A2%2C%22genreId%22%3A%223344%22%7D&scene=genre&type=song']

    # 連結提取器:根據指定規則(allow="正則")進行指定連結的提取
    link = LinkExtractor(allow=r'page=\d+')

    rules = (
        # 規則解析器:將連結提取器提取到的連結進行指定規則(callback)的解析操作
        # 自動傳送請求
        # 如果 follow 為True,則可以將連結提取器 繼續作用到 連線提取器提取到的連結 所對應的頁面中
        Rule(link, callback="parse", follow=True),  # 自動匹配 a標籤,page自動翻頁,自動執行回撥
    )

    def parse(self, response, *args, **kwargs):
        music_name_list = response.xpath(
            "//*[@id='app']//div[@class='table-container'][1]//tr[@class]//div[@class='song-name em']//text()").extract()
        music_album_list = response.xpath(
            "//*[@id='app']//div[@class='table-container'][1]//tr[@class]//div[@class='album']//text()").extract()

        for index in range(len(music_name_list)):
            table.add_row([music_name_list[index].strip(), music_album_list[index].strip()])

    def close(spider, reason):
        print(table)

分散式爬蟲

   分散式爬蟲就是在一臺遠端的機器上儲存爬取的地址,以及爬取的結果。

   由多臺計算機在遠端計算機上拿到爬取地址進行爬取,並且將爬取結果儲存到遠端計算機上。

   單純的Scrapy框架不能實現分散式,所以要用到scrapy-redis這個第三方模組實現:

    - 如何實現分散式?
    - 安裝一個scrapy-redis的元件
    - 原生的scarapy是不可以實現分散式爬蟲,必須要讓scrapy結合著scrapy-redis元件一起實現分散式爬蟲。
    - 為什麼原生的scrapy不可以實現分散式?
        - 排程器不可以被分散式機群共享
        - 管道不可以被分散式機群共享
    - scrapy-redis元件作用:
        - 可以給原生的scrapy框架提供可以被共享的管道和排程器
    - 實現流程
        - 建立一個工程
        - 建立一個基於CrawlSpider的爬蟲檔案
        - 修改當前的爬蟲檔案:
            - 導包:from scrapy_redis.spiders import RedisCrawlSpider
            - 將start_urls和allowed_domains進行註釋
            - 新增一個新屬性:redis_key = 'sun' 可以被共享的排程器佇列的名稱
            - 編寫資料解析相關的操作
            - 將當前爬蟲類的父類修改成RedisCrawlSpider
        - 修改配置檔案settings
            - 指定使用可以被共享的管道:
                ITEM_PIPELINES = {
                    'scrapy_redis.pipelines.RedisPipeline': 400
                }
            - 指定排程器:
                # 增加了一個去重容器類的配置, 作用使用Redis的set集合來儲存請求的指紋資料(去重規則), 從而實現請求去重的持久化
                DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
                # 使用scrapy-redis元件自己的排程器
                SCHEDULER = "scrapy_redis.scheduler.Scheduler"
                # 配置排程器是否要持久化, 也就是當爬蟲結束了, 要不要清空Redis中請求佇列和去重指紋的set。如果是True, 就表示要持久化儲存, 就不清空資料, 否則清空資料
                SCHEDULER_PERSIST = True
            - 指定redis伺服器:

        - redis相關操作配置:
            - 配置redis的配置檔案:
                - linux或者mac:redis.conf
                - windows:redis.windows.conf
                - 代開配置檔案修改:
                    - 將bind 127.0.0.1進行刪除
                    - 關閉保護模式:protected-mode yes改為no
            - 結合著配置檔案開啟redis服務
                - redis-server 配置檔案
            - 啟動客戶端:
                - redis-cli
        - 執行工程:
            - scrapy runspider xxx.py
        - 向排程器的佇列中放入一個起始的url:
            - 排程器的佇列在redis的客戶端中
                - lpush xxx www.xxx.com
        - 爬取到的資料儲存在了redis的proName:items這個資料結構中

   首先第一步:

pip install scrapy-redis

   程式碼如下:

# 爬蟲檔案
# -*- coding: utf-8 -*-

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from fbsPro.items import FbsproItem
from scrapy_redis.spiders import RedisCrawlSpider

class FbsSpider(RedisCrawlSpider):
    name = 'fbs'
    redis_key = 'sun'  # 從sun這個佇列中取出url

    rules = (
        Rule(LinkExtractor(allow=r'type=4&page=\d+'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
        for tr in tr_list:
            new_num = tr.xpath('./td[1]/text()').extract_first()
            new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()

            item = FbsproItem()
            item['title'] = new_title
            item['new_num'] = new_num

            yield item

   然後是items.py

import scrapy

class FbsproItem(scrapy.Item):
    title = scrapy.Field()
    new_num = scrapy.Field()

   需要在settings.py中做配置:

#指定管道
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 400
}

#指定排程器
# 增加了一個去重容器類的配置, 作用使用Redis的set集合來儲存請求的指紋資料, 從而實現請求去重的持久化
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis元件自己的排程器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 配置排程器是否要持久化, 也就是當爬蟲結束了, 要不要清空Redis中請求佇列和去重指紋的set。如果是True, 就表示要持久化儲存, 就不清空資料, 否則清空資料
SCHEDULER_PERSIST = True

#指定redis
REDIS_HOST = '127.0.0.1' #redis遠端伺服器的ip(修改)
REDIS_PORT = 6379

增量式爬蟲

   增量式爬蟲也非常簡單,維護一個set(可以是redis),將每次爬取的url進行檢測。

   如果該url未被爬取,則爬取完成後將url放入set中,下次啟動爬蟲程式時就會檢測,如果urlset中,就跳過本次爬取。

   增量式就是在原本的資料基礎上做增加。

反反扒策略

代理

   如果一個網站對IP進行了頻率限制,可以在傳送請求時指定一個代理,由代理幫助你傳送本次請求,且將返回結果交給你。

   而使用代理又有以下三個名詞:

   透明:被請求伺服器明確知道本次請求是由代理髮起,並且也知道真實請求的IP地址

   匿名:被請求伺服器明確知道本次請求是由代理髮起,但是不知道真實請求的IP地址

   高匿:被請求伺服器不知道本次請求是由代理髮起,並且也不知道真實請求的IP地址

   常用的代理相關網站:

- 快代理
- 西祠代理
- www.goubanjia.com

   image-20201224215650701

   代理的型別一般有HTTP代理和HTTPS代理,我們在使用requests模組傳送請求時可指定代理:

   如下所示:

from requests import Session

headers = {
    "user-agent":"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}

proxies = {
    "http": "36.230.165.45:8088",
    }

session = Session()
response = session.get("https://www.baidu.com/s?wd=ip",headers=headers,proxies=proxies)
print(response.status_code)
with open(file="./testDataDocument.html",mode="w",encoding="utf-8") as f:
    f.write(response.text)

驗證碼

   自動登陸時碰到驗證碼認證,則可以藉助第三方工具超級鷹,新使用者會獲取1000題分。

   超級鷹官網

headers

   一般來說,發起請求時我們要觀察NETWORK的變化,除了User-Agent之外,如果有以下的請求頭也可以對其新增上:

   Host

   Referer

   token

   尤其注意token,他的命名可能不太一樣如xsrf-token,或者jwt等等字樣的都應該帶上。

   這是為使用者登入之後儲存狀態得到的隨機字串。

   一般都會在登入成功後通過cookie進行返回,可以先從cookieget獲取,再新增到請求頭中。

相關文章