相關(guān)源碼
搭建scrapy的開發(fā)環(huán)境钞瀑,本文介紹scrapy的常用命令以及工程目錄結(jié)構(gòu)分析,本文中也會(huì)詳細(xì)的講解xpath和css選擇器的使用锚贱。然后通過scrapy提供的spider完成所有文章的爬取仔戈。然后詳細(xì)講解item以及item loader方式完成具體字段的提取后使用scrapy提供的pipeline分別將數(shù)據(jù)保存到j(luò)son文件以及mysql數(shù)據(jù)庫中.
首先爬取一個(gè)網(wǎng)站前伐蒂,我們需要分析網(wǎng)絡(luò)的url結(jié)構(gòu)倒脓,伯樂在線的網(wǎng)站結(jié)構(gòu)是采用頂級域名下有二級域名虹茶,來區(qū)分每種類別的信息尘吗,并且在文章專欄里面
有一個(gè)
是所有文章的總鏈接
-
在這個(gè)鏈接下使碾,分頁顯示了所有的文章內(nèi)容
因此對于這種爬取內(nèi)容有一個(gè)總鏈接的話睦刃,就不需要采用深度優(yōu)先或者廣度優(yōu)先策略搬俊,只需要將這個(gè)總鏈接下的每一頁的內(nèi)容取出即可.
說到每一頁掷贾,查看url特點(diǎn)倦春,發(fā)現(xiàn)就是在鏈接后面修改了頁數(shù)户敬,但是不能用這個(gè)方法落剪,因?yàn)榫W(wǎng)站上文章數(shù)發(fā)生變化時(shí),就必須要去修改源碼尿庐。
如果是對每個(gè)分頁上的寫一頁的鏈接進(jìn)行跟蹤忠怖,那么有多少頁都無所謂了.
1 scrapy安裝以及目錄結(jié)構(gòu)介紹
1.1 安裝并創(chuàng)建 scrapy 項(xiàng)目
1.1.1 創(chuàng)建一個(gè)虛擬環(huán)境 article_spider
-
注意版本 3.5+
1.1.2 在這個(gè)虛擬環(huán)境內(nèi)安裝scrapy:
pip install -i https://pypi.douban.com/simple/ scrapy
注意安裝的時(shí)候可能會(huì)報(bào)錯(cuò),twisted找不到抄瑟,那么就去https://www.lfd.uci.edu/~gohlke/pythonlibs/下載安裝包凡泣,手動(dòng)安裝,安裝的時(shí)候必須也是在這個(gè)虛擬環(huán)境內(nèi)
1.1.3 建立scrapy項(xiàng)目
-
PyCharm里面沒有提供建立scrapy的項(xiàng)目
需要在命令行內(nèi)手動(dòng)創(chuàng)建項(xiàng)目
1.1.4 在pycharm中打開剛創(chuàng)建的項(xiàng)目
1.2 目錄結(jié)構(gòu)介紹
- scrapy.cfg: 類似于django的配置皮假,它大量的借鑒了django的設(shè)計(jì)理念
- settings.py: 包含了很多scrapy的配置鞋拟,工程名字,spider_modules也指明了存放spiders的路徑
- pipelines.py: 做跟數(shù)據(jù)存儲(chǔ)相關(guān)的
- middlewares.py: 可以存放自己定義的middlewares惹资,讓scrapy變得更加可控
- items.py: 有點(diǎn)類似于django里面的form贺纲,定義數(shù)據(jù)保存的格式
- spiders文件夾:里面存放具體某個(gè)網(wǎng)站的爬蟲,scrapy會(huì)在該文件夾里面找有多少個(gè)爬蟲文件褪测,只需要在這里面繼承了spiders猴誊,就會(huì)被scrapy找到
1.3 初步爬取
剛創(chuàng)建好項(xiàng)目的時(shí)候這個(gè)文件夾是空的,默認(rèn)并沒有創(chuàng)建網(wǎng)站爬取的模板侮措,但是提供了命令
scrapy genspider example example.com
example
是spider的名稱稠肘,后面是網(wǎng)站的域名
這里使用了scrapy提供的basic模板
為了創(chuàng)建一個(gè)Spider,必須繼承 scrapy.Spider
類萝毛, 且定義以下三個(gè)屬性:
-
name
: 用于區(qū)別Spider
該名字必須是唯一的,您不可以為不同的Spider設(shè)定相同的名字滑黔。 -
start_urls
: 包含了Spider在啟動(dòng)時(shí)進(jìn)行爬取的url列表
因此笆包,第一個(gè)被獲取到的頁面將是其中之一。 后續(xù)的URL則從初始的URL獲取到的數(shù)據(jù)中提取略荡。 -
parse()
: 是spider的一個(gè)方法
被調(diào)用時(shí)庵佣,每個(gè)初始URL完成下載后生成的Response
對象將會(huì)作為唯一的參數(shù)傳遞給該函數(shù)。 該方法負(fù)責(zé)解析返回的數(shù)據(jù)(response data)汛兜,提取數(shù)據(jù)(生成item)以及生成需要進(jìn)一步處理的URL的Request
對象巴粪。
2 PyCharm 調(diào)試scrapy 執(zhí)行流程
2.1 注意Python解釋器版本
2.2 讓scrapy在PyCharm中可調(diào)試
-
設(shè)置斷點(diǎn)
PyCharm 中沒有關(guān)于scrapy
的模板,無法直接調(diào)試,需要我們自己手動(dòng)編寫一個(gè)main文件
設(shè)置工程目錄,這樣execute命令才會(huì)生效,找到該目錄;
同時(shí)為了避免因環(huán)境問題無法找到該目錄,使用os相關(guān)庫調(diào)用
-
驗(yàn)證一下
爬取
進(jìn)入項(xiàng)目的根目錄,執(zhí)行下列命令啟動(dòng)spider
scrapy crawl xxx
于是,考慮將該命令配置到我們的main
文件中
-
調(diào)用execute()函數(shù)來執(zhí)行spider命令粥谬,傳入數(shù)組肛根,即是執(zhí)行啟動(dòng)spider的命令
-
注意設(shè)置為
False
-
開始
debug
運(yùn)行main
文件
-
所爬取源文件的內(nèi)容
下一步就是對其中的內(nèi)容進(jìn)行解析,獲取想要爬取的字段內(nèi)容!
3 xpath的用法
3.1 簡介
- xpath使用路徑表達(dá)式在xml和html文件中進(jìn)行導(dǎo)航
- xpath包含標(biāo)準(zhǔn)函數(shù)庫
- xpath是一個(gè)w3c的標(biāo)準(zhǔn)
3.2 xpath節(jié)點(diǎn)關(guān)系
html中被尖括號包起來的被稱為一個(gè)節(jié)點(diǎn)
父節(jié)點(diǎn) 上一層節(jié)點(diǎn)
子節(jié)點(diǎn) 下一層節(jié)點(diǎn)
兄弟節(jié)點(diǎn) 同胞節(jié)點(diǎn)
先輩節(jié)點(diǎn) 父節(jié)節(jié)點(diǎn)漏策,爺爺節(jié)點(diǎn) ...
后代節(jié)點(diǎn) 兒子節(jié)點(diǎn)派哲,孫子節(jié)點(diǎn) ...
3.3 xpath的語法
xpath 謂語
其他語法
- 如果想通過屬性取值則需要給定標(biāo)簽元素的內(nèi)容,如果是任意標(biāo)簽則給定*
- 如果通過@class="class類"取值掺喻,則只會(huì)匹配class只有指定的元素芭届;如果想指定包含指定class的元素則需要使用函數(shù)contains(@class,"class類")
3.4 準(zhǔn)備爬取標(biāo)題
欲爬取以下標(biāo)題
先看看源碼,獲取其xpath
可以看到储矩,我們的標(biāo)題標(biāo)題在 html/body/div[1]/div[3]/div[1]/div[1]/h1 這個(gè)嵌套關(guān)系下
我們在用xpath
解析的時(shí)候,不需要自己一個(gè)一個(gè)地看嵌套關(guān)系
在F12下褂乍,在某個(gè)元素上面右鍵即copy->copy xpath就能獲得該元素的xpath路徑
在Firefox和chrom瀏覽器中右鍵copy xpath得到的結(jié)果可能不一樣
在Firefox中持隧,得到的路徑是/html/body/div[1]/div[3]/div[1]/div[1]/h1
在chrom中,得到的是//*[@id="post-110287"]/div[1]/h1
可以發(fā)現(xiàn)兩種路徑不一樣逃片,經(jīng)過測試屡拨,第一種路徑不能獲得標(biāo)題,第二種可以题诵,原因在于洁仗,一般元素檢查看到的是動(dòng)態(tài)的返回來的html信息,比如js生成的性锭,然后有些節(jié)點(diǎn)可能是在后臺(tái)返回信息時(shí)才創(chuàng)建的赠潦,對于靜態(tài)的網(wǎng)頁就是檢查源代碼,定位的結(jié)果可能不一樣草冈,采用第二種id確定的方式更容易標(biāo)準(zhǔn)的定位她奥。
- -*- coding: utf-8 -*-
import scrapy
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/114610/'] #放入想爬取的url
def parse(self, response):
#/html/body/div[3]/div[3]/div[1]/div[1] # Firefox
#//*[@id="post-114610"]/div[1]/h1 # Chrome
#scrapy返回的是一個(gè)selector而不是node,是為了方便進(jìn)一步獲取selector下面的selector
re_selector = response.xpath('//*[@id="post-114610"]/div[1]/h1')
re2_selector = response.xpath('//*[@id="post-114610"]/div[1]/h1/text()') #利用text()函數(shù)獲取元素中的值
pass
-
爬取
頁面上的查看源碼跟檢查控制臺(tái)的element不一定一樣,源碼是源代碼的html文件怎棱,控制臺(tái)的element會(huì)有js動(dòng)態(tài)生成的dom!!!
-
下面將源代碼拷貝進(jìn)項(xiàng)目來研究
-
所以更改之后,也可正常爬取所需字段了!
3.5 準(zhǔn)備爬取class
-
可看出該寫法更加短小簡潔高效!不易出錯(cuò)!
錯(cuò)誤提示:
同一個(gè)頁面的元素通過不同電腦的chrom瀏覽器進(jìn)行源代碼查看哩俭,標(biāo)簽結(jié)點(diǎn)信息發(fā)現(xiàn)不一樣,在h1標(biāo)簽中多了個(gè)span標(biāo)簽拳恋,解決方法:清除瀏覽器緩存凡资,以下是同一頁面用一個(gè)內(nèi)容的檢查元素的對比圖。
圖1:未清除瀏覽器緩存前
圖2:清除瀏覽器緩存后
3.6 shell命令調(diào)試
每一次調(diào)試都運(yùn)行python腳本發(fā)送HTTP請求獲取內(nèi)容效率低下!
scrapy
提供了一種shell模式谬运,提高了調(diào)試的效率.
具體操作
在命令行中隙赁,之前的啟動(dòng)scrapy
的命令是
scrapy crawl jobbole
現(xiàn)在可以在命令行中使用shell,命令為
scrapy shell 網(wǎng)址
然后就進(jìn)入了調(diào)試區(qū)域
步驟如下圖梆暖,注意啟動(dòng)scrapy必須在命令行中進(jìn)入相應(yīng)的虛擬環(huán)境以及項(xiàng)目的工作目錄
我們關(guān)心的是其中的
response
-
下面開始調(diào)試
title = response.xpath('//div[@class="entry-header"]/h1/text()')
-
訪問數(shù)組的第一個(gè)值即可~
-
獲取title下所有節(jié)點(diǎn)
3.7 爬取文章發(fā)布時(shí)間
-
該class全局唯一
create_date = response.xpath("http://p[@class='entry-meta-hide-on-mobile']/text()").extract() 如果提取的字符串左右有回車符換行符等等,則需要使用
strip()
將其去掉
re_selector.extract()[0].strip()
3.7 爬取文章評論數(shù)
-
找到可能是唯一判斷標(biāo)識(shí)的字段
-
空的呢!怎么肥事???
由于上述字段只是class中的一小部分!并不是class!
-
取得贊數(shù)
response.xpath("http://span[contains(@class,'vote-post-up')]").extract()
response.xpath("http://span[contains(@class,'vote-post-up')]/h10/text()") -
提取得到贊數(shù)
int (response.xpath("http://span[contains(@class,'vote-post-up')]/h10/text()").extract()[0])
3.8 爬取文章收藏?cái)?shù)
-
目標(biāo)代碼
-
目標(biāo)內(nèi)容
可是我們只是想要個(gè)6數(shù)字而已呀,怎么辦呢?使用正則提取即可!
response.xpath("http://span[contains(@class,'bookmark-btn')]/text()").extract()[0]
# ' 收藏'
# 收藏?cái)?shù)的標(biāo)簽設(shè)置和點(diǎn)贊數(shù)不一樣伞访,直接是收藏前面有數(shù)字,這里沒有數(shù)字轰驳,其實(shí)是0收藏的意思厚掷。
# 對于含數(shù)字的話,我們應(yīng)該使用正則表達(dá)式將數(shù)字部分提取出來级解。
import re
match_re = re.match('.*?(\d+).*',' 收藏')
if match_re:
fav_nums = int(match_re.group(1))
else:
fav_nums = 0
# 正則表達(dá)式注意要有冒黑?表示非貪婪匹配,可以獲取兩位數(shù)等
# 還有一點(diǎn)就是老師沒有考慮的勤哗,如果沒有收藏?cái)?shù)薛闪,即匹配不到數(shù)字,說明收藏?cái)?shù)為0.
3.9 爬取文章評論數(shù)
# 評論數(shù)和收藏?cái)?shù)的標(biāo)簽設(shè)計(jì)是一樣的俺陋,只需要更改xpath即可
comment_nums = response.xpath("http://a[@href='#article-comment']/span/text()").extract()[0]
match_re = re.match('.*?(\d+).*', comment_nums)
if match_re:
comment_nums = int(match_re.group(1))
else:
comment_nums = 0
3.10 獲取正文
content = response.xpath('//div[@class="entry"]').extract()[0]
# 對于文章內(nèi)容豁延,不同網(wǎng)站的設(shè)計(jì)不一樣昙篙,我們一般保存html格式的內(nèi)容
關(guān)于extract()方法和text()方法的區(qū)別:extract()是對一個(gè)selector的內(nèi)容取出這個(gè)標(biāo)簽內(nèi)的所有內(nèi)容,包括當(dāng)前的節(jié)點(diǎn)標(biāo)簽诱咏。text()方法一般是在xpath的路徑內(nèi)部苔可,用于獲取當(dāng)前節(jié)點(diǎn)內(nèi)的所有文本內(nèi)容。
3.11 獲取文章標(biāo)簽
# 文章標(biāo)簽
tag = response.xpath('//*[@id="post-114610"]/div[2]/p/a/text()').extract()
-
發(fā)現(xiàn)多了個(gè)評論數(shù)!
response.xpath('//*[@id="post-114610"]/div[2]/p/a/text()').extract() - 通過使用數(shù)組解決
tag_list = response.xpath("http://p[@class = 'entry-meta-hide-on-mobile']/a/text()").extract()
tag_list = [element for element in tag_list if not element.strip().endswith("評論")]
# 有的網(wǎng)頁在類型一欄中會(huì)得到評論數(shù)袋狞,以前的老設(shè)計(jì)焚辅,所以需要將關(guān)于評論的這一項(xiàng)去掉
tags = ",".join(tag_list)
4 css選擇器實(shí)現(xiàn)字段解析
css選擇器:通過一定的語法定位到某一個(gè)元素,與xpath選擇的功能是一樣的
4.1 css選擇器的常見用法
表達(dá)式 | 說明 |
---|---|
* | 選擇所有節(jié)點(diǎn) |
#container | 選擇id為container的節(jié)點(diǎn) |
.container | 選取所有class包含container的節(jié)點(diǎn) |
li a | 選取所有l(wèi)i下的所有a節(jié)點(diǎn) |
ul + p | 選擇ul后面的第一個(gè)p元素 |
div#container>ul | 選取id為container的第一個(gè)ul子元素 |
ul ~ p | 選取與ul相鄰的所有p元素 |
a[title] | 選取所有有title屬性的a元素 |
a[href=“http://jobbole.com”] | 選取所有href屬性為jobbole.com值的a元素 |
a[href*=“jobble”] | 選取所有href屬性包含jobbole的a元素 |
a[href^=“http”] | 選取所有href屬性以http開頭的a元素 |
a[href$=".jpg"] | 選取所有href屬性以jpg結(jié)尾的a元素 |
input[type=radio]:checked | 選擇選中的radio元素 |
div:not(#container) | 選取所有id非container的div屬性 |
li:nth-child(3) | 選取第三個(gè)li元素 |
tr:nth-child(2n) | 第偶數(shù)個(gè)tr |
::text | 利用偽類選擇器獲得選中的元素的內(nèi)容 |
幾乎對于所有的元素來說苟鸯,用xpath和css都是可以完成定位功能的同蜻,但對前端朋友來說比較熟悉前端的寫法,scrapy提供兩種方法早处。css的寫法是比xpath更簡短的湾蔓,在瀏覽器中都能直接獲取。對前端熟悉的人可以優(yōu)先考慮使用css選擇器來定位一個(gè)元素砌梆,對于之前用xpath做實(shí)例的網(wǎng)頁全用css選擇器默责,代碼如下
title = response.xpath("div.entry-header h1::text").extract()[0]
# '用 Vue 編寫一個(gè)長按指令'
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# '2018/08/22'
praise_ums = response.css(".vote-post-up h10::text").extract_first()
if praise_ums:
praise_ums = int(praise_ums)
else:
praise_ums = 0
fav_nums = response.css(".bookmark-btn::text").extract()[0]
match_re = re.match('.*?(\d+).*',fav_nums)
if match_re:
fav_nums = int(match_re.group(1))
else:
fav_nums = 0
comment_nums = response.css("a[href='#article-comment'] span::text").extract()[0]
match_re = re.match('.*?(\d+).*', comment_nums)
if match_re:
comment_nums = int(match_re.group(1))
else:
comment_nums = 0
content = response.css("div.entry").extract()[0]
tags = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag_list = [element for element in tag_list if not element.strip().endswith("評論")]
tags = ",".join(tag_list)
注意,提示一個(gè)新的函數(shù)咸包,extract_first()桃序,這個(gè)函數(shù)就是相當(dāng)于之前的extract()[0],但是前者好處在于避免了當(dāng)取出數(shù)組為空的情況烂瘫,這時(shí)候取[0]元素是會(huì)報(bào)錯(cuò)的媒熊,不得不做異常處理。extract()函數(shù)可以傳入?yún)?shù)坟比,表示如果找到的數(shù)組為空泛释,那么就返回默認(rèn)值。比如extract("")就表示如果前面取出數(shù)組為空温算,那么就返回空字符串.
5 spider批量爬取
首先,我們需要通過列表頁爬取所有文章的url间影,前面部分只爬取了一個(gè)頁面
start_urls
這個(gè)list中只有一個(gè)url注竿,沒有涉及到如何解析這個(gè)字段,通過文章分頁一頁一頁的傳遞給scrapy魂贬,讓scrapy自動(dòng)去下載其他頁面.
5.1 在scrapy中巩割,不需要自己使用request去請求一個(gè)頁面返回,所以問題是如何將眾多的url傳遞給scrapy完成下載呢付燥?
查看伯樂在線的文章布局如下:
5.2 要點(diǎn)
在文章列表頁中宣谈,每一篇文章是一個(gè)div塊;
所以根據(jù)css選擇器就能提取出文章列表中的每一篇的url;
需要考慮的問題是,提取出來的url是否精確键科,有無混雜其他推薦文章的url闻丑,這就需要css選擇器足夠準(zhǔn)確!
獲取了每一個(gè)具體文章的url后漩怎,如何將url傳遞給scrapy進(jìn)行下載并返回response呢?
用到了scrapy.http中的Request類;
這個(gè)類中,可以直接傳遞url和callback參數(shù)嗦嗡,url為一個(gè)頁面地址勋锤,callback為回調(diào)函數(shù),表示對該頁面進(jìn)行的具體操作侥祭,所以將之前的某個(gè)具體文章的解析封裝在另一個(gè)函數(shù)中叁执,作為Request的回調(diào)函數(shù)。
還要考慮的一個(gè)地方是矮冬,提取出來的url可能不是一個(gè)完整的網(wǎng)址谈宛,只是域名的一部分,所以還需要將網(wǎng)址進(jìn)行完善胎署,比如加上域名部分吆录,又或者原本是一個(gè)具體的文章網(wǎng)址,都需要處理
初始化好request之后硝拧,如何交給scrapy下載径筏,使用yield這個(gè)關(guān)鍵字就可以了!
5.3 coding
-
開始調(diào)試
scrapy shell http://blog.jobbole.com/all-posts/ -
看出該范圍并不準(zhǔn)確!
response.css(".floated-thumb .post-thumb a::attr(href)").extract() -
通過增加該id進(jìn)一步限定范圍
-
這就對了嘛!
response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract() -
取到url
response.css(".next.page-numbers::attr(href)").extract_first("")
5.4 開發(fā)流程
- 利用Request函數(shù)執(zhí)行訪問指定url并通過callback回調(diào)函數(shù)處理進(jìn)入url后的操作
- 利用parse.urljoin自動(dòng)將對應(yīng)url添加域名,參數(shù)1是域名障陶,參數(shù)2是url
- 利用yield實(shí)現(xiàn)異步請求
- 利用::attr()偽類選擇器獲取對應(yīng)屬性的值
6 item設(shè)計(jì)
6.1 非結(jié)構(gòu)性數(shù)據(jù) VS 結(jié)構(gòu)性數(shù)據(jù)
6.1.1 為何不使用dict數(shù)據(jù)類型
數(shù)據(jù)爬取的主要目的就是從非結(jié)構(gòu)的數(shù)據(jù)源得到結(jié)構(gòu)性數(shù)據(jù)滋恬,解析完成的數(shù)據(jù)返回問題,
最簡單的就是將這些字段分別都放入一個(gè)字典里,返回給scrapy.
雖然字典也很好用抱究,但是dict缺少結(jié)構(gòu)性的東西恢氯,比如字段的名字容易出錯(cuò),比如fav_nums
寫成了fav_num
鼓寺,那么dict的管理就會(huì)出錯(cuò)勋拟。
6.1.2 item類的作用
為了將這些東西結(jié)構(gòu)化,sccrapy提供了item類妈候,可以像django一樣指定字段敢靡,比如說,定義一個(gè)article_item苦银,這個(gè)article_item有title啸胧,creat_date等字段,通過很多爬取到的item內(nèi)容來實(shí)例化幔虏,就不會(huì)出錯(cuò)了.
item類似于字典纺念,但是比dict的功能強(qiáng)大,對item進(jìn)行實(shí)例化和數(shù)據(jù)賦值之后想括,通過yeild傳遞給scrapy陷谱,scrapy發(fā)現(xiàn)這是一個(gè)item實(shí)例時(shí),將item路由到pipeline中去瑟蜈,那么在pipeline中就可以集中處理數(shù)據(jù)的保存烟逊,去重等渣窜,這就是item的作用.
6.2 item類操作步驟
6.2.1 修改settings.py
文件,使item傳遞給pipeline生效
查看scrapy的源碼焙格,其中就有pipelines,提供了scrapy一些默認(rèn)的pipline图毕,可以加速編碼過程
- pipeline主要用于做數(shù)據(jù)處理,item賦值之后就會(huì)傳遞到
pipeline.py
中,需要將settings中的為了使item傳遞給pipeline生效眷唉,必須在settings.py
文件中將一段注釋的代碼取消注釋 -
在settings中設(shè)置下載圖片的pipeline,添加到配置的ITEM_PIPELINES中(為item流經(jīng)的管道,后面的數(shù)字表示處理順序,數(shù)字越小就越早進(jìn)入pipeline)
設(shè)置好之后可以在pipelines中打斷點(diǎn)予颤,進(jìn)行調(diào)試。
image.py里面就是存放的關(guān)于下載圖片的pipline冬阳,其中ImagesPipeline這個(gè)配置好之后就可以自動(dòng)下載圖片
scrapy 爬蟲中完成圖片下載到本地
將文章封面圖片下載下來蛤虐,并保存到本地,如何做肝陪?
scrapy提供了自動(dòng)下載圖片的機(jī)制驳庭,只需在settings.py
中配置
在ITEM_PIPELINES
中加一個(gè)scrapy
的ImagePipeline
即可
同時(shí)還要配置圖片存放的地址IMAGES_STORE
參數(shù)
以及下載圖片的地址是item中的哪個(gè)字段IMAGES_URLS_FIELD
參數(shù)
scrapy 提供了設(shè)置圖片的保存路徑,后面添加路徑氯窍,可以是絕對路徑饲常,如果放到項(xiàng)目目錄下,可使用相對路徑
-
譬如,想保存在如下目錄
-
配置好下載圖片的pipeline之后運(yùn)行檢驗(yàn)是否配置成功,運(yùn)行
main.py
是因?yàn)橄螺d圖片缺少跟圖片相關(guān)的包PIL
pip install -i https://pypi.douban.com/simple pillow
-
再次運(yùn)行,發(fā)現(xiàn)成功爬了一大堆,保存本地功能實(shí)現(xiàn)!
-
設(shè)置斷點(diǎn),進(jìn)行調(diào)試
-
path即為路徑值
6.2.2 在items.py
文件中定義JobBoleArticleItem類
該類要繼承scrapy.Item狼讨,定義的內(nèi)容就是有哪些字段贝淤,并且寫明字段的類型,scrapy中只有Field()類型政供,所以定義字段的方法為:title = scrapy.Field()播聪,其余同理
在jobbole.py文件中,引入JobBoleArticleItem類布隔,并且實(shí)例化一個(gè)對象离陶,article_item = JobBoleArticleItem(),當(dāng)解析出來每一個(gè)字段值后衅檀,對這個(gè)對象的每一個(gè)屬性或者說字段進(jìn)行填充:article_item["title"] = title,注意都定義好后需要提交給scrapy:yield article_item招刨。
在pipelines.py文件中,如果字段中需要去下載文章封面圖哀军,并且保存到本地沉眶,獲取保存到本地路徑,就涉及到自定義pipeline排苍,自己定義一個(gè)ArticleImagePipeline(ImagesPipeline)類,有繼承關(guān)系学密,并且設(shè)置不同功能的pipeline執(zhí)行的順序淘衙,先下載圖片保存本地,獲取路徑之后將其填充到item的front_image_path屬性中腻暮,再將這個(gè)item提交給ArticlespiderPipeline(object)彤守,完成結(jié)構(gòu)化數(shù)據(jù)的管理毯侦,比如存入數(shù)據(jù)庫等等。
定義MD5函數(shù)
7 將item數(shù)據(jù)保存到MySQL
7.1 保存item到j(luò)son文件方法:
方法一: 在pipelines.py中具垫,自定義pipeline類保存item為json文件侈离,并且在settings.py文件中完成配置
方法二: scrapy本身也提供了寫入json的機(jī)制
scrapy提供了 field exporter機(jī)制,可以將item方便的導(dǎo)出成各種類型的文件筝蚕,比如csv卦碾,xml,pickle起宽,json等洲胖。使用方法,在pipelines.py中引入:from scrapy.exporters import JsonItemExporter
在settings中配置下該pipeline并運(yùn)行
7.2 item存入MySQL
7.2.1 方法一:自定義pipeline完成存入mysql坯沪,同步機(jī)制
1 在navicat中建立article_spider數(shù)據(jù)庫绿映,并且相應(yīng)的表和字段
- 修改jobbole.py中的create_date為date類型(便于存儲(chǔ)到mysql中的date類型)
-
先看時(shí)間是否正確并調(diào)試校驗(yàn)
-
無誤~
2 安裝mysql的驅(qū)動(dòng)
pip3 install mysqlclient
- 如果是在linux下,命令是
sudo apt-get install libmysqlclient-devimp
- 如果是在centos下腐晾,命令
sudo yum install python-devel mysql-devel
3 編寫mysqlpipeline
-
報(bào)錯(cuò)無法解決,等候你們的解答!
上述方法(execute和commit操作是同步操作)在后期爬取加解析會(huì)快于數(shù)據(jù)存儲(chǔ)到mysql,會(huì)導(dǎo)致阻塞叉弦。使用twisted框架提供的api可以完成數(shù)據(jù)的異步寫入。
方法2:用到twisted的異步機(jī)制
有了方法1藻糖,為什么還要方法2淹冰,spider解析的速度肯定是超過mysql數(shù)據(jù)入庫的速度,如果后期爬取的item越來越多颖御,插入速度很不上解析速度榄棵,就會(huì)堵塞。
Twisted這個(gè)框架提供了一種將mysql關(guān)系數(shù)據(jù)庫插入異步化的操作潘拱,將mysql操作變成異步化操作疹鳄,方法一中的execute()和commit()是一種同步化的操作,意思就是execute不執(zhí)行完芦岂,就不能往下執(zhí)行,進(jìn)行提交腺怯。在數(shù)據(jù)量不是很大的情況下還是可以采用方法1的川无,對于方法2,可以直接復(fù)制使用呛占,需要修改的就是do_insert()函數(shù)中的內(nèi)容。
Twisted框架提供了一種工具晾虑,連接池,將mysql操作變成異步操作帜篇,目前支持的是關(guān)系型數(shù)據(jù)庫糙捺。
-
在setting.py中配置相關(guān)數(shù)據(jù)信息
itemloader機(jī)制
當(dāng)需要解析提取的字段越來越多笙隙,寫了很多xpath和css選擇器,后期維護(hù)起來就很麻煩竟痰,scrapy提供的item loader機(jī)制就可以將維護(hù)工作變得簡單。
具體原理
item loader提供的是一種容器凯亮,可以在其中配置item的哪個(gè)字段需要怎么的選擇器.
直接調(diào)用item_loader.load_item()
,可以獲得item柠并,通過選擇器獲得的內(nèi)容都為list富拗,未經(jīng)處理臼予,比如是list的第一個(gè)值或者評論數(shù)需要正則表達(dá)式匹配之類.
而scrapy
又提供了from scrapy.loader.processors import MapCompose
類啃沪,可以在items.py
定義item字段類型的時(shí)候,在Field中可以添加處理函數(shù)
設(shè)計(jì)思路
- 使用itemLoader統(tǒng)一使用add_css/add_xpath/add_value方法獲取對應(yīng)數(shù)據(jù)并存儲(chǔ)到item中
- 在item中使用scrapy.Field的參數(shù)input_processor執(zhí)行MapCompose方法執(zhí)行對輸入值的多次函數(shù)處理
具體操作
-
引入依賴
# jobbole.py 解析字段缰雇,使用選擇器
# 首先需要實(shí)例化一個(gè)ItemLoader類的對象
item_loader = ItemLoader(item=JobBoleArticleItem(),response = response) # 實(shí)例化一個(gè)對象
"""有三種重要的方法
item_loader.add_css() # 通過css選擇器選擇的
item_loader.add_xpath()
item_loader.add_value() # 不是選擇器選擇的追驴,而是直接填充
"""
item_loader.add_css("title",".entry-header h1::text")
item_loader.add_value("url",response.url)
item_loader.add_value("url_object_id",get_md5(response.url))
item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text")
item_loader.add_value("front_image_url",[front_image_url])
item_loader.add_css("praise_nums", ".vote-post-up h10::text")
item_loader.add_css("fav_nums", ".bookmark-btn::text")
item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")
item_loader.add_css("content", "div.entry")
item_loader.add_css("tags","p.entry-meta-hide-on-mobile a::text")
# 獲取article_item
article_item = item_loader.load_item()
"""
調(diào)用默認(rèn)的load_item()方法有兩個(gè)問題,第一個(gè)問題會(huì)將所有的值變成一個(gè)list暇咆,雖然聽起來不合理丙曙,但是從另外的角度來看爸业,也是合理的
因?yàn)橥ㄟ^css選擇器取出來的極有可能就是一個(gè)list亏镰,不管是取第0個(gè)還是第1個(gè),都是一個(gè)list索抓,所以默認(rèn)情況就是list
如何解決問題呢钧忽,list里面只取第一個(gè)某抓,以及對某個(gè)字段的list加一些額外的處理過程
在item.py對字段進(jìn)行定義,scrapy.Field()里面是有參數(shù)的,input_processor表示對輸入的值預(yù)處理過程惰瓜,后面MapCompose()類中可以傳遞很多函數(shù)名的參數(shù),表示從左到右依次處理
title = scrapy.Field(
input_processor = MapCompose(add_jobbole)
)
"""
# items.py 字段定義的時(shí)候加入處理過程
from scrapy.loader.processors import MapCompose,TakeFirst,Join
from scrapy.loader import ItemLoader
import datetime
import re
def add_jobbole(value):
return value+"-jobbole"
def date_convert(value):
try:
create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
return create_date
def get_nums(value):
# 對收藏?cái)?shù)和評論數(shù)的正則處理
try:
match_re = re.match('.*?(\d+).*', value)
if match_re:
nums = int(match_re.group(1))
else:
nums = 0
except:
nums = 0
return nums
def remove_comment_tags(value):
#去掉tags中提取的評論項(xiàng)
# 注意input_processor中的預(yù)處理是對list中的每個(gè)元素進(jìn)行處理汉矿,所以只需要判斷某一項(xiàng)是不是包含評論崎坊,置為空即可
if "評論" in value:
return ""
else:
return value
def return_value(value):
# 這個(gè)函數(shù)是用于處理關(guān)于front_image_url字段的,本來傳入就需要是list洲拇,所以不需要默認(rèn)的輸出處理
# 如此一來奈揍,這個(gè)字段就是一個(gè)list模式的,所以在插入語句插入的時(shí)候赋续,是字符串類型男翰,那么就需要取第一個(gè)值進(jìn)行插入
return value
def image_add_http(value):
if "http" not in value:
value = "http:"+ value
return value
class JobBoleArticleItem(scrapy.Item):
title = scrapy.Field()
# MapCompose這個(gè)類可以將傳進(jìn)來的值纽乱,從左到右,連續(xù)兩個(gè)函數(shù)對它處理租冠,可以傳遞任意多個(gè)函數(shù),甚至可以是匿名函數(shù)
create_date = scrapy.Field(input_processor = MapCompose(date_convert))
url = scrapy.Field()
# url實(shí)際是個(gè)變長的薯嗤,可以對url做一個(gè)md5骆姐,讓長度變成固定長度,把url變成唯一的長度固定的值
url_object_id = scrapy.Field()
front_image_url = scrapy.Field(input_processor = MapCompose(image_add_http),output_processor =MapCompose() )
# 如果希望把封面圖保存到本地中肉渴,把封面下載下來归园,記錄一下在本地存放的路徑
front_image_path = scrapy.Field()
# 在python中數(shù)據(jù)只有一種類型,F(xiàn)ield類型捻浦,不想django可以指明字段是int類型的等等
praise_nums = scrapy.Field(input_processor = MapCompose(get_nums))
fav_nums = scrapy.Field(input_processor = MapCompose(get_nums))
comment_nums = scrapy.Field(input_processor = MapCompose(get_nums))
content = scrapy.Field()
tags = scrapy.Field(input_processor = MapCompose(remove_comment_tags),output_processor = Join(","))
# 很多item的字段都是取list的第一個(gè)桥爽,是否需要在每個(gè)Field中都添加output_processor呢
# 可以通過自定義itemloader來解決,通過重載這個(gè)類钠四,設(shè)置默認(rèn)的輸出處理設(shè)置跪楞,就可以統(tǒng)一處理了
class ArticleItemLoader(ItemLoader):
# 自定義itemloader
default_output_processor = TakeFirst()
整體代碼調(diào)試:爬取伯樂在線文章并且將內(nèi)容存入數(shù)據(jù)庫
在實(shí)際保存到數(shù)據(jù)庫的代碼調(diào)試過程中侣灶,會(huì)遇到很多出其不意的問題褥影,某個(gè)文章出現(xiàn)訪問異常,或者沒有封面圖等異常情況校焦,這種時(shí)候應(yīng)該學(xué)會(huì)使用try_catch统倒,捕獲異常并且進(jìn)行處理,從而處理個(gè)別異常文章耸成。
報(bào)錯(cuò)信息:_mysql_exceptions.OperationalError: (1366, "Incorrect string value: '\xF0\x9F\x98\x8C\xE9\x99...' for column 'content' at row 1")
這個(gè)問題的原因來自于mysql的編碼問題浴鸿,解決辦法為將mysql中數(shù)據(jù)庫以及表的格式和連接數(shù)據(jù)庫時(shí)的charset都要設(shè)置為utf8mb4
格式赚楚,就解決了。