8-Scrapy框架匯總

scrapy架構(gòu)

scrapy.png

Scrapy主要組件
1、引擎(Scrapy): 用來(lái)處理整個(gè)系統(tǒng)的數(shù)據(jù)流處理, 觸發(fā)事務(wù)(框架核心)。
2、調(diào)度器(Scheduler): 用來(lái)接受引擎發(fā)過(guò)來(lái)的請(qǐng)求, 壓入隊(duì)列中, 并在引擎再次請(qǐng)求的時(shí)候返回. 可以想像成一個(gè)URL(抓取網(wǎng)頁(yè)的網(wǎng)址或者說(shuō)是鏈接)的優(yōu)先隊(duì)列, 由它來(lái)決定下一個(gè)要抓取的網(wǎng)址是什么, 同時(shí)去除重復(fù)的網(wǎng)址。
3、下載器(Downloader): 用于下載網(wǎng)頁(yè)內(nèi)容, 并將網(wǎng)頁(yè)內(nèi)容返回給蜘蛛(Scrapy下載器是建立在twisted這個(gè)高效的異步模型上的)。
4、爬蟲(chóng)(Spiders): 爬蟲(chóng)是主要干活的, 用于從特定的網(wǎng)頁(yè)中提取自己需要的信息, 即所謂的實(shí)體(Item)。用戶也可以從中提取出鏈接,讓Scrapy繼續(xù)抓取下一個(gè)頁(yè)面。
5、項(xiàng)目管道(Pipeline): 負(fù)責(zé)處理爬蟲(chóng)從網(wǎng)頁(yè)中抽取的實(shí)體,主要的功能是持久化實(shí)體、驗(yàn)證實(shí)體的有效性、清除不需要的信息。當(dāng)頁(yè)面被爬蟲(chóng)解析后,將被發(fā)送到項(xiàng)目管道,并經(jīng)過(guò)幾個(gè)特定的次序處理數(shù)據(jù)。
6、下載器中間件(Downloader Middlewares): 位于Scrapy引擎和下載器之間的框架,主要是處理Scrapy引擎與下載器之間的請(qǐng)求及響應(yīng)。
7、爬蟲(chóng)中間件(Spider Middlewares): 介于Scrapy引擎和爬蟲(chóng)之間的框架,主要工作是處理蜘蛛的響應(yīng)輸入和請(qǐng)求輸出。
8、調(diào)度中間件(Scheduler Middewares): 介于Scrapy引擎和調(diào)度之間的中間件,從Scrapy引擎發(fā)送到調(diào)度的請(qǐng)求和響應(yīng)。

安裝

  1. 首先考慮使用最簡(jiǎn)單的方法安裝,可能會(huì)有諸多錯(cuò)誤,scrapy安裝需要Lxml、Twisted等庫(kù)。
pip install scrapy  
  • Failed building wheel for lxml
  • Microsoft Visual C++ 10.0 is required
  • Failed building twisted
  • Unable to find vcvarsall.bat
  1. 直接使用pip install scrapy安裝不成功可以安裝whl格式的包

    首先下載scrapy的whl包,下載地址:http://www.lfd.uci.edu/~gohlke/pythonlibs/

    搜索scrapy 找到Scrapy-1.5.0-py2.py3-none-any.whl,下載后不要安裝。

    scrapy依賴twiste,同樣使用whl格式的包進(jìn)行安裝

    還是進(jìn)入http://www.lfd.uci.edu/~gohlke/pythonlibs/,在網(wǎng)頁(yè)中搜索twisted找到其對(duì)應(yīng)的whl包并下載.

    下載完成后使用cmd打開(kāi)windows的命令行窗口,進(jìn)入whl包所在的文件夾執(zhí)行如下命令

    pip install [whl],[whl]是whl包的名字.

    scrapy依賴lxml包,需要先安裝lxml包. pip install lxml即可,再安裝twisted,最后安裝scrapy.

    安裝完成后使用scrapy -h測(cè)試是否安裝成功

  2. windows系統(tǒng)安裝完成后運(yùn)行scrapy可能會(huì)報(bào)no model named win32錯(cuò)誤,到https://sourceforge.net/projects/pywin32/files/下載直接安裝即可。

scrapy命令

C:\Users\Administrator>scrapy
Scrapy 1.5.0 - no active project

Usage:
  scrapy <command> [options] [args]

Available commands:
  bench         Run quick benchmark test
  fetch         Fetch a URL using the Scrapy downloader
  genspider     Generate new spider using pre-defined templates
  runspider     Run a self-contained spider (without creating a project)
  settings      Get settings values
  shell         Interactive scraping console
  startproject  Create new project
  version       Print Scrapy version
  view          Open URL in browser, as seen by Scrapy
  list          List available spiders
  parse         Parse URL (using its spider) and print the results
  check         Check spider contracts
  crawl         Run a spider
  edit          Edit spider
  

  [ more ]      More commands available when run from project directory

Use "scrapy <command> -h" to see more info about a command
# bench 做測(cè)試用,反映當(dāng)前性能,爬蟲(chóng)速度
scrapy bench

# fetch 幫助我們下載網(wǎng)頁(yè),將網(wǎng)頁(yè)源代碼返回(前面是一些日志,后面是源代碼)
scrapy fetch url

#生成爬蟲(chóng)
scrapy genspider +文件名+網(wǎng)址

# runspider運(yùn)行爬蟲(chóng)文件,與crawl的去區(qū)別是runspider運(yùn)行的是spider.py文件,而crawl運(yùn)行整個(gè)項(xiàng)目
scrapy runspider spider.py

# Get settings values
scrapy settings --get BOT_NAME
scrapybot
scrapy settings --get DOWNLOAD_DELAY
0

# shell命令, 進(jìn)入scrpay交互環(huán)境,主要使用這里面的response命令, 例如response.xpath() 括號(hào)里直接加xpath路徑
scrapy shell url

#創(chuàng)建項(xiàng)目
scrapy startproject demo

#查看scrapy版本  -v可以輸出依賴庫(kù)的版本
scrapy version -v

# view請(qǐng)求Url,把它的網(wǎng)頁(yè)源代碼保存成文件,并打開(kāi)網(wǎng)頁(yè)
scrapy view http://www.example.com/some/page.html

#查看爬蟲(chóng)列表
scrapy list

#check檢查錯(cuò)誤
scrapy check

# 運(yùn)行(crawl)
scrapy crawl +爬蟲(chóng)名稱

#使用 EDITOR 中設(shè)定的編輯器編輯給定的spider
#該命令僅僅是提供一個(gè)快捷方式。開(kāi)發(fā)者可以自由選擇其他工具或者IDE來(lái)編寫(xiě)調(diào)試spider。
scrapy edit spider1

settings.py配置

# USER_AGENT 設(shè)置用戶代理
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36 QIHU 360SE'

#設(shè)置是否遵守robots協(xié)議
ROBOTSTXT_OBEY = True

# 設(shè)置抓取中文顯示編碼
FEED_EXPORT_ENCODING = 'utf-8'

# pipelines激活
ITEM_PIPELINES = {
    'BtMovie.pipelines.BtmoviePipeline': 300,
}

scrapy抓取網(wǎng)站

一般需要四個(gè)步驟

  1. 創(chuàng)建一個(gè)爬蟲(chóng)項(xiàng)目
  2. 定義Item容器
  3. 編寫(xiě)爬蟲(chóng),提取數(shù)據(jù)
  4. 存儲(chǔ)內(nèi)容

創(chuàng)建項(xiàng)目

在開(kāi)始爬取之前,您必須創(chuàng)建一個(gè)新的Scrapy項(xiàng)目。 進(jìn)入您打算存儲(chǔ)代碼的目錄中,運(yùn)行下列命令:

scrapy startproject tutorial

該命令將會(huì)創(chuàng)建包含下列內(nèi)容的 tutorial 目錄:

tutorial/
    scrapy.cfg
    tutorial/
        __init__.py
        items.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            ...

這些文件分別是:

  • scrapy.cfg: 項(xiàng)目的配置文件
  • tutorial/: 該項(xiàng)目的python模塊。之后您將在此加入代碼。
  • tutorial/items.py: 項(xiàng)目中的item文件.
  • tutorial/pipelines.py: 項(xiàng)目中的pipelines文件.
  • tutorial/settings.py: 項(xiàng)目的設(shè)置文件.
  • tutorial/spiders/: 放置spider代碼的目錄.

定義Item

Item 是保存爬取到的數(shù)據(jù)的容器;其使用方法和python字典類似, 并且提供了額外保護(hù)機(jī)制來(lái)避免拼寫(xiě)錯(cuò)誤導(dǎo)致的未定義字段錯(cuò)誤。

類似在ORM中做的一樣,您可以通過(guò)創(chuàng)建一個(gè) scrapy.Item 類, 并且定義類型為 scrapy.Field 的類屬性來(lái)定義一個(gè)Item。

首先根據(jù)需要從toscrape.com獲取到的數(shù)據(jù)對(duì)item進(jìn)行建模。 我們需要從quotes.py中獲取名字,url,以及網(wǎng)站的描述。 對(duì)此,在item中定義相應(yīng)的字段。編輯 tutorial 目錄中的 items.py 文件:

class TutorialItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

編寫(xiě)第一個(gè)爬蟲(chóng)(Spider)

Spider是用戶編寫(xiě)用于從單個(gè)網(wǎng)站(或者一些網(wǎng)站)爬取數(shù)據(jù)的類。

其包含了一個(gè)用于下載的初始URL,如何跟進(jìn)網(wǎng)頁(yè)中的鏈接以及如何分析頁(yè)面中的內(nèi)容, 提取生成 item 的方法。

為了創(chuàng)建一個(gè)Spider,您必須繼承 scrapy.Spider 類, 且定義以下三個(gè)屬性:

  • name: 用于區(qū)別Spider。 該名字必須是唯一的,您不可以為不同的Spider設(shè)定相同的名字。
  • start_urls: 包含了Spider在啟動(dòng)時(shí)進(jìn)行爬取的url列表。 因此,第一個(gè)被獲取到的頁(yè)面將是其中之一。 后續(xù)的URL則從初始的URL獲取到的數(shù)據(jù)中提取。
  • parse() 是spider的一個(gè)方法。 被調(diào)用時(shí),每個(gè)初始URL完成下載后生成的 Response 對(duì)象將會(huì)作為唯一的參數(shù)傳遞給該函數(shù)。 該方法負(fù)責(zé)解析返回的數(shù)據(jù)(response data),提取數(shù)據(jù)(生成item)以及生成需要進(jìn)一步處理的URL的 Request 對(duì)象。

以下為我們的第一個(gè)Spider代碼,保存在 tutorial/spiders 目錄下的 quotes.py 文件中:

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    # allowed_domains = ['toscrape.com']
    
    # 簡(jiǎn)潔寫(xiě)法
    '''
    start_urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
    ]
    '''
    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)
    
    #回調(diào)函數(shù)
    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)

爬取

進(jìn)入項(xiàng)目的根目錄,執(zhí)行下列命令啟動(dòng)spider:

#爬蟲(chóng)的名字就是quotes.py中的name
scrapy crawl quotes

scrapy crawl quotes --nolog不生成日志文件

或者在spiders目錄下創(chuàng)建run.py寫(xiě)入

from scrapy import cmdline

# -o表示文件名 -t表示文件格式
cmdline.execute("scrapy crawl news -o news.json -t json".split())

提取Item

Selectors選擇器簡(jiǎn)介

從網(wǎng)頁(yè)中提取數(shù)據(jù)有很多方法。Scrapy使用了一種基于 XPathCSS 表達(dá)式機(jī)制: Scrapy Selectors。 關(guān)于selector和其他提取機(jī)制的信息請(qǐng)參考 Selector文檔

這里給出XPath表達(dá)式的例子及對(duì)應(yīng)的含義:

  • /html/head/title: 選擇HTML文檔中 head 標(biāo)簽內(nèi)的 title 元素
  • /html/head/title/text(): 選擇上面提到的 title 元素的文字
  • //td: 選擇所有的 <td> 元素
  • //div[@class="mine"]: 選擇所有具有 class="mine" 屬性的 div 元素

上邊僅僅是幾個(gè)簡(jiǎn)單的XPath例子,XPath實(shí)際上要比這遠(yuǎn)遠(yuǎn)強(qiáng)大的多。 如果您想了解的更多,我們推薦 這篇XPath教程

為了配合XPath,Scrapy除了提供了 Selector 之外,還提供了方法來(lái)避免每次從response中提取數(shù)據(jù)時(shí)生成selector的麻煩。

Selector有四個(gè)基本的方法(點(diǎn)擊相應(yīng)的方法可以看到詳細(xì)的API文檔):

  • xpath(): 傳入xpath表達(dá)式,返回該表達(dá)式所對(duì)應(yīng)的所有節(jié)點(diǎn)的selector list列表 。
  • css(): 傳入CSS表達(dá)式,返回該表達(dá)式所對(duì)應(yīng)的所有節(jié)點(diǎn)的selector list列表.
  • extract(): 序列化該節(jié)點(diǎn)為unicode字符串并返回list。
  • re(): 根據(jù)傳入的正則表達(dá)式對(duì)數(shù)據(jù)進(jìn)行提取,返回unicode字符串list列表。

在Shell中嘗試Selector選擇器

為了介紹Selector的使用方法,接下來(lái)我們將要使用內(nèi)置的 Scrapy shell 。Scrapy Shell需要您預(yù)裝好IPython(一個(gè)擴(kuò)展的Python終端)。

您需要進(jìn)入項(xiàng)目的根目錄,執(zhí)行下列命令來(lái)啟動(dòng)shell:

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

You will see something like:

[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x7fa91d888c90>
[s]   item       {}
[s]   request    <GET http://quotes.toscrape.com/page/1/>
[s]   response   <200 http://quotes.toscrape.com/page/1/>
[s]   settings   <scrapy.settings.Settings object at 0x7fa91d888c10>
[s]   spider     <DefaultSpider 'default' at 0x7fa91c8af990>
[s] Useful shortcuts:
[s]   shelp()           Shell help (print this help)
[s]   fetch(req_or_url) Fetch request (or URL) and update local objects
[s]   view(response)    View response in a browser

當(dāng)shell載入后,您將得到一個(gè)包含response數(shù)據(jù)的本地 response 變量。輸入 response.body 將輸出response的包體, 輸出 response.headers 可以看到response的包頭。

更為重要的是,當(dāng)輸入 response.selector 時(shí), 您將獲取到一個(gè)可以用于查詢返回?cái)?shù)據(jù)的selector(選擇器), 以及映射到 response.selector.xpath()response.selector.css() 的 快捷方法(shortcut): response.xpath()response.css()

Css selector

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

>>> response.css('title').extract()
['<title>Quotes to Scrape</title>']

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

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

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

>>> response.css('title::text').extract_first()
'Quotes to Scrape'

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

# 正則表達(dá)式
>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']

Each quote in http://quotes.toscrape.com is represented by HTML elements that look like this:

<div class="quote">
    <span class="text">“The world as we have created it is a process of our
    thinking. It cannot be changed without changing our thinking.”</span>
    <span>
        by <small class="author">Albert Einstein</small>
        <a href="/author/Albert-Einstein">(about)</a>
    </span>
    <div class="tags">
        Tags:
        <a class="tag" href="/tag/change/page/1/">change</a>
        <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
        <a class="tag" href="/tag/thinking/page/1/">thinking</a>
        <a class="tag" href="/tag/world/page/1/">world</a>
    </div>
</div>

進(jìn)入shell

$ scrapy shell 'http://quotes.toscrape.com'

>>> response.css("div.quote")
>>> quote = response.css("div.quote")[0]

>>> title = quote.css("span.text::text").extract_first()
>>> title
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").extract_first()
>>> author
'Albert Einstein'


>>> tags = quote.css("div.tags a.tag::text").extract()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']
<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
    </li>
</ul>


>>> response.css('li.next a').extract_first()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'

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

Xpath selector

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

我們可以通過(guò)這段代碼選擇該頁(yè)面中網(wǎng)站列表里所有 <li> 元素:

response.xpath('//ul/li')

網(wǎng)站的描述:

response.xpath('//ul/li/text()').extract()

網(wǎng)站的標(biāo)題:

response.xpath('//ul/li/a/text()').extract()

以及網(wǎng)站的鏈接:

response.xpath('//ul/li/a/@href').extract()

string()和text()的區(qū)別,以原生xpath例子

from lxml import etree

html = """
    <p>我在這里我我我我<b>你呢</b>在哪里</p><p>oooo</p>
"""
tree = etree.HTML(html)
print(type(tree)) #  <class 'lxml.etree._Element'>
content = tree.xpath('//p/text()')
# ['我在這里我我我我', '在哪里', 'oooo']
# p標(biāo)簽下的 b標(biāo)簽不再被解析 第一個(gè)p中的文字分成了2部分
# 而這種情況是無(wú)法使用string()的,因?yàn)樗衟是一個(gè)list, string方法只能對(duì)單個(gè)element使用,所有需要遍歷


content2 = tree.xpath('//p')   # [<Element p at 0x22fff46af08>, <Element p at 0x22fff480048>]
for p in content2:
    print(p.xpath('string(.)'))
# 我在這里我我我我你呢在哪里
# oooo
for p in content2:
    print(p.xpath('text()'))
# ['我在這里我我我我', '在哪里']
# ['oooo']

總結(jié):text()獲得的總是一個(gè)list,而string()直接獲得一個(gè)字符串

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

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"):
         text = quote.css("span.text::text").extract_first()
         author = quote.css("small.author::text").extract_first()
         tags = quote.css("div.tags a.tag::text").extract()
         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

    

使用item

import scrapy
from tutorial.items import TutorialItem

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').extract_first(),
                'author': quote.css('small.author::text').extract_first(),
                'tags': quote.css('div.tags a.tag::text').extract(),
            }
         '''
         item = TutorialItem()
         # item = {}
         item['text'] = quote.css("span.text::text").extract_first()
         item['author']  = quote.css("small.author::text").extract_first()
         item['tags']  = quote.css("div.tags a.tag::text").extract()
         yield item

保存爬取到的數(shù)據(jù)

scrapy crawl quotes -o items.json

? 在這樣小規(guī)模的項(xiàng)目中,這種存儲(chǔ)方式已經(jīng)足夠。 如果需要對(duì)爬取到的item做更多更為復(fù)雜的操作,可以編寫(xiě) piplines.py文件。

爬取下一頁(yè)

我們既然要爬取下一頁(yè),那我們首先要分析鏈接格式,找到下一頁(yè)的鏈接。

<li class="next">
    <a >下一頁(yè) ?</a>
</li>

那到底如何讓蜘蛛自動(dòng)的判斷、并爬取下一頁(yè)、下一頁(yè)的內(nèi)容呢?我們可以這樣來(lái)做,我們每爬一頁(yè)就用css選擇器來(lái)查詢,是否存在下一頁(yè)鏈接,存在:則爬取下一頁(yè)鏈接http://lab.scrapyd.cn/page/*/ ,然后把下一頁(yè)鏈接提交給當(dāng)前爬取的函數(shù),繼續(xù)爬取,繼續(xù)查找下一頁(yè),知道找不到下一頁(yè),說(shuō)明所有頁(yè)面已經(jīng)爬完,那結(jié)束爬蟲(chóng)。

import scrapy
from tutorial.items import TutorialItem

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').extract_first(),
                'author': quote.css('small.author::text').extract_first(),
                'tags': quote.css('div.tags a.tag::text').extract(),
            }
         
    next_page = response.css('li.next a::attr(href)').extract_first()  
    if next_page is not None: 
        next_page = response.urljoin(next_page)
        yield scrapy.Request(next_page, callback=self.parse)
def start_requests():
        yield  scrapy.Request(url,callback=self.page1)
def page1():
        yield  scrapy.Request(url,callback=self.page2)
def page2():
        yield item

官方教程例子:

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):
            #str.strip()
            return response.css(query).extract_first().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'),
        }

Scrapy followlinks

scrapy.Request需要一個(gè)絕對(duì)的url地址,所以使用了urljoin()方法生成絕對(duì)地址。方法1

response.follow不用獲取到絕對(duì)的url,使用follow方法會(huì)自動(dòng)幫我們實(shí)現(xiàn)。方法2

follow還可以不用獲取url字符串,只需要傳入一個(gè)selector 。This selector should extract necessary attributes.方法3

<a>標(biāo)簽有一個(gè)簡(jiǎn)寫(xiě),response.follow可以自動(dòng)使用它們的屬性。方法4

注意傳入的對(duì)象只能是str或selector,不能是SelectorList

  • 方法1
next_page = response.css('li.next a::attr(href)').extract_first()  
    if next_page is not None: 
        next_page = response.urljoin(next_page)
        yield scrapy.Request(next_page, callback=self.parse)
  • 方法2
next_page = response.css('li.next a::attr(href)').extract_first()  
    if next_page is not None: 
        yield response.follow(next_page, callback=self.parse)
  • 方法3
for href in response.css('li.next a::attr(href)')
    yield response.follow(href, callback=self.parse)
  • 方法4
for href in response.css('li.next a')
    yield response.follow(href, callback=self.parse)

爬取BT電影

# -*- coding: utf-8 -*-
import scrapy
from BtMovie.items import BtmovieItem

class BtdySpider(scrapy.Spider):
    name = 'btdy'
    # allowed_domains = ['btbtdy.net']
    start_urls = ['http://www.btbtdy.net/']

    def parse(self, response)
        links = response.xpath('//div[@class="cts_ms"]/p/a/@href').extract()
        for link in links:
            print(link)
            yield response.follow(link,callback=self.parse_content)

        next_page = response.xpath('//div[@class="pages"]/a/@href').extract_first()
        if next_page is not None:
            print(next_page)
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page,callback=self.parse)

    def parse_content(self,response):
        # print(response.xpath('//title'))
        movie = BtmovieItem()
        title = response.xpath('//h1/text()').extract_first()
        content = response.xpath('//div[@class="c05"]/span/text()').extract_first()
        magnet = response.xpath('//*[@id="nucms_downlist"]/div[2]/ul/li/span/a/@href').extract_first()
        movie['title'] = title
        movie['content'] = content
        movie['magnet'] = magnet
        yield movie

傳遞參數(shù)

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').extract_first(),
                'author': quote.css('small.author::text').extract_first(),
            }

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

使用-a傳遞參數(shù),這些參數(shù)傳遞到爬蟲(chóng)類的__init__

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

#url = 'http://quotes.toscrape.com/tag/humor'

傳遞參數(shù)demo

編寫(xiě)爬蟲(chóng)文件 job.py

#框架已經(jīng)自動(dòng)創(chuàng)建以下內(nèi)容
import scrapy

class JobSpider(scrapy.Spider):
    name = 'job'  
    allowed_domains = ['51job.com'] 
    start_urls = ['http://51job.com/'] 
    def parse(self, response): 
        pass
#name 是爬蟲(chóng)的名稱    
#allowed_domains是指允許爬行的域名
#start_urls 是爬行的起始網(wǎng)址,可以定義多個(gè),用逗號(hào)隔開(kāi)
#如果沒(méi)有特別的回調(diào)函數(shù),該方法是處理acrapy爬蟲(chóng)爬行到的網(wǎng)頁(yè)響應(yīng)(response)額默認(rèn)方法,可以對(duì)響應(yīng)進(jìn)行處理冰返回處理后的數(shù)據(jù),也負(fù)責(zé)鏈接的跟蹤。



#對(duì)初始內(nèi)容進(jìn)行修改
import scrapy
from jobproject.items import JobprojectItem

class JobSpider(scrapy.Spider):
    name = 'job'  
    allowed_domains = ['51job.com'] 
    start_urls = ['http://51job.com/'] 
    
    #重新初始化方法,并設(shè)置參數(shù)place,這樣運(yùn)行時(shí)候就可以加上-a place參數(shù)值賦值了
    def __init__(self,place,*args,**kwargs):
        super().__init__(*args,**kwargs)
        #設(shè)置默認(rèn)地址為杭州
        if place is None:
            self.place = '杭州'
        else:
            self.place=place
      #重寫(xiě)start_requests方法,將起始網(wǎng)址設(shè)置未從urls中讀取。      
     def start_requests(self):
        urls = ['https://search.51job.com/list/{place_code},000000,0000,00,9,99,python,2,1.html'.format(place_code=self.get_place_code())]
         for url in urls:
           #make_requests_from_url默認(rèn)方法實(shí)現(xiàn)生成Request的請(qǐng)求對(duì)象
            yield self.make_requests_from_url(url)

    def get_place_code(self):
        # 51job搜索時(shí)候的地址碼,例如搜索杭州python地址如下
        #https://search.51job.com/list/080200,000000,0000,00,9,99,python,2,1.html
        place_map={
            '杭州':'080200',
            '上海':'020000'
        }
        return place_map.get(self.place)

    def parse(self, response):
        print(response.xpath('//title/text()'))
        jobs = response.xpath('//*[@id="resultList"]/div[@class="el"]')
        for job in jobs:
            item = JopItem()
            item['name']=job.xpath('.//p/span/a/text()').extract_first().strip()
            item['money'] = job.xpath('.//span[@class="t4"]/text()').extract_first().strip()
            yield item

數(shù)據(jù)的保存

將數(shù)據(jù)保存為json格式

pipelines.py

import json
import pymysql
import pymongo
from scrapy.contrib.exporter import JsonItemExporter

class JsonPipeline(object):

    def __init__(self):
        self.json_file = open('job.json','w')

    def process_item(self, item, spider):
        json_item = json.dumps(dict(item))
        print(json_item)
        print(json_item.strip())
        self.json_file.write(json_item)
        self.json_file.write('\n')
        return item


    def close_spider(self,spider):
        print("close_spider is call.")
        self.json_file.close()


# 使用系統(tǒng)自帶
class ScrapyJsonPipeline(object):
    def __init__(self):
        self.json_file = open('job_scrapy.json','wb')
        self.exporter = JsonItemExporter(self.json_file,encoding='utf-8')
        self.exporter.start_exporting()


    def process_item(self,item,spider):
        print(item)
        self.exporter.export_item(item)
        return item

    def close_spider(self,spider):
        self.exporter.finish_exporting()

將數(shù)據(jù)存儲(chǔ)到mysql和mongodb

pipelines.py

class MySQLPipeline(object):
    """
    pymyql
    """
    def __init__(self):
        """
        連接數(shù)據(jù)庫(kù)
        """
        self.conn = pymysql.connect(host='127.0.0.1', port=3306,user='root',
                                    password='123456', db='spider', charset='utf8')
        self.cursor = self.conn.cursor()

        
    def process_item(self,item,spider):
        """
        把每條數(shù)據(jù)插入數(shù)據(jù)庫(kù)
        """
        sql = "insert into qcwy(name) value(%s)"
        self.cursor.execute(sql, (item['name'],))
        self.conn.commit()
        return item


    def close_spider(self,spider):
        """
        關(guān)閉數(shù)據(jù)庫(kù)連接
        """
        self.cursor.close()
        self.conn.close()

class MongoDBPipeline(object):
    """
    pymongo
    """
    def __init__(self):
        """
        連接mongo
        """
        #創(chuàng)建一個(gè)客戶端
        self.client = pymongo.MongoClient(host='127.0.0.1',port=27017)
        self.db = self.client['job']

        self.coll = self.db['job_collection']

    def process_item(self,item,spider):
        dict_item = dict(item)
        self.coll.insert(dict_item)
        return item


    def close_spider(self,spider):
        self.client.close()

最后都需要在settings.py中配置

ITEM_PIPELINES = {
    #'JobSpider.pipelines.JobspiderPipeline': 300,
    'JobSpider.pipelines.JsonPipeline': 301,
    'JobSpider.pipelines.ScrapyJsonPipeline': 302,
    'JobSpider.pipelines.MySQLPipeline':303,
    'JobSpider.pipelines.MongoDBPipeline':304,
}

Selenium

Selenium 是一個(gè)用于Web應(yīng)用程序測(cè)試的工具。Selenium測(cè)試直接運(yùn)行在瀏覽器中,就像真正的用戶在操作一樣。支持的瀏覽器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。這個(gè)工具的主要功能包括:測(cè)試與瀏覽器的兼容性——測(cè)試你的應(yīng)用程序看是否能夠很好得工作在不同瀏覽器和操作系統(tǒng)之上。測(cè)試系統(tǒng)功能——?jiǎng)?chuàng)建回歸測(cè)試檢驗(yàn)軟件功能和用戶需求。支持自動(dòng)錄制動(dòng)作和自動(dòng)生成 .Net、Java、Perl等不同語(yǔ)言的測(cè)試腳本。

Selenium安裝

pip install selenium

安裝driver

Firefox瀏覽器需安裝geckdriver,Chrome瀏覽器需要安裝chromedriver,IE瀏覽器要安裝IEdriver。

可以在npm淘寶鏡像中下載。下載chrome驅(qū)動(dòng) 解壓放到python解釋器所在目錄

什么是npm?

npm(node package manager)node的包管理工具.簡(jiǎn)單地說(shuō)npm就是一個(gè)基于nodejs的包管理器,它管理的是javascript。

? 舉例來(lái)說(shuō):如果我們?cè)陂_(kāi)發(fā)過(guò)程中使用jquery,那么是不是要引入jquery,你可能會(huì)下載這個(gè)jquery.js文件,然后在代碼中<script src="jquery.js"></script>是吧 。如果使用 npm ,那么就方便了,直接在npm下使用命令:$ npm install jquery;就自動(dòng)下載了。

    在遠(yuǎn)端有一個(gè)npm服務(wù)器,里面有很多別人寫(xiě)的代碼,我們可以直接使用npm下載使用。

    同時(shí)你也可以把自己寫(xiě)的代碼推送到npm 服務(wù)器,讓別人使用。

包管理工具還有Ubuntu的apt-get,CentOS的yum,微軟的Nuget Package Manager等等。

運(yùn)行一個(gè)例子

使用python自帶的IDLE工具,輸入以下腳本:

from selenium import webdriver # 導(dǎo)入webdriver包
import time
driver = webdriver.Chrome() # 初始化一個(gè)谷歌瀏覽器實(shí)例:driver
driver.maximize_window() # 最大化瀏覽器 
time.sleep(5) # 暫停5秒鐘
driver.get("https://www.baidu.com") # 通過(guò)get()方法,打開(kāi)一個(gè)url站點(diǎn)

API

參考:site-packages/selenium/webdriver/chrome/webdriver.py

創(chuàng)建webdriver對(duì)象

from selenium import webdriver
driver = webdriver.Chrome()
content_div = driver.find_element_by_xpath('//*[@id="bd"]/div[4]/div[1]/div/div[2]/div[1]')
print(content_div.text)

driver.execute_script('')
html_doc = driver.page_source

PhantomJS

PhantomJS是一個(gè)可編程的無(wú)頭瀏覽器.現(xiàn)在已經(jīng)廢棄了,一般通過(guò)Chrome等瀏覽器use headless替換它。
無(wú)頭瀏覽器:一個(gè)完整的瀏覽器內(nèi)核,包括js解析引擎,渲染引擎,請(qǐng)求處理等,但是不包括顯示和用戶交互頁(yè)面的瀏覽器。
通常無(wú)頭瀏覽器可以用于頁(yè)面自動(dòng)化,網(wǎng)頁(yè)監(jiān)控,網(wǎng)絡(luò)爬蟲(chóng)等:

頁(yè)面自動(dòng)化測(cè)試:希望自動(dòng)的登陸網(wǎng)站并做一些操作然后檢查結(jié)果是否正常。
網(wǎng)頁(yè)監(jiān)控:希望定期打開(kāi)頁(yè)面,檢查網(wǎng)站是否能正常加載,加載結(jié)果是否符合預(yù)期。加載速度如何等。
網(wǎng)絡(luò)爬蟲(chóng):獲取頁(yè)面中使用js來(lái)下載和渲染信息,或者是獲取鏈接處使用js來(lái)跳轉(zhuǎn)后的真實(shí)地址。

查找元素

方法 功能
find_element_by_id Finds an element by id 根據(jù)id查找元素
find_element_by_xpath Finds an element by xpath. 根據(jù)xpath查找元素
find_element_by_link_text Finds an element by link text.根據(jù)超鏈接文本查找元素
find_element_by_partial_link_text Finds an element by a partial match of its link text.根據(jù)部分鏈接查找元素
find_element_by_name Finds an element by name.根據(jù)元素的name值查找元素
find_element_by_tag_name Finds an element by tag name.根據(jù)元素的tag名查找元素
find_element_by_class_name Finds an element by class name.根據(jù)元素的class name查找元素
find_element_by_css_selector Finds an element by css selector.根據(jù)css selector查找元素

屬性

current_url:獲得當(dāng)前頁(yè)面的url
page_source:獲得當(dāng)前頁(yè)面的源代碼
current_window_handle:獲得操作當(dāng)前window的句柄
window_handles:獲得當(dāng)前session中的所有window的句柄


>>> from selenium import webdriver
>>> browser = webdriver.Chrome()

DevTools listening on ws://127.0.0.1:12829/devtools/browser/caf2501f-f39f-4792-bfd3-37ecb6b5bf1c
>>> browser.get("http://www.baidu.com")
>>> browser.current_url
'https://www.baidu.com/'
>>> browser.window_handles
['CDwindow-9D2023D530996DB5A116DB6F13CF8C69']
>>> browser.current_window_handle
'CDwindow-9D2023D530996DB5A116DB6F13CF8C69'

基本操作

方法 功能
execute_script Synchronously Executes JavaScript in the current window/frame.以同步的方式執(zhí)行js
execute_async_script Asynchronously Executes JavaScript in the current window/frame.以異步的方式執(zhí)行js
close Closes the current window.關(guān)閉當(dāng)前的窗口
quit Quits the driver and closes every associated window.關(guān)閉所有窗口
maximize_window Maximizes the current window that webdriver is using.最大化當(dāng)前窗口
fullscreen_window Invokes the window manager-specific 'full screen' operation.進(jìn)入全屏模式
minimize_window Invokes the window manager-specific 'minimize' operation最小化窗口
switch_to 切換到某個(gè)地方,例如:driver.switch_to.window('main')<br />alert = driver.switch_to.alert<br />element = driver.switch_to.active_element
back Goes one step backward in the browser history.
forward Goes one step forward in the browser history.
refresh Refreshes the current page.刷新當(dāng)前頁(yè)面
get_cookies Returns a set of dictionaries, corresponding to cookies visible in the current session.獲得當(dāng)前會(huì)話中的所有cookies
get_cookie Get a single cookie by name. Returns the cookie if found, None if not.獲得當(dāng)前會(huì)話中指定name的cookie
delete_cookie Deletes a single cookie with the given name.
delete_all_cookies Delete all cookies in the scope of the session.
add_cookie Adds a cookie to your current session.
implicitly_wait Sets a sticky timeout to implicitly wait for an element to be found,
set_page_load_timeout 設(shè)置頁(yè)面加載超時(shí)時(shí)間
save_screenshot Saves a screenshot of the current window to a PNG image file.

中間件結(jié)合Selenium

taobao.py

import scrapy
class TaobaoSpider(scrapy.Spider):
    name = 'taobao'
    allowed_domains = ['www.taobao.com']

    def start_requests(self):
        for url in urls:
            req = scrapy.Request(url, callback=self.parse, meta={'use_selenium':True})
            yield req

    def parse(self, response):
        print(response.body)
        pass

middlewares.py

from selenium  import webdriver
from selenium.webdriver.chrome.options import Option

class TaobaoDownloaderMiddleware(object):
    ...
    def process_request(self, request, spider):
        if request.meta.get('use_selenium'):
            print('~~~~我是中間件~~~~~請(qǐng)求經(jīng)過(guò)過(guò)了~~~~')
            # 設(shè)置無(wú)界面運(yùn)行ChromeDriver
            option = Options()
            option.add_argument('--headless')
            driver = webdriver.Chrome(chrome_options=option)

            # driver = webdriver.Chrome()
            driver.implicitly_wait(15)
            driver.get(request.url)
            # 執(zhí)行js
            js = 'window.scrollTo(0, document.body.scrollHeight)'
            driver.execute_script(js)
            content = driver.page_source
            from scrapy.http import HtmlResponse
            resp = HtmlResponse(request.url, request=request, body=content, encoding='utf-8')
            return resp
        return None

    
    
#  點(diǎn)擊事件
driver=webdriver.Firefox()

driver.get("https://sg.search.yahoo.com/")

searchWhat=driver.find_element_by_id("yschsp")

#獲取id叫做'yschsp'的元素

searchWhat.clear()

#通過(guò)clear方法,可以將輸入框內(nèi)的字符清空,比較保險(xiǎn)的做法

searchWhat.send_keys("python")

#通過(guò)send_keys方法把'python'傳遞給serchWhat元素,即id叫做'yschsp'的元素

searchBtn=driver.find_element_by_class_name("sbb")

#獲取id叫做'sbb'的元素,但通常不推薦用class找,用selector能更精確的找到

searchBtn.click()

#通過(guò)click()方法點(diǎn)擊    

settings.py

DOWNLOADER_MIDDLEWARES = {
   'Taobao.middlewares.TaobaoDownloaderMiddleware': 543,
}

顯示等待、隱式等待和強(qiáng)制等待

  • sleep(): 強(qiáng)制等待,設(shè)置固定休眠時(shí)間。 python 的 time 包提供了休眠方法 sleep() , 導(dǎo)入 time 包后就可以使用 sleep(),進(jìn)行腳本的執(zhí)行過(guò)程進(jìn)行休眠。
  • implicitly_wait():隱式等待,也叫智能等待,是 webdirver 提供的一個(gè)超時(shí)等待。等待一個(gè)元素被發(fā)現(xiàn),或一個(gè)命令完成。如果超出了設(shè)置時(shí)間的則拋出異常。
  • WebDriverWait():顯示等待,同樣也是 webdirver 提供的方法。在設(shè)置時(shí)間內(nèi),默認(rèn)每隔一段時(shí)間檢測(cè)一次當(dāng)前頁(yè)面元素是否存在,如果超過(guò)設(shè)置時(shí)間檢測(cè)不到則拋出異常。默認(rèn)檢測(cè)頻率為0.5s,默認(rèn)拋出異常為:NoSuchElementException

避免被block的多種方式

1. 禁止Cookie

# 在setting中修改cookie為本地禁止
# Disable cookies (enabled by default)
#去除注釋
COOKIES_ENABLED = False

2. 設(shè)置下載延時(shí)

#在setting中修改延時(shí)時(shí)間:3代表爬蟲(chóng)下載網(wǎng)頁(yè)的時(shí)間間隔為3s.
DOWNLOAD_DELAY = 3

3. 使用ip池

使用代理IP組成ip池,每次爬取可以隨機(jī)選擇ip池的ip下載

# 網(wǎng)上獲取代理代理Ip后,在settings中設(shè)置為Ip池:外層通過(guò)列表形式存儲(chǔ),里層通過(guò)字典形式存儲(chǔ)。
IPPOOL = [
    {'ipaddr':'ip'}
]
"""
在scrapy中,與代理服務(wù)器設(shè)置相關(guān)的下載中間件是HttpProxyMiddleware,同樣在scrapy官方文檔中,HttpProxyMiddleware對(duì)應(yīng)的類為
class scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware
所以編輯時(shí)候需要導(dǎo)入。
"""


# middlewares.py中:
import random
from Taobao.settings import IPPOOL
from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware

class IPPOOLS(HttpProxyMiddleware):
    def process_request(self, request, spider):
        thisip = random.choice(IPPOOL)
        print('當(dāng)前使用的ip是:'+thisip["ipaddr"])
        request.meta['proxy'] = 'http://' + thisip['ipaddr']
        

setting中設(shè)置如下:

DOWNLOADER_MIDDLEWARES = {
    'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware':123,
    'Taobao.middlewares.IPPOOLS':125,
}

4.使用用戶代理池

與ip代理池不同的是此時(shí)我們需要下載中間件是UserAgentMiddleware

首先需要在setting配置文件中設(shè)置好用戶代理池,名稱可以自定義;

setting配置:
UAPOOL = [
    'Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10',
    'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36',
    'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
    'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11',
]

中間件中寫(xiě)上如下代碼:

Middleware.py:
import random
from Taobao.settings import UAPOOL
from scrapy.contrib.downloadermiddleware.useragent import UserAgentMiddleware

class Uamid(UserAgentMiddleware):
    def process_request(self, request, spider):
        thisua = random.choice(UAPOOL)
        print('當(dāng)前使用的user-agent是:'+thisua)
        request.headers.setdefault('User-Agent',thisua)
        
#setting中設(shè)置如下:
DOWNLOADER_MIDDLEWARES = {
    # 讓自定義的中間件優(yōu)先級(jí)高于UserAgentMiddleware 或者將2改為None也可以
    'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware':2,
    'Taobao.middlewares.Uamid':1
}

#設(shè)置好之后會(huì)通過(guò)下載中間件自動(dòng)的隨機(jī)切換用戶代理進(jìn)行網(wǎng)頁(yè)爬取
# 使用第三方庫(kù)隨機(jī)生成user agent 需要安裝  pip install fake_useragent
from fake_useragent import UserAgent

class Uamid(UserAgentMiddleware):
    ...
    def process_request(self, request, spider):        
        thisua = UserAgent().random
        ...

scrapy-splash動(dòng)態(tài)爬蟲(chóng)

安裝Docker

Docker是一個(gè)開(kāi)源的軟件部署解決方案

官方文檔:https://docs.docker.com

中文參考文檔:http://www.docker.org.cn

在Ubuntu中安裝二進(jìn)制Docker (Install Docker CE from binaries)

優(yōu)點(diǎn)是安裝簡(jiǎn)單,缺點(diǎn)是無(wú)服務(wù)、無(wú)配置文件,相當(dāng)于Windows平臺(tái)中的綠色軟件。

使用安裝腳本自動(dòng)安裝Docker及其依賴

wget -qO- https://get.docker.com/ | sh

sudo service docker start

docker run hello-world

安裝scrapy-splash

pip install scrapy-splash

配置scrapy-splash

# 渲染服務(wù)的url
SPLASH_URL = 'http://127.0.0.1:8050'

#下載器中間件
DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 800,
    'scrapy_splash.SplashMiddleware': 801,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 802,
}
# 去重過(guò)濾器
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
# 使用Splash的Http緩存
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'

編寫(xiě)spider

class SplashSpider(scrapy.Spider):
    name = 'scrapy_splash'
    start_urls = [
        'https://movie.douban.com/subject/26752088/'
    ]

    #request需要封裝成SplashRequest
    def start_requests(self):
        urls = ['https://movie.douban.com/subject/26752088/']
        for url in urls:

            yield SplashRequest(url, callback=self.parse)

    def parse(self, response):
        item = SplashTestItem()
        print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`")
        #//*[@id="hot-comments"]/div[@class="comment-item"]'
        comments = response.xpath('//*[@id="hot-comments"]/div'
                                  '[@class="comment-item"]')
        for comment in comments:
            #.//span[@class="votes"]/text()
            item['votes'] = comment.xpath('.//span[@class="votes"]/text()'
                                          '').extract_first()
            # .//span[@class="short"]/text()
            item['short'] = comment.xpath('.//span[@class="short"]/text()').extract_first()
            yield item

CrawlSpider類自動(dòng)爬蟲(chóng)

簡(jiǎn)要說(shuō)明

CrawlSpider是爬取那些具有一定規(guī)則網(wǎng)站的常用的爬蟲(chóng),它基于Spider并有一些獨(dú)特屬性

  • rules: 是Rule對(duì)象的集合,用于匹配目標(biāo)網(wǎng)站并排除干擾
  • parse_start_url: 用于爬取起始響應(yīng),必須要返回ItemRequest中的一個(gè)
class Rule(object):

    def __init__(self, link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=identity):
        self.link_extractor = link_extractor
        self.callback = callback
        self.cb_kwargs = cb_kwargs or {}
        self.process_links = process_links
        self.process_request = process_request
        if follow is None:
            self.follow = False if callback else True
        else:
            self.follow = follow
  1. link_extractor 是一個(gè) Link Extractor 對(duì)象。 其定義了如何從爬取到的頁(yè)面提取鏈接。

    其中的link_extractor既可以自己定義,也可以使用已有LinkExtractor類,主要參數(shù)為:

    • allow:滿足括號(hào)中“正則表達(dá)式”的值會(huì)被提取,如果為空,則全部匹配。
    • deny:與這個(gè)正則表達(dá)式(或正則表達(dá)式列表)不匹配的URL一定不提取。
    • allow_domains:會(huì)被提取的鏈接的domains。
    • deny_domains:一定不會(huì)被提取鏈接的domains。
    • restrict_xpaths:使用xpath表達(dá)式,和allow共同作用過(guò)濾鏈接。
    • 還有一個(gè)類似的restrict_css
  2. callback 是一個(gè)callable或string(該spider中同名的函數(shù)將會(huì)被調(diào)用)。 從link_extractor中每獲取到鏈接時(shí)將會(huì)調(diào)用該函數(shù)。該回調(diào)函數(shù)接受一個(gè)response作為其第一個(gè)參數(shù), 并返回一個(gè)包含Item 以及(或) Request 對(duì)象(或者這兩者的子類)的列表(list)。

  3. 當(dāng)編寫(xiě)爬蟲(chóng)規(guī)則時(shí),請(qǐng)避免使用 parse 作為回調(diào)函數(shù)。 由于 CrawlSpider 使用 parse 方法來(lái)實(shí)現(xiàn)其邏輯,如果 您覆蓋了 parse 方法,crawl spider 將會(huì)運(yùn)行失敗。

  4. cb_kwargs 包含傳遞給回調(diào)函數(shù)的參數(shù)(keyword argument)的字典。

  5. follow 是一個(gè)布爾(boolean)值,指定了根據(jù)該規(guī)則從response提取的鏈接是否需要跟進(jìn)。 如果callback 為None, follow 默認(rèn)設(shè)置為 True ,否則默認(rèn)為 False

  6. process_links 是一個(gè)callable或string(該spider中同名的函數(shù)將會(huì)被調(diào)用)。 從link_extractor中獲取到鏈接列表時(shí)將會(huì)調(diào)用該函數(shù)。該方法主要用來(lái)過(guò)濾。

  7. process_request 是一個(gè)callable或string(該spider中同名的函數(shù)將會(huì)被調(diào)用)。 該規(guī)則提取到每個(gè)request時(shí)都會(huì)調(diào)用該函數(shù)。該函數(shù)必須返回一個(gè)request或者None。 (用來(lái)過(guò)濾request)

CrawlSpider樣例

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com']

    rules = (
        # Extract links matching 'category.php' (but not matching 'subsection.php')
        # and follow links from them (since no callback means follow=True by default).
        # 提取匹配 'category.php' (但不匹配 'subsection.php') 的鏈接并跟進(jìn)鏈接
        #(沒(méi)有callback意味著follow默認(rèn)為T(mén)rue)
        Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),

        # Extract links matching 'item.php' and parse them with the spider's method parse_item
        # 提取匹配 'item.php' 的鏈接并使用spider的parse_item方法進(jìn)行分析
        Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'),
    )

    def parse_item(self, response):
        self.logger.info('Hi, this is an item page! %s', response.url)
        item = scrapy.Item()
        item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
        item['name'] = response.xpath('//td[@id="item_name"]/text()').extract()
        item['description'] = response.xpath('//td[@id="item_description"]/text()').extract()
        return item

創(chuàng)建Scrapy工程

#scrapy startproject 工程名
scrapy startproject demo4

根據(jù)爬蟲(chóng)模板生成爬蟲(chóng)文件

#scrapy genspider -l # 查看可用模板
#scrapy genspider -t 模板名 爬蟲(chóng)文件名 允許的域名
scrapy genspider -t crawl test sohu.com

增加并發(fā)

并發(fā)是指同時(shí)處理的request的數(shù)量。其有全局限制和局部(每個(gè)網(wǎng)站)的限制。

Scrapy默認(rèn)的全局并發(fā)限制對(duì)同時(shí)爬取大量網(wǎng)站的情況并不適用,因此您需要增加這個(gè)值。 增加多少取決于您的爬蟲(chóng)能占用多少CPU。 一般開(kāi)始可以設(shè)置為 100 。不過(guò)最好的方式是做一些測(cè)試,獲得Scrapy進(jìn)程占取CPU與并發(fā)數(shù)的關(guān)系。 為了優(yōu)化性能,您應(yīng)該選擇一個(gè)能使CPU占用率在80%-90%的并發(fā)數(shù)。

增加全局并發(fā)數(shù):

CONCURRENT_REQUESTS = 100

降低log級(jí)別

當(dāng)進(jìn)行通用爬取時(shí),一般您所注意的僅僅是爬取的速率以及遇到的錯(cuò)誤。 Scrapy使用 INFO log級(jí)別來(lái)報(bào)告這些信息。為了減少CPU使用率(及記錄log存儲(chǔ)的要求), 在生產(chǎn)環(huán)境中進(jìn)行通用爬取時(shí)您不應(yīng)該使用 DEBUG log級(jí)別。 不過(guò)在開(kāi)發(fā)的時(shí)候使用 DEBUG 應(yīng)該還能接受。

設(shè)置Log級(jí)別:

LOG_LEVEL = 'INFO'

禁止cookies

除非您 真的 需要,否則請(qǐng)禁止cookies。在進(jìn)行通用爬取時(shí)cookies并不需要, (搜索引擎則忽略cookies)。禁止cookies能減少CPU使用率及Scrapy爬蟲(chóng)在內(nèi)存中記錄的蹤跡,提高性能。

禁止cookies:

COOKIES_ENABLED = False

禁止重試

對(duì)失敗的HTTP請(qǐng)求進(jìn)行重試會(huì)減慢爬取的效率,尤其是當(dāng)站點(diǎn)響應(yīng)很慢(甚至失敗)時(shí), 訪問(wèn)這樣的站點(diǎn)會(huì)造成超時(shí)并重試多次。這是不必要的,同時(shí)也占用了爬蟲(chóng)爬取其他站點(diǎn)的能力。

禁止重試:

RETRY_ENABLED = False

減小下載超時(shí)

如果您對(duì)一個(gè)非常慢的連接進(jìn)行爬取(一般對(duì)通用爬蟲(chóng)來(lái)說(shuō)并不重要), 減小下載超時(shí)能讓卡住的連接能被快速的放棄并解放處理其他站點(diǎn)的能力。

減小下載超時(shí):

DOWNLOAD_TIMEOUT = 15

禁止重定向

除非您對(duì)跟進(jìn)重定向感興趣,否則請(qǐng)考慮關(guān)閉重定向。 當(dāng)進(jìn)行通用爬取時(shí),一般的做法是保存重定向的地址,并在之后的爬取進(jìn)行解析。 這保證了每批爬取的request數(shù)目在一定的數(shù)量, 否則重定向循環(huán)可能會(huì)導(dǎo)致爬蟲(chóng)在某個(gè)站點(diǎn)耗費(fèi)過(guò)多資源。

關(guān)閉重定向:

REDIRECT_ENABLED = False

運(yùn)行爬蟲(chóng)

使用runspider命令運(yùn)行

scrapy runspider some_spider.py

使用crawl命令運(yùn)行

scrapy crawl spider_name

使用cmdline運(yùn)行

from scrapy import cmdline

cmdline.execute("scrapy crawl movie".split())

使用CrawlerProcess運(yùn)行

import scrapy
from scrapy.crawler import CrawlerProcess
from .spiders import spider_a
from .spiders import spider_b
process = CrawlerProcess()

process.crawl(spider_a)
process.crawl(spider_b)
process.start()

暫停,恢復(fù)爬蟲(chóng)

scrapy crawl somespider -s JOBDIR=crawls/somespider-1

使用中間件實(shí)現(xiàn)IP代理池

class MyProxyMidleware(object):
     
    def process_request(self, request, spider):
        request.meta['proxy']  = random.choice(my_proxies.PROXY)

使用中間件實(shí)現(xiàn)User-Agent代理池

from . import settings

class RandomUAMiddleware(object):
    def process_request(self, request, spider):
        ua = random.choice(settings.USER_AGENT_LIST)
        print(request.headers)
        if ua:
            request.headers.setdefault('User-Agent', ua)
        print(request.headers)

使用Scrapy登錄網(wǎng)站

class LoginSpider(scrapy.Spider):
    name = 'login'
    allowed_domains = ['127.0.0.1']
 
    def start_requests(self):
        start_urls = ['http://127.0.0.1:8888/login/']
        for url in start_urls:
            request = scrapy.Request(url, callback=self.login)
            yield request
 
    def login(self, response):
        request = FormRequest.from_response(response, formdata={
            'username': 'admin', 'password': 'qianfeng'},
            callback=self.after_login
 
        )
        yield request
 
    def after_login(self, response):
        print(response.text)

使用scrapy—redis實(shí)現(xiàn)分布式爬蟲(chóng)

優(yōu)點(diǎn):可以充分地利用多個(gè)電腦的資源

scrapy本身支持分布式嗎?
不支持的!!!
為什么不支持呢?
scrapy的url隊(duì)列存在哪里? (單機(jī)內(nèi)存)
如何實(shí)現(xiàn)分布式呢?
替換url隊(duì)列的存儲(chǔ)介質(zhì) (redis支持分布式的內(nèi)存數(shù)據(jù)庫(kù))
為scrapy做一個(gè)新的調(diào)度器(redis),替換scapy的默認(rèn)調(diào)度器, 從而實(shí)現(xiàn)分布式功能。

scrapy-redis

scrapy-redis是scrapy的一個(gè)組件(插件),和 scrapy 、redis配合。從而實(shí)現(xiàn)支持分布式爬蟲(chóng)
start_urls= ['http://www.dushu.com' ] # 以前
redis-cli lpush myspider:start_urls 'http://www.dushu.com' # 使用分布式

scrapy-redis是大神們寫(xiě)的一個(gè)scrapy的組件,主要用來(lái)負(fù)責(zé)分布式爬蟲(chóng)的調(diào)度任務(wù)它依賴于Scrapy和redis。

scrapy-redis提供了幾個(gè)新的組件(新類)用來(lái)補(bǔ)充scrapy不能實(shí)現(xiàn)分布式的問(wèn)題,它們分別是Scheduler、Dupefilter、Pipeline和Spider。

scrapy用類似Python中collection.deque的對(duì)象來(lái)保存待爬取的urls,

scrapy-redis用redis數(shù)據(jù)庫(kù)來(lái)保存待爬取的urls(redis支持分布式,支持隊(duì)列共享)

scrapy-redis.png
  • MasterSpider 對(duì) start_urls 中的 urls 構(gòu)造 request,獲取 response
  • MasterSpiderresponse 解析,獲取目標(biāo)頁(yè)面的 url, 利用 redis 對(duì) url 去重并生成待爬 request 隊(duì)列
  • SlaveSpider 讀取 redis 中的待爬隊(duì)列,構(gòu)造 request
  • SlaveSpider 發(fā)起請(qǐng)求,獲取目標(biāo)頁(yè)面的 response
  • Slavespider 解析 response,獲取目標(biāo)數(shù)據(jù),寫(xiě)入生產(chǎn)數(shù)據(jù)庫(kù)

redis在爬蟲(chóng)系統(tǒng)中的作用:

  1. 存儲(chǔ)鏈接
  2. 和scrapy一起工作,redis用來(lái)調(diào)度spiders(多個(gè)spider共用一個(gè)redis隊(duì)列,即分布式處理)

充分利用redis結(jié)構(gòu)的作用:

set:set中沒(méi)有重復(fù)元素,利用這個(gè)特性,可以用來(lái)過(guò)濾重復(fù)元素;

list:實(shí)現(xiàn)隊(duì)列和棧的功能;

zset(sorted set):元素需要排序的時(shí)候用;

hash:存儲(chǔ)的信息字段比較多時(shí),可以用hash;

使用scrapy-redis實(shí)現(xiàn)分布式處理的步驟

創(chuàng)建項(xiàng)目

scrapy  startproject example-project
cd example-project
Scrapy genspider dmoz dmoz.org
scrapy genspider myspider_redis  dmoz.org
scrapy genspider mycrawler_redis dmoz.org

編寫(xiě)代碼

(這里我們直接使用官方網(wǎng)站的演示代碼,演示分布式的配置、運(yùn)行等)

使用scrapy redis,需要注意以下幾個(gè)區(qū)別:

  1. 傳統(tǒng)的spiders中,每個(gè)spider類繼承的是scrapy.Spider類或Scrapy.spiders.CrawlSpider類,而分布式寫(xiě)法每個(gè)spider繼承的是scrapy_redis.spiders.RedisSpider或scrapy_redis.spiders.RedisCrawlSpider類。

    from scrapy_redis.spiders import RedisCrawlSpider
    class MyCrawler(RedisCrawlSpider):
        ......
    # 或者
    from scrapy_redis.spiders import RedisSpider
    class MySpider(RedisSpider):
        ...... 
    
  1. 在分布式寫(xiě)法中,start_urls=[]換成了 redis_key = 'myspider:start_urls'

    class MyCrawler(RedisCrawlSpider):
        """Spider that reads urls from redis queue (myspider:start_urls)."""
        name = 'mycrawler_redis'
        redis_key = 'mycrawler:start_urls'
    
  2. 在分布式寫(xiě)法中, allowed_domains = ['dmoz.org'] 換成了

    domain = kwargs.pop('domain', '')
     # Dynamically define the allowed domains list.
        domain = kwargs.pop('domain', '')
        self.allowed_domains = filter(None, domain.split(','))
        super(Myspider1Spider, self).__init__(*args, **kwargs)
    

搭建分布式爬蟲(chóng)環(huán)境

環(huán)境準(zhǔn)備:4臺(tái)服務(wù)器,一個(gè)做master,另外3個(gè)做slave。

scrapy、scrapy-redis、redis

master服務(wù)器的配置:

  1. 安裝scrapy、scrapy-redis、redis。

  2. 修改master的redis配置文件redis.conf:

    1)將 bind 127.0.0.1 修改為bind 0.0.0.0。(注意防火墻設(shè)置)

    2)重啟redis-server。

  3. 在爬蟲(chóng)項(xiàng)目中的setting.py文件中添加配置信息:

    REDIS_HOST = 'localhost'
    REDIS_PORT = 6379
    

slave端的配置:

  1. 在爬蟲(chóng)項(xiàng)目中的setting.py文件中添加配置信息:

    REDIS_URL = 'redis://redis_server ip:6379'
    

master和slave端中共同的配置

在setting.py中啟用redis存儲(chǔ)

ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 400,
}

運(yùn)行分布式爬蟲(chóng):爬蟲(chóng)將處于監(jiān)聽(tīng)狀態(tài),等待獲取redis隊(duì)列中的url

# scrapy runspider myspider_redis.py
scrapy crawl myspider

向redis隊(duì)列添加url

redis-cli -h redis_server_ip 
redis-cli> lpush myspider_redis:start_urls http://www.xxxxxx.com/aaa/

案例完整代碼

myspider1.py

# -*- coding: utf-8 -*-
import scrapy
from scrapy_redis.spiders import RedisCrawlSpider
from redis_project.items import RedisProjectItem


class Myspider1Spider(RedisCrawlSpider):
    name = 'myspider1'
    redis_key = 'mycrawler:start_urls'
    # allowed_domains = ['blog.jobbole.com']
    #start_urls = ['http://blog.jobbole.com/all-posts/']

    def __init__(self, *args, **kwargs):
        # Dynamically define the allowed domains list.
        # 命令行可以傳參 scrapy crawl -a domain='blog.jobble.com'
        domain = kwargs.pop('domain', '')
        self.allowed_domains = filter(None, domain.split(','))
        super(Myspider1Spider, self).__init__(*args, **kwargs)

    def parse(self, response):
        links = response.xpath('//a[@class="archive-title"]')
        for link in links:
            item = RedisProjectItem()
            title = link.xpath('./@title').extract_first()
            link = link.xpath('./@href').extract_first()
            item['title'] = title
            item['link'] = link
            yield item


settings.py

REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_ITEMS_KEY = 'jobbole:posts'
REDIS_URL = 'redis://127.0.0.1:6379'

BOT_NAME = 'redis_project'

SPIDER_MODULES = ['redis_project.spiders']
NEWSPIDER_MODULE = 'redis_project.spiders'

USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit'
'/605.1.15 (KHTML, like Gecko) Version/11.1.1 Safari/605.1.15'

ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
    'redis_project.pipelines.RedisProjectPipeline': 300,
    'scrapy_redis.pipelines.RedisPipeline': 400,
}

items.py

import scrapy

class RedisProjectItem(scrapy.Item):
    # define the fields for your item here like:
    title = scrapy.Field()
    link = scrapy.Field()

process_redis.py: 用于將redis中數(shù)據(jù)轉(zhuǎn)存到mysql的腳本文件

"""
把redis中的數(shù)據(jù)轉(zhuǎn)存到數(shù)據(jù)庫(kù)中
"""
from redis import Redis
import settings
import pymysql
import json


class RedisPipeline(object):

    def __init__(self):
        """
        連接數(shù)據(jù)庫(kù)
        """

        self.conn = pymysql.connect(
            host='127.0.0.1',
            port=3306,
            user='root',
            password='123456',
            db='spider',
            charset='utf8'
        )

        self.cursor = self.conn.cursor()
        # 連接redis
        self.rds = Redis('127.0.0.1', 6379)

    def process_item(self):
        while True:
            _, data = self.rds.blpop(settings.REDIS_ITEMS_KEY)
            item = json.loads(data.decode('utf-8'))
            sql = "insert into jobbole(title,link) values (%s,%s)"
            self.cursor.execute(sql, (item['title'], item['link']))
            self.conn.commit()

    def close(self):
        self.cursor.close()
        self.conn.close()


redis_pipelie = RedisPipeline()
redis_pipelie.process_item()
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容