手把手教你用Python實(shí)現(xiàn)分布式爬蟲(四) - scrapy爬取技術(shù)文章網(wǎng)站

相關(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

mkvirtualenv --python=/Library/Frameworks/Python.framework/Versions/3.7/bin/python3 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)目


scrapy startproject ArticleSpider

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)目來研究



Chrome控制臺(tái)

  • 所以更改之后,也可正常爬取所需字段了!


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()')
title.extract()
  • 訪問數(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()
有點(diǎn)號哦,想辦法去掉喲!

使用空串替換即可~

3.7 爬取文章評論數(shù)

  • 找到可能是唯一判斷標(biāo)識(shí)的字段


  • 空的呢!怎么肥事???



    由于上述字段只是class中的一小部分!并不是class!

response.xpath("http://span[contains(@class,'vote-post-up')]")
  • 取得贊數(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è)scrapyImagePipeline即可
同時(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

image.png

  • 報(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格式赚楚,就解決了。

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末左胞,一起剝皮案震驚了整個(gè)濱河市举户,隨后出現(xiàn)的幾起案子俭嘁,更是在濱河造成了極大的恐慌,老刑警劉巖拐云,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件近她,死亡現(xiàn)場離奇詭異,居然都是意外死亡薇缅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門汤徽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泻骤,“玉大人梧奢,你說我怎么就攤上這事演痒。” “怎么了惦蚊?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵蹦锋,是天一觀的道長欧芽。 經(jīng)常有香客問我,道長憎妙,這世上最難降的妖魔是什么曲楚? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任龙誊,我火速辦了婚禮,結(jié)果婚禮上鹤树,老公的妹妹穿的比我還像新娘逊朽。我一直安慰自己,他們只是感情好捣炬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婿屹,像睡著了一般推溃。 火紅的嫁衣襯著肌膚如雪铁坎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天扩所,我揣著相機(jī)與錄音朴乖,去河邊找鬼。 笑死袁勺,一個(gè)胖子當(dāng)著我的面吹牛畜普,可吹牛的內(nèi)容都是我干的吃挑。 我是一名探鬼主播弓摘,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼凳忙,長吁一口氣:“原來是場噩夢啊……” “哼约炎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起掠手,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤喷鸽,失蹤者是張志新(化名)和其女友劉穎灸拍,沒想到半個(gè)月后砾省,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體混槐,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡声登,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年悯嗓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铅祸。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俄认,死狀恐怖洪乍,靈堂內(nèi)的尸體忽然破棺而出壳澳,到底是詐尸還是另有隱情,我是刑警寧澤萎津,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布抹镊,位于F島的核電站,受9級特大地震影響颈渊,放射性物質(zhì)發(fā)生泄漏终佛。R本人自食惡果不足惜铃彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望竹揍。 院中可真熱鬧,春花似錦驶拱、人聲如沸晶衷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锹漱。三九已至哥牍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撼泛,已是汗流浹背澡谭。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留潘酗,地道東北人雁仲。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓攒砖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灶体。 傳聞我的和親對象是個(gè)殘疾皇子掐暮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355