前面的關卡中,我們學習瞭如何用協程來提升爬蟲的速度,並且通過項目實操,將協程運用於抓取HI運動的食物數據。
不知道你會不會有這樣一種感覺:要寫出一個完整的爬蟲程序需要做很多瑣碎的工作。比如,要針對不同的網站制定不同的解析方式;要導入不同功能的模塊;還要編寫各種爬取流程的代碼。
我們在日常工作中會使用PPT模板來製作PPT。那麼有沒有一個現成的爬蟲模板,讓我們能夠改之即用,也就是說對這個模板進行適當的修改,就能完成一個爬蟲項目的開發呢?
這種模板在Python中還真的存在,只不過我們一般稱之爲框架。
就像PPT模板會幫你設置好背景、字體和顏色一樣,一個爬蟲框架裏包含了能實現爬蟲整個流程的各種模塊,。
這一關,我們要學習的就是一個功能強大的爬蟲框架——Scrapy。
一、Scrapy是什麼
在我們之前的課程中,我們需要不同模塊實現不同的功能:使用requests模塊進行請求、使用csv模塊存儲數據等。而在Scrapy裏,你不需要這麼做,因爲很多爬蟲需要涉及的功能,比如麻煩的異步,在Scrapy框架都自動實現了。
我們之前編寫爬蟲的方式,就相當於我們在自己裝修一個毛坯房。需要自己去找施工隊來「水電施工」「泥工砌牆貼磚」「木工吊頂打櫃子」「油漆工刷乳膠漆」等等…而框架就相當於一個精裝房,拎包入住!
1)Scrapy的結構
上面的這張圖是Scrapy的整個結構。你可以把整個Scrapy框架當成是你所在的部門。最中心位置的Scrapy Engine(引擎)就是你所在部門的大boss,負責統籌規劃你這個部門的四大職能。
以爬蟲流程的順序來依次介紹Scrapy爬蟲部門的4大職能:
-
Scheduler(調度器)主要負責處理引擎發送過來的requests對象(即網頁請求的相關信息集合,包括params,data,cookies,request headers…等),會把請求的url以有序的方式排列成隊,並等待引擎來提取(功能上類似於gevent庫的queue模塊)。
-
Downloader(下載器)則是負責處理引擎發送過來的requests,進行網頁爬取,並將返回的response(爬取到的內容)交給引擎。它對應的是爬蟲流程【獲取數據】這一步。
-
Spiders(爬蟲)的主要任務是創建requests對象和接受引擎發送過來的response(Downloader部門爬取到的內容),從中解析並提取出有用的數據。它對應的是爬蟲流程【解析數據】和【提取數據】這兩步。
-
Item Pipeline(數據管道)負責存儲和處理Spiders提取到的有用數據。這個對應的是爬蟲流程【存儲數據】這一步。
-
Downloader Middlewares(下載中間件)的工作相當於下載器的助手,比如會提前對引擎發送的諸多requests做出處理。
-
Spider Middlewares(爬蟲中間件)的工作則相當於爬蟲的助手,比如會提前接收並處理引擎發送來的response,過濾掉一些重複無用的東西。
3)Scrapy的工作原理
從這個圖中可以看出 Scrapy 框架的工作原理,即是——引擎就是大Boss,統領其他成員協同工作。
在使用 Scrapy 的時候。我們不需要去關心爬蟲的每個流程。並且Scrapy中的網絡請求都是默認異步模式,請求和返回都會由引擎去自動分配處理。
如果某個請求出現異常,框架也會幫我們做異常處理,跳過這個異常的請求,繼續去執行後面的程序。
所以說,我們使用 Scrapy 可以省掉很多的時間和工作!就像我們直接住進精裝房一樣!
二、Scrapy的用法
現在,我們已經初步瞭解Scrapy的結構以及工作原理。接下來,爲了熟悉Scrapy的用法,我們使用它來完成一個小項目——爬取豆瓣Top250圖書。
1)明確目標與分析過程
我們還是來遵循「項目實現」的三步驟:、
- 明確目標
- 分析過程
- 代碼實現
我們在代碼實現部分,重點講解Scrapy的使用方法。
明確目標,讓我們觀察一下要爬取的網站:https://book.douban.com/top250。豆瓣Top250圖書一共有10頁,每頁有25本書籍。
我們的初步目標是:先只爬取前三頁書籍的信息,總共是 25 x 3 = 75 本,包含書名、出版信息和書籍評分。
那就是分析要爬取的網頁結構。 既然我們要爬取書籍信息,我們就得先判斷這些信息被存在了哪裏。
右擊打開“檢查”工具,點開Network,刷新頁面,然後點擊第0個請求top250,看Response.
我們能在裏面找到書名、出版信息,說明我們想要的書籍信息就藏在這個網址的HTML裏。
https://book.douban.com/top250?start=25
我們可以看到,網址發生了變化,後面多了?start=25。現在的你,應該一眼就能猜到後面的數字應該是代表一頁的25本書籍吧?
你可以翻到第3頁,驗證一下猜想是不是正確的。
事實證明,我們猜對了。每翻一頁,網址後面的數字都會增加25,說明這個start的參數就是代表每頁的25本書籍。
我們要爬取的網址的構造規律就出來了。只要改變?start=後面的數字(翻一頁加25),我們就能得到每一頁的網址。
找到了網址的構造規律之後,我們就可以重點來分析HTML的結構,看看等下怎麼才能提取出我們想要的書籍信息。
還是是右擊打開“檢查”工具,點擊Elements,再點擊光標,把鼠標依次移到書名、出版信息、評分處,就能在HTML裏找到這些書籍信息。如下圖,《追風箏的人》的書籍信息就全部放在
標籤裏。仔細觀察你就會發現,其實每一頁的25本書籍信息都分別藏在了一個
我們得繼續再找一個既方便我們提取,又能包含所有書籍信息的標籤。
我們可以看看
標籤下的元素,好像剛好都能滿足我們的要求,既有class屬性,又包含了書籍的信息。我們只要取出元素下元素的title屬性的值、
元素、
頁面分析完畢,接着進入代碼實現的步驟,要開始寫 Scrapy 的代碼啦。
2)代碼實現——創建項目
如果你想着自己本地的電腦使用Scrapy,需要提前安裝好它。
- Windows:在終端輸入命令:pip install scrapy;
- mac:在終端輸入命令:pip3 install scrapy
創建Scrapy項目
首先,在本地電腦打開終端,然後跳轉到你想要保存項目的目錄下。
- windows:Win+R,輸入cmd;
- mac:command+空格,搜索“終端”
假設你想跳轉到D盤裏名爲Python文件夾中的scrapy_project子文件夾。
- 你需要再命令行輸入d:,就會跳轉到D盤
- 再輸入cd Python,就能跳轉到Python文件夾。
- 接着輸入cd scrapy_project,就能跳轉到Python文件夾裏的scrapy_project子文件夾。
- 再輸入一行能幫我們創建Scrapy項目的命令:scrapy startproject douban(douban就是Scrapy項目的名字)。按下enter鍵,一個Scrapy項目就創建成功了。
在mac系統中的終端裏,也是使用cd命令來跳轉到你要的文件夾位置
整個scrapy項目的結構,如下圖所示:
Scrapy項目裏每個文件都有它對應的具體功能。例如:
- settings.py 是scrapy裏的各種設置
- items.py是用來定義數據的
- pipelines.py是用來處理數據的,它們對應的就是Scrapy的結構中的Item Pipeline(數據管道)。
如前所述,spiders是放置爬蟲的目錄。我們可以在spiders這個文件夾裏創建爬蟲文件。
我們來把這個文件,命名爲Book_douban_Top250。後面的大部分代碼都需要在這個Book_douban_Top250.py文件裏編寫。
代碼實現——編輯爬蟲
先在Book_douban_Top250.py文件裏導入我們需要的模塊。
import scrapy
import bs4
- 導入BeautifulSoup用於解析和提取數據;
- 導入scrapy是待會我們要用創建類的方式寫這個爬蟲,我們所創建的類將直接繼承scrapy中的scrapy.Spider類。這樣,有許多好用的屬性和方法,就能夠直接使用。
接着我們開始編寫爬蟲的核心代碼。
在Scrapy中,每個爬蟲的代碼結構基本都如下所示:
import scrapy
import bs4
class DoubanSpider(scrapy.Spider):
name = 'book_douban'
allowed_domains = ['book.douban.com']
start_urls = ['https://book.douban.com/top250?start=0']
def parse(self, response):
print(response.text)
代碼解釋:
-
第1行代碼:定義一個爬蟲類DoubanSpider。就像我剛剛講過的那樣,DoubanSpider類繼承自scrapy.Spider類。
-
第2行代碼:name是定義爬蟲的名字,這個名字是爬蟲的唯一標識。name = 'book_douban’意思是定義爬蟲的名字爲book_douban。等會我們啓動爬蟲的時候,要用到這個名字。
-
第3行代碼:allowed_domains是定義允許爬蟲爬取的網址域名(不需要加https://)。如果網址的域名不在這個列表裏,就會被過濾掉。
爲什麼會有這個設置呢?
當你在爬取大量數據時,經常是從一個URL開始爬取,然後關聯爬取更多的網頁。
假設我們在今天的爬蟲目標是爬豆瓣top250每本書籍的書評信息,我們會先爬取書單,再找到每本書的URL,再進入每本書的詳情頁面去抓取評論。
allowed_domains就限制了我們這種關聯爬取的URL,一定在book.douban.com這個域名之下,不會跳轉到某個奇怪的廣告頁面。 -
第4行代碼:start_urls是定義起始網址,就是爬蟲從哪個網址開始抓取。在此,allowed_domains的設定對start_urls裏的網址不會有影響。
-
第5行代碼:parse是Scrapy裏默認處理response的一個方法。
是不是覺得和之前自己手動寫爬蟲的完全不一樣了呢?怎麼連 requests.get() 都沒有了呀?其實在框架裏,我們並不需要寫這一句。
scrapy框架會爲我們代勞做這件事,寫好你的請求,接下來你就可以直接寫對 response 如何做處理了。
豆瓣Top250圖書一共有10頁,每一頁的網址我們都知道。我們可以把10頁網址都塞進start_urls的列表裏。
我們可以利用豆瓣Top250圖書的網址規律,用for循環構造出每個網址,再把網址添加進start_urls的列表裏。
完善後的代碼如下:
import scrapy
import bs4
class DoubanSpider(scrapy.Spider):
name = 'book_douban'
allowed_domains = ['book.douban.com']
start_urls = []
for i in range(3):
url = 'https://book.douban.com/top250?start={}'.format(i * 25)
start_urls.append(url)
def parse(self, response):
print(response.text)
接下來,只要再借助DoubanSpider(scrapy.Spider)中的 parse 方法處理 response,藉助 BeautifulSoup 來取出我們想要的書籍信息的數據。這些在前面的課程我們都使用過,所以肯定是很熟練啦!
我們前面在分析項目過程的時候,已經知道書籍信息都藏在了哪些元素裏,現在可以利用find_all和find方法提取出來。比如,書名是元素下元素的title屬性的值;出版信息在
元素裏;評分在
import scrapy
import bs4
# 定義一個爬蟲類DoubanSpider
class DoubanSpider(scrapy.Spider):
# 定義爬蟲的名字
name = 'book_douban'
# 定義爬蟲的爬取的網址的域名
allowed_domains = ['book.douban.com']
# 定義起始網址
start_urls = []
# 將豆瓣Top250圖書的前3頁網址添加進start_urls列表中
for i in range(3):
url = 'https://book.douban.com/top250?start={}'.format(i * 25)
start_urls.append(url)
# parse是默認處理response的方法
def parse(self, response):
# 使用BeautifulSoup解析response
bs = bs4.BeautifulSoup(response.text, 'html.parser')
# 用find_all提取<tr class="item">元素,這個元素裏含有書籍信息。
datas = bs.find_all('tr', class_='item')
# 遍歷書籍信息列表
for data in datas:
# 取出書名
title = data.find_all('a')[1].find('title')
# 取出出版信息
publish = data.find('p', class_='pl').text
# 取出評分
score = data.find('span', class_='rating_nums').text
# 打印書籍信息
print([title, publish, score])
按照之前寫爬蟲的方式,我們會把書名、出版信息、評分,分別賦值,然後統一做處理——或是打印,或是存儲。但在scrapy這裏,事情卻有所不同。
- spiders(如Book_douban_Top250.py)只幹spiders應該做的事。對數據的後續處理,另有其他“部門”負責。
在scrapy中,我們會專門定義一個用於記錄數據的類。
當我們需要記錄數據的時候,比如前面在每一個最小循環裏,都要記錄“書名”,“出版信息”,“評分”。我們會實例化一個對象,利用這個對象來記錄數據。
定義這些數據類的python文件,正是items.py。
我們已經知道,我們要爬取的數據是書名、出版信息和評分。接下來讓我們在 items.py 中編寫這部分的代碼。
# 導入scrapy
import scrapy
# 定義一個類BookDoubanItem,它繼承自scrapy.Item
class BookDoubanItem(scrapy.Item):
# 定義書名的數據屬性
title = scrapy.Field()
# 定義出版信息的數據屬性
publish = scrapy.Field()
# 定義評分的數據屬性
score = scrapy.Field()
代碼解釋:
-
第1行代碼,我們導入了scrapy。因爲我們等會所創建的類將直接繼承scrapy中的scrapy.Item類。這樣,有很多好用屬性和方法,就能夠直接使用。比如,引擎能將item類的對象發給Item Pipeline(數據管道)處理。
-
第3行代碼:我們定義了一個BookDoubanItem類。它繼承自scrapy.Item類。
-
第5、7、9行代碼:我們定義了書名、出版信息和評分三種數據。scrapy.Field()這行代碼實現的是,讓數據能以類似字典的形式記錄。
下面的代碼展示了一下如何使用 BookDoubanItem:
# 導入scrapy
import scrapy
# 定義一個類BookDoubanItem,它繼承自scrapy.Item
class BookDoubanItem(scrapy.Item):
# 定義書名的數據屬性
title = scrapy.Field()
# 定義出版信息的數據屬性
publish = scrapy.Field()
# 定義評分的數據屬性
score = scrapy.Field()
# 實例化一個DoubanbookItem對象
book = BookDoubanItem()
book['title'] = '追風箏的人'
book['publish'] = '[美]卡勒德·胡塞尼 / 李繼宏 / 上海人民出版社 / 2006-5 / 29.00元'
book['score'] = '8.9'
print(book)
print(type(book))
# 輸出結果:
{'publish': '[美]卡勒德·胡塞尼 / 李繼宏 / 上海人民出版社 / 2006-5 / 29.00元',
'score': '8.9',
'title': '追風箏的人'}
<class '__main__.BookDoubanItem'>
你會看到打印出來的結果的確和字典非常相像,但它卻並不是dict,它的數據類型是我們定義的DoubanItem,屬於“自定義的Python字典”。
我們可以利用類似上述代碼的樣式,去重新寫Book_douban_Top250.py。如下所示:
import scrapy
import bs4
from ..items import BookDoubanItem
# 定義一個爬蟲類DoubanSpider
class DoubanSpider(scrapy.Spider):
# 定義爬蟲的名字
name = 'book_douban'
# 定義爬蟲的爬取的網址的域名
allowed_domains = ['book.douban.com']
# 定義起始網址
start_urls = []
# 將豆瓣Top250圖書的前3頁網址添加進start_urls列表中
for i in range(3):
url = 'https://book.douban.com/top250?start={}'.format(i * 25)
start_urls.append(url)
# parse是默認處理response的方法
def parse(self, response):
# 使用BeautifulSoup解析response
bs = bs4.BeautifulSoup(response.text, 'html.parser')
# 用find_all提取<tr class="item">元素,這個元素裏含有書籍信息。
datas = bs.find_all('tr', class_='item')
# 遍歷書籍信息列表
for data in datas:
# 實例化BookDoubanItem類
item = BookDoubanItem()
# 提取書名,並把這個數據放回BookDoubanItem類的title屬性裏
item['title'] = data.find_all('a')[1]['title']
# 提取出版信息,並把這個數據放回BookDoubanItem類的title屬性裏
item['publish'] = data.find('p',class_='pl').text
# 提取評分,並把這個數據放回BookDoubanItem類的title屬性裏
item['score'] = data.find('span',class_='rating_nums').text
# 打印書名
print(item['title'])
# yield item是把獲得的item傳遞給引擎
yield item
當我們需要記錄一次數據的時候,比如前面在每一個最小循環裏,都要記錄“書名”,“出版信息”,“評分”。我們會實例化一個item對象,利用這個對象來記錄數據。一個對象對應一次數據。
在Scrapy框架裏,每一次當數據完成記錄,它會離開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(數據管道)處理。
代碼實操——設置
到這裏,我們就用代碼編寫好了一個爬蟲。不過,如果現在就運行的話,可能還是會報錯。
原因在於Scrapy裏的默認設置沒被修改。比如我們需要修改請求頭(User-Agent需要設置)。點擊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 = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36'}
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
現在,我們已經編寫好了spider文件,也修改好了setting文件。最後,我們要來運行整個 scrapy 工程。
代碼實操——運行
想要運行Scrapy有兩種方法:
- 一種是在本地電腦的終端跳轉到scrapy項目的文件夾(跳轉方法:cd+文件夾的路徑名),然後輸入命令行:scrapy crawl book_douban(book_douban就是我們爬蟲的名字)。
- 另一種運行方式需要我們在最外層的大文件夾裏新建一個main.py文件(與scrapy.cfg同級)。
# 導入cdmline模塊,可以實現控制終端命令行
from scrapy import cmdline
# 用execute()方法,輸入運行scrapy命令
cmdline.execute(['scrapy', 'crawl', 'book_douban'])
代碼解釋:
-
第1行代碼:在Scrapy中有一個可以控制終端命令的模塊cmdline。導入了這個模塊,我們就能操控終端。
-
第3行代碼:在cmdline模塊中,有一個execute方法能執行終端的命令行,不過這個方法需要傳入參數。我們輸入運行Scrapy的代碼scrapy crawl book_douban,就需要寫成[‘scrapy’,‘crawl’,‘book_douban’]這樣。
需要注意的是:爲了便於理解和掌握,在這次的內容中,我們是先寫了爬蟲,再定義數據。但是,在我們平常實際做爬蟲項目的時候,順序卻是相反的——先定義數據,再寫爬蟲。所以,流程圖應如下:
三、練習
1.之前的內容中, 我們沒有使用任何框架來爬取豆瓣新片榜。在本次的練習中,我們使用Scrapy來爬取豆瓣新片榜。 地址:https://movie.douban.com/。
實現步驟如下:
1. 創建Scrapy
在命令行中,cd到對應的文件夾路徑
# 打開對應的文件夾路徑
scrapy startproject douban_movie
2. 定義Items
獲取電影標題、導演、評分、簡介信息
import scrapy
class DoubanMovieItem(scrapy.Item):
title = scrapy.Field()
director = scrapy.Field()
score = scrapy.Field()
info = scrapy.Field()
3. 編寫spiders
在spiders文件夾下方,創建“douban_movie.py"文件,編輯如下代碼
import scrapy
import bs4
from ..items import DoubanMovieItem
# 定義一個爬蟲類Douban_moiveSpider
class Douban_movieSpider(scrapy.Spider):
# 定義爬蟲的名字
name = 'douban_movie'
# 定義爬蟲爬取的網址的域名
allowed_domains = ['movie.douban.com']
# 定義起始網址
start_urls = []
for i in range(3):
url = 'https://movie.douban.com/top250?start={}&filter='.format(i*25)
start_urls.append(url)
# 定義處理返回數據的類
def parse(self, response):
# 使用Beautifulsoup解析response
bs = bs4.BeautifulSoup(response.text,'html.parser')
datas = bs.find_all('div',class_='info')
for data in datas:
# 實例化DoubanMovieItem類
item = DoubanMovieItem()
# 爬下來的電影標題、導演文本中,有“ ”及"\xa0",使用replace替換掉
item['title'] = data.find('div', class_='hd').find('a').text.replace(u'\xa0','').replace('\n','')
item['director'] = data.find('p').text.replace(' ', '').replace(u'\xa0','').replace('\n', '')
# 評分內容有評分和評價人數,中間有換行符和空格,先替換掉空格,再將換行符替換爲空格,後去掉頭尾的空格
item['score'] = data.find('div', class_='star').text.replace(' ', '').replace('\n', ' ').strip()
item['info'] = data.find('p', class_='quote').text.replace('\n', '')
# yield item 把獲得的item傳遞給引擎
yield item
網頁源代碼中的 的utf-8 編碼是:\xc2\xa0,解析後,轉換爲Unicode字符爲:\xa0,當使用print() 顯示到DOS窗口上的時候,轉換爲GBK編碼的字符串,但是\xa0這個Unicode字符沒有對應的 GBK 編碼的字符串,所以出現錯誤。
解決方法:用空格 來替換 \xa0 ( )
- 方法1:在網頁原碼上替換
soup = BeautifulSoup(html.replace(' ', ' '), 'html.parser')
- 方法2:在解析爲Unicode之後替代\xa0
job_detail = soup.select('.job-detail')[0].text.replace(u'\xa0', ' ')
4. 修改settings文件
BOT_NAME = 'douban_movie'
SPIDER_MODULES = ['douban_movie.spiders']
NEWSPIDER_MODULE = 'douban_movie.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36'}
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
5. 運行scrapy
- 在命令行中,cd到項目所在文件夾,運行“scrapy crawl douban_movie"
- 在與"scray.cfg"文件同級的文件夾中,新建文件"main.py"
# 導入cdmline模塊,可以實現控制終端命令行
from scrapy import cmdline
#用execute()方法,輸入運行scrapy的命令
cmdline.execute(['scrapy','crawl','douban_movie'])