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 href="http://lab.scrapyd.cn/page/2/">下一页 »</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阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容