初識 Scrapy

1. 前言

scrapy的體系架構如下:
在這裏插入圖片描述
在Scrapy中數據流由 execution engine 控制, 過程如下:

  1. EngineSpider中獲取到爬取的初始請求;
  2. EngineScheduler 中調度請求,並請求下一個要爬取的請求。
  3. Scheduler 返回下一個請求給 Engine
  4. Engine 發送請求給 Downloader, 需要經過 Downloader Middlewares (見 process_request()).
  5. 一旦頁面下載完成, Downloader 會生成該頁面的響應,並將響應發送給 Engine, 需要經過 Downloader Middlewares (見 process_response()).
  6. EngineDownloader 接收到響應,並將響應發送給 Spider 處理,需要經過 Spider Middleware (見 process_spider_input()).
  7. Spider 處理響應並返回 item 和需要跟蹤的新請求給 Engine, 需要經過 Spider Middleware (見 process_spider_output()).
  8. Engine 發送處理過的 itemItem Pipelines, 然後將已處理的請求發送到調度程序,並要求進行爬取的下一個請求。
  9. 從第一步開始重複,直到 Scheduler 中沒有請求了。

1.1 什麼是Scrapy?

Scrapy是一個應用框架,常用於爬取網站抽取結構化數據

1.2 scrapy、BS 和 lxml

BeautifulSoup 是解析 HTML 的庫。在Python程序員中是一個非常流行的Web抓取庫,它基於HTML代碼的結構構造了一個Python對象,並且能夠很好地處理錯誤的標記,但是它有一個缺點:速度慢
lxml 是一個XML解析庫(它也解析HTML),使用基於 ElementTree . (LXML不是Python標準庫。)
scrapy 是爬蟲應用框架

1.3 爬取順序

默認的爬取順序是DFS,因爲默認使用來存放爬蟲請求。

1.4 需要解析的數據過大怎麼辦?

在使用XPath選擇器解析HTML數據時,需要在內存中構建整個HTMLDOM,這可能會導致解析很慢並且會佔用大量內存。

因此,爲了避免一次在內存中解析所有的HTML數據,我們可以使用 scrapy.utils.iterators 模塊中的 xmlitercsviter

1.5 部署 Scrapy 爬蟲

有兩種部署方式:

  • Scrapyd (open source)
  • Scrapy Cloud (cloud-based)

1.6 Scrapy的依賴

Scrapy是純python寫的,它依賴於如下幾個關鍵的python包。

  • lxml, 高效的XMLHTML解析器
  • parsel, 是在lxml之上編寫的HTML / XML數據提取庫
  • w3lib, 用於處理URL和網頁編碼的多功能幫助器
  • twisted, 異步網絡框架
  • cryptography and pyOpenSSL, 處理各種網絡級安全需求

2. 創建Scrapy項目

在當前目錄下,創建一個名爲 recruitment 的爬蟲項目。

scrapy startproject recruitment 項目目錄(不寫,則在當前目錄創建該項目)

recruitment中的目錄結構如下:

recruitment/
    scrapy.cfg            # deploy configuration file

    recruitment/             # project's Python module, you'll import your code from here
        __init__.py

        items.py          # project items definition file

        middlewares.py    # project middlewares file

        pipelines.py      # project pipelines file

        settings.py       # project settings file

        spiders/          # a directory where you'll later put your spiders
            __init__.py

爬蟲的主要邏輯代碼寫在spiders 目錄中。比如,定義最初的爬取網站的url,選擇如何遵循頁面中url,如何解析下載的頁面,如何提取數據等。

import scrapy

class RecruitmentSpider(spcrapy.Spider):
    # spider的標識,在一個項目中,此name必須是唯一的。在同一個項目不能爲兩個不同spider取相同的name。
    name = "recruitment" 

    def start_requests(self):
        """
        返回請求的可迭代對象,標識spider從哪些url開始爬取。後序的請求都是由此請求產生。
        """
        urls = [
            'https://www.zhipin.com/',
            'https://www.lagou.com/',
            'https://landing.zhaopin.com/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        """
        response參數必須是 TextResponse的實例對象,
        解析每一個URL響應、抽取數據、發現新的需爬取的URL。
        """
        page = response.url.split("/")[-2]
        filename = 'recruitment-{}.html'.format(page)

        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file {}'.format(filename))

3. 如何運行spider

在項目目錄下運行如下命令,這裏 recruitment 是上面定義的spider的name

scrapy crawl recruitment

終端中會有一些輸出信息。

start_requests方法的返回值是 scrapy.Request 對象。

start_requests() 方法的快捷方式。很多時候可以不用我們來實現start_requests()方法,而是直接在start_urls這個類屬性中定義需要爬取的URLscrapy會使用自己start_requests()方法的默認實現。

所以上面的代碼可以簡化一下:

import scrapy

class RecruitmentSpider(scrapy.Spider):
    # spider的標識,在一個項目中,此name必須是唯一的。在同一個項目不能爲兩個不同spider取相同的name。
    name = "recruitment" 
    start_urls = [
        'https://www.zhipin.com/',
        'https://www.lagou.com/',
        'https://landing.zhaopin.com/',
    ]

    def parse(self, response):
        """
        response參數必須是 TextResponse的實例對象,
        解析每一個URL響應、抽取數據、發現新的需爬取的URL。
        """
        page = response.url.split("/")[-2]
        filename = 'recruitment-{}.html'.format(page)

        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file {}'.format(filename))

4. Scrapy中的基礎概念

4.1 命令行工具

命令行工具控制Scrapy

Scrapy從如下文件中查找配置:

  1. /etc/scrapy.cfgc:\scrapy\scrapy.cfg (系統範圍的配置)
  2. ~/.config/scrapy.cfg ($XDG_CONFIG_HOME) 和 ~/.scrapy.cfg ($HOME) ,這是全局配置(用戶範圍的配置)
  3. 項目根目錄下的scrapy.cfg。(項目範圍的配置)
    這些文件中的設置將按照優先順序進行合併。從上到下,優先級越來越高。

一個項目的根目錄(包含scrapy.cfg的目錄)可以被多個Scrapy項目共享,每個Scrapy項目都有自己的設置模塊。

# 創建一個項目
scrapy startproject myproject 項目目錄
cd myproject
# 查看help
scrapy -h
scarpy 命令 -h

4.2 spiders

主要的爬取邏輯在這裏。怎麼爬,怎麼提取數據等等。

抓取週期如下:

  1. 首先,生成對爬取第一個URL的初始請求,然後指定一個回調函數,該函數在調用時會使用從這些請求下載的響應。 要執行的第一個請求是通過調用 start_requests() (默認)方法 生成的對應 start_urlsRequest,然後將parse()方法作爲Request的回調函數。
  2. 在回調函數中,解析response並返回Item 對象,或Request對象,或者是它們的可迭代對象。這些Requests還將包含回調函數(可以相同,可以不同),在scrapy下載之後,會由指定的回調函數來處理該request對應的response
  3. 在回調函數中,通常使用 選擇器(也可以使用beautifulsouplxml或任何喜歡的機制)來解析頁面內容,並使用解析的數據生成item
  4. 最後,從spider返回的項目通常被持久化到數據庫(通過 Item Pipeline )或者使用 Feed 導出 寫入文件中。

內置了幾個通用spider類:

  • CrawlSpider。這是最常用的爬行常規網站的蜘蛛,因爲它通過定義一組規則爲跟蹤鏈接提供了一種方便的機制。
  • XMLFeedSpider
  • CSVFeedSpider
  • SitemapSpider

4.3 選擇器

使用選擇器從頁面中提取數據。

Scrapy選擇器支持如下:

  • CSS選擇器。其實,CSS選擇器也是在後臺轉換爲XPath
  • XPath表達式。是Scrapy選擇器的基礎,很強大。

scrapy使用自己的機制來提取數據,這就是scrapyselector。在scrapy中要提取HTML中的數據,可以使用3種方式:

  1. response.selector.css()response.selector.xpath()
  2. response.css()::text表示提取該元素的文本內容,::attr(name)表示提取該元素的某屬性值。
  3. response.xpath()

他們的返回值都是SelectorList的實例對象,對此實例對象調用get().getall()方法,分別獲得第一個符合條件的元素,和所有符合條件的元素的list。使用.attrib屬性可以獲取提取的元素中的某屬性值。

嵌套調用

由於.xpath().css()的返回值都是selector,所以還可以繼續調用.xpath().css()方法。

elem_div = response.xpath("//div")
elem_a = elem_div.xpath(".//a").getall()

選擇元素屬性

HTML元素中提取元素屬性的值,有如下3種方法。

  1. response.xpath("//a/@href").getall()
  2. response.css("a::attr(href)").getall()
  3. [a.attrib['href'] for a in reponse.css('a')]

使用正則表達式

Selector還可以使用.re()方法來提取數據。返回值是unicode字符串的list。 .re_first() 提取滿足正則表達式的第一個結果。

  • .get() 的別名是.extract_first()
  • .getall()的別名是.extract()
    因爲使用前者更清晰明瞭,所以推薦使用前者。

XPaths

在使用.xpath()的時候,/開頭表示是絕對的xpath路徑。
./開頭表示相對的xpath路徑。

>>> divs = response.xpath('//div')
>>> for p in divs.xpath('//p'):  # this is wrong - gets all <p> from the whole document
...     print(p.get())
# correct way
>>> for p in divs.xpath('.//p'):  # extracts all <p> inside
...     print(p.get())

按照 class來查詢時,使用.css()是更好的選擇。

# class名爲 shout 的 <a>標籤的href屬性
>>> response.css('.shout').xpath('./a/@href/').getall()

4.4 Items

Spiders會將提取的數據存放在items中,這是一個key-value的Python對象。

Scrapy支持一下類型的Item

  1. 字典
  2. Item對象
  3. dataclass 對象
  4. attrs對象

聲明一個Item

import scrapy

class Product(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    stock = scrapy.Field()
    tags = scrapy.Field()
    last_updated = scrapy.Field(serializer=str)

Scrapy中的Item定義有些像Django中的Model,但是更簡單,沒有不同的Field之分。

聲明field

Field對象是用來指明每一個字段的元數據的。Scrapy中沒有對Field對象所能接受的值做限制

需要注意的是,在Item子類中定義的這些字段,並不是該子類的類屬性,而是應該通過Item.fields來獲取這些字段。

其實Field類就是Python中dict類的別名,,它並沒有任何其它的功能或屬性。

創建items

>>> product = Product(name='Desktop PC', price=1000)
>>> print(product)
Product(name='Desktop PC', price=1000)

獲取字段的值

# 用法就跟字典很相似

>>> product['name']
Desktop PC
>>> product.get('name''unknown field')
Desktop PC

設置字段的值

>>> product['name'] = 'Desktop PC for sjl'

獲取所有填充了的值

>>> product.keys()
['price', 'name']
>>> product.items()
[('price, 1000), ('name', 'Desktop PC for sjl')]

複製 items

# 可以選擇淺拷貝和深拷貝,淺拷貝和深拷貝這裏就不多講了
product2 = product.copy()
product2 = product.deepcopy()

Scrapy並不是直接填充items,而是有自己的機制。

items爲爬取的數據提供容器,Item Loader爲該容器提供了填充機制。

4.5 Item Loader

Item Loader用來填充數據進item中。

詳情 初識 Scrapy - Item Loader

from scrapy.loader import ItemLoader
from myproject.items import Product

def parse(self, response):
    l = ItemLoader(item=Product(), response=response)
    l.add_xpath('name', '//div[@class="product_name"]')
    l.add_xpath('name', '//div[@class="product_title"]')
    l.add_xpath('price', '//p[@id="price"]')
    l.add_css('stock', 'p#stock]')
    l.add_value('last_updated', 'today') # you can also use literal values
    return l.load_item()

輸入和輸出處理器

一個Item Loader 爲每一個 item的字段都指定了一個輸入處理器和輸出處理器。

  • 當提取的數據被Item Loader接收(如:add_xpath(), add_css(), add_value()方法)時會使用輸入處理器處理它們,並保存在ItemLoader中。
  • 然後調用ItemLoader.load_item()方法獲得Item對象並在輸出處理器處理這些數據之後進行填充到item中。

4.6 Scrapy shell

用於開發和調試你的spiders代碼的。

scrapy shell <needing_scraped_url>

4.7 item pipeline

詳情見 初識 Scrapy - Item Pipeline
itemspider抓取之後,它會被髮送到Item Pipeline,該管道通過幾個按順序執行的組件來處理它。

每一個item pipeline組件都是Python的類。它們接收item,並對它執行操作,還決定該項目是否應繼續通過管道,或者是否應刪除並不再處理。

item pipeline的典型用途有:

  • 清理HTML數據
  • 驗證抓取的數據(檢查項目是否包含某些字段)
  • 檢查重複項(並刪除它們)。通過pipelineprocess_item()方法實現
  • 將爬取的項目存儲在數據庫中

4.8 feed導出

詳情見 初識 Scrapy - Feed導出
在實現scraper時,常需要的功能之一是能夠正確地存儲被抓取的數據,這意味着用被抓取的數據(通常稱爲“導出提要”)生成一個“導出文件”,供其他系統使用。

Scrapy通過Feed導出提供了這樣一個開箱即用的功能。允許你根據抓取的items使用多種序列化格式存儲後端生成feeds。

4.9 Scrapy的Request和Response

Scrapy使用RequestResponse對象來爬取網站。

通常,Request對象是在Spider中生成的,並在整個系統中傳遞,直到它們到達Downloader,該Downloader執行請求並返回Response對象,該Response對象返回到發出請求的Spider中。

4.10 link提取器

用於從repsonse中提取link

LxmlLinkExtractor 中的 __init__ 方法決定應該提取什麼樣的link。

LxmlLinkExtractor.extract_links 接收 Response 對象並返回 scrapy.link.Link對象。

4.11 設置

設置的優先級:

  1. 命令行選項(最高優先級)
  2. 每一個spider的設置
  3. 項目的setting模塊
  4. 每個命令的默認設置
  5. 默認的全局設置

在Spider中通過self.settings來訪問設置。

4.12 異常

Scrapy中有一些內置的異常:

  • CloseSpider。在spider的request關閉或停止時,在request的回調函數中引發。
  • DontCloseSpider。在spider_idle信號處理器中引發,以阻止關閉spider。
  • DropItem。在item pipeline階段引發,以停止處理某Item。
  • IgnoreRequest。由Scheduler或任何downloader中間件引發,以表示此request應當被忽略。
  • NotConfigured。由某些組件引發,以表示這些組件是禁用的。
  • NotSupported。引發此異常表示不支持某特性。
  • StopDownload。由bytes_received信號處理器引發,以表示response沒有更多的可下載的bytes了。

5. 參考文獻

[1] Scrapy 官方文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章