Python 爬蟲系列

爬蟲簡介

網路爬蟲

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

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

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

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

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

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

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

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

爬蟲分類

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

   通用爬蟲

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

   聚焦式爬蟲

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

   增量式爬蟲

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

robots協議

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

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

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

requests模組

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

pip install requests

發送請求

   支援所有的請求方式:

import requests

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

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

   當請求發送成功後,會返回一個response對象。

get請求

   基本的get請求參數如下:

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

   演示如下:

import requests

res = requests.get(
    url="//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="//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='//127.0.0.1:8000/test/')
    # requests.request(method='post', url='//127.0.0.1:8000/test/')
    pass


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

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

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

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

    # 錯誤
    # requests.request(method='get',
    # url='//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='//127.0.0.1:8000/test/',
    # data={'k1': 'v1', 'k2': '水電費'})

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

    # requests.request(method='POST',
    # url='//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='//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='//127.0.0.1:8000/test/',
                     json={'k1': 'v1', 'k2': '水電費'})


def param_headers():
    # 發送請求頭到伺服器端
    requests.request(method='POST',
                     url='//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='//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='//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='//127.0.0.1:8000/test/',
    # files=file_dict)

    # 發送文件,訂製文件名
    # file_dict = {
    # 'f1': ('test.txt', open('readme', 'rb'))
    # }
    # requests.request(method='POST',
    # url='//127.0.0.1:8000/test/',
    # files=file_dict)

    # 發送文件,訂製文件名
    # file_dict = {
    # 'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf")
    # }
    # requests.request(method='POST',
    # url='//127.0.0.1:8000/test/',
    # files=file_dict)

    # 發送文件,訂製文件名
    # file_dict = {
    #     'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf", 'application/text', {'k1': '0'})
    # }
    # requests.request(method='POST',
    #                  url='//127.0.0.1:8000/test/',
    #                  files=file_dict)

    pass


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

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

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

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


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


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


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

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

    # ret = requests.get("//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("//www.google.com", proxies=proxyDict, auth=auth)
    # print(r.text)

    pass


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

    # from contextlib import closing
    # with closing(requests.get('//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="//dig.chouti.com/help/service")

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

    i3 = session.post(
        url="//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('//www.autohome.com/news')
response.encoding='gbk'
print(response.text)

   下載文件

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

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

import requests

response=requests.get('//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("//127.0.0.1:5000/index")
print(response.json())

   歷史記錄

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

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

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

import requests
r = requests.get('//127.0.0.1:5000/index',allow_redirects=False)  # 禁止重定向
print(r.status_code)  # 302
print(r.url)  # //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"//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 = [
    "//127.0.0.1:5000/index",
    "//127.0.0.1:5000/news",
    "//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 = [
    "//127.0.0.1:5000/index",
    "//127.0.0.1:5000/news",
    "//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 = [
    "//127.0.0.1:5000/index",
    "//127.0.0.1:5000/news",
    "//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("//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="//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('//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('//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('//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('//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('//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('//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('//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('//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("//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("//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文件官網://www.lfd.uci.edu/~gohlke/pythonlibs
pip3 install lxml
pip3 install pyopenssl

   下載並安裝pywin32

pip3 install pywin32

   下載並安裝twistedwheel文件,CP對應Python版本:

# 下載whell文件://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 //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 = ['//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 = ['//www.cnblogs.com/']  # 具有start_request()方法,start_urls列表中的urls不會自動發起請求

    def start_requests(self):
        yield Request(url="//www.baidu.com",callback=self.baidu)
        yield Request(url="//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 = [‘//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="//www.cnblogs.com",meta={"name":"部落格園"},callback=self.parse,dont_filter=True)
        yield Request(url="//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("//www.cnblogs.com/",callback=self.parse,dont_filter=False)
        yield Request("//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 = ['//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="//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': '//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('//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:
#
#     //doc.scrapy.org/en/latest/topics/settings.html
#     //scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#     //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 (+//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 //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 //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 //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 //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 //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://root:[email protected]:9999/
    https_proxy://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("//%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("//%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 = [
        '//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("//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獲取,再添加到請求頭中。

Tags: