8-Scrapy框架匯總

scrapy架構

scrapy.png

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

安裝

  1. 首先考慮使用最簡單的方法安裝,可能會有諸多錯誤,scrapy安裝需要Lxml、Twisted等庫。
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格式的包進行安裝

    還是進入http://www.lfd.uci.edu/~gohlke/pythonlibs/,在網頁中搜索twisted找到其對應的whl包并下載.

    下載完成后使用cmd打開windows的命令行窗口,進入whl包所在的文件夾執行如下命令

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

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

    安裝完成后使用scrapy -h測試是否安裝成功

  2. windows系統安裝完成后運行scrapy可能會報no model named win32錯誤,到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 做測試用,反映當前性能,爬蟲速度
scrapy bench

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

#生成爬蟲
scrapy genspider +文件名+網址

# runspider運行爬蟲文件,與crawl的去區別是runspider運行的是spider.py文件,而crawl運行整個項目
scrapy runspider spider.py

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

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

#創建項目
scrapy startproject demo

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

# view請求Url,把它的網頁源代碼保存成文件,并打開網頁
scrapy view http://www.example.com/some/page.html

#查看爬蟲列表
scrapy list

#check檢查錯誤
scrapy check

# 運行(crawl)
scrapy crawl +爬蟲名稱

#使用 EDITOR 中設定的編輯器編輯給定的spider
#該命令僅僅是提供一個快捷方式。開發者可以自由選擇其他工具或者IDE來編寫調試spider。
scrapy edit spider1

settings.py配置

# USER_AGENT 設置用戶代理
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'

#設置是否遵守robots協議
ROBOTSTXT_OBEY = True

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

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

scrapy抓取網站

一般需要四個步驟

  1. 創建一個爬蟲項目
  2. 定義Item容器
  3. 編寫爬蟲,提取數據
  4. 存儲內容

創建項目

在開始爬取之前,您必須創建一個新的Scrapy項目。 進入您打算存儲代碼的目錄中,運行下列命令:

scrapy startproject tutorial

該命令將會創建包含下列內容的 tutorial 目錄:

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

這些文件分別是:

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

定義Item

Item 是保存爬取到的數據的容器;其使用方法和python字典類似, 并且提供了額外保護機制來避免拼寫錯誤導致的未定義字段錯誤。

類似在ORM中做的一樣,您可以通過創建一個 scrapy.Item 類, 并且定義類型為 scrapy.Field 的類屬性來定義一個Item。

首先根據需要從toscrape.com獲取到的數據對item進行建模。 我們需要從quotes.py中獲取名字,url,以及網站的描述。 對此,在item中定義相應的字段。編輯 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()

編寫第一個爬蟲(Spider)

Spider是用戶編寫用于從單個網站(或者一些網站)爬取數據的類。

其包含了一個用于下載的初始URL,如何跟進網頁中的鏈接以及如何分析頁面中的內容, 提取生成 item 的方法。

為了創建一個Spider,您必須繼承 scrapy.Spider 類, 且定義以下三個屬性:

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

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

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    # allowed_domains = ['toscrape.com']
    
    # 簡潔寫法
    '''
    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)
    
    #回調函數
    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:

#爬蟲的名字就是quotes.py中的name
scrapy crawl quotes

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

或者在spiders目錄下創建run.py寫入

from scrapy import cmdline

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

提取Item

Selectors選擇器簡介

從網頁中提取數據有很多方法。Scrapy使用了一種基于 XPathCSS 表達式機制: Scrapy Selectors。 關于selector和其他提取機制的信息請參考 Selector文檔

這里給出XPath表達式的例子及對應的含義:

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

上邊僅僅是幾個簡單的XPath例子,XPath實際上要比這遠遠強大的多。 如果您想了解的更多,我們推薦 這篇XPath教程

為了配合XPath,Scrapy除了提供了 Selector 之外,還提供了方法來避免每次從response中提取數據時生成selector的麻煩。

Selector有四個基本的方法(點擊相應的方法可以看到詳細的API文檔):

  • xpath(): 傳入xpath表達式,返回該表達式所對應的所有節點的selector list列表 。
  • css(): 傳入CSS表達式,返回該表達式所對應的所有節點的selector list列表.
  • extract(): 序列化該節點為unicode字符串并返回list。
  • re(): 根據傳入的正則表達式對數據進行提取,返回unicode字符串list列表。

在Shell中嘗試Selector選擇器

為了介紹Selector的使用方法,接下來我們將要使用內置的 Scrapy shell 。Scrapy Shell需要您預裝好IPython(一個擴展的Python終端)。

您需要進入項目的根目錄,執行下列命令來啟動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

當shell載入后,您將得到一個包含response數據的本地 response 變量。輸入 response.body 將輸出response的包體, 輸出 response.headers 可以看到response的包頭。

更為重要的是,當輸入 response.selector 時, 您將獲取到一個可以用于查詢返回數據的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'

# 正則表達式
>>> 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>

進入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'

我們可以通過這段代碼選擇該頁面中網站列表里所有 <li> 元素:

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

網站的描述:

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

網站的標題:

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

以及網站的鏈接:

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

string()和text()的區別,以原生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標簽下的 b標簽不再被解析 第一個p中的文字分成了2部分
# 而這種情況是無法使用string()的,因為所有p是一個list, string方法只能對單個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']

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

提取數據

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

保存爬取到的數據

scrapy crawl quotes -o items.json

? 在這樣小規模的項目中,這種存儲方式已經足夠。 如果需要對爬取到的item做更多更為復雜的操作,可以編寫 piplines.py文件。

爬取下一頁

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

<li class="next">
    <a >下一頁 ?</a>
</li>

那到底如何讓蜘蛛自動的判斷、并爬取下一頁、下一頁的內容呢?我們可以這樣來做,我們每爬一頁就用css選擇器來查詢,是否存在下一頁鏈接,存在:則爬取下一頁鏈接http://lab.scrapyd.cn/page/*/ ,然后把下一頁鏈接提交給當前爬取的函數,繼續爬取,繼續查找下一頁,知道找不到下一頁,說明所有頁面已經爬完,那結束爬蟲。

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需要一個絕對的url地址,所以使用了urljoin()方法生成絕對地址。方法1

response.follow不用獲取到絕對的url,使用follow方法會自動幫我們實現。方法2

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

<a>標簽有一個簡寫,response.follow可以自動使用它們的屬性。方法4

注意傳入的對象只能是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

傳遞參數

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傳遞參數,這些參數傳遞到爬蟲類的__init__

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

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

傳遞參數demo

編寫爬蟲文件 job.py

#框架已經自動創建以下內容
import scrapy

class JobSpider(scrapy.Spider):
    name = 'job'  
    allowed_domains = ['51job.com'] 
    start_urls = ['http://51job.com/'] 
    def parse(self, response): 
        pass
#name 是爬蟲的名稱    
#allowed_domains是指允許爬行的域名
#start_urls 是爬行的起始網址,可以定義多個,用逗號隔開
#如果沒有特別的回調函數,該方法是處理acrapy爬蟲爬行到的網頁響應(response)額默認方法,可以對響應進行處理冰返回處理后的數據,也負責鏈接的跟蹤。



#對初始內容進行修改
import scrapy
from jobproject.items import JobprojectItem

class JobSpider(scrapy.Spider):
    name = 'job'  
    allowed_domains = ['51job.com'] 
    start_urls = ['http://51job.com/'] 
    
    #重新初始化方法,并設置參數place,這樣運行時候就可以加上-a place參數值賦值了
    def __init__(self,place,*args,**kwargs):
        super().__init__(*args,**kwargs)
        #設置默認地址為杭州
        if place is None:
            self.place = '杭州'
        else:
            self.place=place
      #重寫start_requests方法,將起始網址設置未從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默認方法實現生成Request的請求對象
            yield self.make_requests_from_url(url)

    def get_place_code(self):
        # 51job搜索時候的地址碼,例如搜索杭州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

數據的保存

將數據保存為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()


# 使用系統自帶
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()

將數據存儲到mysql和mongodb

pipelines.py

class MySQLPipeline(object):
    """
    pymyql
    """
    def __init__(self):
        """
        連接數據庫
        """
        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):
        """
        把每條數據插入數據庫
        """
        sql = "insert into qcwy(name) value(%s)"
        self.cursor.execute(sql, (item['name'],))
        self.conn.commit()
        return item


    def close_spider(self,spider):
        """
        關閉數據庫連接
        """
        self.cursor.close()
        self.conn.close()

class MongoDBPipeline(object):
    """
    pymongo
    """
    def __init__(self):
        """
        連接mongo
        """
        #創建一個客戶端
        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 是一個用于Web應用程序測試的工具。Selenium測試直接運行在瀏覽器中,就像真正的用戶在操作一樣。支持的瀏覽器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。這個工具的主要功能包括:測試與瀏覽器的兼容性——測試你的應用程序看是否能夠很好得工作在不同瀏覽器和操作系統之上。測試系統功能——創建回歸測試檢驗軟件功能和用戶需求。支持自動錄制動作和自動生成 .Net、Java、Perl等不同語言的測試腳本。

Selenium安裝

pip install selenium

安裝driver

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

可以在npm淘寶鏡像中下載。下載chrome驅動 解壓放到python解釋器所在目錄

什么是npm?

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

? 舉例來說:如果我們在開發過程中使用jquery,那么是不是要引入jquery,你可能會下載這個jquery.js文件,然后在代碼中<script src="jquery.js"></script>是吧 。如果使用 npm ,那么就方便了,直接在npm下使用命令:$ npm install jquery;就自動下載了。

    在遠端有一個npm服務器,里面有很多別人寫的代碼,我們可以直接使用npm下載使用。

    同時你也可以把自己寫的代碼推送到npm 服務器,讓別人使用。

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

運行一個例子

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

from selenium import webdriver # 導入webdriver包
import time
driver = webdriver.Chrome() # 初始化一個谷歌瀏覽器實例:driver
driver.maximize_window() # 最大化瀏覽器 
time.sleep(5) # 暫停5秒鐘
driver.get("https://www.baidu.com") # 通過get()方法,打開一個url站點

API

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

創建webdriver對象

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是一個可編程的無頭瀏覽器.現在已經廢棄了,一般通過Chrome等瀏覽器use headless替換它。
無頭瀏覽器:一個完整的瀏覽器內核,包括js解析引擎,渲染引擎,請求處理等,但是不包括顯示和用戶交互頁面的瀏覽器。
通常無頭瀏覽器可以用于頁面自動化,網頁監控,網絡爬蟲等:

頁面自動化測試:希望自動的登陸網站并做一些操作然后檢查結果是否正常。
網頁監控:希望定期打開頁面,檢查網站是否能正常加載,加載結果是否符合預期。加載速度如何等。
網絡爬蟲:獲取頁面中使用js來下載和渲染信息,或者是獲取鏈接處使用js來跳轉后的真實地址。

查找元素

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

屬性

current_url:獲得當前頁面的url
page_source:獲得當前頁面的源代碼
current_window_handle:獲得操作當前window的句柄
window_handles:獲得當前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.以同步的方式執行js
execute_async_script Asynchronously Executes JavaScript in the current window/frame.以異步的方式執行js
close Closes the current window.關閉當前的窗口
quit Quits the driver and closes every associated window.關閉所有窗口
maximize_window Maximizes the current window that webdriver is using.最大化當前窗口
fullscreen_window Invokes the window manager-specific 'full screen' operation.進入全屏模式
minimize_window Invokes the window manager-specific 'minimize' operation最小化窗口
switch_to 切換到某個地方,例如: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.刷新當前頁面
get_cookies Returns a set of dictionaries, corresponding to cookies visible in the current session.獲得當前會話中的所有cookies
get_cookie Get a single cookie by name. Returns the cookie if found, None if not.獲得當前會話中指定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 設置頁面加載超時時間
save_screenshot Saves a screenshot of the current window to a PNG image file.

中間件結合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('~~~~我是中間件~~~~~請求經過過了~~~~')
            # 設置無界面運行ChromeDriver
            option = Options()
            option.add_argument('--headless')
            driver = webdriver.Chrome(chrome_options=option)

            # driver = webdriver.Chrome()
            driver.implicitly_wait(15)
            driver.get(request.url)
            # 執行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

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

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

searchWhat=driver.find_element_by_id("yschsp")

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

searchWhat.clear()

#通過clear方法,可以將輸入框內的字符清空,比較保險的做法

searchWhat.send_keys("python")

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

searchBtn=driver.find_element_by_class_name("sbb")

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

searchBtn.click()

#通過click()方法點擊    

settings.py

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

顯示等待、隱式等待和強制等待

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

避免被block的多種方式

1. 禁止Cookie

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

2. 設置下載延時

#在setting中修改延時時間:3代表爬蟲下載網頁的時間間隔為3s.
DOWNLOAD_DELAY = 3

3. 使用ip池

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

# 網上獲取代理代理Ip后,在settings中設置為Ip池:外層通過列表形式存儲,里層通過字典形式存儲。
IPPOOL = [
    {'ipaddr':'ip'}
]
"""
在scrapy中,與代理服務器設置相關的下載中間件是HttpProxyMiddleware,同樣在scrapy官方文檔中,HttpProxyMiddleware對應的類為
class scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware
所以編輯時候需要導入。
"""


# 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('當前使用的ip是:'+thisip["ipaddr"])
        request.meta['proxy'] = 'http://' + thisip['ipaddr']
        

setting中設置如下:

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

4.使用用戶代理池

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

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

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',
]

中間件中寫上如下代碼:

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('當前使用的user-agent是:'+thisua)
        request.headers.setdefault('User-Agent',thisua)
        
#setting中設置如下:
DOWNLOADER_MIDDLEWARES = {
    # 讓自定義的中間件優先級高于UserAgentMiddleware 或者將2改為None也可以
    'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware':2,
    'Taobao.middlewares.Uamid':1
}

#設置好之后會通過下載中間件自動的隨機切換用戶代理進行網頁爬取
# 使用第三方庫隨機生成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動態爬蟲

安裝Docker

Docker是一個開源的軟件部署解決方案

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

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

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

優點是安裝簡單,缺點是無服務、無配置文件,相當于Windows平臺中的綠色軟件。

使用安裝腳本自動安裝Docker及其依賴

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

sudo service docker start

docker run hello-world

安裝scrapy-splash

pip install scrapy-splash

配置scrapy-splash

# 渲染服務的url
SPLASH_URL = 'http://127.0.0.1:8050'

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

編寫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類自動爬蟲

簡要說明

CrawlSpider是爬取那些具有一定規則網站的常用的爬蟲,它基于Spider并有一些獨特屬性

  • rules: 是Rule對象的集合,用于匹配目標網站并排除干擾
  • parse_start_url: 用于爬取起始響應,必須要返回ItemRequest中的一個
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 是一個 Link Extractor 對象。 其定義了如何從爬取到的頁面提取鏈接。

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

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

  3. 當編寫爬蟲規則時,請避免使用 parse 作為回調函數。 由于 CrawlSpider 使用 parse 方法來實現其邏輯,如果 您覆蓋了 parse 方法,crawl spider 將會運行失敗。

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

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

  6. process_links 是一個callable或string(該spider中同名的函數將會被調用)。 從link_extractor中獲取到鏈接列表時將會調用該函數。該方法主要用來過濾。

  7. process_request 是一個callable或string(該spider中同名的函數將會被調用)。 該規則提取到每個request時都會調用該函數。該函數必須返回一個request或者None。 (用來過濾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') 的鏈接并跟進鏈接
        #(沒有callback意味著follow默認為True)
        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方法進行分析
        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

創建Scrapy工程

#scrapy startproject 工程名
scrapy startproject demo4

根據爬蟲模板生成爬蟲文件

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

增加并發

并發是指同時處理的request的數量。其有全局限制和局部(每個網站)的限制。

Scrapy默認的全局并發限制對同時爬取大量網站的情況并不適用,因此您需要增加這個值。 增加多少取決于您的爬蟲能占用多少CPU。 一般開始可以設置為 100 。不過最好的方式是做一些測試,獲得Scrapy進程占取CPU與并發數的關系。 為了優化性能,您應該選擇一個能使CPU占用率在80%-90%的并發數。

增加全局并發數:

CONCURRENT_REQUESTS = 100

降低log級別

當進行通用爬取時,一般您所注意的僅僅是爬取的速率以及遇到的錯誤。 Scrapy使用 INFO log級別來報告這些信息。為了減少CPU使用率(及記錄log存儲的要求), 在生產環境中進行通用爬取時您不應該使用 DEBUG log級別。 不過在開發的時候使用 DEBUG 應該還能接受。

設置Log級別:

LOG_LEVEL = 'INFO'

禁止cookies

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

禁止cookies:

COOKIES_ENABLED = False

禁止重試

對失敗的HTTP請求進行重試會減慢爬取的效率,尤其是當站點響應很慢(甚至失敗)時, 訪問這樣的站點會造成超時并重試多次。這是不必要的,同時也占用了爬蟲爬取其他站點的能力。

禁止重試:

RETRY_ENABLED = False

減小下載超時

如果您對一個非常慢的連接進行爬取(一般對通用爬蟲來說并不重要), 減小下載超時能讓卡住的連接能被快速的放棄并解放處理其他站點的能力。

減小下載超時:

DOWNLOAD_TIMEOUT = 15

禁止重定向

除非您對跟進重定向感興趣,否則請考慮關閉重定向。 當進行通用爬取時,一般的做法是保存重定向的地址,并在之后的爬取進行解析。 這保證了每批爬取的request數目在一定的數量, 否則重定向循環可能會導致爬蟲在某個站點耗費過多資源。

關閉重定向:

REDIRECT_ENABLED = False

運行爬蟲

使用runspider命令運行

scrapy runspider some_spider.py

使用crawl命令運行

scrapy crawl spider_name

使用cmdline運行

from scrapy import cmdline

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

使用CrawlerProcess運行

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()

暫停,恢復爬蟲

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

使用中間件實現IP代理池

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

使用中間件實現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登錄網站

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實現分布式爬蟲

優點:可以充分地利用多個電腦的資源

scrapy本身支持分布式嗎?
不支持的!!!
為什么不支持呢?
scrapy的url隊列存在哪里? (單機內存)
如何實現分布式呢?
替換url隊列的存儲介質 (redis支持分布式的內存數據庫)
為scrapy做一個新的調度器(redis),替換scapy的默認調度器, 從而實現分布式功能。

scrapy-redis

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

scrapy-redis是大神們寫的一個scrapy的組件,主要用來負責分布式爬蟲的調度任務它依賴于Scrapy和redis。

scrapy-redis提供了幾個新的組件(新類)用來補充scrapy不能實現分布式的問題,它們分別是Scheduler、Dupefilter、Pipeline和Spider。

scrapy用類似Python中collection.deque的對象來保存待爬取的urls,

scrapy-redis用redis數據庫來保存待爬取的urls(redis支持分布式,支持隊列共享)

scrapy-redis.png
  • MasterSpiderstart_urls 中的 urls 構造 request,獲取 response
  • MasterSpiderresponse 解析,獲取目標頁面的 url, 利用 redis 對 url 去重并生成待爬 request 隊列
  • SlaveSpider 讀取 redis 中的待爬隊列,構造 request
  • SlaveSpider 發起請求,獲取目標頁面的 response
  • Slavespider 解析 response,獲取目標數據,寫入生產數據庫

redis在爬蟲系統中的作用:

  1. 存儲鏈接
  2. 和scrapy一起工作,redis用來調度spiders(多個spider共用一個redis隊列,即分布式處理)

充分利用redis結構的作用:

set:set中沒有重復元素,利用這個特性,可以用來過濾重復元素;

list:實現隊列和棧的功能;

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

hash:存儲的信息字段比較多時,可以用hash;

使用scrapy-redis實現分布式處理的步驟

創建項目

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

編寫代碼

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

使用scrapy redis,需要注意以下幾個區別:

  1. 傳統的spiders中,每個spider類繼承的是scrapy.Spider類或Scrapy.spiders.CrawlSpider類,而分布式寫法每個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. 在分布式寫法中,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. 在分布式寫法中, 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)
    

搭建分布式爬蟲環境

環境準備:4臺服務器,一個做master,另外3個做slave。

scrapy、scrapy-redis、redis

master服務器的配置:

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

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

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

    2)重啟redis-server。

  3. 在爬蟲項目中的setting.py文件中添加配置信息:

    REDIS_HOST = 'localhost'
    REDIS_PORT = 6379
    

slave端的配置:

  1. 在爬蟲項目中的setting.py文件中添加配置信息:

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

master和slave端中共同的配置

在setting.py中啟用redis存儲

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

運行分布式爬蟲:爬蟲將處于監聽狀態,等待獲取redis隊列中的url

# scrapy runspider myspider_redis.py
scrapy crawl myspider

向redis隊列添加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中數據轉存到mysql的腳本文件

"""
把redis中的數據轉存到數據庫中
"""
from redis import Redis
import settings
import pymysql
import json


class RedisPipeline(object):

    def __init__(self):
        """
        連接數據庫
        """

        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()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容