Python 爬蟲框架Scrapy入門 官方手冊翻譯版

將以 'quotes.toscrape.com' 網(wǎng)站作為爬取的對象。

在這個教程中將圍繞如下內(nèi)容展開:

  1. 創(chuàng)建一個新的 Scrapy 項目
  2. 編寫一個 spider 去爬網(wǎng)站薄湿,提取數(shù)據(jù)
  3. 使用命令行導(dǎo)出抓取數(shù)據(jù)
  4. 修改爬蟲遞歸下一個鏈接
  5. 使用 spider 屬性

創(chuàng)建項目

進入目標(biāo)項目文件夾坯汤,執(zhí)行以下代碼:

scrapy startproject tutorial

這會創(chuàng)建一個 tutorial 路徑骗奖,包含以下內(nèi)容

turorial/
    scrapy.cfg        # 部署配置的文件
    
    tutorial/         # 項目的 Python 模塊嫩舟,import 導(dǎo)入的代碼在模塊中
        __init__.py
        
        items.py      # 項目的 items(條目)自定義文件
        
        middlewares.py # 項目的中間件文件
        
        pipelines.py   # 項目的管道文件
        
        settings.py    # 配置文件
        
        spiders/       # 后期放 spider 的文件
            __init__.py

第一個項目

自定義的 Spider 類用于從網(wǎng)站(一組網(wǎng)站)中抓取數(shù)據(jù),它們必須為 scrapy.Spider 子類叛复,初始化的請求為必選項仔引,可選項包括對下一頁的相關(guān)處理、從解析下載的頁面內(nèi)容提取數(shù)據(jù)的細節(jié)褐奥。

我們的第一個 Spider 保存在 quotes_spider.py 文件中咖耘,在 tutorial/spiders 路徑下:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)

可以看出來,我們定義的 Spider 是 scrapy.Spider 的子類抖僵,并且定義了一些屬性和方法:

  • name:唯一識別名鲤看,在當(dāng)前項目中必須唯一缘揪。
  • start_requests():必須返回請求的迭代(可以是請求列表或生成器函數(shù))耍群,Spider 從這里開始爬。
  • parse(): 每個請求發(fā)起后調(diào)用找筝,用于處理響應(yīng)蹈垢。response 參數(shù)是 TextResponse 的實例,它包含了頁面內(nèi)容袖裕,內(nèi)容隨后會被處理曹抬。parse() 方法實現(xiàn)許多功能,包括解析響應(yīng)急鳄,提取爬取的數(shù)據(jù)并解析為字典谤民,尋找下一個爬取的 URL 并對此鏈接發(fā)起新的請求。

如何執(zhí)行我們的 Spider

為了使 Spider 生效疾宏,需要進入項目頂層路徑张足,并執(zhí)行如下命令:

scrapy crawl quotes

這個命令以 quotes 名稱作為參數(shù)執(zhí)行,并且會對 qotes.toscrape.com 域名的網(wǎng)站發(fā)起請求坎藐。

執(zhí)行命令后为牍,可以發(fā)現(xiàn)在當(dāng)前路徑下,有兩個新文件: quotes-1.html quotes-2.html

剛才發(fā)生了什么

Scrapy 會規(guī)劃 Spider 中 start_requests 返回的 scrapy.Request岩馍。一旦接受到每個請求的響應(yīng)碉咆,就會實例化 Response 對象,將響應(yīng)實例作為回調(diào)方法的參數(shù)(在這個例子中蛀恩,是 parse 方法)疫铜,回調(diào)與請求 Request 關(guān)聯(lián)。

start_requests 方法的簡寫

我們可以僅僅定義 start_urls 類屬性双谆,它是 URL 的列表組合壳咕,用于替代 start_requests() 中通過生成 scrapy.Reuqest 對象的做法励稳。定義了 start_urls 后,會使用默認的 start_requests() 來創(chuàng)建 Spider 中的初始化請求囱井。
于是代碼可以改成:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)

變化在于 start_urls 替代了 start_requests() 方法驹尼,只提供 URL 列表,其余的初始化請求操作交給父類中默認的 start_requests() 方法庞呕。

parse() 方法會被在每個 URL 被請求時調(diào)用新翎,即使我們沒有明確的告訴 Scrapy 這么去做,這是因為 parse() 是 Scrapy 默認自動調(diào)用的方法住练。

提取數(shù)據(jù)

使用 Scrapy 提取數(shù)據(jù)最好的調(diào)試方式:Scrapy shell地啰,執(zhí)行:

scrapy shell 'http://quotes.toscrape.com/page/1'

使用 shell,你可以嘗試從響應(yīng)中選擇元素讲逛,以 CSS 選擇器為例:

In [4]: response.css('title')
Out[4]: [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

返回的結(jié)果 SelectorList亏吝,類列表對象,它擴展了list功能盏混,可以理解為 Selector 的列表蔚鸥。這個列表對象被 XML/HTML 元素包裹,允許你執(zhí)行進一步的查詢和更細微力度的選擇许赃。

提取標(biāo)簽文本

比如止喷,從剛才的 title 對象中提取文本,可以這么做:

>>> response.css('title::text').getall()
['Quotes to Scrape']

這里有兩點要說明:

  1. 在 CSS 查詢中添加了 '::text'混聊,這意味著只提取 <title> 中的文本弹谁。如果定義 '::text',會返回整個 title 元素內(nèi)容(包括標(biāo)簽):
>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']
  1. 調(diào)用 '.getall()'句喜,它返回一個列表:這意味著選擇器可能返回一個或多個結(jié)果预愤;如果你只需要第一個結(jié)果,可以使用 '.get()'咳胃,也可以通過 python 代碼先選擇第一個結(jié)果:
response.css("title::text")[0].get()

但是植康,如果直接對 SelectorList 實例使用 get() 方法可以避免 'IndexError' 錯誤,它會處理成 None 返回拙绊,所以推薦直接使用 '.get()'

正則匹配

除了使用 'getall()' 和 'get()' 方法向图,可以使用 're()' 方法來提取數(shù)據(jù)

瀏覽器查看響應(yīng)結(jié)果

為了更好的使用合適的 CSS 選擇器,可以使用 view(response) 來查看響應(yīng)标沪。它會在瀏覽器中顯示響應(yīng)內(nèi)容榄攀,你可以使用瀏覽器開發(fā)者工具來檢測 HTML 并選擇合適的選擇器。

XPath 介紹

除了 CSS金句,Scrapy 選擇器還支持使用 XPath 表達式:

>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'

XPath 表達式非常強大檩赢,是 Scrapy 選擇器的基石。實際上,CSS選擇器最終會被轉(zhuǎn)換成 XPath贞瞒。

盡管 XPath 表達式?jīng)]有 CSS 活躍偶房,但是它更有用,因為除了導(dǎo)航定位文件結(jié)構(gòu)军浆,它還能獲取文本內(nèi)容棕洋。
使用 XPath,你可以選擇這樣的內(nèi)容:選擇一個包含 "Next Page" 的鏈接乒融。
這使得 XPath 足以勝任爬蟲的任務(wù)掰盘,我們鼓勵你去學(xué)習(xí) XPath ,即使你已經(jīng)了解了如何構(gòu)建 CSS 選擇器赞季。

更多 XPath 使用方法可以查看 http://zvon.org/comp/r/tut-XPath_1.htmlhttp://plasmasturm.org/log/xpath101/

提取 quotes 和 authors

現(xiàn)在你已經(jīng)知道了一點選擇和提取數(shù)據(jù)的方法愧捕,繼續(xù)完善代碼。

>>> for quote in response.css("div.quote"):
...     text = quote.css("span.text::text").get()
...     author = quote.css("small.author::text").get()
...     tags = quote.css("div.tags a.tag::text").getall()
...     print(dict(text=text, author=author, tags=tags))
{'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'}
{'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'}
    ... a few more of these, omitted for brevity
>>>

使用 spider 提取數(shù)據(jù)

上面都是在 shell 中調(diào)試申钩,在返回 spider 中次绘。目前 Spider 沒有提取任何數(shù)據(jù),只是保存了整個 HTML 文件到本地撒遣。
接下來整合提取數(shù)據(jù)的邏輯到 spider 中邮偎。

一個 Scrapy spider 可以生成許多包含已提取數(shù)據(jù)的字典。
為做到這個目的愉舔,我們使用 yield 關(guān)鍵字來構(gòu)建生成器:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

對比之前的 spider 代碼钢猛,在這里主要發(fā)生變化的是 yield 子句。是一個調(diào)試的三個選擇器轩缤,我們獲取了每個 quote 的指定內(nèi)容。

存儲爬取的數(shù)據(jù)

最簡單的存儲方式是使用 'Feed exports'贩绕,命令如下:

scrapy crawl -o quotes.json

這會生成 quotes.json 文件火的,包含爬取的項目,以 JSON 序列化淑倾。

由于歷史原因馏鹤,Scrapy 使用附加的方式而非替代原文件內(nèi)容生成文件。也就是說娇哆,如果你執(zhí)行兩次存儲名命令湃累,會再添加內(nèi)容到文件中。由于 JSON 格式問題碍讨,存儲兩次治力,將破壞 JSON 的格式。

其他格式 JSON Lines

scrapy crawl -o quotes.jl

'JSON Lines' 是一種流類型結(jié)構(gòu)勃黍,可以輕易的添加新內(nèi)容到文件中宵统,而不用擔(dān)心執(zhí)行了兩次破壞文件格式。
因為每個記錄都以單獨的一行記錄覆获。
JL 當(dāng)可以搭配工具马澈,如 'JQ' 來輔助執(zhí)行這樣的命令瓢省。

項目管道 Item Pipeline

在小項目中,上面提到才這些內(nèi)容都足夠了痊班。然而勤婚,如果你想執(zhí)行更加復(fù)雜的爬取,你可以寫一個 'tiem pipleline'

「下一頁」鏈接

除了僅僅抓取第一頁和第二頁的內(nèi)容涤伐,你想要網(wǎng)頁中所有的內(nèi)容也可以蛔六。

在上面已經(jīng)了解到如何提取頁面上的鏈接,接下來看看如何獲取并進入下一頁鏈接

提取下一頁標(biāo)簽

首先觀察一下頁面中下一頁內(nèi)容

<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
    </li>
</ul>

需要提取的內(nèi)容是 href 的屬性值废亭,它告訴我們下一頁的相對地址

>>> response.css('li.next a::attr(href)').get()
'/page/2/'

>>> response.css('li.next a').attrib['href']
'/page/2'

那么如何遞歸所有的下一頁鏈接呢国章,spider 內(nèi)容可以這樣改動:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)

這里的關(guān)鍵在于:一、判斷是否有下一頁豆村,二液兽、如果有,如何拼接 URL 遞歸請求訪問和解析新頁面內(nèi)容掌动。

現(xiàn)在四啰,已經(jīng)提取了數(shù)據(jù),parse() 中拼接絕對路徑的 URL 使用了 urljoin() 方法粗恢,并且生成一個新的請求到下一頁柑晒;它將自身注冊作為回調(diào),以處理下一頁的數(shù)據(jù)眷射,并且持續(xù)爬取新頁面匙赞。

總結(jié)一下 Scrapy 的下一頁獲取機制:如果你在回調(diào)函數(shù)中生成了一個請求,Scrapy 會適配已發(fā)送的請求妖碉,并在請求完成注冊一個要執(zhí)行的回調(diào)方法涌庭。

基于這個機制,你可以構(gòu)建復(fù)雜的爬蟲自定義點擊超鏈接欧宜,也可以根據(jù)訪問的頁面不同自定義提取規(guī)則坐榆。

在這個例子中,我們創(chuàng)建了一個循環(huán)冗茸,一直進入下一頁界面直到最后一頁席镀。這個功能非常適合爬取博客、論壇或者擁有分頁的網(wǎng)站夏漱。

一個簡單的方式創(chuàng)建請求

我們可以使用 response.follow 來請求下一個頁面

...
        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)

將上面例子的最后兩行改成 response.follow() 請求網(wǎng)站豪诲,不同于 scrapy.Request,response.follow 提供相對路徑請求麻蹋,這意味著你不需要再添加 urljoin 拼接 URL跛溉。
記住,response.follow 返回的還是請求實例,所以需要 yield 生成這個請求芳室。

由于 <a> 標(biāo)簽是一個鏈接专肪,所以 response.follow 自動使用 href 屬性,所以代碼還可以簡寫為:

        for a in response.css('li.next a'):
            yield response.follow(a, callback=self.parse)

更多的例子和模型

這里繼續(xù)將回調(diào)和更多操作的內(nèi)容堪侯,這次爬取作者信息

import scrapy


class AuthorSpider(scrapy.Spider):
    name = 'author'

    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        # follow links to author pages
        for href in response.css('.author + a::attr(href)'):
            yield response.follow(href, self.parse_author)

        # follow pagination links
        for href in response.css('li.next a::attr(href)'):
            yield response.follow(href, self.parse)

    def parse_author(self, response):
        def extract_with_css(query):
            return response.css(query).get(default='').strip()

        yield {
            'name': extract_with_css('h3.author-title::text'),
            'birthdate': extract_with_css('.author-born-date::text'),
            'bio': extract_with_css('.author-description::text'),
        }

這個 spider 開始于主頁面嚎尤,為所有的作家頁面調(diào)用 parse_author 回調(diào),分頁鏈接的 parse 回調(diào)伍宦。

parse_author 回調(diào)定義了一個方法從 CSS 查詢中提取和清理數(shù)據(jù)芽死,再產(chǎn)生作者資料的字典。

在這個 spider 中展示的另一個有趣的事情:即使有許多 quotes 對應(yīng)一個 author次洼,我們也不用擔(dān)心會多次訪問同一個 author 界面关贵。
默認情況下,Scrapy 過濾重復(fù)URL請求卖毁,避免因訪問服務(wù)器過多而產(chǎn)生問題揖曾。
可以在設(shè)置中配置參數(shù) DUPFILTER_CLASS

使用 spider 參數(shù)

提供命令行參數(shù)給 spider,添加 '-a' 選項即可:

scrapy crawl quotes -o quotes-humor.json -a tag=humor

這些參數(shù)會傳遞給 Spider 類的 '__init__' 方法亥啦,并作為 spider 實例的默認屬性炭剪。

在這個例子中,提供給 tag 屬性的值可以使用 self.tag 獲取翔脱。
添加 tag 選項后奴拦,spider 抓取的內(nèi)容被限制在特定的標(biāo)簽中:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        url = 'http://quotes.toscrape.com/'
        tag = getattr(self, 'tag', None)
        if tag is not None:
            url = url + 'tag/' + tag
        yield scrapy.Request(url, self.parse)

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

在這里,添加了 humor 標(biāo)簽/tag 的 spider 訪問的 URL 變成了 'http://quotes.toscrape.com/tag/humor'

[1] http://docs.scrapy.org/en/latest/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末届吁,一起剝皮案震驚了整個濱河市错妖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瓷产,老刑警劉巖站玄,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異濒旦,居然都是意外死亡,警方通過查閱死者的電腦和手機再登,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門尔邓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锉矢,你說我怎么就攤上這事梯嗽。” “怎么了沽损?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵灯节,是天一觀的道長。 經(jīng)常有香客問我,道長炎疆,這世上最難降的妖魔是什么卡骂? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮形入,結(jié)果婚禮上全跨,老公的妹妹穿的比我還像新娘。我一直安慰自己亿遂,他們只是感情好浓若,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛇数,像睡著了一般挪钓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上耳舅,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天碌上,我揣著相機與錄音,去河邊找鬼挽放。 笑死绍赛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的辑畦。 我是一名探鬼主播吗蚌,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼纯出!你這毒婦竟也來了蚯妇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤暂筝,失蹤者是張志新(化名)和其女友劉穎箩言,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焕襟,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡陨收,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鸵赖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片务漩。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖它褪,靈堂內(nèi)的尸體忽然破棺而出饵骨,到底是詐尸還是另有隱情,我是刑警寧澤茫打,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布居触,位于F島的核電站妖混,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏轮洋。R本人自食惡果不足惜制市,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砖瞧。 院中可真熱鬧息堂,春花似錦、人聲如沸块促。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竭翠。三九已至振坚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間斋扰,已是汗流浹背渡八。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留传货,地道東北人屎鳍。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像问裕,于是被迫代替她去往敵國和親逮壁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348