風變編程筆記(二)-Python爬蟲精進


關鍵字: requests  BeautifulSoup  html.parser  str.strip()  quote()  json  replace()  openpyxl  cookies  session  filter()  tkinter  selenium  schedule  gevent  scrapy

第0關  認識爬蟲

1. 瀏覽器的工作原理

首先,我們在瀏覽器輸入網址(也可以叫URL),然后瀏覽器向服務器傳達了我們想訪問某個網頁的需求,這個過程就叫做【請求】
緊接着,服務器把你想要的網站數據發送給瀏覽器,這個過程叫做【響應】
所以瀏覽器和服務器之間,先請求,后響應,有這么一層關系
當服務器把數據響應給瀏覽器之后,瀏覽器並不會直接把數據丟給你,因為這些數據是用計算機的語言寫的,瀏覽器還要把這些數據翻譯成你能看得懂的樣子,這是瀏覽器做的另一項工作【解析數據】
緊接着,我們就可以在拿到的數據中,挑選出對我們有用的數據,這是【提取數據】
最后,我們把這些有用的數據保存好,這是【存儲數據】
以上,就是瀏覽器的工作原理,是人、瀏覽器、服務器三者之間的交流過程
2. 爬蟲的工作原理
爬蟲可以幫我們代勞這個過程的其中幾步↓

當你決定去某個網頁后,首先,爬蟲可以模擬瀏覽器去向服務器發出請求;其次,等服務器響應后,爬蟲程序還可以代替瀏覽器幫我們解析數據;接着,爬蟲可以根據我們設定的規則批量提取相關數據,而不需要我們去手動提取;最后,爬蟲可以批量地把數據存儲到本地
簡化上圖,就是爬蟲的工作原理了↓

其實,還可以把最開始的【請求——響應】封裝為一個步驟——獲取數據。由此,我們得出,爬蟲的工作分為四步↓

第0步:獲取數據。爬蟲程序會根據我們提供的網址,向服務器發起請求,然后返回數據
第1步:解析數據。爬蟲程序會把服務器返回的數據解析成我們能讀懂的格式
第2步:提取數據。爬蟲程序再從中提取出我們需要的數據
第3步:儲存數據。爬蟲程序把這些有用的數據保存起來,便於你日后的使用和分析
3. requests
Mac安裝方法:打開終端軟件(terminal),輸入pip3 install requests,然后點擊enter即可
requests庫可以幫我們下載網頁源代碼、文本、圖片,甚至是音頻。其實,“下載”本質上是向服務器發送請求並得到響應
4. requests.get()

import requests
# 引入requests庫
res = requests.get('URL')
# requests.get()方法向服務器發送了一個請求,括號里的參數是你需要的數據所在的網址,然后服務器對請求作出了響應
# 服務器返回的結果是個Response對象,賦值到變量res上

如果用圖片展示,那就是這樣的↓

5. Response對象的常用屬性

import requests
res = requests.get('https://www.cnblogs.com/oyster25/p/12334132.html')
print(type(res))  # 打印變量res的數據類型
# 》》<class 'requests.models.Response'>

打印結果顯示,res是一個對象,屬於requests.models.Response

response.status_code

import requests
res = requests.get('https://www.cnblogs.com/oyster25/p/12334132.html')
print(res.status_code)  # 打印變量res的響應狀態碼,以檢查請求是否成功
# 》》200

status_code用來檢查requests請求是否得到了成功的響應,終端結果顯示了200,這個數字代表服務器同意了請求,並返回了數據給我們
下面表格供參考不同的狀態碼代表什么↓

 
response.content
response.content能把Response對象的內容以二進制數據的形式返回,適用於圖片、音頻、視頻的下載

import requests

res = requests.get('https://res.pandateacher.com/2018-12-18-10-43-07.png')  # 發出請求,並把返回的結果放在變量res中
pic = res.content  # 把Reponse對象的內容以二進制數據的形式返回
photo = open('ppt.jpg', 'wb')  # 新建了一個文件ppt.jpg,圖片內容需要以二進制wb讀寫
photo.write(pic)  # 獲取pic的二進制內容
photo.close()  # 關閉文件

response.text
response.text這個屬性可以把Response對象的內容以字符串的形式返回,適用於文字、網頁源代碼的下載
下載《HTTP狀態響應碼》全部內容↓

import requests

res = requests.get(
    'https://localprod.pandateacher.com/python-manuscript/crawler-html/exercise/HTTP%E5%93%8D%E5%BA%94%E7%8A%B6%E6%80%81%E7%A0%81.md')
article = res.text  # 把Response對象的內容以字符串的形式返回
with open('HTTP狀態響應碼.txt', 'w', encoding='utf-8') as f:
    f.write(article)

response.encoding
response.encoding能定義Response對象的編碼,當遇上文本的亂碼問題,會考慮用res.encoding把編碼定義成和目標數據一致的類型

import requests

res = requests.get(
    'https://localprod.pandateacher.com/python-manuscript/crawler-html/sanguo.md')
# 定義Response對象的編碼為utf-8
res.encoding = 'utf-8'  # 如果寫成res.encoding='gbk'就會打印出亂碼
novel = res.text
print(novel[:800])  # 考慮到整篇文章太長,只輸出800字看看就好


6. 爬蟲倫理
通常情況下,服務器不太會在意小爬蟲,但是,服務器會拒絕頻率很高的大型爬蟲和惡意爬蟲,因為這會給服務器帶來極大的壓力或傷害
不過,服務器在通常情況下,對搜索引擎是歡迎的態度(谷歌和百度的核心技術之一就是爬蟲)。當然,這是有條件的,而這些條件會寫在Robots協議
Robots協議是互聯網爬蟲的一項公認的道德規范,它的全稱是“網絡爬蟲排除標准”(Robots exclusion protocol),這個協議用來告訴爬蟲,哪些頁面是可以抓取的,哪些不可以
如何查看網站的robots協議,在網站的域名后加上/robots.txt就可以了
下面截取了一部分淘寶的robots協議,可以看到淘寶對百度和谷歌這兩個爬蟲的訪問規定,以及對其它爬蟲的規定

協議里最常出現的英文是Allow和Disallow,Allow代表可以被訪問,Disallow代表禁止被訪問。而且有趣的是,淘寶限制了百度對產品頁面的爬蟲,卻允許谷歌訪問
所以,當你在百度搜索“淘寶網”時,會看到下圖的這兩行小字

因為百度很好地遵守了淘寶網的robots.txt協議,自然,你在百度中也查不到淘寶網的具體商品信息了

第1關  HTML基礎

HTML(Hyper Text Markup Language)是用來描述網頁的一種語言,也叫超文本標記語言
HTML文檔就是前端工程師設計網頁時使用的語言,瀏覽器會根據HTML文檔的描述,解析出它所描述的網頁
1. 查看網頁的HTML代碼
在網頁任意地方點擊鼠標右鍵,然后點擊“顯示網頁源代碼”
這樣查看的好處是,整個網頁的源代碼都完整地呈現在面前。壞處是,在大部分情況下,它都會經過壓縮,導致結構不夠清晰,不太容易懂每行代碼的含義。而且,源代碼和網頁分開在兩個頁面展示
所以更多時候,會用這樣一種方法:在網頁的空白處點擊右鍵,然后選擇“檢查”

接着,會看到一個新的界面——開發者工具欄

上圖中標亮的部分就是網頁的HTML代碼
將鼠標放在HTML源代碼上,會發現,左邊網頁上有一些內容會被標亮。這其實就是這行代碼所描述的網頁內容,它們一左一右,相互對應

2. HTML的層級
再回到剛才網頁,仔細看開發者工具欄


可以看到,HTML源代碼中有一些小三角形,每一個三角形都可以展開或合上。尖角向下代表展開,向右代表合上了,這就是HTML的層級關系
3. 標簽和元素
夾在尖括號<>中間的字母叫做【標簽】

也有標簽是形單影只地出現,比如<meta charset="utf-8">(定義網頁編碼格式為 utf-8)
開始標簽+結束標簽+中間的所有內容,它們在一起就組成了【元素】

下面的表格列出了幾個常見元素

注意一下:HTML標簽是可以嵌套標簽的,而且可以多層嵌套
4. 網頁頭和網頁體
HTML文檔的基本是由【網頁頭】和【網頁體】組成的

HTML文檔的最外層標簽一定是<html>,里面嵌套着<head>元素與<body>元素。<head>元素代表了【網頁頭】,<body>元素代表了【網頁體】,這是最基本的網頁結構
HTML文檔和網頁的內容一定是一一對應的。只是,【網頁頭】的內容不會被直接呈現在瀏覽器里的網頁正文中,而【網頁體】的內容是會直接顯示在網頁正文中的
<head>元素,【網頁頭】里面一般會有的內容↓

<head>
    <meta charset="utf-8"> 
    <title>我是網頁的名字</title>
</head>

<meta charset="utf-8">定義了HTML文檔的字符編碼
<title>元素用來定義網頁的標題,這個標題就是顯示在瀏覽器的標簽頁中的內容

【網頁頭】中的編碼是沒辦法在網頁中直接被看到的,標簽頁的內容也不屬於網頁的正文
而<body>元素中,即【網頁體】,就是那些能看到的顯示在網頁中的內容了

<html>

<head>
    <meta charset="utf-8">
    <title>豆瓣2019年度電影榜單1.0</title>
</head>

<body>
    <h1>豆瓣2019年度電影榜單</h1>
    <h3>評分最高華語電影</h3>
    <h2>《少年的你》</h2>
    <p>根據玖月晞小說改編的電影,由曾國祥執導,周冬雨、易烊千璽領銜主演。該片講述在高考前夕,被一場校園意外改變命運的兩個少年,如何守護彼此成為想成為的成年人的故事</p>
</body>

</html>

網頁體中依次有四個內容:<h1>元素代表一級標題,對應網頁中的“豆瓣2019年度電影榜單”;<h3>元素代表三級標題,對應網頁中的“評分最高華語電影”;<h2>元素代表二級標題,對應網頁中的“《少年的你》”;然后是<p>元素,對應網頁中“根據玖月晞小說......的故事”這一整段文本
5. 屬性
HTML標簽可以通過設置【屬性】來為HTML元素描述更多的信息

<html>

<head>
    <meta charset="utf-8">
    <title>豆瓣2019年度電影榜單2.0</title>
    <style>
        /*以下是.film的具體樣式規定*/
        .film {
            float: left; /*控制元素浮動*/
            margin: 5px; /*外邊距為5像素*/
            padding: 15px; /*內邊距為15像素*/
            width: 350px; /*寬度為350像素*/
            height: 240px; /*高度為240像素*/
            border: 3px solid #20b2aa; /*邊框為3像素*/
        }
    </style>
</head>

<body>
    <h1 style="color:#20b2aa;">豆瓣2019年度電影榜單</h1>
    <h3>評分最高華語電影</h3>
    <div class="film">
        <h2><a href="https://movie.douban.com/subject/30166972/" target="_blank">《少年的你》</a></h2>
        <p>根據玖月晞小說改編的電影,由曾國祥執導,周冬雨、易烊千璽領銜主演。該片講述在高考前夕,被一場校園意外改變命運的兩個少年,如何守護彼此成為想成為的成年人的故事</p>
    </div>
    <div class="film">
        <h2><a href="https://movie.douban.com/subject/27119586/" target="_blank">《誰先愛上他的》</a></h2>
        <p>由徐譽庭、許智彥聯合執導,邱澤、謝盈萱、陳如山、黃聖球主演的劇情片。該片講述了劉三蓮為了奪回丈夫宋正遠的保險理賠金,與丈夫的同性情人阿傑對抗的故事</p>
    </div>
    <div class="film">
        <h2><a href="https://movie.douban.com/subject/27191431/" target="_blank">《過春天》</a></h2>
        <p>由白雪執導,田壯壯監制的劇情電影,由黃堯、孫陽、湯加文、倪虹潔主演。該片講述了十六歲少女佩佩為完成和閨蜜一起去日本看雪的約定,從而冒險走上水客道路的獨特經歷</p>
    </div>
</body>

</html>

<h1>元素添加了一個style屬性,屬性中的內容規定了這行文字的顏色。style屬性可以用來定義網頁文本的樣式,比如字體大小、顏色、間距、對齊方式等等
電影名稱增加了鏈接,點擊可以打開電影的詳情頁面。鏈接一般由<a>標簽定義,href屬性用於規定指向頁面的URL
網頁頭中多了一個很長的<style>元素,/*控制元素浮動*/是對代碼的注釋
<style>元素的內容,是一段對網頁布局的描述,在大括號內部寫的就是一條條的樣式規定

其實.對應class,所以.film這段內容對應的是網頁體塊級元素<div>中的屬性class="film"
在HTML中,class屬性可以被多次利用,<div class="film"> 在代碼中出現了三次,與此對應,網頁中也有三個一樣的塊
網頁頭的<style>元素中定義了.film的樣式,因此,凡是class="film"的元素都會繼承它的樣式
id屬性和class屬性的用法類似,給元素定義id和class的目的都是為了查找、定位元素,或者為元素設置樣式
但id屬性用於標識唯一的元素,而class用於標識一系列的元素。id就像是學生的學生證號碼,每個人都是唯一的;而學生們可以屬於同一個班級,班級就像class

最常用的幾個HTML屬性↓

6. 讀懂HTML
以【這個書苑不太冷5.0】為例

網頁體有三大部分,<div id="header">,<div id="article">,和<div id="footer">,對應的是網頁布局中的三部分↓

點開HTML的<div id="article">元素

包含了兩個<div>元素,<div id="nav">和<div id="main">分別對應着網頁中間的左邊欄和正文部分

在<div id="main">中,又包含了三個<div>元素,它們都有同樣的屬性:<class="books">,每個<div>元素分別介紹了一本書的內容
點擊導航欄的“科幻小說”、“人文讀物”、“技術參考”會分別跳轉到正文的對應部分,這是通過超鏈接和錨點實現的

<div id="article">
    <div id="nav">
        <a href="#type1" class="catlog">科幻小說</a><br>
        <a href="#type2" class="catlog">人文讀物</a><br>
        <a href="#type3" class="catlog">技術參考</a><br>
    </div>
    <div id="main">
        <div class="books">...
        </div>
        
        <div class="books">
            <h2><a name="type2">人文讀物</a></h2>
            <a href="https://book.douban.com/subject/26943161/" class="title">《未來簡史》</a>
            <p class="info">未來,人類將面臨着三大問題:生物本身就是算法,生命是不斷處理數據的過程;意識與智能的分離;擁有大數據積累的外部環境將比我們自己更了解自己。如何看待這三大問題,以及如何采取應對措施,將直接影響着人類未來的發展。</p> 
            <img class="img" src="./spider-men5.0_files/s29287103.jpg">
            <br/>
            <br/>
            <hr size="1">
        </div>
        
        <div class="books">...
        </div>
    </div>
</div>

<div id="nav">中的超鏈接<a href="#type2">以每個<div class="books">中name屬性<a name="type2">為標識,設置了跳轉到這個標題的錨點
<img>標簽添加了書的封面圖片
7. 修改網頁
打開【這個書苑不太冷5.0】,能看到開發者工具的左上角,有一個圖標↓

點擊它,然后再把鼠標放在網頁中,會發現和點擊源代碼的情景恰恰相反,當鼠標放在網頁上,右邊代碼區中描述它的代碼會被標亮出來

可以在網頁的開發者工具這里修改HTML文件,把鼠標點上去,雙擊,就和修改word文檔一樣↓

改完之后,按下enter確認,網頁就變成了剛才修改后的樣子
當然,這樣的修改只是在本地的修改,而服務器上的源文件是修改不了的,但可以使用這個方法,在開發者工具這里,調試HTML代碼
8. 通過Python將網頁下載到本地

import requests

with open('spider5.html', 'w', encoding='utf-8') as f:
    # 將下載的網頁源代碼寫入文件
    f.write(requests.get(
        'https://localprod.pandateacher.com/python-manuscript/crawler-html/spider-men5.0.html').text)

第2關  BeautifulSoup

本關學習目標:使用BeautifulSoup解析和提取網頁中的數據
我們平時使用瀏覽器上網,瀏覽器會把服務器返回來的HTML源代碼翻譯為我們能看懂的樣子,之后我們才能在網頁上做各種操作
而在爬蟲中,也要使用能讀懂html的工具,才能提取到想要的數據,這就是【解析數據】
【提取數據】是指把需要的數據從眾多數據中挑選出來
由於BeautifulSoup不是Python標准庫,需要單獨安裝它,在終端輸入pip3 install BeautifulSoup4
1. 解析數據

在括號中,要輸入兩個參數,第0個參數是要被解析的文本,注意了,它必須必須必須是字符串
括號中的第1個參數用來標識解析器,用的是一個Python內置庫:html.parser(它不是唯一的解析器,但是比較簡單的)

import requests
from bs4 import BeautifulSoup  # 引入BS庫

res = requests.get(
    'https://localprod.pandateacher.com/python-manuscript/crawler-html/spider-men5.0.html')
soup = BeautifulSoup(res.text, 'html.parser')  # 把網頁解析為BeautifulSoup對象
print(type(soup))  # 查看soup的類型
# 》》<class 'bs4.BeautifulSoup'>
print(soup)  # 打印soup

soup的數據類型是<class 'bs4.BeautifulSoup'>,說明soup是一個BeautifulSoup對象
打印的soup,是所請求網頁的完整HTML源代碼
雖然response.text和soup打印出的內容表面上看長得一模一樣,卻有着不同的內心,它們屬於不同的類:<class 'str'> 與<class 'bs4.BeautifulSoup'>。前者是字符串,后者是已經被解析過的BeautifulSoup對象。之所以打印出來的是一樣的文本,是因為BeautifulSoup對象在直接打印它的時候會調用該對象內的str方法,所以直接打印 bs 對象顯示字符串是str的返回結果
現在完成了第1步,使用BeautifulSoup去解析數據↓

from bs4 import BeautifulSoup
soup = BeautifulSoup(字符串,'html.parser') 

2. 提取數據

find()find_all()是BeautifulSoup對象的兩個方法,它們可以匹配html的標簽和屬性,把BeautifulSoup對象里符合要求的數據都提取出來
它倆的用法基本是一樣的,區別在於,find()只提取首個滿足要求的數據,而find_all()提取出的是所有滿足要求的數據


在網頁的HTML代碼中,有三個<div>元素,用find()可以提取出首個元素,而find_all()可以提取出全部

import requests
from bs4 import BeautifulSoup

res = requests.get(
    'https://localprod.pandateacher.com/python-manuscript/crawler-html/spder-men0.0.html')
soup = BeautifulSoup(res.text, 'html.parser')
item = soup.find('div')  # 使用find()方法提取首個<div>元素,並放到變量item里
print(type(item))  # 打印item的數據類型
# 》》<class 'bs4.element.Tag'>
print(item)  # 打印item
# 》》<div>大家好,我是一個塊</div>
items = soup.find_all('div')  # 用find_all()把所有符合要求的數據提取出來,並放在變量items里
print(type(items))  # 打印items的數據類型
# 》》<class 'bs4.element.ResultSet'>
print(items)  # 打印items
# 》》[<div>大家好,我是一個塊</div>, <div>我也是一個塊</div>, <div>我還是一個塊</div>]
print(type(items[0]))  # 打印items的第一個元素的數據類型
# 》》<class 'bs4.element.Tag'>

item的數據類型顯示的是<class 'bs4.element.Tag'>,說明這是一個Tag類對象
items的數據類型顯示的是<class 'bs4.element.ResultSet'>,是一個ResultSet類的對象。其實是Tag對象以列表結構儲存了起來,可以把它當做列表來處理
items[0]的數據類型顯示的是<class 'bs4.element.Tag'>,,是Tag對象,這與find()提取出的數據類型是一樣的

以爬取【這個書苑不太冷5.0】中的三本書的書名、鏈接、和書籍介紹為例

import requests  # 調用requests庫
from bs4 import BeautifulSoup  # 調用BeautifulSoup庫
# 返回一個response對象,賦值給res
res = requests.get(
    'https://localprod.pandateacher.com/python-manuscript/crawler-html/spider-men5.0.html')

html = res.text  # 把res解析為字符串

soup = BeautifulSoup(html, 'html.parser')  # 把網頁解析為BeautifulSoup對象

items = soup.find_all(class_='books')   # 通過匹配屬性class='books'提取出想要的數據
for item in items:                      # 遍歷列表items
    kind = item.find('h2')              # 在列表中的每個元素里,匹配標簽<h2>提取出數據
    title = item.find(class_='title')   # 在列表中的每個元素里,匹配屬性class_='title'提取出數據
    brief = item.find(class_='info')    # 在列表中的每個元素里,匹配屬性class_='info'提取出數據
    print(type(kind), type(title), type(brief))  # 打印提取出的數據類型
    print(kind.text, '\n', title.text, '\n',
          title['href'], '\n', brief.text)  # 打印書籍的類型、名字、鏈接和簡介的文字

打印的kind、titile、brief的數據類型,又是<class 'bs4.element.Tag'>,用find()提取出來的數據類型和剛才一樣,還是Tag對象
然后用Tag.text提出Tag對象中的文字(Tag.get_text()也可以),用Tag['href']提取出URL
3. 對象的變化過程
從最開始用requests庫獲取數據,到用BeautifulSoup庫來解析數據,再繼續用BeautifulSoup庫提取數據,不斷經歷的是操作對象的類型轉換↓

在BeautifulSoup中,不止find()和find_all(),還有select()也可以達到相同目的
在bs的官方文檔中,find()與find_all()的方法,其實不止標簽和屬性兩種,還有這些↓

使用str.strip()去除特殊字符串,可以使打印結果整潔很多

第3關  BeautifulSoup實踐

text獲取到的是該標簽內的純文本信息,即便是在它的子標簽內,也能拿得到。但提取屬性的值,只能提取該標簽本身的
在爬豆瓣的時候遇到status_code為418的情況,解決辦法是在request.get()時加個頭
安裝fake-useragent模塊,在終端輸入pip3 install fake-useragent

import requests
from fake_useragent import UserAgent

ua = UserAgent()  # 實例化
headers = {'User-Agent': ua.random}  # 也可以是ua.ie,ua.chrome,ua.safari等
res = requests.get(url, headers=headers)

quote()實現url編碼↓

from urllib.request import quote, unquote

# quote()函數,可以把內容轉為標准的url格式,作為網址的一部分打開
print(quote('海邊的卡夫卡'))
# 》》E6%B5%B7%E8%BE%B9%E7%9A%84%E5%8D%A1%E5%A4%AB%E5%8D%A1

# unquote()函數,可以轉換回編碼前數據
print(unquote('%E6%B5%B7%E8%BE%B9%E7%9A%84%E5%8D%A1%E5%A4%AB%E5%8D%A1'))
# 》》海邊的卡夫卡

也可以↓

import urllib.parse

a = urllib.parse.quote('海邊的卡夫卡')  # 轉為url編碼
print(a)
# 》》%E6%B5%B7%E8%BE%B9%E7%9A%84%E5%8D%A1%E5%A4%AB%E5%8D%A1

b = urllib.parse.unquote('%E6%B5%B7%E8%BE%B9%E7%9A%84%E5%8D%A1%E5%A4%AB%E5%8D%A1')  # 轉為編碼前數據
print(b)
# 》》海邊的卡夫卡

第4關  json

目標是從QQ音樂獲取周傑倫的歌曲信息,也就是這個頁面https://y.qq.com/portal/search.html#page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周傑倫
但requests.get()這個網址獲取到的html里面卻沒有歌曲信息

1. Network

Network的功能是:記錄在當前頁面上發生的所有請求。由於Network記錄的是實時網絡請求,現在網頁都已經加載完成,所以不會有東西
點擊一下刷新,瀏覽器會重新訪問網絡,這樣就會有記錄↓

瀏覽器總是在向服務器,發起各式各樣的請求,當這些請求完成,它們會一起組成在Elements中看到的網頁源代碼
剛才的requests.get()只是模擬了這52個請求中的一個,第0個請求:search.html,查看它的Response↓

它就是剛剛用requests.get()獲取到的網頁源代碼,里面不包含歌曲清單
一般來說,都是這種第0個請求先啟動了,其他的請求才會關聯啟動,一點點地將網頁給填充起來
當然,也有一些網頁,直接把所有的關鍵信息都放在第0個請求里,尤其是一些比較老(或比較輕量)的網站,用requests和BeautifulSoup就能解決它們,比如豆瓣
為了成功抓取到歌曲清單,得先找到歌名藏在哪一個請求當中,再用requests庫,去模擬這個請求
 
從上往下,只看圈起來的內容的話,它有四行信息
第0行的左側,紅色的圓鈕是啟用Network監控(默認高亮打開),灰色圓圈是清空面板上的信息。右側勾選框Preserve log,它的作用是“保留請求日志”。如果不點擊這個,當發生頁面跳轉的時候,記錄就會被清空。所以,在爬取一些會發生跳轉的網頁時,會點亮它
第1行,是對請求進行分類查看。最常用的是:ALL(查看全部)/XHR(僅查看XHR)/Doc(Document,第0個請求一般在這里),有時候也會看看:Img(僅查看圖片)/Media(僅查看媒體文件)/Other(其他)。最后,JS和CSS,則是前端代碼,負責發起請求和頁面實現;Font是文字的字體;而理解WS和Manifest,需要網絡編程的知識,倘若不是專門做這個,便不需要了解

夾在第2行和第1行中間的,是一個時間軸,記錄什么時間,有哪些請求。而第2行,就是各個請求

在第3行,是個統計:有多少個請求,一共多大,花了多長時間
2. XHR
在Network中,有一類非常重要的請求叫做XHR(當把鼠標在XHR上懸停,可以看到它的完整表述是XHR and Fetch)
平時使用瀏覽器上網的時候,經常有這樣的情況:瀏覽器上方,它所訪問的網址沒變,但是網頁里卻新加了內容
典型代表:如購物網站,下滑自動加載出更多商品。在線翻譯網站,輸入中文實時變英文
這個,叫做Ajax技術,應用這種技術,好處是顯而易見的——更新網頁內容,而不用重新加載整個網頁,又省流量又省時間
如今,比較新潮的網站都在使用這種技術來實現數據傳輸。只剩下一些特別老,或是特別輕量的網站,還在用老辦法——加載新的內容,必須要跳轉一個新網址
這種技術在工作的時候,會創建一個XHR(或是Fetch)對象,然后利用XHR對象來實現,服務器和瀏覽器之間傳輸數據。在這里,XHR和Fetch並沒有本質區別,只是Fetch出現得比XHR更晚一些,所以對一些開發人員來說會更好用,但作用都是一樣的
點擊XHR按鈕,可以看到這個網頁里一共有10個XHR或Fetch,想從里面找出帶有歌單的那一個,可以從觀察名字和大小入手,於是看到了client_search_cp..,它最大,有10.9KB

點擊Preview,能在里面發現想要的信息:歌名就藏在里面(只是有點難找,需要一層一層展開:data-song-list-0-name)

這個XHR是一個字典,鍵data對應的值也是一個字典;在該字典里,鍵song對應的值也是一個字典;在該字典里,鍵list對應的值是一個列表;在該列表里,一共有20個元素;每一個元素都是一個字典;在每個字典里,鍵name的值,對應的是歌曲名
那如何把這些歌曲名拿到呢?這就需要去看看最左側的Headers↓

它被分為四個板塊,點開第0個General,會看到Requests URL就是應該去訪問的鏈接

3. json
用requests.get()這個鏈接可以得到response對象,想把response對象轉成列表/字典則需要用json
在Python語言當中,json是一種特殊的字符串,這種字符串特殊在它的寫法——它是用列表/字典的語法寫成的

a = '1,2,3,4'
# 這是字符串
b = [1, 2, 3, 4]
# 這是列表
c = '[1,2,3,4]'
# 這是字符串,但它是用json格式寫的字符串

這種特殊的寫法決定了,json能夠有組織地存儲信息

一般來說,這三條占得越多,數據的結構越清晰;占得越少,數據的結構越混沌
html通過標簽、屬性來實現分層和對應,json則是另一種組織數據的格式,長得和Python中的列表/字典非常相像。它和html一樣,常用來做網絡數據傳輸。剛剛在XHR里查看到的列表/字典,嚴格來說其實它不是列表/字典,它是json
為什么不直接寫成列表/字典,非要把它表示成字符串?因為不是所有的編程語言都能讀懂Python里的數據類型,但是所有的編程語言,都支持文本(比如在Python中,用字符串這種數據類型來表示文本)這種最朴素的數據類型
如此,json數據才能實現,跨平台,跨語言工作
而json和XHR之間的關系:XHR用於傳輸數據,它能傳輸很多種數據,json是被傳輸的一種數據格式,就是這樣而已
Requests中有一個內置的json解碼器,幫助處理json數據

import requests  # 引用requests庫

res_music = requests.get('https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.song&searchid=60997426243444153&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&n=20&w=%E5%91%A8%E6%9D%B0%E4%BC%A6&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0')  # 調用get方法,下載這個字典
json_music = res_music.json()  # 使用json()方法,將response對象,轉為列表/字典
print(type(json_music))  # 打印json_music的數據類型
# 》》<class 'dict'>

拿到前20個歌曲信息的完整代碼↓

import requests  # 引用requests庫

res_music = requests.get('https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.song&searchid=60997426243444153&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&n=20&w=%E5%91%A8%E6%9D%B0%E4%BC%A6&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0')  # 調用get方法,下載這個字典
json_music = res_music.json()  # 使用json()方法,將response對象,轉為列表/字典
list_music = json_music['data']['song']['list']  # 一層一層地取字典,獲取歌單列表
for music in list_music:  # list_music是一個列表,music是它里面的元素
    print(music['name'])  # 以name為鍵,查找歌曲名
    print('所屬專輯:'+music['album']['name'])  # 查找專輯名
    print('播放時長:'+str(music['interval'])+'')  # 查找播放時長
    print('播放鏈接:https://y.qq.com/n/yqq/song/' +
          music['mid']+'.html\n\n')  # 查找播放鏈接

在Python語言中,實現列表/字典轉json,json轉列表/字典,需要借助json模塊,官方文檔地址:https://docs.python.org/3/library/json.html

import json  # 引入json模塊

a = [1, 2, 3, 4]  # 創建一個列表a
b = json.dumps(a)  # 使用dumps()函數,將列表a轉換為json格式的字符串,賦值給b
print(b)  # 打印b
# 》》[1, 2, 3, 4]
print(type(b))  # 打印b的數據類型
# 》》<class 'str'>
c = json.loads(b)  # 使用loads()函數,將json格式的字符串b轉為列表,賦值給c
print(c)  # 打印c
# 》》[1, 2, 3, 4]
print(type(c))  # 打印c的數據類型
# 》》<class 'list'>
# 想輸出真正的中文需要指定ensure_ascii=False
print(json.dumps(['', '', '']))
# 》》["\u4f60", "\u6211", "\u5979"]
print(json.dumps(['', '', ''], ensure_ascii=False))
# 》》["你", "我", "她"]

第5關  帶參數請求數據

快速查找XHR的方法:先把Network面板清空,再點擊一下點擊加載更多,看看有沒有多出來的新XHR,多出來的那一個,就應該是要找的了

總結一下↓

1. params
上一關只爬取了周傑倫20首歌曲信息,本關目標是爬取更多的歌曲信息,但是qq音樂卻顯示:查看更多內容,請下載客戶端

看下這個頁面的鏈接:https://y.qq.com/portal/search.html#page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周傑倫
這個鏈接的前半部分是https://y.qq.com/portal/search.html,是所請求的地址,它告訴服務器,我想訪問這里;后半部分是page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周傑倫,是請求所附帶的參數,它會告訴服務器想要什么樣的數據,這參數的結構,會和字典很像,有鍵有值,鍵值用=連接;每組鍵值之間,使用&來連接;分隔這兩部分的符號是#,#和?的功能是一樣的,作用都是分隔,若把鏈接的#替換成?,訪問的效果是一樣的(注意:用?分隔的url不一定可以用#代替)
觀察一下后半部分的參數page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周傑倫,page(中文:頁面),searchid(中文:搜索id),remoteplace(中文:遠程位置),后面的t和w這倆參數雖然不知道是什么,但根據他們的值(song和周傑倫)可窺得一斑,應該是指類型和關鍵字
將網頁鏈接中的page=1改成page=2是可以訪問到下一頁的數據的

用快速查找XHR的方法,1️⃣先把Network面板清空,2️⃣再修改page值按回車鍵,3️⃣查看Network多出來的新XHR,也是個client_search_cp..,在這個請求的Preview中能找到歌曲信息
在Headers的General中能看到Request URL,但這樣一個長鏈接閱讀體驗非常之差,Network面板提供了一個更友好的查看方式
點開Headers的第3個板塊Query String Parameters,它里面的內容正是鏈接請求中所附帶的參數,Query String Parameters,它的中文翻譯是:查詢字符串參數
這個面板用類似字典的形式,呈現了各個參數的鍵值,閱讀體驗會好一些
比較網頁鏈接page=1到page=3的XHR的Query String Parameters↓

可以看到變化的是這個p參數,第1頁XHR的參數p值為1,第2、3頁XHR的參數p值則為2和3,說明在這個client_search_cp..的請求中,代表頁碼的參數是p(page的縮寫)
通過循環變更鏈接中的p參數就可以拿到更多歌曲信息,但這樣的代碼不夠優雅
事實上,requests模塊里的requests.get()提供了一個參數叫params,可以用字典的形式,把參數傳進去
所以可以把Query String Parameters里的內容,直接復制下來,封裝為一個字典,傳遞給params。只是有一點要特別注意:要給他們打引號,讓它們變字符串

import requests
# 引用requests模塊
url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp'
for x in range(5):
    params = {
        'ct': '24',
        'qqmusic_ver': '1298',
        'new_json': '1',
        'remoteplace': 'sizer.yqq.song_next',
        'searchid': '64405487069162918',
        't': '0',
        'aggr': '1',
        'cr': '1',
        'catZhida': '1',
        'lossless': '0',
        'flag_qc': '0',
        'p': str(x+1),
        'n': '20',
        'w': '周傑倫',
        'g_tk': '5381',
        'loginUin': '0',
        'hostUin': '0',
        'format': 'json',
        'inCharset': 'utf8',
        'outCharset': 'utf-8',
        'notice': '0',
        'platform': 'yqq.json',
        'needNewCode': '0'
    }
    # 將參數封裝為字典
    res_music = requests.get(url, params=params)
    # 調用get方法,下載這個字典
    json_music = res_music.json()
    # 使用json()方法,將response對象,轉為列表/字典
    list_music = json_music['data']['song']['list']
    # 一層一層地取字典,獲取歌單列表
    for music in list_music:
        # list_music是一個列表,music是它里面的元素
        print(music['name'])
        # 以name為鍵,查找歌曲名
        print('所屬專輯:'+music['album']['name'])
        # 查找專輯名
        print('播放時長:'+str(music['interval'])+'')
        # 查找播放時長
        print('播放鏈接:https://y.qq.com/n/yqq/song/'+music['mid']+'.html\n\n')
        # 查找播放鏈接

2. headers
每一個請求,都會有一個Requests Headers,稱作請求頭,Headers的第2個板塊,它里面會有一些關於該請求的基本信息,比如:這個請求是從什么設備什么瀏覽器上發出?這個請求是從哪個頁面跳轉而來?

user-agent(中文:用戶代理)會記錄電腦的信息和瀏覽器版本
origin(中文:源頭)和referer(中文:引用來源)則記錄了這個請求,最初的起源是來自哪個頁面。它們的區別是referer會比origin攜帶的信息更多些
如果想告知服務器,我們不是爬蟲是一個正常的瀏覽器,就要去修改user-agent。倘若不修改,那么這里的默認值就會是Python,會被瀏覽器認出來
而對於爬取某些特定信息,也要求注明請求的來源,即origin或referer的內容
requests模塊允許修改headers的值,只需要封裝一個字典就好了,和寫params非常相像

import requests
url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp'

headers = {
    'origin':'https://y.qq.com',
    # 請求來源,本案例中其實是不需要加這個參數的,只是為了演示
    'referer':'https://y.qq.com/n/yqq/song/004Z8Ihr0JIu5s.html',
    # 請求來源,攜帶的信息比“origin”更豐富,本案例中其實是不需要加這個參數的,只是為了演示
    'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
    # 標記了請求從什么設備,什么瀏覽器上發出
    }
# 偽裝請求頭

params = {
'ct':'24',
'qqmusic_ver': '1298',
'new_json':'1',
'remoteplace':'sizer.yqq.song_next',
'searchid':'64405487069162918',
't':'0',
'aggr':'1',
'cr':'1',
'catZhida':'1',
'lossless':'0',
'flag_qc':'0',
'p':1,
'n':'20',
'w':'周傑倫',
'g_tk':'5381',
'loginUin':'0',
'hostUin':'0',
'format':'json',
'inCharset':'utf8',
'outCharset':'utf-8',
'notice':'0',
'platform':'yqq.json',
'needNewCode':'0'    
}
# 將參數封裝為字典
res_music = requests.get(url,headers=headers,params=params)
# 發起請求,填入請求頭和參數

3. replace()函數

a = '你\\n好\\n么\\n'
b = a.replace('\\n', '\n')
print(a)
# 》》你\n好\n么\n
print(b)
# 》》你
# 》》好
# 》》么

replace()是字符串對象的一個方法,它的意思是,把第一個參數的字符串用第二個參數的字符串替代

第6關  csv&excel

常用的存儲數據的方式有兩種——存儲成csv格式文件、存儲成Excel文件
csv是一種字符串文件的格式,它組織數據的語法就是在字符串之間加分隔符——行與行之間是加換行符,同行字符之間是加逗號分隔
它可以用任意的文本編輯器打開(如記事本),也可以用Excel打開,還可以通過Excel把文件另存為csv格式(因為Excel支持csv格式文件)

file = open('test.csv', 'a+')
# 創建test.csv文件,以追加的讀寫模式
file.write('美國隊長,鋼鐵俠,蜘蛛俠')
# 寫入test.csv文件
file.close()
# 關閉文件

用記事本打開↓

用Excel打開↓ 

用csv格式存儲數據,讀寫比較方便,易於實現,文件也會比Excel文件小,但csv文件缺少Excel文件本身的很多功能,比如不能嵌入圖像和圖表,不能生成公式
至於Excel文件,就是電子表格,它有專門保存文件的格式,即xls和xlsx(Excel2003版本的文件格式是xls,Excel2007及之后的版本的文件格式就是xlsx)

存儲數據的基礎知識

1. 基礎知識:csv寫入與讀取
Python自帶了csv模塊,所以不需要安裝就能引用它

import csv
# 引用csv模塊。
csv_file = open('demo.csv', 'w', newline='', encoding='utf-8')
# 調用open()函數打開csv文件,傳入參數:文件名“demo.csv”、寫入模式“w”、newline=''、encoding='utf-8'
writer = csv.writer(csv_file)
# 用csv.writer()函數創建一個writer對象
writer.writerow(['電影', '豆瓣評分'])
# 調用writer對象的writerow()方法,可以在csv文件里寫入一行文字 “電影”和“豆瓣評分”
writer.writerow(['銀河護衛隊', '8.0'])
# 在csv文件里寫入一行文字 “銀河護衛隊”和“8.0”
writer.writerow(['復仇者聯盟', '8.1'])
# 在csv文件里寫入一行文字 “復仇者聯盟”和“8.1”
csv_file.close()
# 寫入完成后,關閉文件

創建一個新的csv文件,命名為“demo.csv”
“w”就是writer,即文件寫入模式,它會以覆蓋原內容的形式寫入新添加的內容
附上一張文件讀寫模式表↓

加newline=' '參數的原因是,可以避免csv文件出現兩倍的行距(就是能避免表格的行與行之間出現空白行)
加encoding='utf-8',可以避免編碼問題導致的報錯或亂碼
創建完csv文件后,要借助csv.writer()函數來建立一個writer對象
調用writer對象的writerow()方法往csv文件里寫入新的內容
提醒:writerow()函數里,需要放入列表參數,所以得把要寫入的內容寫成列表,就像['電影','豆瓣評分']
最后關閉文件,就完成csv文件的寫入了

運行代碼后,名為“demo.csv”的文件會被創建,用Excel或記事本打開這個文件↓

csv讀取↓

import csv
# 導入csv模塊
csv_file = open('demo.csv', 'r', newline='', encoding='utf-8')
# 用open()打開“demo.csv”文件,'r'是read讀取模式,newline=''是避免出現兩倍行距。encoding='utf-8'能避免編碼問題導致的報錯或亂碼
reader = csv.reader(csv_file)
# 用csv.reader()函數創建一個reader對象
for row in reader:
    # 用for循環遍歷reader對象的每一行
    print(row)
# 打印row,就能讀取出“demo.csv”文件里的內容
csv_file.close()
# 讀取完成后,關閉文件
# 》》['電影', '豆瓣評分']
# 》》['銀河護衛隊', '8.0']
# 》》['復仇者聯盟', '8.1']


csv模塊本身還有很多函數和方法,附上csv模塊官方文檔鏈接:https://yiyibooks.cn/xx/python_352/library/csv.html#module-csv
2. 基礎知識:Excel寫入與讀取

一個Excel文檔也稱為一個工作薄(workbook),每個工作薄里可以有多個工作表(worksheet),當前打開的工作表又叫活動表
每個工作表里有行和列,特定的行與列相交的方格稱為單元格(cell),比如上圖第A列和第1行相交的方格可以直接表示為A1單元格
openpyxl模塊需要安裝,mac電腦在終端輸入命令:pip3 install openpyxl

import openpyxl
# 引用openpyxl
wb = openpyxl.Workbook()
# 利用openpyxl.Workbook()函數創建新的workbook(工作薄)對象,就是創建新的空的Excel文件
sheet = wb.active
# wb.active就是獲取這個工作薄的活動表,通常就是第一個工作表
sheet.title = 'new title'
# 可以用.title給工作表重命名。現在第一個工作表的名稱就會由原來默認的“sheet1”改為"new title"
sheet['A1'] = '漫威宇宙'
# 把'漫威宇宙'賦值給第一個工作表的A1單元格,就是往A1的單元格中寫入了'漫威宇宙'
row = ['滅霸', '', '響指']
# 把想寫入的一行內容寫成列表,賦值給row。
sheet.append(row)
# 用sheet.append()就能往表格里添加這一行文字
rows = [['美國隊長', '鋼鐵俠', '蜘蛛俠'], ['', '漫威', '宇宙', '經典', '人物']]
# 把要寫入的多行內容寫成列表,再放進大列表里,賦值給rows
for i in rows:
    sheet.append(i)
# 遍歷rows,同時把遍歷的內容添加到表格里,這樣就實現了多行寫入
wb.save('Marvel.xlsx')
# 保存新建的Excel文件,並命名為“Marvel.xlsx”

Excel讀取↓

import openpyxl
# 引用openpyxl
wb = openpyxl.load_workbook('Marvel.xlsx')
# 調用openpyxl.load_workbook()函數,打開“Marvel.xlsx”文件
sheet = wb['new title']
# 獲取“Marvel.xlsx”工作薄中名為“new title”的工作表
sheetname = wb.sheetnames
# sheetnames是用來獲取工作薄所有工作表的名字的,如果不知道工作薄到底有幾個工作表,就可以把工作表的名字都打印出來
print(sheetname)
# 》》['new title']
A1_cell = sheet['A1']
A1_value = A1_cell.value
# 把“new title”工作表中A1單元格賦值給A1_cell,再利用單元格value屬性,就能打印出A1單元格的值
print(A1_value)
# 》》漫威宇宙


openpyxl模塊的官方文檔:https://openpyxl.readthedocs.io/en/stable/
3. 存儲周傑倫的歌曲信息

import requests
import openpyxl

wb = openpyxl.Workbook()
# 創建工作薄
sheet = wb.active
# 獲取工作薄的活動表
sheet.title = 'song'
# 工作表重命名

sheet['A1'] = '歌曲名'  # 加表頭,給A1單元格賦值
sheet['B1'] = '所屬專輯'  # 加表頭,給B1單元格賦值
sheet['C1'] = '播放時長'  # 加表頭,給C1單元格賦值
sheet['D1'] = '播放鏈接'  # 加表頭,給D1單元格賦值

url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp'
for x in range(5):
    params = {
        'ct': '24',
        'qqmusic_ver': '1298',
        'new_json': '1',
        'remoteplace': 'txt.yqq.song',
        'searchid': '64405487069162918',
        't': '0',
        'aggr': '1',
        'cr': '1',
        'catZhida': '1',
        'lossless': '0',
        'flag_qc': '0',
        'p': str(x + 1),
        'n': '20',
        'w': '周傑倫',
        'g_tk': '5381',
        'loginUin': '0',
        'hostUin': '0',
        'format': 'json',
        'inCharset': 'utf8',
        'outCharset': 'utf-8',
        'notice': '0',
        'platform': 'yqq.json',
        'needNewCode': '0'
    }

    res_music = requests.get(url, params=params)
    json_music = res_music.json()
    list_music = json_music['data']['song']['list']
    for music in list_music:
        name = music['name']
        # 以name為鍵,查找歌曲名,把歌曲名賦值給name
        album = music['album']['name']
        # 查找專輯名,把專輯名賦給album
        time = music['interval']
        # 查找播放時長,把時長賦值給time
        link = 'https://y.qq.com/n/yqq/song/' + str(music['mid']) + '.html\n\n'
        # 查找播放鏈接,把鏈接賦值給link
        sheet.append([name, album, time, link])
        # 把name、album、time和link寫成列表,用append函數多行寫入Excel
        print('歌曲名:' + name + '\n' + '所屬專輯:' + album + '\n' +
              '播放時長:' + str(time) + '\n' + '播放鏈接:' + link)

wb.save('Jay.xlsx')
# 最后保存並命名這個Excel文件

運行代碼,“Jay.xlsx”文件就會被創建,打開這個文件就可以看到存儲的數據

第7關  爬取知乎文章

前面6關所講的爬蟲原理,在本質上,是一個所操作的對象在不斷轉換的過程

總體上來說,從Response對象開始,就分成了兩條路徑,一條路徑是數據放在HTML里,所以用BeautifulSoup庫去解析數據和提取數據;另一條,數據作為Json存儲起來,所以用response.json()方法去解析,然后提取、存儲數據
本關目標:爬取知乎大v張佳瑋的文章“標題”、“摘要”、“鏈接”,並存儲到本地文件
張佳瑋的知乎文章URL:https://www.zhihu.com/people/zhang-jia-wei/posts?page=1

import requests
import csv

csv_file = open('articles.csv', 'w', newline='', encoding='utf-8')
writer = csv.writer(csv_file)
writer.writerow(['標題', '鏈接', '摘要'])

offset = 0
while True:
    url = 'https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles'
    headers = {
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36'}
    params = {
        'include': 'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
        'offset': str(offset),
        'limit': '20',
        'sort_by': 'created'
    }

    res = requests.get(url, params=params, headers=headers)
    resjson = res.json()
    articles = resjson['data']
    for article in articles:
        title = article['title']
        link = article['url']
        excerpt = article['excerpt']
        writer.writerow([title, link, excerpt])

    offset += 20
    if offset > 40:
        break
    # if resjson['paging']['is_end'] == True:
    #     break
csv_file.close()

測詞匯量小工具

第8關  cookies

博客登錄網址:https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-login.php
賬號:spiderman,密碼:crawler334566
現在來分析瀏覽器的登錄請求是怎么發送的

先正常操作——填寫完賬號密碼(別點擊登錄),然后右擊打開“檢查”工具,點擊【network】,勾選【preserve log】(持續顯示請求記錄,防止請求記錄被刷新),再點擊登錄
1. post請求
展開第0個請求【wp-login.php】,瀏覽一下【headers】,在【General】鍵里,可以先只看前兩個參數【Request URL】(請求網址)和【Request Method】(請求方式)

這里的請求方式是post,而不是get。其實,post和get都可以帶着參數請求,不過get請求的參數會在url上顯示出來,但post請求的參數就不會直接顯示,而是隱藏起來。像賬號密碼這種私密的信息,就應該用post的請求,如果用get請求的話,賬號密碼全部會顯示在網址上。可以這么理解,get是明文顯示,post是非明文顯示
通常,get請求會應用於獲取網頁數據,比如requests.get()。post請求則應用於向網頁提交數據,比如提交表單類型數據(像賬號密碼就是網頁表單的數據)
get和post是兩種最常用的請求方式,除此之外,還有其他類型的請求方式,如head、options等,不過一般很少用到
2. cookies及其用法

【requests headers】存儲的是瀏覽器的請求信息,【response headers】存儲的是服務器的響應信息
在【response headers】里有set cookies的參數,set cookies就是服務器往瀏覽器寫入了cookies

當登錄博客賬號spiderman,並勾選“記住我”,服務器就會生成一個cookies和spiderman這個賬號綁定。接着,它把這個cookies告訴瀏覽器,讓瀏覽器把cookies存儲到本地電腦。當下一次,瀏覽器帶着cookies訪問博客,服務器會知道是spiderman,就不需要再重復輸入賬號密碼,即可直接訪問
當然,cookies也是有時效性的,過期后就會失效。哪怕勾選了“記住我”,但一段時間過去了,網站還是會提示要重新登錄,就是之前的cookies已經失效
繼續看【headers】,拉到【form data】,可以看到5個參數↓

log和pwd顯然是賬號和密碼,wp-submit能知道是登錄的按鈕,redirect_to后面帶的鏈接是登錄后會跳轉到的這個頁面網址,testcookie不知道是什么
在《未來已來(一)——技術變革》這篇文章下面發表一條評論(不要關閉檢查工具,這樣才能看到請求的記錄)
點開【wp-comments-post.php】,看headers,剛剛發表的評論就藏在這里

comment是評論內容,submit是發表評論的按鈕,還有另外兩個和評論有關的參數
想要發表博客評論,首先得登錄,其次得提取和調用登錄的cookies,然后還需要評論的參數,才能發起評論的請求

import requests
# 引入requests
url = ' https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-login.php'
# 把請求登錄的網址賦值給url
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
# 加請求頭,加請求頭是為了模擬瀏覽器正常的訪問,避免被反爬蟲
data = {
    'log': 'spiderman',  # 寫入賬戶
    'pwd': 'crawler334566',  # 寫入密碼
    'wp-submit': '登錄',
    'redirect_to': 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn',
    'testcookie': '1'
}
# 把有關登錄的參數封裝成字典,賦值給data
login_in = requests.post(url, headers=headers, data=data)
# 用requests.post發起請求,放入參數:請求登錄的網址、請求頭和登錄參數,然后賦值給login_in
print(login_in)
# 打印login_in
# 》》<Response [200]>
cookies = login_in.cookies
# 提取cookies的方法:調用requests對象(login_in)的cookies屬性獲得登錄的cookies,並賦值給變量cookies

url_1 = 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-comments-post.php'
# 想要評論的文章網址
data_1 = {
    'comment': 'cookies測試',
    'submit': '發表評論',
    'comment_post_ID': '13',
    'comment_parent': '0'
}
# 把有關評論的參數封裝成字典
comment = requests.post(url_1, headers=headers, data=data_1, cookies=cookies)
# 用requests.post發起發表評論的請求,放入參數:文章網址、headers、評論參數、cookies參數,賦值給comment
# 調用cookies的方法就是在post請求中傳入cookies=cookies的參數
print(comment.status_code)
# 打印出comment的狀態碼,若狀態碼等於200,則證明評論成功
# 》》200

<Response [200]>,是返回了200的狀態碼,意味着服務器接收到並響應了登錄請求 
cookies = login_in.cookies這句是提取cookies的方法,調用requests對象的cookies屬性獲得登錄的cookies
調用cookies的方法是在post請求中傳入cookies=cookies的參數就可以了
最后之所以加一行打印狀態碼的代碼,是想運行整個代碼后,能立馬判斷出評論到底有沒有成功發表。只要狀態碼等於200,就說明服務器成功接收並響應了評論請求
登錄的cookies其實包含了很多名稱和值,真正能幫助發表評論的cookies,只是取了登錄cookies中某一小段值而已。所以登錄的cookies和評論成功后,在【wp-comments-post.php】里的headers面板中看到的cookies是不一致的

用requests模塊發表博客評論的三個重點↓
① post帶着參數地請求登錄
② 獲得登錄的cookies
③ 帶cookies去請求發表評論
3. session及其用法
所謂的會話,可以理解成用瀏覽器上網,到關閉瀏覽器的這一過程。session是會話過程中,服務器用來記錄特定用戶會話的信息
比如打開瀏覽器逛購物網頁的整個過程中,瀏覽了哪些商品,在購物車里放了多少件物品,這些記錄都會被服務器保存在session中

如果沒有session,可能會出現這樣搞笑的情況:加購了很多商品在購物車,打算結算時,發現購物車空無一物,因為服務器根本沒有記錄想買的商品
session和cookies的關系還非常密切——cookies中存儲着session的編碼信息,session中又存儲了cookies的信息
當瀏覽器第一次訪問購物網頁時,服務器會返回set cookies的字段給瀏覽器,而瀏覽器會把cookies保存到本地
等瀏覽器第二次訪問這個購物網頁時,就會帶着cookies去請求,而因為cookies里帶有會話的編碼信息,服務器立馬就能辨認出這個用戶,同時返回和這個用戶相關的特定編碼的session
這也是為什么每次重新登錄購物網站后,之前在購物車放入的商品並不會消失的原因。因為在登錄時,服務器可以通過瀏覽器攜帶的cookies,找到保存了購物車信息的session
在requests的高級用法里,有通過創建session來處理cookies的方法。發表博客評論的代碼可以寫為↓

import requests
# 引用requests
session = requests.session()
# 用requests.session()創建session對象,相當於創建了一個特定的會話,自動保持了cookies
url = 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-login.php'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
}
data = {
    'log': 'spiderman',
    'pwd': 'crawler334566',
    'wp-submit': '登錄',
    'redirect_to': 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn',
    'testcookie': '1'
}
session.post(url, headers=headers, data=data)
# 在創建的session下用post發起登錄請求,放入參數:請求登錄的網址、請求頭和登錄參數

url_1 = 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-comments-post.php'
# 把想要評論的文章網址賦值給url_1
data_1 = {
    'comment': 'session測試',
    'submit': '發表評論',
    'comment_post_ID': '13',
    'comment_parent': '0'
}
# 把有關評論的參數封裝成字典
comment = session.post(url_1, headers=headers, data=data_1)
# 在創建的session下用post發起評論請求,放入參數:文章網址,請求頭和評論參數,並賦值給comment
print(comment)
# 打印comment
# 》》<Response [200]>

用session模塊發表博客評論的三個重點↓
① 創建會話(session)
② 在創建的會話下發起post登錄請求
③ 在創建的會話下發起post評論請求
4. 存儲cookies
先把登錄的cookies打印出來看看↓

import requests
session = requests.session()
url = 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-login.php'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
}
data = {
    'log': 'spiderman',
    'pwd': 'crawler334566',
    'wp-submit': '登錄',
    'redirect_to': 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn',
    'testcookie': '1'
}
session.post(url, headers=headers, data=data)
print(type(session.cookies))
# 打印cookies的類型,session.cookies就是登錄的cookies
print(session.cookies)
# 打印cookies
# 》》<class 'requests.cookies.RequestsCookieJar'>
# 》》<RequestsCookieJar[<Cookie 328dab9653f517ceea1f6dfce2255032=7cd1e98da7c519edc41374a980c6c80c for wordpress-edu-3autumn.localprod.oc.forchange.cn/>, <Cookie wordpress_logged_in_dc180e44ec13b4c601eeef962104f0fe=spiderman%7C1586880428%7CA9bYNaShymZmonXtwiZztH9M7umN7yKI79zYRBN4nvR%7C6a43f8d7c2163d940c9c6814ab905d945ee20b4eac985583b18bb1c9a93e32fb for wordpress-edu-3autumn.localprod.oc.forchange.cn/>, <Cookie wordpress_test_cookie=WP+Cookie+check for wordpress-edu-3autumn.localprod.oc.forchange.cn/>, <Cookie wordpress_sec_dc180e44ec13b4c601eeef962104f0fe=spiderman%7C1586880428%7CA9bYNaShymZmonXtwiZztH9M7umN7yKI79zYRBN4nvR%7C7bfb4219b9ff58209108b2186464f4c35df924d732dfbd9769183639bb6152b9 for wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-admin>, <Cookie wordpress_sec_dc180e44ec13b4c601eeef962104f0fe=spiderman%7C1586880428%7CA9bYNaShymZmonXtwiZztH9M7umN7yKI79zYRBN4nvR%7C7bfb4219b9ff58209108b2186464f4c35df924d732dfbd9769183639bb6152b9 for wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-content/plugins>]>

RequestsCookieJar是cookies對象的類,cookies本身的內容有點像一個列表,里面又有點像字典的鍵與值
想要把cookies存儲到txt文件,可是txt文件存儲的是字符串,中間需要進行轉換

把cookies存儲成txt文件的代碼如下↓

import requests,json
# 引入requests和json模塊
session = requests.session()
url = ' https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-login.php'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
}
data = {
    'log': 'spiderman',
    'pwd': 'crawler334566',
    'wp-submit': '登錄',
    'redirect_to': 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn',
    'testcookie': '1'
}
session.post(url, headers=headers, data=data)

cookies_dict = requests.utils.dict_from_cookiejar(session.cookies)
# 把cookies轉化成字典
print(cookies_dict)
# 打印cookies_dict
# 》》{'328dab9653f517ceea1f6dfce2255032': '7cd1e98da7c519edc41374a980c6c80c', 'wordpress_logged_in_dc180e44ec13b4c601eeef962104f0fe': 'spiderman%7C1586885493%7CCCwOuR2nO5BphlCQT4DlfKcDto8abAxD4TH57IUwjso%7C3be08285d54b2c7bd527e0491d1e358efcaf6c8b252061e07baeda28bd071332', 'wordpress_test_cookie': 'WP+Cookie+check', 'wordpress_sec_dc180e44ec13b4c601eeef962104f0fe': 'spiderman%7C1586885493%7CCCwOuR2nO5BphlCQT4DlfKcDto8abAxD4TH57IUwjso%7C4322ad4a66be7589813f82133ad212bfe4f2d314076b2df91bddb595ea6b0a09'}
cookies_str = json.dumps(cookies_dict)
# 調用json模塊的dumps函數,把cookies從字典再轉成字符串
print(cookies_str)
# 打印cookies_str
# 》》{"328dab9653f517ceea1f6dfce2255032": "7cd1e98da7c519edc41374a980c6c80c", "wordpress_logged_in_dc180e44ec13b4c601eeef962104f0fe": "spiderman%7C1586885493%7CCCwOuR2nO5BphlCQT4DlfKcDto8abAxD4TH57IUwjso%7C3be08285d54b2c7bd527e0491d1e358efcaf6c8b252061e07baeda28bd071332", "wordpress_test_cookie": "WP+Cookie+check", "wordpress_sec_dc180e44ec13b4c601eeef962104f0fe": "spiderman%7C1586885493%7CCCwOuR2nO5BphlCQT4DlfKcDto8abAxD4TH57IUwjso%7C4322ad4a66be7589813f82133ad212bfe4f2d314076b2df91bddb595ea6b0a09"}
f = open('cookies.txt', 'w')
# 創建名為cookies.txt的文件,以寫入模式寫入內容
f.write(cookies_str)
# 把已經轉成字符串的cookies寫入文件
f.close()
# 關閉文件

5. 讀取cookies
存儲cookies時,是把它先轉成字典,再轉成字符串。讀取cookies則剛好相反,要先把字符串轉成字典,再把字典轉成cookies本來的格式

讀取cookies的代碼如下↓

import requests,json
session = requests.session()

cookies_txt = open('cookies.txt', 'r')
# 以reader讀取模式,打開名為cookies.txt的文件
cookies_dict = json.loads(cookies_txt.read())
# 調用json模塊的loads函數,把字符串轉成字典
cookies = requests.utils.cookiejar_from_dict(cookies_dict)
# 把轉成字典的cookies再轉成cookies本來的格式
session.cookies = cookies
# 獲取cookies:就是調用requests對象(session)的cookies屬性
print(session.cookies)
# 》》<RequestsCookieJar[<Cookie 328dab9653f517ceea1f6dfce2255032=7cd1e98da7c519edc41374a980c6c80c for />, <Cookie wordpress_logged_in_dc180e44ec13b4c601eeef962104f0fe=spiderman%7C1586885493%7CCCwOuR2nO5BphlCQT4DlfKcDto8abAxD4TH57IUwjso%7C3be08285d54b2c7bd527e0491d1e358efcaf6c8b252061e07baeda28bd071332 for />, <Cookie wordpress_sec_dc180e44ec13b4c601eeef962104f0fe=spiderman%7C1586885493%7CCCwOuR2nO5BphlCQT4DlfKcDto8abAxD4TH57IUwjso%7C4322ad4a66be7589813f82133ad212bfe4f2d314076b2df91bddb595ea6b0a09 for />, <Cookie wordpress_test_cookie=WP+Cookie+check for />]>

現在可以寫成更完整的代碼了:如果程序能讀取到cookies,就自動登錄,發表評論;如果讀取不到,就重新輸入賬號密碼登錄,再評論;另外,如果cookies過期,也要重新獲取新的cookies

import requests,json
session = requests.session()
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'}

def cookies_read():
    cookies_txt = open('cookies.txt', 'r')
    cookies_dict = json.loads(cookies_txt.read())
    cookies = requests.utils.cookiejar_from_dict(cookies_dict)
    return (cookies)

def sign_in():
    url = ' https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-login.php'
    data = {'log': 'spiderman',
            'pwd': 'crawler334566',
            'wp-submit': '登錄',
            'redirect_to': 'https://wordpress-edu-3autumn.localprod.oc.forchange.cn',
            'testcookie': '1'}
    session.post(url, headers = headers, data = data)
    cookies_dict=requests.utils.dict_from_cookiejar(session.cookies)
    cookies_str=json.dumps(cookies_dict)
    f=open('cookies.txt', 'w')
    f.write(cookies_str)
    f.close()

def write_message():
    url_2='https://wordpress-edu-3autumn.localprod.oc.forchange.cn/wp-comments-post.php'
    data_2={
        'comment': '測試測試測試',
        'submit': '發表評論',
        'comment_post_ID': '13',
        'comment_parent': '0'
    }
    return (session.post(url_2, headers = headers, data = data_2))

try:
    session.cookies=cookies_read()
except FileNotFoundError:
    sign_in()

num=write_message()
if num.status_code == 200:
    print('成功啦!')
else:
    sign_in()
    num=write_message()

其實,計算機之所以需要cookies和session,是因為HTTP協議是無狀態的協議
何為無狀態?就是一旦瀏覽器和服務器之間的請求和響應完畢后,兩者會立馬斷開連接,也就是恢復成無狀態
這樣會導致:服務器永遠無法辨認,也記不住用戶的信息,像一條只有7秒記憶的金魚。是cookies和session的出現,才破除了web發展史上的這個難題
cookies不僅僅能實現自動登錄,因為它本身攜帶了session的編碼信息,網站還能根據cookies,記錄用戶的瀏覽足跡,從而知道用戶的偏好,只要再加以推薦算法,就可以實現給用戶推送定制化的內容
比如,淘寶會根據用戶搜索和瀏覽商品的記錄,推送符合偏好的商品,增加用戶的購買率。cookies和session在這其中起到的作用,可謂舉足輕重
6. filter()函數
filter() 函數用於過濾序列,過濾掉不符合條件的元素,返回一個迭代器對象,如果要轉換為列表,可以使用list()來轉換
語法:filter(function, iterable)
filter()接收兩個參數,第一個為函數(function-判斷函數),第二個為序列(iterable-可迭代對象),序列的每個元素作為參數傳遞給函數進行判斷,然后返回True或False,最后將返回True的元素放到新列表中

# 過濾出網址中的數字
id_filter = filter(str.isdigit, 'https://www.xslou.com/yuedu/22177/')
print(type(id_filter))
# 》》<class 'filter'>
id_list = list(id_filter)
print(id_list)
# 》》['2', '2', '1', '7', '7']
book_id = ''.join(id_list)
print(book_id)
# 》》22177

# 過濾出列表中的所有奇數
def is_odd(n):
    return n % 2 == 1

tmplist = filter(is_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
newlist = list(tmplist)
print(newlist)
# 》》[1, 3, 5, 7, 9]

7. 自制翻譯小程序
新的知識點tkinter,程序終於有界面了

import requests
from tkinter import Tk, Text, Button, Label, END

def crawl(word):
    url = 'http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36'}
    data = {'i': word,
            'from': 'AUTO',
            'to': 'AUTO',
            'smartresult': 'dict',
            'client': 'fanyideskweb',
            'doctype': 'json',
            'version': '2.1',
            'keyfrom': 'fanyi.web',
            'action': 'FY_BY_REALTIME',
            'typoResult': 'false'}
    res = requests.post(url, data=data, headers=headers)
    try:
        result = res.json()['translateResult'][0][0]['tgt']
    except:
        result = ""
    return result

def trans():
    content = text.get(0.0, END).strip().replace("\n", " ")
    result = crawl(content)
    result_text.configure(state='normal')
    result_text.delete(0.0, END)
    result_text.insert(END, result)
    result_text.configure(state='disabled')

def clean():
    text.delete(0.0, END)
    result_text.configure(state='normal')
    result_text.delete(0.0, END)
    result_text.configure(state='disabled')

root = Tk()
# 生成主窗口
root.title('翻譯器')
# 修改框體的名字,也可在創建時使用className參數來命名
root.geometry('380x500')
# 指定主框體大小,geometry(寬度x高度+左上角水平坐標+左上角垂直坐標)(是英文x不是乘號)
text = Text(root, bg='gray90')
# 生成多行文本框,bg指定了背景色
# 選顏色的鏈接:http://www.science.smith.edu/dftwiki/index.php/Color_Charts_for_TKinter
text.place(x=5, y=5, width=370, height=230)
# 布局控件:pack,grid,place,這里使用的是place。x:組件左上角的x坐標,y:組件右上角的y坐標,width:組件的寬度,heitht:組件的高度
trans_btn = Button(root, text='翻譯', command=trans)
# 生成“翻譯”按鈕,調用trans方法
trans_btn.place(x=278, y=238, width=45, height=24)
wipe_btn = Button(root, text='清空', command=clean)
# 生成“清空”按鈕,調用clean方法
wipe_btn.place(x=328, y=238, width=45, height=24)
title_label = Label(root, text='翻譯結果')
# 生成“翻譯結果”標題標簽˝
title_label.place(x=5, y=238)
result_text = Text(root, bg='gray90')
# 生成顯示結果的多行文本框
result_text.configure(state='disabled')
# 為了實現只讀效果,將文本框狀態設置為disabled
result_text.place(x=5, y=265, width=370, height=230)
root.mainloop()
# 窗口事件主循環

8. 圖靈機器人
圖靈機器人官網:http://www.tuling123.com/ 
跟機器人聊天的簡易程序代碼↓

import requests
import json

url = 'http://openapi.tuling123.com/openapi/api/v2'
# 接口地址
while True:
    chat = input('我:')
    data = {
        "reqType": 0,
        "perception": {
            "inputText": {
                "text": chat
            }
        },
        "userInfo": {
            "apiKey": "...",
            # apiKey是針對接口訪問的授權方式。注冊登錄后創建機器人,會生成apiKey
            "userId": "xiaomei"
            # userId:長度需小於32,是用戶的唯一標識
        }
    }
    # perception和userInfo是必須要填寫的參數
    res = requests.post(url, data=json.dumps(data))
    # 請求參數格式為 json
    result = res.json()['results'][0]['values']['text']
    print('圖小智:'+result) 

第9關  Selenium

selenium是一個強大的Python庫,它可以用幾行代碼,控制瀏覽器,做出自動打開、輸入、點擊等操作,就像是有一個真正的用戶在操作一樣
當你遇到驗證碼很復雜的網站時,selenium允許讓人去手動輸入驗證碼,然后把剩下的操作交給機器
而對於那些交互復雜、加密復雜的網站,selenium問題簡化,爬動態網頁如爬靜態網頁一樣簡單
第1關用html寫出的網頁,就是靜態網頁,這類型網頁使用beautifulsoup爬取,因為網頁源代碼中就包含着網頁的所有信息,因此,網頁地址欄的url就是網頁源代碼的url

之后接觸的更復雜的網頁,比如QQ音樂,要爬取的數據不在HTML源代碼中,而是在json中,就不能直接使用網址欄的url了,而需要找到json數據的真實url,這就是一種動態網頁 

不論數據存在哪里,瀏覽器總是在向服務器發起各式各樣的請求,當這些請求完成后,它們會一起組成開發者工具的Elements中所展示的,渲染完成的網頁源代碼
在遇到頁面交互復雜或是url加密邏輯復雜的情況時,selenium就派上了用場,它可以真實地打開一個瀏覽器,等待所有數據都加載到Elements中之后,再把這個網頁當做靜態網頁爬取就好了
當然selenium也有美中不足之處,由於要真實地運行本地瀏覽器,打開瀏覽器以及等待網渲染完成需要一些時間,selenium的工作不可避免地犧牲了速度和更多資源,不過,至少不會比人慢
1. 安裝selenium庫和瀏覽器驅動
selenium需要安裝,mac電腦在終端輸入命令:pip3 install selenium
selenium的腳本可以控制所有常見瀏覽器的操作,在使用之前,需要安裝瀏覽器的驅動
推薦使用Chrome瀏覽器,瀏覽器安裝好后,在終端輸入命令:
curl -s https://localprod.pandateacher.com/python-manuscript/crawler-html/chromedriver/chromedriver-for-Macos.sh | bash
驅動安裝方法也可以見鏈接:https://localprod.pandateacher.com/python-manuscript/crawler-html/chromedriver/ChromeDriver.html
2. 設置瀏覽器引擎

# 本地Chrome瀏覽器設置方法
from selenium import webdriver  # 從selenium庫中調用webdriver模塊
driver = webdriver.Chrome()  # 設置引擎為Chrome,真實地打開一個Chrome瀏覽器
# 本地Chrome瀏覽器的靜默模式設置:
from selenium import webdriver  # 從selenium庫中調用webdriver模塊
from selenium.webdriver.chrome.options import Options  # 從options模塊中調用Options類

chrome_options = Options()  # 實例化Option對象
chrome_options.add_argument('--headless')  # 把Chrome瀏覽器設置為靜默模式
driver = webdriver.Chrome(options=chrome_options)  # 設置引擎為Chrome,在后台默默運行

以上就是瀏覽器的設置方式:把Chrome瀏覽器設置為引擎,然后賦值給變量driver,driver是實例化的瀏覽器
3. 獲取、解析與提取數據 
本關學習以【你好蜘蛛俠!】這個網站為例

import time
# 本地Chrome瀏覽器設置方法
from selenium import webdriver  # 從selenium庫中調用webdriver模塊
driver = webdriver.Chrome()  # 設置引擎為Chrome,真實地打開一個Chrome瀏覽器

# 獲取數據
driver.get('https://localprod.pandateacher.com/python-manuscript/hello-spiderman/') # 通過實例化的瀏覽器打開網頁
time.sleep(2)  # 等待2秒,等瀏覽器加載緩沖數據

# 解析與提取數據
# 解析數據是由driver自動完成的,提取數據是driver的一個方法
label = driver.find_element_by_tag_name('label')  # 解析網頁並提取第一個<lable>標簽中的文字
print(type(label))  # 打印label的數據類型
# 》》<class 'selenium.webdriver.remote.webelement.WebElement'>
print(label.text)  # 打印label的文本
# 》》(提示:吳楓)
print(label)  # 打印label
# 》》<selenium.webdriver.remote.webelement.WebElement (session="d776d7492e34a61bc565e755ce082388", element="0.30820374741568446-1")>
teacher = driver.find_element_by_class_name('teacher')  # 根據類名找到元素
print(type(teacher))  # 打印teacher的數據類型
#》》<class 'selenium.webdriver.remote.webelement.WebElement'>
print(teacher.get_attribute('type'))  # 獲取type這個屬性的值
#》》text
driver.close()  # 關閉瀏覽器驅動,每次調用了webdriver之后,都要在用完它之后加上一行driver.close()用來關閉它

selenium和BeautifulSoup的底層原理一致,但在一些細節和語法上有所出入
selenium所解析提取的,是Elements中的所有數據,而BeautifulSoup所解析的則只是Network中第0個請求的響應
用selenium把網頁打開,所有信息就都加載到了Elements那里,之后,就可以把動態網頁用靜態網頁的方法爬取了
selenium有很多查找和提取元素的方法↓

selenium提取出的數據屬於WebElement類對象,如果直接打印它,返回的是一串對它的描述
它與BeautifulSoup中的Tag對象類似,也有一個屬性.text,可以把提取出的元素用字符串格式顯示
有一個方法,也可以通過屬性名提取屬性的值,這個方法是.get_attribute()

可以總結出selenium解析與提取數據的過程中,操作的對象轉換↓

find_element_by_與BeautifulSoup中的find類似,可以提取出網頁中第一個符合要求的元素,selenium也同樣有與find_all類似的方法,find_elements_by_,可以提取多個元素

這樣提取出的是一個列表,<class 'list'>,而列表的內容就是WebElement對象
除了用selenium解析與提取數據,還有一種解決方案,那就是,使用selenium獲取網頁,然后交給BeautifulSoup解析和提取
BeautifulSoup需要把字符串格式的網頁源代碼解析為BeautifulSoup對象,然后再從中提取數據
而selenium剛好可以獲取到渲染完整的網頁源代碼,並且是字符串類型

import time
# 本地Chrome瀏覽器的靜默模式設置:
from selenium import webdriver  # 從selenium庫中調用webdriver模塊
from selenium.webdriver.chrome.options import Options  # 從options模塊中調用Options類

chrome_options = Options()  # 實例化Option對象
chrome_options.add_argument('--headless')  # 把Chrome瀏覽器設置為靜默模式
driver = webdriver.Chrome(options=chrome_options)  # 設置引擎為Chrome,在后台默默運行

driver.get('https://localprod.pandateacher.com/python-manuscript/hello-spiderman/')
time.sleep(2)

pageSource = driver.page_source  # 獲取完整渲染的網頁源代碼
print(type(pageSource))  # 打印pageSource的類型
# 》》<class 'str'>
print(pageSource)  # 打印pageSource
driver.close()  # 關閉瀏覽器

 

獲取到了字符串格式的網頁源代碼之后,就可以用BeautifulSoup解析和提取數據了
4. 自動操作瀏覽器

# 本地Chrome瀏覽器設置方法
from selenium import webdriver  # 從selenium庫中調用webdriver模塊
from bs4 import BeautifulSoup  # 導入BeautifulSoup
import time  # 調用time模塊

driver = webdriver.Chrome()  # 設置引擎為Chrome,真實地打開一個Chrome瀏覽器

driver.get(
    'https://localprod.pandateacher.com/python-manuscript/hello-spiderman/')  # 訪問頁面
time.sleep(2)  # 暫停兩秒,等待瀏覽器緩沖

teacher = driver.find_element_by_id('teacher')  # 找到【請輸入你喜歡的老師】下面的輸入框位置
teacher.send_keys('蜘蛛俠')  # 輸入文字
time.sleep(1)
teacher.clear()  # 清除文字
time.sleep(1)
teacher.send_keys('穿着熊')  # 再次輸入文字
time.sleep(1)
assistant = driver.find_element_by_name('assistant')  # 找到【請輸入你喜歡的助教】下面的輸入框位置
assistant.send_keys('都喜歡')  # 輸入文字
time.sleep(1)
button = driver.find_element_by_class_name('sub')  # 找到【提交】按鈕
button.click()  # 點擊【提交】按鈕
time.sleep(1)
bs = BeautifulSoup(driver.page_source, 'html.parser')
content_en = bs.find_all('div', class_='content')[0]
title_en = content_en.find('h1').text
zen_en = content_en.find('p').text
content_ch = bs.find_all('div', class_='content')[1]
title_ch = content_ch.find('h1').text
zen_ch = content_ch.find('p').text
driver.close()  # 關閉瀏覽器

在每一次輸入和點擊之前,都要先定位到對應的位置,查找定位用的方法就是解析與提取數據的方法

selenium的官方文檔鏈接:https://seleniumhq.github.io/selenium/docs/api/py/api.html
還可以參考這個中文文檔:https://selenium-python-zh.readthedocs.io/en/latest/

第10關  定時與郵件

1. schedule
通過第三方庫schedule實現定時功能
標准庫一般意味着最原始最基礎的功能,第三方庫很多是去調用標准庫中封裝好了的操作函數。比如schedule,就是用time和datetime來實現的
對於定時功能,time和datetime當然能實現,但操作邏輯會相對復雜,而schedule可以直接解決定時功能,代碼比較簡單
schedule需要先安裝,mac電腦在終端輸入:pip3 install schedule

import schedule
import time
# 引入schedule和time模塊

def job():
    print("I'm working...")
# 定義一個叫job的函數,函數的功能是打印'I'm working...'

schedule.every(2).seconds.do(job)
# 每2s執行一次job()函數

while True:
    schedule.run_pending()
    time.sleep(1)
# 檢查部署的情況,如果任務准備就緒,就開始執行任務。time.sleep(1)是讓程序按秒來檢查,如果檢查太快,會浪費計算機的資源

執行結果如下圖↓

再列一些其他的時間設置↓

import schedule
import time

def job():
    print("I'm working...")

schedule.every(10).minutes.do(job)  # 部署每10分鍾執行一次job()函數的任務
schedule.every().hour.do(job)  # 部署每×小時執行一次job()函數的任務
schedule.every().day.at("10:30").do(job)  # 部署在每天的10:30執行job()函數的任務
schedule.every().monday.do(job)  # 部署每個星期一執行job()函數的任務
schedule.every().wednesday.at("13:15").do(job)  # 部署每周三的13:15執行函數的任務

while True:
    schedule.run_pending()
    time.sleep(1)

2. 定時發送天氣情況

import requests
import smtplib
import schedule
import time
from bs4 import BeautifulSoup
from email.mime.text import MIMEText
from email.header import Header

account = input('請輸入發件人郵箱:')
password = input('請輸入郵箱授權碼:')
receiver = input('請輸入收件人郵箱:')

def weather_spider():
    headers = {
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
    url = 'http://www.weather.com.cn/weather/101280601.shtml'
    res = requests.get(url, headers=headers)
    res.encoding = 'utf-8'
    soup = BeautifulSoup(res.text, 'html.parser')
    tem1 = soup.find(class_='tem')
    weather1 = soup.find(class_='wea')
    tem = tem1.text
    weather = weather1.text
    return tem, weather

def send_email(tem, weather):
    mailhost = 'smtp.qq.com'
    qqmail = smtplib.SMTP()
    qqmail.connect(mailhost, 25)
    qqmail.login(account, password)
    content = tem+weather
    message = MIMEText(content, 'plain', 'utf-8')
    subject = '今日天氣預報'
    message['Subject'] = Header(subject, 'utf-8')
    try:
        qqmail.sendmail(account, receiver, message.as_string())
        print('郵件發送成功')
    except:
        print('郵件發送失敗')
    qqmail.quit()

def job():
    print('開始一次任務')
    tem, weather = weather_spider()
    send_email(tem, weather)
    print('任務完成')

schedule.every().day.at("07:30").do(job)
while True:
    schedule.run_pending()
    time.sleep(1)

保持程序一直運行的狀態,和電腦一直開機的狀態。因為如果程序結束或者電腦關機了的話,就不會定時爬取天氣信息了
真實的開發環境中,程序一般都會掛在遠端服務器,因為遠端服務器24小時都不會關機,就能保證定時功能的有效性了
關於郵件發送↓

風變編程筆記(一)-Python基礎語法有專門介紹
3. 必做練習【周末吃什么】的代碼↓

import requests,schedule,time,smtplib,json
from bs4 import BeautifulSoup
from email.mime.text import MIMEText
from email.header import Header

url = 'http://www.xiachufang.com/explore/'
headers = {
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
from_addr = 'xxx@qq.com'
password = 'xxx'
to_addr = 'xxx@sina.com'
smtp_server = 'smtp.qq.com'

def job():
    res_foods = requests.get(url, headers=headers)
    bs_foods = BeautifulSoup(res_foods.text, 'html.parser')
    list_foods = bs_foods.find_all('div', class_='info pure-u')
    list_all = []

    for i in range(len(list_foods)):
        tag_a = list_foods[i].find('a')
        name = tag_a.text[17:-13]
        URL = 'http://www.xiachufang.com'+tag_a['href']
        tag_p = list_foods[i].find('p', class_='ing ellipsis')
        ingredients = tag_p.text[1:-1]
        list_all.append([name, URL, ingredients])

    msg = MIMEText(json.dumps(list_all, ensure_ascii=False), 'plain', 'utf-8')
    msg['From'] = Header(from_addr)
    msg['To'] = Header(to_addr)
    msg['Subject'] = Header('本周最受歡迎菜譜', 'utf-8')
    server = smtplib.SMTP_SSL(smtp_server)
    server.connect(smtp_server, 465)
    server.login(from_addr, password)
    try:
        server.sendmail(from_addr, to_addr, msg.as_string())
        print('恭喜,發送成功')
    except:
        print('發送失敗,請重試')
    server.quit()

schedule.every().friday.do(job)
while True:
    schedule.run_pending()
    time.sleep(1)

 

第11關  協程

一個任務未完成時,就可以執行其他多個任務,彼此不受影響,叫異步
同步就是一個任務結束才能啟動下一個
顯然,異步執行任務會比同步更加節省時間,因為它能減少不必要的等待。如果需要對時間做優化,異步是一個很值得考慮的方案

同步與異步是計算機里的概念,如果把這個概念遷移到網絡爬蟲的場景中,那前面講的爬蟲方式都是同步的
爬蟲每發起一個請求,都要等服務器返回響應后,才會執行下一步。而很多時候,由於網絡不穩定,加上服務器自身也需要響應的時間,導致爬蟲會浪費大量時間在等待上。這也是爬取大量數據時,爬蟲的速度會比較慢的原因

那是不是可以采取異步的爬蟲方式,讓多個爬蟲在執行任務時保持相對獨立,彼此不受干擾,這樣不就可以免去等待時間?顯然這樣爬蟲的效率和速度都會提高
一點點計算機的歷史小知識:每台計算機都靠着CPU(中央處理器)干活。在過去,單核CPU的計算機在處理多任務時,會出現一個問題:每個任務都要搶占CPU,執行完了一個任務才開啟下一個任務。CPU畢竟只有一個,這會讓計算機處理的效率很低

為了解決這樣的問題,一種非搶占式的異步技術被創造了出來,這種方式叫多協程
多協程,是一種非搶占式的異步方式。使用多協程的話,就能讓多個爬取任務用異步的方式交替執行
它的原理是:一個任務在執行過程中,如果遇到等待,就先去執行其他的任務,當等待結束,再回來繼續之前的那個任務。在計算機的世界,這種任務來回切換得非常快速,看上去就像多個任務在被同時執行一樣
所以,要實現異步的爬蟲方式的話,需要用到多協程。在它的幫助下,能實現前面提到的“讓多個爬蟲干活”
1. gevent庫

先用同步的爬蟲方式爬取百度、新浪、搜狐、騰訊、網易、愛奇藝、天貓、鳳凰這個8個網站,看下用了多長時間↓

import requests
import time
# 導入requests和time
start = time.time()
# 記錄程序開始時間

url_list = ['https://www.baidu.com/',
            'https://www.sina.com.cn/',
            'http://www.sohu.com/',
            'https://www.qq.com/',
            'https://www.163.com/',
            'http://www.iqiyi.com/',
            'https://www.tmall.com/',
            'http://www.ifeng.com/']
# 把8個網站封裝成列表

for url in url_list:
# 遍歷url_list
    r = requests.get(url)
    # 用requests.get()函數爬取網站
    print(url, r.status_code)
    # 打印網址和抓取請求的狀態碼

end = time.time()
# 記錄程序結束時間
print(end-start)
# end-start是結束時間減去開始時間,就是最終所花時間
# 最后,把時間打印出來
# 》》https://www.baidu.com/ 200
# 》》https://www.sina.com.cn/ 200
# 》》http://www.sohu.com/ 200
# 》》https://www.qq.com/ 200
# 》》https://www.163.com/ 200
# 》》http://www.iqiyi.com/ 200
# 》》https://www.tmall.com/ 200
# 》》http://www.ifeng.com/ 200
# 》》0.5697987079620361

同步的爬蟲方式,是依次爬取網站,並等待服務器響應(狀態碼為200表示正常響應)后,才爬取下一個網站。比如第一個先爬取了百度的網址,等服務器響應后,再去爬取新浪的網址,以此類推,直至全部爬取完畢。再看看用多協程
gevent庫需要先安裝,mac電腦在終端輸入:pip3 install gevent

from gevent import monkey
# 從gevent庫里導入monkey模塊
monkey.patch_all()
# monkey.patch_all()能把程序變成協作式運行,就是可以幫助程序實現異步
import gevent,time,requests
# 導入gevent、time、requests

start = time.time()
# 記錄程序開始時間

url_list = ['https://www.baidu.com/',
            'https://www.sina.com.cn/',
            'http://www.sohu.com/',
            'https://www.qq.com/',
            'https://www.163.com/',
            'http://www.iqiyi.com/',
            'https://www.tmall.com/',
            'http://www.ifeng.com/']
# 把8個網站封裝成列表。


def crawler(url):
# 定義一個crawler()函數
    r = requests.get(url)
    # 用requests.get()函數爬取網站
    print(url, time.time()-start, r.status_code)
    # 打印網址、請求運行時間、狀態碼


tasks_list = []
# 創建空的任務列表

for url in url_list:
# 遍歷url_list
    task = gevent.spawn(crawler, url)
    # 用gevent.spawn()函數創建任務
    tasks_list.append(task)
    # 往任務列表添加任務
gevent.joinall(tasks_list)
# 執行任務列表里的所有任務,就是讓爬蟲開始爬取網站
end = time.time()
# 記錄程序結束時間
print(end-start)
# 打印程序最終所需時間
# 》》http://www.ifeng.com/ 0.08891010284423828 200
# 》》https://www.baidu.com/ 0.1046142578125 200
# 》》https://www.sina.com.cn/ 0.12417197227478027 200
# 》》https://www.163.com/ 0.12881922721862793 200
# 》》https://www.qq.com/ 0.1435079574584961 200
# 》》http://www.sohu.com/ 0.1657421588897705 200
# 》》https://www.tmall.com/ 0.18058228492736816 200
# 》》http://www.iqiyi.com/ 0.18367409706115723 200
# 》》0.18380022048950195

程序運行后,打印出了網址、每個請求運行的時間、狀態碼和爬取8個網站最終所用時間 
通過每個請求運行的時間能知道:爬蟲用了異步的方式抓取了8個網站,因為每個請求完成的時間並不是按着順序來的。比如這次運行最先爬取到的網站是鳳凰,接着是百度,並不是百度和新浪
且每個請求完成時間之間的間隔都非常短,可以看作這些請求幾乎是“同時”發起的
通過對比同步和異步爬取最終所花的時間,用多協程異步的爬取方式,確實比同步的爬蟲方式速度更快
其實,案例爬取的數據量還比較小,不能直接體現出更大的速度差異。如果爬的是大量的數據,運用多協程會有更顯著的速度優勢
下面具體解釋一下多協程的代碼

第1、3行代碼:從gevent庫里導入了monkey模塊,這個模塊能將程序轉換成可異步的程序。monkey.patch_all(),它的作用其實就像電腦有時會彈出“是否要用補丁修補漏洞或更新”一樣。它能給程序打上補丁,讓程序變成是異步模式,而不是同步模式。它也叫“猴子補丁” 
要在導入其他庫和模塊前,先把monkey模塊導入進來,並運行monkey.patch_all()。這樣,才能先給程序打上補丁。也可以理解成這是一個規范的寫法
第5行代碼:導入了gevent庫來實現多協程,導入了time模塊來記錄爬取所需時間,導入了requests模塊實現爬取8個網站

第21、23、25行代碼:定義了一個crawler函數,只要調用這個函數,它就會執行【用requests.get()爬取網站】和【打印網址、請求運行時間、狀態碼】這兩個任務

第33行代碼:因為gevent只能處理gevent的任務對象,不能直接調用普通函數,所以需要借助gevent.spawn()來創建任務對象
這里需要注意一點:gevent.spawn()的參數需為要調用的函數名及該函數的參數。比如,gevent.spawn(crawler,url)就是創建一個執行crawler函數的任務,參數為crawler函數名和它自身的參數url

第35行代碼:用append函數把任務添加到tasks_list的任務列表里
第37行代碼:調用gevent庫里的gevent.joinall()方法,能啟動執行所有的任務。gevent.joinall(tasks_list)就是執行tasks_list這個任務列表里的所有任務,開始爬取

總結一下用gevent實現多協程爬取的重點↓

如果要爬的不是8個網站,而是1000個網站,可以怎么做?
用gevent.spawn()創建1000個爬取任務,再用gevent.joinall()執行這1000個任務
這種方法會有問題:執行1000個任務,就是一下子發起1000次請求,這樣子的惡意請求,會拖垮網站的服務器

既然直接創建1000個任務的方式不可取,那創建5個任務,每個任務爬取200個網站
這么做也是會有問題的:就算用gevent.spawn()創建了5個分別執行爬取200個網站的任務,這5個任務之間是異步執行的,但是每個任務(爬取200個網站)內部是同步的。這意味着如果有一個任務在執行的過程中,它要爬取的一個網站一直在等待響應,哪怕其他任務都完成了200個網站的爬取,它也還是不能完成200個網站的爬取

銀行是怎么在一天內辦理1000個客戶的業務的,銀行會開設辦理業務的多個窗口,讓客戶取號排隊,由銀行的叫號系統分配客戶到不同的窗口去辦理業務
在gevent庫中,也有一個模塊可以實現這種功能——queue模塊
2. queue模塊
當用多協程來爬蟲,需要創建大量任務時,可以借助queue模塊
queue翻譯成中文是隊列的意思。可以用queue模塊來存儲任務,讓任務都變成一條整齊的隊列,就像銀行窗口的排號做法。因為queue其實是一種有序的數據結構,可以用來存取數據
這樣,協程就可以從隊列里把任務提取出來執行,直到隊列空了,任務也就處理完了。就像銀行窗口的工作人員會根據排號系統里的排號,處理客人的業務,如果已經沒有新的排號,就意味着客戶的業務都已辦理完畢

依舊是爬取剛才的8個網站

from gevent import monkey
monkey.patch_all()
import gevent,time,requests
from gevent.queue import Queue
# 從gevent庫的queue模塊導入Queue類

start = time.time()

url_list = ['https://www.baidu.com/',
            'https://www.sina.com.cn/',
            'http://www.sohu.com/',
            'https://www.qq.com/',
            'https://www.163.com/',
            'http://www.iqiyi.com/',
            'https://www.tmall.com/',
            'http://www.ifeng.com/']

work = Queue()
# 創建隊列對象,並賦值給work
for url in url_list:
    work.put_nowait(url)
    # 用put_nowait()函數可以把網址都放進隊列里

def crawler():
    while not work.empty():
    # 當隊列不是空的時候,就執行下面的程序
        url = work.get_nowait()
        # 用get_nowait()函數可以把隊列里的網址都取出
        r = requests.get(url)
        # 用requests.get()函數抓取網址
        print(url, work.qsize(), r.status_code)
        # 打印網址、隊列長度、抓取請求的狀態碼

tasks_list = []

for x in range(2):
# 相當於創建了2個爬蟲
    task = gevent.spawn(crawler)
    # 用gevent.spawn()函數創建執行crawler()函數的任務
    tasks_list.append(task)
    
gevent.joinall(tasks_list)

end = time.time()
print(end-start)
# 》》https://www.baidu.com/ 6 200
# 》》https://www.sina.com.cn/ 5 200
# 》》http://www.sohu.com/ 4 200
# 》》https://www.163.com/ 3 200
# 》》https://www.qq.com/ 2 200
# 》》http://www.iqiyi.com/ 1 200
# 》》http://www.ifeng.com/ 0 200
# 》》https://www.tmall.com/ 0 200
# 》》0.3898591995239258

運行程序后,打印的網址后面的數字指的是隊列里還剩的任務數,比如第一個網址后面的數字6,就是此時隊列里還剩6個抓取其他網址的任務
Queue()創建的對象,相當於創建了一個不限任何存儲數量的空隊列。如果往Queue()中傳入參數,比如Queue(10),則表示這個隊列只能存儲10個任務
創建了Queue對象后,就能調用這個對象的put_nowait()方法,把每個網址都存儲進剛剛建立好的空隊列里
work.put_nowait(url)這行代碼就是把遍歷的8個網站,都存儲進隊列里
empty()方法,是用來判斷隊列是不是空了的;get_nowait()方法,是用來從隊列里提取數據的;qsize()方法,是用來判斷隊列里還剩多少數量的

queue模塊的重點內容就是隊列怎么創建、數據怎么存儲進隊列,以及怎么從隊列里提取出的數據

然后創建了兩只可以異步爬取的爬蟲。它們會從隊列里取走網址,執行爬取任務。一旦一個網址被一只爬蟲取走,另一只爬蟲就取不到了,另一只爬蟲就會取走下一個網址。直至所有網址都被取走,隊列為空時,爬蟲就停止工作

繼續說計算機歷史小知識:在后來,CPU從單核終於進化到了多核,每個核都能夠獨立運作。計算機開始能夠真正意義上同時執行多個任務(術語叫並行執行),而不是在多個任務之間來回切換(術語叫並發執行)
比如現在打開瀏覽器看着網頁的同時,可以打開音樂播放器聽歌,還可以打開Excel。對於多核CPU而言,這些任務就都是同時運行的
時至今日,電腦一般都會是多核CPU。多協程,其實只占用了CPU的一個核運行,沒有充分利用到其他核。利用CPU的多個核同時執行任務的技術,把它叫做“多進程”
所以,真正大型的爬蟲程序不會單單只靠多協程來提升爬取速度的。比如,百度搜索引擎,可以說是超大型的爬蟲程序,它除了靠多協程,一定還會靠多進程,甚至是分布式爬蟲

第12關  協程實踐

項目:用多協程爬取薄荷網的食物熱量
任何完成項目的過程,都是由以下三步構成的↓

1. 明確目標
用多協程爬取薄荷網11個常見食物分類里的食物信息(包含食物名、熱量、食物詳情頁面鏈接)
2. 分析過程

按照上圖方法,得知想要的數據直接存在HTML里
再看第0個請求的Headers,可以發現薄荷網的網頁請求方式是get
經過分析可以得出薄荷網每個食物類別的每一頁食物記錄的網址規律↓

然后在<li class="item clearfix">元素下,找到了食物的信息,包括食物詳情的鏈接、食物名和熱量

總結一下分析得出的思路↓

3. 代碼實現
這里僅展示
前3個常見食物分類的前3頁和第11個常見食物分類的前3頁的食物信息

# 導入所需的庫和模塊
from gevent import monkey
monkey.patch_all()
# 讓程序變成異步模式
import gevent,requests, bs4, csv
from gevent.queue import Queue

work = Queue()
# 創建隊列對象,並賦值給work

# 前3個常見食物分類的前3頁的食物記錄的網址
url_1 = 'http://www.boohee.com/food/group/{type}?page={page}'
for x in range(1, 4):
    for y in range(1, 4):
        real_url = url_1.format(type=x, page=y)
        work.put_nowait(real_url)
# 通過兩個for循環,能設置分類的數字和頁數的數字
# 然后,把構造好的網址用put_nowait方法添加進隊列里

# 第11個常見食物分類的前3頁的食物記錄的網址
url_2 = 'http://www.boohee.com/food/view_menu?page={page}'
for x in range(1, 4):
    real_url = url_2.format(page=x)
    work.put_nowait(real_url)
# 通過for循環,能設置第11個常見食物分類的食物的頁數
# 然后,把構造好的網址用put_nowait方法添加進隊列里

def crawler():
# 定義crawler函數
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
    }
    # 添加請求頭
    while not work.empty():
    # 當隊列不是空的時候,就執行下面的程序
        url = work.get_nowait()
        # 用get_nowait()方法從隊列里把剛剛放入的網址提取出來
        res = requests.get(url, headers=headers)
        # 用requests.get獲取網頁源代碼
        bs_res = bs4.BeautifulSoup(res.text, 'html.parser')
        # 用BeautifulSoup解析網頁源代碼
        foods = bs_res.find_all('li', class_='item clearfix')
        # 用find_all提取出<li class="item clearfix">標簽的內容
        for food in foods:
        # 遍歷foods
            food_name = food.find_all('a')[1]['title']
            # 用find_all在<li class="item clearfix">標簽下,提取出第2個<a>元素title屬性的值,也就是食物名稱
            food_url = 'http://www.boohee.com' + food.find_all('a')[1]['href']
            # 用find_all在<li class="item clearfix">元素下,提取出第2個<a>元素href屬性的值,跟'http://www.boohee.com'組合在一起,就是食物詳情頁的鏈接
            food_calorie = food.find('p').text
            # 用find在<li class="item clearfix">標簽下,提取<p>元素,再用text方法留下純文本,也提取出了食物的熱量
            writer.writerow([food_name, food_calorie, food_url])
            # 借助writerow()函數,把提取到的數據:食物名稱、食物熱量、食物詳情鏈接,寫入csv文件
            print(food_name)
            # 打印食物的名稱


csv_file = open('boohee.csv', 'w', newline='')
# 調用open()函數打開csv文件,傳入參數:文件名“boohee.csv”、寫入模式“w”、newline=''
writer = csv.writer(csv_file)
# 用csv.writer()函數創建一個writer對象
writer.writerow(['食物', '熱量', '鏈接'])
# 借助writerow()函數往csv文件里寫入文字:食物、熱量、鏈接

tasks_list = []
# 創建空的任務列表
for x in range(5):
# 相當於創建了5個爬蟲
    task = gevent.spawn(crawler)
    # 用gevent.spawn()函數創建執行crawler()函數的任務
    tasks_list.append(task)
    # 往任務列表添加任務
gevent.joinall(tasks_list)
# 用gevent.joinall方法,啟動協程,執行任務列表里的所有任務,讓爬蟲開始爬取網站

第13關  Scrapy框架

之前寫的爬蟲,要導入和操作不同的模塊,比如requests模塊、gevent庫、csv模塊等。而在Scrapy里,不需要這么做,因為很多爬蟲需要涉及的功能,比如麻煩的異步,在Scrapy框架都自動實現了
之前編寫爬蟲的方式,相當於在一個個地在拼零件,拼成一輛能跑的車。而Scrapy框架則是已經造好的、現成的車,只要踩下它的油門,它就能跑起來。這樣便節省了開發項目的時間

1. Scrapy的結構

上面的這張圖是Scrapy的整個結構。可以把整個Scrapy框架看成是一家爬蟲公司。最中心位置的Scrapy Engine(引擎)就是這家爬蟲公司的大boss,負責統籌公司的4大部門,每個部門都只聽從它的命令,並只向它匯報工作
Scheduler(調度器)部門主要負責處理引擎發送過來的requests對象(即網頁請求的相關信息集合,包括params,data,cookies,request headers…等),會把請求的url以有序的方式排列成隊,並等待引擎來提取(功能上類似於gevent庫的queue模塊)
Downloader(下載器)部門則是負責處理引擎發送過來的requests,進行網頁爬取,並將返回的response(爬取到的內容)交給引擎。它對應的是爬蟲流程【獲取數據】這一步
Spiders(爬蟲)部門是公司的核心業務部門,主要任務是創建requests對象和接受引擎發送過來的response(Downloader部門爬取到的內容),從中解析並提取出有用的數據。它對應的是爬蟲流程【解析數據】和【提取數據】這兩步
Item Pipeline(數據管道)部門則是公司的數據部門,只負責存儲和處理Spiders部門提取到的有用數據。這個對應的是爬蟲流程【存儲數據】這一步
Downloader Middlewares(下載中間件)的工作相當於下載器部門的秘書,比如會提前對引擎大boss發送的諸多requests做出處理
Spider Middlewares(爬蟲中間件)的工作則相當於爬蟲部門的秘書,比如會提前接收並處理引擎大boss發送來的response,過濾掉一些重復無用的東西

2. Scrapy的工作原理
Scrapy框架的工作原理——引擎是中心,其他組成部分由引擎調度
在Scrapy里,整個爬蟲程序的流程都不需要我們去操心,且Scrapy中的程序全部都是異步模式,所有的請求或返回的響應都由引擎自動分配去處理
哪怕有某個請求出現異常,程序也會做異常處理,跳過報錯的請求,繼續往下運行程序
在一定程度上,Scrapy可以說是非常讓人省心的一套爬蟲框架
3. Scrapy的用法-創建項目
Scrapy需要安裝,mac電腦在終端輸入命令:pip3 install scrapy
記錄一下安裝過程↓

Collecting scrapy
  Downloading Scrapy-2.0.1-py2.py3-none-any.whl (242 kB)
     |████████████████████████████████| 242 kB 341 kB/s 
Collecting zope.interface>=4.1.3
  Downloading zope.interface-5.0.2-cp38-cp38-macosx_10_9_x86_64.whl (184 kB)
     |████████████████████████████████| 184 kB 875 kB/s 
Collecting lxml>=3.5.0
  Downloading lxml-4.5.0-cp38-cp38-macosx_10_9_x86_64.whl (4.6 MB)
     |████████████████████████████████| 4.6 MB 626 kB/s 
Collecting parsel>=1.5.0
  Downloading parsel-1.5.2-py2.py3-none-any.whl (12 kB)
Collecting pyOpenSSL>=16.2.0
  Downloading pyOpenSSL-19.1.0-py2.py3-none-any.whl (53 kB)
     |████████████████████████████████| 53 kB 4.8 MB/s 
Collecting cssselect>=0.9.1
  Downloading cssselect-1.1.0-py2.py3-none-any.whl (16 kB)
Collecting protego>=0.1.15
  Downloading Protego-0.1.16.tar.gz (3.2 MB)
     |████████████████████████████████| 3.2 MB 520 kB/s 
Collecting Twisted>=17.9.0
  Downloading Twisted-20.3.0.tar.bz2 (3.1 MB)
     |████████████████████████████████| 3.1 MB 626 kB/s 
Collecting queuelib>=1.4.2
  Downloading queuelib-1.5.0-py2.py3-none-any.whl (13 kB)
Collecting service-identity>=16.0.0
  Downloading service_identity-18.1.0-py2.py3-none-any.whl (11 kB)
Collecting PyDispatcher>=2.0.5
  Downloading PyDispatcher-2.0.5.tar.gz (34 kB)
Collecting w3lib>=1.17.0
  Downloading w3lib-1.21.0-py2.py3-none-any.whl (20 kB)
Collecting cryptography>=2.0
  Downloading cryptography-2.9-cp35-abi3-macosx_10_9_intel.whl (1.7 MB)
     |████████████████████████████████| 1.7 MB 615 kB/s 
Requirement already satisfied: setuptools in /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages (from zope.interface>=4.1.3->scrapy) (41.2.0)
Collecting six>=1.5.2
  Downloading six-1.14.0-py2.py3-none-any.whl (10 kB)
Collecting constantly>=15.1
  Downloading constantly-15.1.0-py2.py3-none-any.whl (7.9 kB)
Collecting incremental>=16.10.1
  Downloading incremental-17.5.0-py2.py3-none-any.whl (16 kB)
Collecting Automat>=0.3.0
  Downloading Automat-20.2.0-py2.py3-none-any.whl (31 kB)
Collecting hyperlink>=17.1.1
  Downloading hyperlink-19.0.0-py2.py3-none-any.whl (38 kB)
Collecting PyHamcrest!=1.10.0,>=1.9.0
  Downloading PyHamcrest-2.0.2-py3-none-any.whl (52 kB)
     |████████████████████████████████| 52 kB 1.9 MB/s 
Collecting attrs>=19.2.0
  Downloading attrs-19.3.0-py2.py3-none-any.whl (39 kB)
Collecting pyasn1-modules
  Downloading pyasn1_modules-0.2.8-py2.py3-none-any.whl (155 kB)
     |████████████████████████████████| 155 kB 506 kB/s 
Collecting pyasn1
  Downloading pyasn1-0.4.8-py2.py3-none-any.whl (77 kB)
     |████████████████████████████████| 77 kB 778 kB/s 
Collecting cffi!=1.11.3,>=1.8
  Downloading cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl (175 kB)
     |████████████████████████████████| 175 kB 972 kB/s 
Requirement already satisfied: idna>=2.5 in /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages (from hyperlink>=17.1.1->Twisted>=17.9.0->scrapy) (2.8)
Collecting pycparser
  Downloading pycparser-2.20-py2.py3-none-any.whl (112 kB)
     |████████████████████████████████| 112 kB 496 kB/s 
Installing collected packages: zope.interface, lxml, six, w3lib, cssselect, parsel, pycparser, cffi, cryptography, pyOpenSSL, protego, constantly, incremental, attrs, Automat, hyperlink, PyHamcrest, Twisted, queuelib, pyasn1, pyasn1-modules, service-identity, PyDispatcher, scrapy
    Running setup.py install for protego ... done
    Running setup.py install for Twisted ... done
    Running setup.py install for PyDispatcher ... done
Successfully installed Automat-20.2.0 PyDispatcher-2.0.5 PyHamcrest-2.0.2 Twisted-20.3.0 attrs-19.3.0 cffi-1.14.0 constantly-15.1.0 cryptography-2.9 cssselect-1.1.0 hyperlink-19.0.0 incremental-17.5.0 lxml-4.5.0 parsel-1.5.2 protego-0.1.16 pyOpenSSL-19.1.0 pyasn1-0.4.8 pyasn1-modules-0.2.8 pycparser-2.20 queuelib-1.5.0 scrapy-2.0.1 service-identity-18.1.0 six-1.14.0 w3lib-1.21.0 zope.interface-5.0.2

下面通過Scrapy框架來爬取豆瓣Top250圖書前三頁書籍的信息,也就是爬取前75本書籍的信息(包含書名、出版信息和書籍評分)
豆瓣Top250圖書的鏈接:https://book.douban.com/top250
首先在終端進入到想要保存項目的目錄,輸入一行創建Scrapy項目的命令:scrapy startproject douban,douban就是Scrapy項目的名字。按下enter鍵,一個Scrapy項目就創建成功了

整個scrapy項目的結構,如下圖所示↓

Scrapy項目里每個文件都有特定的功能,比如settings.py是scrapy里的各種設置,items.py是用來定義數據的,pipelines.py是用來處理數據的,它們對應的就是Scrapy的結構中的Item Pipeline(數據管道)
4. Scrapy的用法-編輯爬蟲
可以在spiders這個文件夾里創建爬蟲文件,這個文件命名為top250。后面的大部分代碼都需要在這個top250.py文件里編寫

先在top250.py文件里導入需要的模塊

import scrapy
import bs4

導入scrapy是待會要用創建類的方式寫這個爬蟲,所創建的類將直接繼承scrapy中的scrapy.Spider類。這樣,有許多好用屬性和方法,就能夠直接使用
在Scrapy中,每個爬蟲的代碼結構基本都如下所示↓

class DoubanSpider(scrapy.Spider):
    name = 'douban'
    allowed_domains = ['book.douban.com']
    start_urls = ['https://book.douban.com/top250?start=0']

    def parse(self, response):
        print(response.text)

定義一個爬蟲類DoubanSpider,繼承自scrapy.Spider類
name是定義爬蟲的名字,這個名字是爬蟲的唯一標識。name='douban'意思是定義爬蟲的名字為douban。等會啟動爬蟲的時候,要用到這個名字
allowed_domains是定義允許爬蟲爬取的網址域名(不需要加https://)。如果網址的域名不在這個列表里,就會被過濾掉
當在爬取大量數據時,經常是從一個URL開始爬取,然后關聯爬取更多的網頁。比如,假設這個爬蟲目標不是爬書籍信息,而是要爬豆瓣圖書top250的書評,就會先爬取書單,再找到每本書的URL,再進入每本書的詳情頁面去抓取評論,allowed_domains就限制了這種關聯爬取的URL一定在book.douban.com這個域名之下,不會跳轉到某個奇怪的廣告頁面
start_urls是定義起始網址,就是爬蟲從哪個網址開始抓取。在此,allowed_domains的設定對start_urls里的網址不會有影響
parse是Scrapy里默認處理response的一個方法,中文是解析

把豆瓣Top250圖書前3頁網址塞進start_urls的列表里,完善后的代碼如下↓

class DoubanSpider(scrapy.Spider):
    name = 'douban'
    allowed_domains = ['book.douban.com']
    start_urls = []
    for x in range(3):
        url = 'https://book.douban.com/top250?start=' + str(x * 25)
        start_urls.append(url)

接下來,只要再借助parse方法處理response,借助BeautifulSoup來取出想要的書籍信息的數據,代碼即可完成
按照過去的知識,可能會把代碼寫成這個模樣↓

import scrapy
import bs4
from ..items import DoubanItem


class DoubanSpider(scrapy.Spider):
# 定義一個爬蟲類DoubanSpider。
    name = 'douban'
    # 定義爬蟲的名字為douban。
    allowed_domains = ['book.douban.com']
    # 定義爬蟲爬取網址的域名。
    start_urls = []
    # 定義起始網址。
    for x in range(3):
        url = 'https://book.douban.com/top250?start=' + str(x * 25)
        start_urls.append(url)
        # 把豆瓣Top250圖書的前3頁網址添加進start_urls。

    def parse(self, response):
    # parse是默認處理response的方法。
        bs = bs4.BeautifulSoup(response.text, 'html.parser')
        # 用BeautifulSoup解析response。
        datas = bs.find_all('tr', class_="item")
        # 用find_all提取<tr class="item">元素,這個元素里含有書籍信息。
        for data in datas:
            # 遍歷data。
            title = data.find_all('a')[1]['title']
            # 提取出書名。
            publish = data.find('p', class_='pl').text
            # 提取出出版信息。
            score = data.find('span', class_='rating_nums').text
            # 提取出評分。
            print([title, publish, score])
            # 打印上述信息。

按照過去,會把書名、出版信息、評分,分別賦值,然后統一做處理——或是打印,或是存儲。但在scrapy這里,事情卻有所不同
spiders(如top250.py)只干spiders應該做的事。對數據的后續處理,另有人負責
5. Scrapy的用法-定義數據
在scrapy中會專門定義一個用於記錄數據的類
當每一次,要記錄數據的時候,比如前面在每一個最小循環里,都要記錄“書名”,“出版信息”,“評分”。會實例化一個對象,利用這個對象來記錄數據
每一次,當數據完成記錄,它會離開spiders,來到Scrapy Engine(引擎),引擎將它送入Item Pipeline(數據管道)處理
定義這個類的py文件,正是items.py
要爬取的數據是書名、出版信息和評分,來看看如何在items.py里定義這些數據。代碼如下↓

import scrapy
# 導入scrapy
class DoubanItem(scrapy.Item):
# 定義一個類DoubanItem,它繼承自scrapy.Item
    title = scrapy.Field()
    # 定義書名的數據屬性
    publish = scrapy.Field()
    # 定義出版信息的數據屬性
    score = scrapy.Field()
    # 定義評分的數據屬性

導入了scrapy,目的是,等會所創建的類將直接繼承scrapy中的scrapy.Item類。這樣,有許多好用屬性和方法,就能夠直接使用。比如到后面,引擎能將item類的對象發給Item Pipeline(數據管道)處理
然后定義了一個DoubanItem類,它繼承自scrapy.Item類
之后的代碼是定義了書名、出版信息和評分三種數據。scrapy.Field()這句代碼實現的是,讓數據能以類似字典的形式記錄。舉例看下↓

import scrapy
# 導入scrapy
class DoubanItem(scrapy.Item):
# 定義一個類DoubanItem,它繼承自scrapy.Item
    title = scrapy.Field()
    # 定義書名的數據屬性
    publish = scrapy.Field()
    # 定義出版信息的數據屬性
    score = scrapy.Field()
    # 定義評分的數據屬性

book = DoubanItem()
# 實例化一個DoubanItem對象
book['title'] = '海邊的卡夫卡'
book['publish'] = '[日] 村上春樹 / 林少華 / 上海譯文出版社 / 2003'
book['score'] = '8.1'
print(book)
print(type(book))
# 》》{'publish': '[日] 村上春樹 / 林少華 / 上海譯文出版社 / 2003',
# 》》'score': '8.1',
# 》》'title': '海邊的卡夫卡'}
# 》》<class '__main__.DoubanItem'>

會看到打印出來的結果的確和字典非常相像,但它卻並不是dict,它的數據類型是DoubanItem,屬於“自定義的Python字典”。可以利用類似上述代碼的樣式,去重新寫top250.py

import scrapy
import bs4
from ..items import DoubanItem
# 需要引用DoubanItem,它在items里面。因為是items在top250.py的上一級目錄,所以要用..items,這是一個固定用法


class DoubanSpider(scrapy.Spider):
# 定義一個爬蟲類DoubanSpider
    name = 'douban'
    # 定義爬蟲的名字為douban
    allowed_domains = ['book.douban.com']
    # 定義爬蟲爬取網址的域名
    start_urls = []
    # 定義起始網址
    for x in range(3):
        url = 'https://book.douban.com/top250?start=' + str(x * 25)
        start_urls.append(url)
        # 把豆瓣Top250圖書的前3頁網址添加進start_urls

    def parse(self, response):
    # parse是默認處理response的方法
        bs = bs4.BeautifulSoup(response.text, 'html.parser')
        # 用BeautifulSoup解析response
        datas = bs.find_all('tr', class_="item")
        # 用find_all提取<tr class="item">元素,這個元素里含有書籍信息
        for data in datas:
        # 遍歷data
            item = DoubanItem()
            # 實例化DoubanItem這個類
            item['title'] = data.find_all('a')[1]['title']
            # 提取出書名,並把這個數據放回DoubanItem類的title屬性里
            item['publish'] = data.find('p', class_='pl').text
            # 提取出出版信息,並把這個數據放回DoubanItem類的publish里
            item['score'] = data.find('span', class_='rating_nums').text
            # 提取出評分,並把這個數據放回DoubanItem類的score屬性里
            print(item['title'])
            # 打印書名
            yield item
            # yield item是把獲得的item傳遞給引擎

當每一次,要記錄數據的時候,會實例化一個item對象,利用這個對象來記錄數據
每一次,當數據完成記錄,它會離開spiders,來到Scrapy Engine(引擎),引擎將它送入Item Pipeline(數據管道)處理。這里,要用到yield語句
yield語句可以簡單理解為:它有點類似return,不過它和return不同的點在於,它不會結束函數,且能多次返回信息
程序運行的過程:爬蟲(Spiders)會把豆瓣的10個網址封裝成requests對象,引擎會從爬蟲(Spiders)里提取出requests對象,再交給調度器(Scheduler),讓調度器把這些requests對象排序處理。然后引擎再把經過調度器處理的requests對象發給下載器(Downloader),下載器會立馬按照引擎的命令爬取,並把response返回給引擎。緊接着引擎就會把response發回給爬蟲(Spiders),這時爬蟲會啟動默認的處理response的parse方法,解析和提取出書籍信息的數據,使用item做記錄,返回給引擎。引擎將它送入Item Pipeline(數據管道)處理
5. Scrapy的用法-設置
運行時可能還是會報錯,原因在於Scrapy里的默認設置沒被修改。比如需要修改請求頭,點擊settings.py文件,能在里面找到如下的默認設置代碼↓

# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'douban (+http://www.yourdomain.com)'

# Obey robots.txt rules
ROBOTSTXT_OBEY = True 

USER_AGENT的注釋取消(刪除#),然后替換掉user-agent的內容,就是修改了請求頭
又因為Scrapy是遵守robots協議的,如果是robots協議禁止爬取的內容,Scrapy也會默認不去爬取,所以還得修改Scrapy中的默認設置
ROBOTSTXT_OBEY=True改成ROBOTSTXT_OBEY=False,就是把遵守robots協議換成無需遵從robots協議,這樣Scrapy就能不受限制地運行

# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

5. Scrapy的用法-運行
想要運行Scrapy有兩種方法,一種是在本地電腦的終端跳轉到scrapy項目的文件夾(跳轉方法:cd+文件夾的路徑名),然后輸入命令行:scrapy crawl douban(douban 就是爬蟲的名字)

另一種運行方式需要在最外層的大文件夾里新建一個main.py文件(與scrapy.cfg同級)

只需要在這個main.py文件里,輸入以下代碼,點擊運行,Scrapy的程序就會啟動

from scrapy import cmdline
# 導入cmdline模塊,可以實現控制終端命令行
cmdline.execute(['scrapy', 'crawl', 'douban'])
# 用execute()方法,輸入運行scrapy的命令

在Scrapy中有一個可以控制終端命令的模塊cmdline。導入了這個模塊,就能操控終端
在cmdline模塊中,有一個execute方法能執行終端的命令行,不過這個方法需要傳入列表的參數。想輸入運行Scrapy的代碼scrapy crawl douban,就需要寫成['scrapy','crawl','douban']這樣
這個項目先寫了爬蟲,再定義數據。但是,在實際項目實戰中,常常順序卻是相反的——先定義數據,再寫爬蟲。所以,流程圖應如下↓

第14關  Scrapy實操

1. 明確目標
爬取職友集本月最佳人氣企業榜10家公司的招聘信息,包括公司名稱、職位名稱、工作地點和招聘要求。鏈接:https://www.jobui.com/rank/company/
2. 分析過程
經“檢查”,公司和后面的招聘信息均在html里

通過<ul class="textList flsty cfix">里面的<a>標簽中的href屬性可以通往公司的詳情頁面
到公司詳情頁面,點擊“招聘”頁簽,可以總結出公司招聘信息的網址規律↓

公司名稱可以在<a class="company-banner-name">的文本中找到

在<div class="c-job-list">下可以找到各個職位信息
通過<a>元素里的<h3>元素的文本取到職位名稱
通過第1個<span>標簽的title屬性取到工作地點
通過第2個<span>標簽的title屬性取到職位要求
3. 代碼實現-創建Scrapy項目
在終端跳轉到想要保存項目的目錄,輸入創建Scrapy項目的命令:scrapy startproject jobui(jobui是職友集網站的英文名,在這里可以把它作為Scrapy項目的名字)
4. 代碼實現-定義item(數據)

import scrapy

class JobuiItem(scrapy.Item):
# 定義了一個繼承自scrapy.Item的JobuiItem類
    company = scrapy.Field()
    # 定義公司名稱的數據屬性
    position = scrapy.Field()
    # 定義職位名稱的數據屬性
    address = scrapy.Field()
    # 定義工作地點的數據屬性
    detail = scrapy.Field()
    # 定義招聘要求的數據屬性

5. 創建和編寫spiders文件
在spiders文件夾下創建爬蟲文件,命名為jobui_ jobs.py
在Scrapy里,獲取網頁源代碼這件事兒,會由引擎分配給下載器去做,不需要自己處理。之所以要構造新的requests對象,是為了告訴引擎,新的請求需要傳入什么參數,這樣才能讓引擎拿到的是正確requests對象,交給下載器處理
構造了新的requests對象,就得定義與之匹配的用來處理response的新方法。這樣才能提取出想要的招聘信息的數據

# 導入模塊
import scrapy
import bs4
from ..items import JobuiItem

class JobuiSpider(scrapy.Spider):
# 定義一個爬蟲類JobuiSpider
    name = 'jobui'
    # 定義爬蟲的名字為jobui
    allowed_domains = ['www.jobui.com']
    # 定義允許爬蟲爬取網址的域名——職友集網站的域名
    start_urls = ['https://www.jobui.com/rank/company/']
    # 定義起始網址——職友集企業排行榜的網址

    # 提取公司id標識和構造公司招聘信息的網址
    def parse(self, response):
    # parse是默認處理response的方法
        bs = bs4.BeautifulSoup(response.text, 'html.parser')
        # 用BeautifulSoup解析response(企業排行榜的網頁源代碼)
        ul_list = bs.find_all('ul', class_="textList flsty cfix")
        # 用find_all提取<ul class_="textList flsty cfix">標簽
        for ul in ul_list:
        # 遍歷ul_list
            a_list = ul.find_all('a')
            # 用find_all提取出<ul class_="textList flsty cfix">元素里的所有<a>元素
            for a in a_list:
            # 再遍歷a_list
                company_id = a['href']
                # 提取出所有<a>元素的href屬性的值,也就是公司id標識
                url = 'https://www.jobui.com{id}jobs'.format(id=company_id)
                # 構造出公司招聘信息的網址鏈接
                yield scrapy.Request(url, callback=self.parse_job)
                # 用yield語句把構造好的request對象傳遞給引擎。用scrapy.Request構造request對象。callback參數設置調用parse_job方法

    # 解析和提取公司招聘信息的數據
    def parse_job(self, response):
    # 定義新的處理response的方法parse_job(方法的名字可以自己起)
        bs = bs4.BeautifulSoup(response.text, 'html.parser')
        # 用BeautifulSoup解析response(公司招聘信息的網頁源代碼)
        company = bs.find('a',class_='company-banner-name').text
        # 用find方法提取出公司名稱
        datas = bs.find_all('div', class_="c-job-list")
        # 用find_all提取<div class_="c-job-list">標簽,里面含有招聘信息的數據
        for data in datas:
            # 遍歷datas
            item = JobuiItem()
            # 實例化JobuiItem這個類
            item['company'] = company
            # 把公司名稱放回JobuiItem類的company屬性里
            item['position'] = data.find('a').find('h3').text
            # 提取出職位名稱,並把這個數據放回JobuiItem類的position屬性里
            item['address'] = data.find_all('span')[0]['title']
            # 提取出工作地點,並把這個數據放回JobuiItem類的address屬性里
            item['detail'] = data.find_all('span')[1]['title']
            # 提取出招聘要求,並把這個數據放回JobuiItem類的detail屬性里
            yield item
            # 用yield語句把item傳遞給引擎 

scrapy.Request是構造requests對象的類;url是我們往requests對象里傳入的每家公司招聘信息網址的參數;callback的中文意思是回調,self.parse_job是新定義的parse_job方法,往requests對象里傳入callback=self.parse_job這個參數后,引擎就能知道response要前往的下一站,是parse_job()方法;yield語句就是用來把這個構造好的requests對象傳遞給引擎
6. 存儲文件
在Scrapy里,把數據存儲成csv文件和Excel文件,也有分別對應的方法
存儲成csv文件的方法,只需在settings.py文件里,添加如下的代碼↓

FEED_URI = './storage/data/%(name)s.csv'
FEED_FORMAT = 'CSV'
FEED_EXPORT_ENCODING = 'utf-8'

FEED_URI是導出文件的路徑。'./storage/data/%(name)s.csv',就是把存儲的文件放到與scrapy.cfg文件同級的storage文件夾的data子文件夾里
FEED_FORMAT 是導出數據格式,寫CSV就能得到CSV格式
FEED_EXPORT_ENCODING 是導出文件編碼,utf-8是用在mac電腦上的編碼格式,寫ansi是一種在windows上的編碼格式
存儲成Excel文件的方法,需要先在settings.py里設置啟用ITEM_PIPELINES,設置方法只要取消ITEM_PIPELINES的注釋(刪掉#)

# 取消`ITEM_PIPELINES`的注釋后:

# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'jobui.pipelines.JobuiPipeline': 300,
}

接着,就可以去編輯pipelines.py文件。存儲Excel文件,依舊是用openpyxl模塊來實現

import openpyxl

class JobuiPipeline(object):
# 定義一個JobuiPipeline類,負責處理item
    def __init__(self):
    # 初始化函數 當類實例化時這個方法會自啟動
        self.wb = openpyxl.Workbook()
        # 創建工作薄
        self.ws = self.wb.active
        # 定位活動表
        self.ws.append(['公司', '職位', '地址', '招聘信息'])
        # 用append函數往表格添加表頭

    def process_item(self, item, spider):
    # process_item是默認的處理item的方法,就像parse是默認處理response的方法
        line = [item['company'], item['position'],
                item['address'], item['detail']]
        # 把公司名稱、職位名稱、工作地點和招聘要求都寫成列表的形式,賦值給line
        self.ws.append(line)
        # 用append函數把公司名稱、職位名稱、工作地點和招聘要求的數據都添加進表格
        return item
        # 將item丟回給引擎,如果后面還有這個item需要經過的itempipeline,引擎會自己調度

    def close_spider(self, spider):
    # close_spider是當爬蟲結束運行時,這個方法就會執行
        self.wb.save('./jobui.xlsx')
        # 保存文件
        self.wb.close()
        # 關閉文件 

7. 修改設置
修改Scrapy中settings.py文件里的默認設置:添加請求頭,以及把ROBOTSTXT_OBEY=True改成ROBOTSTXT_OBEY=False
還有一處默認設置需要修改,需要取消DOWNLOAD_DELAY = 0這行的注釋(刪掉#)。DOWNLOAD_DELAY翻譯成中文是下載延遲的意思,這行代碼可以控制爬蟲的速度。因為這個項目的爬取速度不宜過快,要把下載延遲的時間改成0.5秒

# Configure a delay for requests for the same website (default: 0)
# See https://doc.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 0.5

修改完設置,已經可以運行代碼

第15關  復習與反爬蟲

1. 爬蟲進階路線指引
· 解析與提取
當說要學習解析和提取,是指學習解析庫。除了BeautifulSoup解析、Selenium的自帶解析庫之外,還會有:xpath/lxml等。它們可能語法有所不同,但底層原理都一致,能很輕松上手,然后在一些合適的場景去用它們
正則表達式(re模塊)功能強大,它能讓自己設定一套復雜的規則,然后把目標文本里符合條件的相關內容給找出來
· 存儲
說到存儲,目前已經掌握的知識是csv和excel。它們並不是非常高難度的模塊,可以翻閱它們的官方文檔了解更多的用法。這樣,對於平時的自動化辦公也會有所幫助
但是當數據量變得十分巨大(這在爬蟲界並不是什么新鮮事),同時數據與數據之間的關系,應難以用一張簡單的二維平面表格來承載。那么,需要數據庫的幫助
推薦從MySQLMongoDB這兩個庫開始學起,它們一個是關系型數據庫的典型代表,一個是非關系型數據庫的典型代表
設計兩份表格,一張存儲用戶們的賬戶信息(昵稱,頭像等),一張則存儲用戶們的學習記錄,兩個表格之間,通過唯一的用戶id來進行關聯,這樣的就是關系型數據庫。沒有這種特征的,自然就是非關系型數據庫
學習數據庫,需要接觸另一種語言:SQL
· 數據分析與可視化
徒有海量的數據的意義非常有限。數據,要被分析過才能創造出更深遠的價值。這里邊的技能,叫做數據分析。將數據分析的結論,直觀、有力地傳遞出來,是可視化
這並不是很簡單的技能,學習數據分析,要比爬蟲還要花費更多的時間。推薦的模塊與庫:Pandas/Matplotlib/Numpy/Scikit-Learn/Scipy
· 更多的爬蟲
當有太多的數據要爬取,就要開始關心爬蟲的速度。已經學習過一個可以讓多個爬蟲一起工作的工具——協程
嚴格來說這並不是同時工作,而是電腦在多個任務之間快速地來回切換,看上去就仿佛是爬蟲們同時工作
所以這種工作方式對速度的優化有瓶頸。那么,如果還想有所突破還可以怎么做?
協程在本質上只用到CPU的一個核。而多進程(multiprocessing庫)爬蟲允許使用CPU的多個核,所以可以使用多進程,或者是多進程與多協程結合的方式進一步優化爬蟲
理論上來說,只要CPU允許,開多少個進程,就能讓爬蟲速度提高多少倍
那要是CPU不允許呢?一台普通的電腦,也就8核,但想突破8個進程,應該怎么辦?
答,分布式爬蟲。分布式爬蟲,就是讓多個設備,去跑同一個項目
創建一個共享的隊列,隊列里塞滿了待執行的爬蟲任務,讓多個設備從這個共享隊列當中,去獲取任務,並完成執行。這就是分布式爬蟲
如此,就不再有限制爬蟲的瓶頸——多加設備就行。在企業內,面對大量爬蟲任務,他們也是使用分布式的方式來進行爬蟲作業
實現分布式爬蟲,需要下一個組塊的內容——框架
· 更強大的爬蟲——框架
目前,已經簡單地學過Scrapy框架的基本原理和用法。而一些更深入的用法:使用Scrapy模擬登錄、存儲數據庫、使用HTTP代理、分布式爬蟲……這些還不曾涉及
不過這些知識大數都能輕松上手,因為它們的底層邏輯都已掌握,剩下的不過是一些語法問題罷了
推薦先去更深入地學習、了解Scrapy框架,然后再了解一些其他的優秀框架,如:PySpider
2. 反爬蟲應對策略匯總
幾乎所有的技術人員對反爬蟲都有一個共識:所謂的反爬蟲,從不是將爬蟲完全杜絕;而是想辦法將爬蟲的訪問量限制在一個可接納的范圍,不要讓它過於肆無忌憚
原因很簡單:爬蟲代碼寫到最后,已經和真人訪問網絡毫無區別。服務器的那一端完全無法判斷是人還是爬蟲。如果想要完全禁止爬蟲,正常用戶也會無法訪問。所以只能想辦法進行限制,而非禁止
有的網站會限制請求頭,即Request Headers。那就去填寫user-agent聲明自己的身份,有時還要去填寫originreferer聲明請求的來源
有的網站會限制登錄,不登錄就不給訪問。那就用cookies和session去模擬登錄
有的網站會做一些復雜的交互,比如設置“驗證碼”來阻攔登錄。這就比較難做,解決方案一般有二:用Selenium去手動輸入驗證碼;用一些圖像處理的庫自動識別驗證碼(tesserocr/pytesserart/pillow)
有的網站會做IP限制,如果一個IP地址爬取網站頻次太高,那么服務器就會暫時封掉來自這個IP地址的請求。解決方案有二:使用time.sleep()來對爬蟲的速度進行限制;建立IP代理池(可以在網絡上搜索可用的IP代理),一個IP不能用了就換一個用。大致語法是這樣↓

import requests
url = 'https://…'
proxies = {'http': 'http://…'}
# ip地址
response = requests.get(url, proxies=proxies)

寫爬蟲最重要的是什么?確認目標-分析過程-先面向過程一行行實現代碼-代碼封裝。最重要的是確認目標,最難的是分析過程,寫代碼不過是水到渠成的事


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM