爬虫教程( 2 ) --- scrapy 教程、实战

​scrapy 英文文档:https://docs.scrapy.org/en/latest/index.html
scrapy 中文文档:https://www.osgeo.cn/scrapy/index.html
参考:https://piaosanlang.gitbooks.io/spiders/content/

1、常用 爬虫 框架

爬虫 框架

github 爬虫 框架

爬虫框架中比较好用的是 Scrapy 和 PySpider。

PySpider

文档:https://docs.pyspider.org/en/latest/Quickstart/

github:https://github.com/binux/pyspider

优点:分布式框架,上手更简单,操作更加简便,因为它增加了 WEB 界面,写爬虫迅速,集成了phantomjs,可以用来抓取js渲染的页面。
缺点:自定义程度低

Scrapy

英文文档:https://docs.scrapy.org/en/latest/

中文文档 ( 版本比较旧 ) :https://www.osgeo.cn/scrapy/intro/overview.html

优点:自定义程度高,比 PySpider 更底层一些,适合学习研究,需要学习的相关知识多,拿来研究分布式和多线程等等是最合适不过的。

缺点:非分布式框架(可以用 scrapy-redis 分布式框架)

安装:pip install Scrapy

aio-scrapy

pypi 中搜索 scrapy,可以看到还有其他框架:https://pypi.org/search/?q=scrapy
安装:pip install aio-scrapy
文档:https://pypi.org/project/aio-scrapy/

  • aio-scrapy 框架基于开源项目 Scrapy & scrapy_redis。
  • aio-scrapy 实现了与 scrapyd 的兼容性。
  • aio-scrapy 实现了 redis 队列和 rabbitmq 队列。
  • 分布式爬网/抓取。

默认安装:pip install aio-scrapy

安装所有依赖:pip install aio-scrapy[all]

feapder

feapder:https://feapder.com/

feapder 命名源于 fast-easy-air-pro-spider 缩写,秉承着开发快速、抓取快速、简单、轻量且功能强大的原则,倾心打造。支持轻量级爬虫、分布式爬虫、批次爬虫、爬虫集成,以及完善的报警等。

  1. feapder是一款上手简单,功能强大的Python爬虫框架,内置AirSpider、Spider、TaskSpider、BatchSpider四种爬虫解决不同场景的需求。
  2. 支持断点续爬、监控报警、浏览器渲染、海量数据去重等功能。
  3. 更有功能强大的爬虫管理系统feaplat为其提供方便的部署及调度

文档地址

可视化 爬虫 框架

Selenium 

selenium 在爬虫中的应用

  • 获取动态网页中的数据,一些动态的数据我们在获取的源码中并没有显示的之一类动态加载数据
  • 可用于模拟登录

安装:pip install selenium

驱动下载:http://chromedriver.storage.googleapis.com/index.html

Playwright

新一代爬虫利器 -- Playwright:https://zhuanlan.zhihu.com/p/499597451

github:https://github.com/microsoft/playwright

Playwright for TypeScriptJavaScriptPython.NET, or Java

Playwright (剧作家) 是专门为满足端到端测试的需求而创建的。Playwright支持所有现代渲染引擎,包括Chromium,WebKit和Firefox。在Windows,Linux和macOS上进行测试,本地或CI,无头或以Google Chrome for Android和Mobile Safari的本机移动仿真为标题。

它可以通过单个API自动执行 Chromium,Firefox 和 WebKit 浏览器,连代码都不用写,就能实现自动化功能。虽然测试工具 selenium 具有完备的文档,但是其学习成本让一众小白们望而却步,对比之下 playwright-python 简直是小白们的神器。

提示:playwright 还可支持移动端的浏览器模拟。

ichrome

github:https://github.com/ClericPy/ichrome

基于 Chrome Devtools Protocol(CDP) 和 python3.7+ 来人为的控制 Chrome

pychrome 也是 Google Chrome Dev Protocol [threading base] 的一个 Python 包

2、Scrapy 官方文档

scrapy-cookbook ( 中文版 ):https://scrapy-cookbook.readthedocs.io/zh_CN/latest/index.html

Scrapy 官网文档 ( 英文版 ):https://docs.scrapy.org/en/latest/index.html

Scrapy 是一个快速的高级网络抓取和网络抓取框架,用于抓取网站并从其页面中提取结构化数据。它可以用于广泛的目的,从数据挖掘到监控和自动化测试。

2.1 入门

Scrapy 一瞥

Scrapy at a glance

安装指南

Installation guide

Scrapy 教程

Scrapy Tutorial

Scrapy 一些示例

Examples

2.2 基本概念

基本概念

命令行工具

Spiders

Selectors

Items

Item Loaders

Scrapy shell

Item Pipeline 

Feed exports ( 命令行 导出 数据 )

Requests 和 Responses ( 请求、响应 )

Link Extractors ( 链接提取 )

Settings ( 设置 )

Exceptions ( 异常 )

2.3 内置服务

内置服务

日志

状态统计信息

发送 Email

telnet 控制

2.4 一些常见问题

一些常见问题

经常被问的问题

调试 脚本

Spider 约定

一些通用用法

大规模抓取

使用浏览器的开发人员工具进行抓取

选择动态加载的内容

调试内存泄漏

下载和处理文件和图像

部署 Spider

自动负载限制爬网速度

基准测试

暂停和恢复爬行

协程

asyncio ( 异步 )

2.5 Scrapy 扩展知识

Scrapy 扩展

体系结构概述 ( 框架结构 )

Scrapy 官方文档:https://docs.scrapy.org/en/latest/topics/architecture.html

框架图显示了 Scrapy 体系结构及其概述 系统内发生的数据流的组件和概述 (由红色箭头显示)。包括组件的简要说明。

另一个框架图:

Scrapy 中的数据流由执行引擎控制,流程如下:

  1. 引擎 首先从自己编写的 spider 中读取起始 url (从 start_urls 列表读取),然后封装成 Request 对象
  2. 引擎 把 "封装后的Request对象" 传递给 调度器 ( 调度器主要作用就是管理、调度url,可以简单的看作是一个 "Request对象的队列",对 Requestd对象 管理、过滤 等操作)。
  3. 引擎 请求 调度器,调度器返回一个 Request对象 给引擎。
  4. 引擎 将 Request对象 发送到下载器。下载器会将请求通过下载器中间件。( process_request() )
  5. 下载器完成页面下载后,下载器会将生成的 响应Response 通过下载器中间件(process_response() ),最后将其发送到引擎。
  6. 引擎接收来自下载器的 响应 并将其发送给 自己编写的 spider 进行处理,但是在发送之前会先 传递 通过spider中间件(参见 process_spider_input() )。
  7. 自己编写的 spider 处理响应并返回 "抓取的数据Item" 及(跟进的)新的Request给引擎,通过蜘蛛中间件(参见process_spider_output() )。
  8. 引擎将 "抓取的数据Item" 发送到 pipeline,将 "新的请求" 发送到 调度程序。并继续从调度器中获取 下一个 "Request对象" 来抓取。
  9. 该过程重复(从步骤 3 开始),直到没有更多地 request对象 ,最后关闭引擎。

整个工作流程

  • 1.引擎 将爬虫中起始的url构造成request对象,并传递给调度器。
  • 2.引擎 从 调度器 中获取到request对象然后交给下载器。
  • 3.由 下载器 来获取到页面源代码,并封装成response对象,并返回给引擎。
  • 4.引擎 将获取到的response对象传递给 spider,由 spider 对数据进行解析(parse),并返回给引擎
  • 5.引擎将数据传递给 pipeline 进行数据持久化保存或进一步的数据处理
  • 6.在此期间如果spider中提取到的并不是数据。而是子页面ur.可以进一步提交给调度器,进而重复 步骤2 的过程

Scrapy 主要组件

  • 引擎(Scrapy):用来处理整个系统的数据流处理。控制各个模块之间的通信
  • 调度器(Scheduler): 负责引擎发过来的请求,从 待下载链接 中取出一个链接(URL)并压入队列同时去除重复的网址,决定下一个要抓取的网址是什么 。启动采集模块,即 Spiders模块。
  • 下载器(Downloader): 用于下载网页内容, 并将下载的网页内容返回给引擎Scrapy
  • 爬虫(Spiders): 解析并提取 下载器获取的 response,用户也可以从中提取出链接,让Scrapy继续抓取下一个页面。提取的 内容就是 item,item 会传递给 pipeline 处理。
  • 项目管道(item Pipeline): 负责处理爬虫从网页中抽取 item,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。

三大中间件

  • 下载器中间件(Downloader Middlewares): 位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
  • 爬虫中间件(Spider Middlewares): 介于 Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
  • 调度中间件(Scheduler Middewares): 介于 Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

下载器 中间件

示例:

class MyDownloaderMiddleware(object):

    @classmethod
    def from_crawler(cls, crawler):
        # 在创建 自定义spider的时候. 自动的执行这个函数
        s = cls()  # 创建当前类的对象 -> 理解成每个函数中的self就可以了

        # singles: scrapy定义好的一些信号
        # scrapy的信号,链接(s.spider_opened, signal=signals.spider_opened)
        # 信号: 通信
        #  当触发了xxxx信号的时候. 自动运行某些东西
        #  火警报警器
        #  触发了烟雾信号, 自动放水
        # 相当于给引擎绑定一些功能

        crawler.signals.connect(s.selenium_start, signal=signals.spider_opened)
        crawler.signals.connect(s.selenium_stop, signal=signals.spider_closed)
        return s

    def selenium_start(self, spider):
        # 使用 selenium 来完成页面源代码(elements)的抓取
        # 无头自己加
        self.web = Chrome()  # 程序跑起来之后。 去创建Chrome对象。 程序跑完了之后。 关掉web对象
        self.web.implicitly_wait(10)  # 等待

    def selenium_stop(self, spider):
        self.web.close()

    def process_request(self, req, spider):

        # 小总结:
        # 引擎,如果接收到request, 走调度器
        # 引擎,如果接收到response, 走spider
        # 引擎,如果接收到item, dict, 走pipeline

        # return Must either: 返回值必须是以下的某一个
        # - None: 继续向后走,走到后面的中间件或者走到下载器
        # - Response object, 停下来. 这个请求就不会走下载器, 而是直接把响应对象给到引擎
        # - Request object, 停下来,请求不再走下载器. 而是直接把请求对象给到引擎. 引擎继续走调度器。

        # isinstance: 判断xxxx是否是xxx类型的
        if isinstance(req, SeleniumRequest):
            print("我是selenium的")
            self.web.get(req.url)  # 直接访问即可
            # 随便找个东西。如果找到了。就算加载完了
            self.web.find_element(By.XPATH, '//*[@id="header"]/div[1]/div[3]/div/a[1]')
            page_source = self.web.page_source  # 就可以拿elements
            # 页面源代码有了? 下载器还去么?
            # 组装一个响应对象。 返回 -> 引擎即可
            resp = HtmlResponse(  # 组装响应对象
                status=200,  # 状态码
                url=req.url,  # url
                body=page_source.encode("utf-8"),  # 页面源代码
                request=req  # 请求对象
            )
            return resp  # ?
        else:
            print(req.url)
            print("我是普通的")
            return None  # 正常放行。 正常走下载器那一套

    def process_response(self, request, response, spider):
        return response

Spider 中间件

扩展

信号

import scrapy
from scrapy import signals
from redis import Redis


# 去除重复
class TySpider(scrapy.Spider):
    name = 'ty'
    allowed_domains = ['tianya.cn']
    start_urls = ['http://bbs.tianya.cn/list.jsp?item=free&order=1']
    # 需要redis来去除重复的url
    # 先打开redis 先创建好连接

    # 用scrapy的方式来绑定事件
    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)
        return s

    def spider_opened(self, spider):  # 在这里建立连接
        self.conn = Redis(host="127.0.0.1", port=6379, password="123456", db=6)

    def spider_closed(self, spider):  # 关闭redis连接
        if self.conn:  # 好习惯
            self.conn.close()

    def parse(self, resp, **kwargs):
        tbodys = resp.xpath("//div[@class='mt5']/table/tbody")[1:]
        for tbody in tbodys:
            trs = tbody.xpath("./tr")
            for tr in trs:
                title = tr.xpath("./td[1]/a/text()").extract_first()
                href = tr.xpath("./td[1]/a/@href").extract_first()
                href = resp.urljoin(href)
                print(href)
                # 判断是否已经抓取过了, 如果已经抓取过了。 不再重复抓取了
                # 如果没有被抓取过。就会发送请求出去
                # "ty:urls" 大key
                t = self.conn.sismember("ty:urls", href)
                if t:
                    print("该数据已经被抓取过了。 不需要重复的抓取")
                else:
                    yield scrapy.Request(url=href, callback=self.parse_detail, meta={"href": href})
                    # 1 如果这一次请求没有成功?

        # 技术上的东西。请求时一定要把延迟配置上.
        # 下一页, 请求3页
        # 找到下一页的链接
        # hh = "http://www.baidu.com"
        # yield scrapy.Request(url= hh, callback=self.parse, meta={"page": 变量})

    def parse_detail(self, resp, **kwargs):  # .  /   //
        href = resp.meta['href']  # href别手懒。 一定手工传递过来。 防止重定向的发生
        txts = resp.xpath("//div[@class='bbs-content clearfix']//text()").extract()  # 解析详情页的内容
        txt = "".join(txts)
        txt = txt.strip()
        # print(txt)
        # yield 给管道返回数据了 # 管道中出现错误的几率是小的。 可控的。
        # 2
        self.conn.sadd("ty:urls", href)
        # 管道 3
        # 数据去重
        yield {"content": txt}

调度器

Item Exporters

组件

核心API

3、Scrapy 入门教程

大致流程:

  1. 创建一个 Scrapy 项目
  2. 定义提取的结构化数据 (Item)
  3. 编写爬取网站的 spider 并提取出结构化数据 (Item)
  4. 编写 Item Pipeline 来存储提取到的 Item (即结构化数据)
  5. 配置 setting.py

scrapy --help

scrapy --help

用法:scrapy <command> [options] [args]
可用的命令:
        bench         运行快速基准测试
        check         查看正在运行的 spider
        crawl         运行 spider
        edit          编辑 spider
        fetch         使用Scrapy下载器来来下载给定URL的内容
        genspider     使用预定义模板生成一个spider
        list          列出所有可用的 spiders
        parse         解析URL的response(使用它的爬虫)并打印结果
        runspider     运行自包含 spider,而不不创建项目
        settings      获取 settings 中的配置
        shell         交互的 scraping 控制台
        startproject  创建一个新项目
        version       打印 scrapy 版本
        view          在浏览器中打开URL,如Scrapy所见
使用 "scrapy <command> -h" 查看命令的更多帮助信息

创建一个 Scrapy 项目

按顺序执行下面命令,即可成功创建一个scrapy 项目

scrapy startproject myspider
cd myspider
scrapy genspider example example.com

该命令将会创建包含下列内容的 myspider 目录,这些文件分别是

  • scrapy.cfg:项目的配置文件;(用于发布到服务器)
  • myspider/: 该项目文件夹。之后将在此目录编写Python代码。
  • myspider/items.py: 项目中的item文件;(定义结构化数据字段field).
  • myspider/pipelines.py: 项目中的pipelines文件;(定义如何存储结构化数据)
  • myspider/settings.py: 项目的设置文件;(如何修改User-Agent,设置爬取时间间隔,设置代理,配置中间件等等)
  • myspider/spiders/: 放置spider代码的目录;(编写爬取网站规则)

注意:一般创建爬虫文件时,以网站域名命名

定义 Item

Item 定义结构化数据字段,用来保存爬取到的数据;其使用方法和 python 字典类似。

定义一个 Item:创建一个 scrapy.Item 类,添加 类属性,并且类属性是 scrapy.Field 类型

# -*- coding: utf-8 -*-
  
# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html
  
import scrapy
  
class JieYiCaiItem(scrapy.Item):
  
    company = scrapy.Field()
    title = scrapy.Field()
    qq = scrapy.Field()
    info = scrapy.Field()
    more = scrapy.Field()

spider 中可以返回字典,但是官方并不推荐返回数据的时候直接返回字典,因为字典没有约束。官方推荐使用 Item 来约束数据结构,提前定义好这个结构,用起来和字典几乎一样。

编写 爬虫 (Spider) 文件

创建一个 Spider,必须继承 'scrapy.Spider' 类, 需要定义以下三个属性:

  • name: spider 名字;如果没有 name,会报错。因为源码中是这样定义的
  • start_urls: 初始的 URL 列表
  • parse(self, response):每个初始 URL 完成下载后被调用。这个函数要完成的功能:
            1. 负责解析返回的网页数据(response.body),提取结构化数据(生成item)
            2. 生成需要下一页的请求 URL。

在 myspider/spiders 目录下创建 tencent_spider.py 文件中,内容如下

import scrapy


class ExampleSpider(scrapy.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "https://careers.tencent.com/search.html?&start=0#a"
    ]

    def parse(self, response):
        with open('./tencent.txt', 'wb') as f:
            f.write(response.body)
        pass

xpath 相关查询:

  • 查询子子孙孙中的某个标签(以div标签为例)://div
  • 查询儿子中的某个标签(以div标签为例):/div
  • 查询标签中带有某个class属性的标签://div[@class='c1']即子子孙孙中标签是div且class=‘c1’的标签
  • 查询标签中带有某个class=‘c1’并且自定义属性name=‘alex’的标签://div[@class='c1'][@name='alex']
  • 查询某个标签的文本内容://div/span/text() 即查询子子孙孙中div下面的span标签中的文本内容
  • 查询某个属性的值(例如查询a标签的href属性)://a/@href

在 myspider/spiders 目录下创建 xiaohua_spider.py 文件中,内容如下

import scrapy
import os
import requests
from lxml import etree


class XiaoHuarSpider(scrapy.spiders.Spider):
    name = "xiaohuar"
    allowed_domains = ["xiaohuar.com"]
    start_urls = [
        "http://www.xiaohuar.com/hua/",
    ]
    dont_proxy = True

    # 自定义配置。自定义配置会覆盖项目级别(即setting.py)配置
    custom_settings = {
        'DEFAULT_REQUEST_HEADERS': {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
            'Accept-Encoding': 'gzip, deflate',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Connection': 'keep-alive',
            'Host': 'www.xiaohuar.com',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                          'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36'
        },

        # 'ITEM_PIPELINES': {
        #     'maoyan.pipelines.pipelines.MainPipelineKafka': 300,
        #     # 'maoyan.pipelines.pipelines.MainPipelineSQLServer': 300,
        # },

        # 'DOWNLOADER_MIDDLEWARES': {
        #     # 'maoyan.middlewares.useragent.RandomUserAgentMiddleware': 90,
        #     # 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 100,
        #     # 'maoyan.middlewares.middlewares.ZhiMaIPMiddleware': 125,
        #     'maoyan.middlewares.proxy_middlewares.ProxyMiddleware': 125,
        # },

        # 'CONCURRENT_REQUESTS': 100,
        # 'DOWNLOAD_DELAY': 0.5,
        # 'RETRY_ENABLED': False,
        # 'RETRY_TIMES': 1,
        # 'RETRY_HTTP_CODES': [500, 503, 504, 400, 403, 404, 408],

        # 'REDIRECT_ENABLED': False,  # 关掉重定向,不会重定向到新的地址
        # 'HTTPERROR_ALLOWED_CODES': [301, 302, 403],  # 返回301, 302时,按正常返回对待,可以正常写入cookie
    }

    def parse(self, response):
        current_url = response.url
        print(current_url)
        # 创建查询的 xpath 对象 (也可以使用 scrapy 中 response 中 xpath)
        # selector = etree.HTML(response.text)

        div_xpath = '//div[@class="item_t"]'
        items = response.xpath(div_xpath)

        for item in items:
            # 图片 地址
            # /d/file/20190117/07a7e6bc4639ded4972d0dc00bfc331b.jpg
            img_src = item.xpath('.//img/@src').extract_first()
            img_url = 'http://www.xiaohuar.com{0}'.format(img_src) if 'https://' not in img_src else img_src
            # 校花 名字
            mm_name = item.xpath('.//span[@class="price"]/text()').extract_first()
            # 校花 学校
            mm_school = item.xpath('.//a[@class="img_album_btn"]/text()').extract_first()
            if not os.path.exists('./img/'):
                os.mkdir('./img')
            file_name = "%s_%s.jpg" % (mm_school, mm_name)
            file_path = os.path.join("./img", file_name)
            r = requests.get(img_url)
            if r.status_code == 200:
                with open(file_path, 'wb') as f:
                    f.write(r.content)
            else:
                print('status code : {0}'.format(r.status_code))

        next_page_xpath = '//div[@class="page_num"]//a[contains(text(), "下一页")]/@href'
        next_page_url = response.xpath(next_page_xpath).extract_first()
        if next_page_url:
            r_headers = {
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
                'Accept-Encoding': 'gzip, deflate',
                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
                'Connection': 'keep-alive',
                'Host': 'www.xiaohuar.com',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                              'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36'
            }
            yield scrapy.Request(next_page_url, headers=r_headers, callback=self.parse)


def test_1():
    from scrapy import cmdline
    cmdline.execute('scrapy crawl xiaohuar'.split())


def test_2():
    from scrapy.crawler import CrawlerProcess
    from scrapy.utils.project import get_project_settings
    process = CrawlerProcess(get_project_settings())
    process.crawl('xiaohuar')
    process.start()


if __name__ == '__main__':
    test_1()
    # test_2()
    pass

递归爬取网页。如果爬取的url内容中包含了其他url,而也想对其进行爬取,那么如何实现递归爬取网页呢?可以通过yield生成器向每一个url发送request请求

# 获取所有的url,继续访问,并在其中寻找相同的url
       all_urls = response.xpath('//a/@href').extract()
       for url in all_urls:
           if url.startswith('http://www.xiaohuar.com/list-1-'):
               yield Request(url, callback=self.parse)

提示:可以修改settings.py 中的配置文件,以此来指定“递归”的层数,如: DEPTH_LIMIT = 1

def parse(self, response): #获取响应cookies
    from scrapy.http.cookies import CookieJar
    cookieJar = CookieJar()
    cookieJar.extract_cookies(response, response.request)
    print(cookieJar._cookies)

更多选择器规则:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/selectors.html

在 spider 中使用 item:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# spider
import scrapy
import hashlib
from beauty.items import JieYiCaiItem
from scrapy.http import Request
from scrapy.selector import HtmlXPathSelector
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor


class JieYiCaiSpider(scrapy.spiders.Spider):
    count = 0
    url_set = set()

    name = "jieyicai"
    domain = 'http://www.jieyicai.com'
    allowed_domains = ["jieyicai.com"]

    start_urls = [
        "http://www.jieyicai.com",
    ]

    rules = [
        #下面是符合规则的网址,但是不抓取内容,只是提取该页的链接(这里网址是虚构的,实际使用时请替换)
        #Rule(SgmlLinkExtractor(allow=(r'http://test_url/test?page_index=\d+'))),
        #下面是符合规则的网址,提取内容,(这里网址是虚构的,实际使用时请替换)
        #Rule(LinkExtractor(allow=(r'http://www.jieyicai.com/Product/Detail.aspx?pid=\d+')), callback="parse"),
    ]

    def parse(self, response):
        md5_obj = hashlib.md5()
        md5_obj.update(response.url)
        md5_url = md5_obj.hexdigest()
        if md5_url in JieYiCaiSpider.url_set:
            pass
        else:
            JieYiCaiSpider.url_set.add(md5_url)
            
            hxs = HtmlXPathSelector(response)
            if response.url.startswith('http://www.jieyicai.com/Product/Detail.aspx'):
                item = JieYiCaiItem()
                item['company'] = hxs.select('//span[@class="username g-fs-14"]/text()').extract()
                item['qq'] = hxs.select('//span[@class="g-left bor1qq"]/a/@href').re('.*uin=(?P<qq>\d*)&')
                item['info'] = hxs.select('//div[@class="padd20 bor1 comard"]/text()').extract()
                item['more'] = hxs.select('//li[@class="style4"]/a/@href').extract()
                item['title'] = hxs.select('//div[@class="g-left prodetail-text"]/h2/text()').extract()
                yield item

            current_page_urls = hxs.select('//a/@href').extract()
            for i in range(len(current_page_urls)):
                url = current_page_urls[i]
                if url.startswith('/'):
                    url_ab = JieYiCaiSpider.domain + url
                    yield Request(url_ab, callback=self.parse)

上述代码中:对url进行md5加密的目的是避免url过长,也方便保存在缓存或数据库中。

此处代码的关键在于:

  • 将获取的数据封装在了Item对象中
  • yield Item对象 (一旦parse中执行yield Item对象,则自动将该对象交个pipelines的类来处理)

运行 / 调试 

方法 1:命令行中运行

执行命令:scrapy crawl tencent

调试方式:在命令行输入:scrapy shell <url_name>

scrapy crawl tencent 执行成功后,会生成 tencent.txt 文件

刚才发生了什么?

  • Scrapy 为 Spider 的 start_urls 属性中的每个 URL 创建了 scrapy.Request 对象,并将 parse 方法作为回调函数(callback)赋值给了 Request。
  • Request 对象经过调度,执行生成 scrapy.http.Response 对象并送回给 parse() 方法。

方法2: 在Python脚本中运行

官网文档:https://docs.scrapy.org/en/latest/topics/practices.html

使用 scrapy.cmdline 的 execute 方法

import scrapy


class ExampleSpider(scrapy.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "https://careers.tencent.com/search.html?&start=0#a"
    ]

    def parse(self, response):
        with open('./tencent.txt', 'wb') as f:
            f.write(response.body)
        pass


if __name__ == '__main__':
    from scrapy import cmdline
    cmdline.execute(f'scrapy crawl {ExampleSpider.name}'.split())

使用 scrapy 的 CrawlerProcess 方法

运行单个 spider

import scrapy
from scrapy.crawler import CrawlerProcess


class ExampleSpider(scrapy.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "https://careers.tencent.com/search.html?&start=0#a"
    ]

    def parse(self, response):
        with open('./tencent.txt', 'wb') as f:
            f.write(response.body)
        pass


if __name__ == '__main__':
    process = CrawlerProcess(
        settings={
            "FEEDS": {
                "items.json": {"format": "json"},
            },
        }
    )

    process.crawl(ExampleSpider)
    process.start()  # 抓取完成之前,一直会阻塞到这里

示例:通过 get_project_settings 获取项目设置

from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings

process = CrawlerProcess(get_project_settings())

process.crawl("spider_name", domain="scrapy.org")
process.start()  # 抓取结束之前, 一直会阻塞到这。

示例:运行 MySpider 完成后,手动停止反应堆

from twisted.internet import reactor
import scrapy
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
runner = CrawlerRunner()

d = runner.crawl(MySpider)
d.addBoth(lambda _: reactor.stop())
reactor.run()  # the script will block here until the crawling is finished

在同一进程中运行多个爬虫

import scrapy
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings


class MySpider1(scrapy.Spider):
    # Your first spider definition
    ...


class MySpider2(scrapy.Spider):
    # Your second spider definition
    ...


settings = get_project_settings()
process = CrawlerProcess(settings)
process.crawl(MySpider1)
process.crawl(MySpider2)
process.start()  # the script will block here until all crawling jobs are finished

相同的示例使用 CrawlerRunner :

import scrapy
from twisted.internet import reactor
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
from scrapy.utils.project import get_project_settings


class MySpider1(scrapy.Spider):
    # Your first spider definition
    ...


class MySpider2(scrapy.Spider):
    # Your second spider definition
    ...


configure_logging()
settings = get_project_settings()
runner = CrawlerRunner(settings)
runner.crawl(MySpider1)
runner.crawl(MySpider2)
d = runner.join()
d.addBoth(lambda _: reactor.stop())

reactor.run()  # the script will block here until all crawling jobs are finished

相同的示例,但通过链接延迟按顺序运行 spider:

from twisted.internet import reactor, defer
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
from scrapy.utils.project import get_project_settings


class MySpider1(scrapy.Spider):
    # Your first spider definition
    ...


class MySpider2(scrapy.Spider):
    # Your second spider definition
    ...


settings = get_project_settings()
configure_logging(settings)
runner = CrawlerRunner(settings)


@defer.inlineCallbacks
def crawl():
    yield runner.crawl(MySpider1)
    yield runner.crawl(MySpider2)
    reactor.stop()


crawl()
reactor.run()  # the script will block here until the last crawl call is finished

提取 Item ( xpath、CSS、re )

Scrapy 内置的 Selectors 模块提供了对  XPath 和 CSS Selector 的支持。也可以单独拿出来使用

单独使用 示例:

from scrapy import Selector


temp_string = '''
<bookstore>
  <book>
    <title lang="eng">Harry Potter</title>
    <price>29.99</price>
  </book>
  <book>
    <title lang="eng">Learning XML</title>
    <price>39.95</price>
  </book>
</bookstore>
'''


if __name__ == '__main__':
    s = Selector(text=temp_string)
    print(s.xpath('//book[1]/title/text()').extract_first())
    print(s.xpath('//book[1]/price/text()').extract_first())
    pass

XPath 表达式的例子及对应的含义:

/html/head/title           选择<HTML>文档中 <head> 标签内的 <title> 元素
/html/head/title/text()    选择上面提到的 <title> 元素的文字
//td                       选择所有的 <td> 元素
//div[@class="mine"]       选择所有具有 class="mine" 属性的 div 元素

Selector 有四个基本的方法:

xpath()       传入xpath表达式,返回该表达式所对应的所有节点的selector list列表 。
css()         传入CSS表达式,返回该表达式所对应的所有节点的selector list列表.
extract()     序列化该节点为unicode字符串并返回list。
re()          根据传入的正则表达式对数据进行提取,返回unicode字符串list列表。

scrapy shell

尝试 Selector 选择器

为了介绍 Selector的使用方法,接下来我们将要使用内置的 scrapy shell 。Scrapy Shell 需要您预装好 IPython (一个扩展的Python终端)。您需要进入项目的根目录,执行下列命令来启动 shell: 

注意:url 地址一定要加上引号

示例 1:scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
示例 2:scrapy shell "https://www.meinvtu1234.cc/"

当 shell 载入后,将得到一个包含 response 数据的本地 response 变量

  • 输入 response.body 将输出 response 的包体,
  • 输出 response.headers 可以看到 response 的包头,
  • 输入 response.selector 时, 将获取到一个response 初始化的类 Selector 的对象。通过使用 response.selector.xpath() 或 response.selector.css() 来对 response 进行查询。scrapy 对 response.selector.xpath() 及 response.selector.css() 提供了一些快捷方式,例如 response.xpath() 或 response.css()
response.xpath('//title')
[<Selector xpath='//title' data=u'<title>\u804c\u4f4d\u641c\u7d22 | \u793e\u4f1a\u62db\u8058 | Tencent \u817e\u8baf\u62db\u8058</title'>]

response.xpath('//title').extract()
[u'<title>\u804c\u4f4d\u641c\u7d22 | \u793e\u4f1a\u62db\u8058 | Tencent \u817e\u8baf\u62db\u8058</title>']

print response.xpath('//title').extract()[0]
<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>

response.xpath('//title/text()')
<Selector xpath='//title/text()' data=u'\u804c\u4f4d\u641c\u7d22 | \u793e\u4f1a\u62db\u8058 | Tencent \u817e\u8baf\u62db\u8058'>

response.xpath('//title/text()')[0].extract()
u'\u804c\u4f4d\u641c\u7d22 | \u793e\u4f1a\u62db\u8058 | Tencent \u817e\u8baf\u62db\u8058'

print response.xpath('//title/text()')[0].extract()
职位搜索 | 社会招聘 | Tencent 腾讯招聘

response.xpath('//title/text()').re('(\w+):')
[u'\u804c\u4f4d\u641c\u7d22',
 u'\u793e\u4f1a\u62db\u8058',
 u'Tencent',
 u'\u817e\u8baf\u62db\u8058']

提取数据

现在,从页面中提取些有用的数据。

# 通过 XPath 选择该页面中网站列表里所有 lass=even 元素
site = response.xpath('//*[@class="even"]')

# 职位名称:
print site[0].xpath('./td[1]/a/text()').extract()[0]
# TEG15-运营开发工程师(深圳)

# 职位名称详情页:
print site[0].xpath('./td[1]/a/@href').extract()[0]
position_detail.php?id=20744&keywords=&tid=0&lid=0

# 职位类别:
print site[0].xpath('./td[2]/text()').extract()[0]
# 技术类

调用 .xpath() 返回的是  selector 组成的 list, 因此可以拼接更多的 .xpath() 来进一步获取某个节点。

for sel in response.xpath('//*[@class="even"]'):
    name = sel.xpath('./td[1]/a/text()').extract()[0]
    detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
    catalog = sel.xpath('./td[2]/text()').extract()[0]
    recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
    workLocation = sel.xpath('./td[4]/text()').extract()[0]
    publishTime = sel.xpath('./td[5]/text()').extract()[0]
    print name, detailLink, catalog,recruitNumber,workLocation,publishTime

在我们的 tencent_spider.py 文件修改成如下代码:

import scrapy

class RecruitSpider(scrapy.spiders.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]

    def parse(self, response):
        for sel in response.xpath('//*[@class="even"]'):
            name = sel.xpath('./td[1]/a/text()').extract()[0]
            detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
            catalog = sel.xpath('./td[2]/text()').extract()[0]
            recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
            workLocation = sel.xpath('./td[4]/text()').extract()[0]
            publishTime = sel.xpath('./td[5]/text()').extract()[0]
            print name, detailLink, catalog,recruitNumber,workLocation,publishTime

如图所示:

现在尝试再次爬取 hr.tencent.com,您将看到爬取到的网站信息被成功输出:

scrapy crawl tencent

运行过程:

编写 item

Item 对象是自定义的 python 字典。可以使用标准的字典语法来获取到其每个字段的值。输入 `scrapy shell'

import scrapy

class RecruitItem(scrapy.Item):
    name  = scrapy.Field()
    detailLink = scrapy.Field()
    catalog = scrapy.Field()
    recruitNumber = scrapy.Field()
    workLocation = scrapy.Field()
    publishTime = scrapy.Field()

item = RecruitItem()
item['name'] = 'sanlang'
item['name']
'sanlang'

一般来说,Spider 将会将爬取到的数据以 Item 对象返回。所以为了将爬取的数据返回,最终 tencent_spider.py 代码将是:

import scrapy
from tutorial.items import RecruitItem
class RecruitSpider(scrapy.spiders.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]

    def parse(self, response):
        for sel in response.xpath('//*[@class="even"]'):
            name = sel.xpath('./td[1]/a/text()').extract()[0]
            detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
            catalog = sel.xpath('./td[2]/text()').extract()[0]
            recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
            workLocation = sel.xpath('./td[4]/text()').extract()[0]
            publishTime = sel.xpath('./td[5]/text()').extract()[0]
            print name, detailLink, catalog,recruitNumber,workLocation,publishTime
            item = RecruitItem()
            item['name']=name.encode('utf-8')
            item['detailLink']=detailLink.encode('utf-8')
            item['catalog']=catalog.encode('utf-8')
            item['recruitNumber']=recruitNumber.encode('utf-8')
            item['workLocation']=workLocation.encode('utf-8')
            item['publishTime']=publishTime.encode('utf-8')

            yield item

现在对 hr.tencent.com 进行爬取将会产生 RecruitItem 对象:

命令行 保存 抓取的数据

最简单存储爬取的数据的方式是使用 Feed exports:

scrapy crawl tencent -o items.json

该命令将采用 JSON 格式对爬取的数据进行序列化,生成 items.json 文件。

如果需要对爬取到的item做更多更为复杂的操作,您可以编写 Item Pipeline 。 类似于我们在创建项目时对Item做的,用于您编写自己的 tutorial/pipelines.py 也被创建。 不过如果您仅仅想要保存item,您不需要实现任何的pipeline。

命令行运行

格式:scrapy crawl+爬虫名 --nolog 即不显示日志
示例:scrapy crawl xiaohau --nolog

编写 Pipelines

item pipiline 组件是一个独立的 Python 类,必须实现 process_item 方法:

  • process_item(self, item, spider):当 Item 在 Spider 中被收集之后,都需要调用该方法。参数:  item - 爬取的结构化数据。 spider – 爬取该 item 的 spider
  • open_spider(self, spider):当 spider 被开启时,这个方法被调用。参数:spider   – 被开启的spider
  • close_spider(spider):当 spider 被关闭时,这个方法被调用。参数:spider – 被关闭的spider
import pymysql
import pymongo

"""
优先级可以理解为距离引擎的距离
数字越小,距离引擎越近,就先执行
数字越大,距离引擎越远,就后执行
"""


# 设置优先级 120
class Pipeline2File(object):
    # 在程序跑起来的时候。打开一个w模式的文件
    # 在获取数据的时候正常写入
    # 在程序结束的时候。 关闭f
    # open_spider, 爬虫在开始的时候。 执行
    def open_spider(self, spider_name):
        self.f = open("xxx.csv", mode="w", encoding="utf-8")

    # close_spider, 爬虫结束的时候。 执行
    def close_spider(self, spider_name):
        self.f.close()

    # process_item 的作用就是接受spider返回的数据
    # spider每次返回一条数据. 这里都会自动的执行一次process_item
    # 数据以参数的形式传递过来. item
    def process_item(self, item, spider):
        self.f.write(item['qi'])
        self.f.write(",")
        self.f.write("_".join(item['red_ball']))
        self.f.write(",")
        self.f.write(item['blue_ball'])
        self.f.write("\n")
        # self.f.close()  # 这里不能写
        return item  # return在process_item中的逻辑, 是将数据传递给一下管道


# 存MySQL
# 准备表. 创建好表.
# 设置优先级 150
class Pipeline2MySQL(object):
    def open_spider(self, spider_name):
        # 连接mysql
        self.conn = pymysql.connect(
            host="127.0.0.1",
            port=3306,
            database="test",
            user="root",
            password="root"
        )

    def close_spider(self, spider_name):
        self.conn.close()

    def process_item(self, item, spider):
        try:  
            cur = self.conn.cursor()
            sql = f"insert into xxxxx"
            cur.execute(sql)
            self.conn.commit()
        except Exception as e:
            print(e)
            if cur:
                cur.close()
            self.conn.rollback()
        # item['red_ball'] = "1234456"
        return item


# 存MongoDB
# 设置优先级 180
class Pipeline2MongoDB(object):
    def open_spider(self, spider_name):
        self.conn = pymongo.MongoClient(
            host="127.0.0.1",
            port=27017
        )
        self.db = self.conn['python']

    def close_spider(self, spider_name):
        self.conn.close()

    def process_item(self, item, spider):
        print(item)
        self.db.ssq.insert_one(item)
        return item  # 给到下一个管道
from redis import Redis


class TianyaPipeline:

    def open_spider(self, spider):
        self.conn = Redis(host="127.0.0.1", port=6379, password="123456", db=6)

    def close_spider(self, spider):
        if self.conn:  # 好习惯
            self.conn.close()

    def process_item(self, item, spider):
        content = item['content']
        # redis
        if self.conn.sismember("ty:pipeline", content):
            print("已经有了。 不需要重复存储")
        else:
            self.conn.sadd("ty:pipeline", content)
            print("之前没有, 现在有了")
        return item

启用一个Item Pipeline组件

为了启用 Item Pipeline 组件,必须将它的类添加到 settings.py 文件 ITEM_PIPELINES 配置,就像下面这个例子:

ITEM_PIPELINES = {
    'tutorial.pipelines.Pipeline2File': 120,
    'tutorial.pipelines.Pipeline2MySQL': 150,
    'tutorial.pipelines.Pipeline2MongoDB': 180,
}

分配给每个类的整型值,确定了他们运行的顺序,item按数字从低到高的顺序,通过 pipeline,通常将这些数字定义在0-1000范围内。

将 item 写入 MongoDB

  • from_crawler(cls, crawler):如果使用,这类方法被调用创建爬虫管道实例。必须返回管道的一个新实例。crawler提供存取所有Scrapy核心组件配置和信号管理器; 对于pipelines这是一种访问配置和信号管理器 的方式。参数: crawler (Crawler object) – crawler that uses this pipeline

例子中,我们将使用 pymongo 将 Item 写到 MongoDB。MongoDB 的地址和数据库名称在 Scrapy setttings.py 配置文件中;这个例子主要是说明如何使用 from_crawler() 方法

import pymongo

class MongoPipeline(object):

    collection_name = 'scrapy_items'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

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

    def process_item(self, item, spider):
        self.db[self.collection_name].insert(dict(item))
        return item

发送 post 请求

方法 1:scrapy.Request

import scrapy

class MySpider(scrapy.Spider):
    name = 'example_spider'
    
    def start_requests(self):
        url = 'https://example.com/form_submit'
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
        form_data = 'key=value&another_key=another_value'
        
        yield scrapy.Request(url, method='POST', body=form_data, headers=headers,
                             callback=self.parse_response)

    def parse_response(self, response):
        # 处理响应内容
        self.log(response.text)
my_data = {'field1': 'value1', 'field2': 'value2'}
request = scrapy.Request(
    url, method='POST', 
    body=json.dumps(my_data), 
    headers={'Content-Type':'application/json'}
)

方法 2:scrapy.FormRequest

from scrapy.spider import CrawlSpider
from scrapy.selector import Selector
import scrapy
import json
class LaGou(CrawlSpider):
    name = 'myspider'
    def start_requests(self):
        yield scrapy.FormRequest(
          url='https://www.******.com/jobs/positionAjax.json?city=%E5%B9%BF%E5%B7%9E&needAddtionalResult=false',
          formdata={
            'first': 'true',#这里不能给bool类型的True,requests模块中可以
            'pn': '1',#这里不能给int类型的1,requests模块中可以
            'kd': 'python'
          },  # 这里的formdata相当于request模块中的data,key和value只能是键值对形式
          callback=self.parse
        )
    def parse(self, response):
        datas=json.loads(response.body.decode())['content']['positionResult']['result']
        for data in datas:
            print(data['companyFullName'] + str(data['positionId']))
# 发送post请求, 方案一
yield scrapy.Request(
    url=login_url,
    method="POST",  # post请求
    # 要求body的格式是下面这个格式. 这个格式http post请求的请求体
    # name=alex&age=18&xxx=123
    body="loginName=16538989670&password=q6035945",  # 注意,这里要求body是字符串
    callback=self.login_success
)

# 发送post请求, 方案二
yield scrapy.FormRequest(
	url=login_url,
	method="POST",
	formdata={  # 相当于requests.post(url, data={})
		"loginName": "16538989670",
		"password": "q6035945",
	},
	# 当前这个请求的url. 在得到响应之后. 要执行的函数
	callback=self.login_success
)

示例:

import scrapy


class DengSpider(scrapy.Spider):
    name = 'deng'
    allowed_domains = ['17k.com']
    start_urls = ["https://user.17k.com/ck/author/shelf?page=1&appKey=2406394919"]

    # 需要先登录, 登录完成之后. 才开始start_urls
    def start_requests(self):
        # 完成登录的操作
        # scrapy想要发送POST请求:两种方式
        login_url = "https://passport.17k.com/ck/user/login"
        yield scrapy.FormRequest(
            url=login_url,
            method="POST",
            formdata={  # 相当于requests.post(url, data={})
                "loginName": "16538989670",
                "password": "q6035945",
            },
            # 当前这个请求的url. 在得到响应之后. 要执行的函数
            callback=self.login_success
        )

    def login_success(self, resp, **kwargs):
        print(resp.text)
        # 登录成功之后. 需要请求到start_urls里面
        yield scrapy.Request(url=self.start_urls[0], callback=self.parse, dont_filter=True)

    def parse(self, resp, **kwargs):
        print(resp.text)

scrapy.Request 和 scrapy.FormRequest 区别

FormRequest 新增加了一个参数 formdata,接受包含表单数据的字典或者可迭代的元组,并将其转化为请求的body。并且 FormRequest 是继承 Request 的。

类似 requests模块中的request用法

scrapy 模拟登陆

使用 scrapy 框架

使用 url + 用户名 和 密码形式登录

一帧网不登录的时候,“排行榜单”可以查看 30条数据,登录之后可以查看 100条数据。

首先打开 fiddler(fiddler介绍及使用教程:https://blog.csdn.net/freeking101/article/category/6531758),如图:

然后打开一帧的登录页面,输入账号、密码,点击创作者登录,如图:

登录之后在打开 fiddler ,发现 fiddler 已经抓取了从登录到登录成功后的所有http 包,如图:

登录的 URL 找到了,还有发送的 post 数据找到了,下面就是写代码模拟 post 请求登录了。

只要登录后,就可以访问登录后任意一个页面。

现在我需要的数据是“热度榜单”,通过fiddle 抓包 和对比 网页上显示数据,需要的数据如图所示:

把 fiddle 抓取到的 json 数据复制下来,然后随便找一个 “json 在线解析工具”粘贴上去,就可以看到结果。

展开上面的 list 节点,可以看到 有 100条数据,因展开太长,截图没有展开。

模拟登陆 一帧网 示例代码:

#!/usr/bin/python3
# -*- coding: utf-8 -*-


import scrapy
import time
import json


class SlaveSpider(scrapy.Spider):
    name = "master_yizhen"
    start_urls = ['http://www.1zhen.com/api/user/login']
    main_url = 'http://www.1zhen.com/account'
    login_url = start_urls[0]

    login_headers = {
        'Host': 'www.1zhen.com',
        "Connection": "keep-alive",
        'Accept': 'application/json, text/plain, */*',
        'X-Requested-With': 'XMLHttpRequest',
        'Origin': 'http://www.1zhen.com',
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36',
        'Content-Type': 'application/json;charset=UTF-8',
        'Referer': 'http://www.1zhen.com/account',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9'
    }

    form_data = {
        "mobile": "12345678901",  # 抓取 的 登陆的账号(手机号)
        "password": "加密密码",    # 抓取 的 加密的密码
        "role": "author"
    }

    def __init__(self):
        super(SlaveSpider, self).__init__()
        self.__base_url = 'http://www.1zhen.com'

    def start_requests(self):
        '''
        # 如果登录 url 在浏览器中能打开,也可以使用这个方法进行登录
        yield scrapy.Request(
            url=self.login_url,
            headers=self.login_headers,
            meta={'cookiejar': 1},
            callback=self.login,       #  登录函数
        )
        '''
        # 如果登录 url 在浏览器中打开返回 404 ,则只有使用下面。
        # 一帧网(http://www.1zhen.com/)登录页面(http://www.1zhen.com/api/user/login)就属于返回 404这种类型

        yield scrapy.Request(
            url=self.login_url,
            headers=self.login_headers,
            meta={'cookiejar': 1},
            callback=self.after_login,
            method='post',                   # 设置请求方法为 post 请求
            body=json.dumps(self.form_data)  # 设置请求体,即请求参数
        )  # 通过上面 fiddle 抓取的 请求登录 的 URL ,可以看到 请求登录的URL使用的是 post 方法

    def login(self, response):  # 这个函数使用与 当 登录页面可以访问时的情况
        print(response.url)
        print(response.text)
        form_data = {
            "mobile": "12345678901",  # 抓取 的 登录账号
            "password": "加密的密码",  # 抓取 的 加密密码
            "role": "author"
        }
        yield scrapy.FormRequest.from_response(
            response,
            formdata=form_data,
            headers=self.login_headers,
            meta={'cookiejar': response.meta['cookiejar']},
            callback=self.after_login,
        )

    def after_login(self, response):
        print(response.url)
        t = time.localtime(time.time())
        week_time = '{0}-{1}-{2}'.format(t.tm_year, t.tm_mon, t.tm_mday)
        page_url = '{0}/api/rank/author?during=week&pt_week={1}&platform=all&category=1'.format(
            self.__base_url,
            week_time
        ) # 构造请求的 URL
        yield scrapy.Request(
            url=page_url,
            headers=self.login_headers,
            meta={'cookiejar': response.meta['cookiejar']},
            callback=self.parse_data
        )  # 通过上面 fiddle 抓包,可以看到请求的 URL 使用的 get 方法
        pass

    def parse_data(self, response):
        data = json.dumps(response.text, ensure_ascii=False, indent=4)
        print(data)
        pass

运行结果截图:

网上找的一个使用 scrapy 模拟登录的示例代码:

#!/usr/bin/python3
# -*- coding: utf-8 -*-


import scrapy
from scrapy import FormRequest, Request


class ExampleLoginSpider(scrapy.Spider):
    name = "login_"
    allowed_domains = ["example.webscraping.com"]
    start_urls = ['http://example.webscraping.com/user/profile']
    login_url = 'http://example.webscraping.com/places/default/user/login'

    def parse(self, response):
        print(response.text)

    def start_requests(self):
        yield scrapy.Request(self.login_url, callback=self.login)

    def login(self, response):
        form_data = {
            'email': 'liushuo@webscraping.com',
            'password': '12345678'
        }
        yield FormRequest.from_response(response, formdata=form_data, callback=self.parse_login)

    def parse_login(self, response):
        # print('>>>>>>>>'+response.text)
        if 'Welcome Liu' in response.text:
            yield from super().start_requests()

使用 requests 模块登录

使用 url + 用户名 和 密码形式登录

现在使用 python 的 requests 这个牛逼的模块进行模拟登录,并把数据存到本地 redis  里面

代码如下:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author      : 
# @File        : general.py
# @Software    : PyCharm
# @description : 

import requests
import json
import redis
import hashlib
import time


class OneZhen(object):
    def __init__(self):
        self.__custom_headers = {
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
            "Accept-Encoding": "gzip, deflate",
            "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6",
            "Cache-Control": "max-age=0",
            "Connection": "keep-alive",
            "Content-Type": "application/x-www-form-urlencoded",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
            "Referer": "http://www.1zhen.com/account",
            "Host": "www.1zhen.com",
        }
        self.__post_data = {
            "mobile": "12345678901",   # 登录的账号
            "password": "加密的密码",   # 加密的密码
            "role": "author",
        }
        self.__login_url = 'http://www.1zhen.com/api/user/login'
        self.__base_url = 'http://www.1zhen.com'
        self.data = None
        self.__session = requests.session()    # 定义 session 会话(session可以自动管理cookies, scrapy貌似需要通过meta传递cookies)
        self.__session.headers = self.__custom_headers  # 设置请求头

    def login_onezhen(self, week_time):
        r = self.__session.post(self.__login_url, self.__post_data)
        if r.status_code == 200:
            # print(r.content)
            page_url = '{0}/api/rank/author?during=week&pt_week={1}&platform=all&category=1'.format(self.__base_url, week_time)
            page_content = self.__session.get(url=page_url)
            json_data = page_content.content.decode('utf-8')
            self.data = json.loads(json_data)
        else:
            print('login fail and status_code is {0}'.format(r.status_code))
        return self.data

    def get_data(self, week_time):
        return self.login_onezhen(week_time)


redis_host = '127.0.0.1'
redis_port = 6379
r_db = redis.Redis(host=redis_host, port=redis_port, db=0)


def write_redis(key, data_dict):
    r_db.hmset(key, data_dict)
    pass


def main():
    # current_time = '2018-06-19'
    t = time.localtime(time.time())
    current_time = '{0}-{1}-{2}'.format(t.tm_year, t.tm_mon, t.tm_mday)
    onezhen = OneZhen()
    data = onezhen.get_data(current_time)
    print('from yizhen get data success and write redis...')
    for d in data['data']['list']:
        # key = md5(d['author']['name'])
        user_name = d['author']['name']
        user_info = dict(
            name=user_name,
            head_img_url_yizhen=d['author']['avatar'],
            category=d['author']['category']
        )
        write_redis(d['author']['name'], user_info)
    print('write  redis success and exit')


def md5(src):
    m = hashlib.md5()
    m.update(src.encode('UTF-8'))
    return m.hexdigest()


if __name__ == "__main__":
    main()
    pass


通过 Redis Desktop Manager 连接到本地 Redis ,可以看到本地 Redis 里面的数据。

使用 Cookies 模拟登录

使用Cookie登录的好处:不需要知道登录url和表单字段以及其他参数,不需要了解登录的过程和细节。由于不是采用登录url, 用户名+密码的方式。配合工具使用,快速方便。

所谓用Cookie实现登录,就把过登录过的信息(包括用户名、密码以及其他的验证信息)打包一起发给服务器,告诉服务器我是登录验证过的。

不足之处,Cookie有过期时间,过一段时间再运行这个爬虫,需要重新获取一下Cookie的值。抓取数据过程是没有问题的。

关于Cookie的介绍:

  1. Cookie分类
    Cookie总是保存在用户客户端中,按在客户端中的存储位置,可分为内存Cookie和硬盘Cookie。Cookie的有效性,最短的浏览器关闭后就消失了,最长是可以一直保存,直到被删除。

  2. Cookie用途
    因为HTTP协议是无状态的,即服务器不知道用户上一次做了什么,这严重阻碍了交互式Web应用程序的实现。
    在典型的应用是网上购物场景中,用户浏览了几个页面,买了一盒饼干和两饮料。最后结帐时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么。
    所以Cookie就是用来绕开HTTP的无状态性的“额外手段”之一。服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器中的状态。

  3. Cookie的缺陷
    1)Cookie会被附加在每个HTTP请求中,所以无形中增加了流量。

  1. 由于在HTTP请求中的Cookie是明文传递的,所以安全性成问题。(除非用HTTPS)
  2. Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。

4、Spider、CrawlSpider

scrapy 提供了5种 spider 用于构造请求,解析数据、返回 item。

  • scrapy.Spider  常用
  • scrapy.CrawlSpider  常用
  • scrapy.XMLFeedSpider
  • scrapy.CSVFeedSpider
  • scrapy.SitemapSpider

class scrapy.spider.Spider

Spider类 定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接) 以及如何从网页的内容中提取结构化数据(爬取item)。 换句话说,Spider 就是定义爬取的动作及分析某个网页(或者是有些网页)的地方。

Spider 是最简单的 spider。每个 spider 必须继承自该类。Spider 并没有提供什么特殊的功能。其仅仅请求给定的 start_urls / start_requests,并根据返回的结果调用 spider 的 parse 方法。

  • name:定义 spider 名字的字符串。例如,如果spider爬取 mywebsite.com ,该 spider 通常会被命名为 mywebsite
  • allowed_domains:可选。包含了spider允许爬取的域名(domain)列表(list)
  • start_urls:初始 URL 列表。当没有制定特定的 URL 时,spider 将从该列表中开始进行爬取。
  • start_requests():当 spider 启动爬取并且未指定 start_urls 时,该方法被调用。如果您想要修改最初爬取某个网站。
  • parse(self, response):当请求 url 返回网页没有指定回调函数时,默认下载回调方法。参数:response (Response) – 返回网页信息的 response
  • log(message[, level, component]):使用 scrapy.log.msg() 方法记录(log)message。 更多数据请参见 Logging

下面是 spider 常用到的 属性 和 方法。( 想要了解更多,可以查看源码 )

属性、方法功能简述
name爬虫的名称启动爬虫的时候会用到
start_urls起始 url是一个列表,默认被 start_requests 调用
allowd_doamins对 url 进行的简单过滤

当请求 url 没有被 allowd_doamins 匹配到时,会报一个非常恶心的错,

start_requests()第一次请求自己的 spider 可以重写,突破一些简易的反爬机制
custom_settings定制 settings可以对每个爬虫定制 settings 配置
from_crawler实例化入口在 scrapy 的各个组件的源码中,首先执行的就是它

关于 spider 我们可以定制 start_requests、可以单独的设置 custom_settings、也可以设置请

例如,如果您需要在启动时以 POST 登录某个网站,你可以这么写:

class MySpider(scrapy.Spider):
    name = 'myspider'

    def start_requests(self):
        return [scrapy.FormRequest("http://www.example.com/login",
                                   formdata={'user': 'john', 'pass': 'secret'},
                                   callback=self.logged_in)]

    def logged_in(self, response):
        # here you would extract links to follow and return Requests for
        # each of them, with another callback
        pass

Spider 示例

让我们来看一个例子:

import scrapy

class MySpider(scrapy.Spider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = [
        'http://www.example.com/1.html',
        'http://www.example.com/2.html',
        'http://www.example.com/3.html',
    ]

    def parse(self, response):
        self.log('A response from %s just arrived!' % response.url)

另一个在单个回调函数中返回多个 Request 以及 Item 的例子:

import scrapy
from myproject.items import MyItem

class MySpider(scrapy.Spider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = [
        'http://www.example.com/1.html',
        'http://www.example.com/2.html',
        'http://www.example.com/3.html',
    ]

    def parse(self, response):
        sel = scrapy.Selector(response)
        for h3 in response.xpath('//h3').extract():
            yield MyItem(title=h3)

        for url in response.xpath('//a/@href').extract():
            yield scrapy.Request(url, callback=self.parse)

案例:腾讯招聘网翻页功能

import scrapy
from tutorial.items import RecruitItem
import re
class RecruitSpider(scrapy.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]

    def parse(self, response):
        for sel in response.xpath('//*[@class="even"]'):
            name = sel.xpath('./td[1]/a/text()').extract()[0]
            detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
            catalog =None
            if sel.xpath('./td[2]/text()'):
                catalog = sel.xpath('./td[2]/text()').extract()[0]

            recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
            workLocation = sel.xpath('./td[4]/text()').extract()[0]
            publishTime = sel.xpath('./td[5]/text()').extract()[0]
            #print name, detailLink, catalog,recruitNumber,workLocation,publishTime
            item = RecruitItem()
            item['name']=name.encode('utf-8')
            item['detailLink']=detailLink.encode('utf-8')
            if catalog:
                item['catalog']=catalog.encode('utf-8')
            item['recruitNumber']=recruitNumber.encode('utf-8')
            item['workLocation']=workLocation.encode('utf-8')
            item['publishTime']=publishTime.encode('utf-8')
            yield item

        nextFlag = response.xpath('//*[@id="next"]/@href')[0].extract()
        if 'start' in nextFlag:
            curpage = re.search('(\d+)',response.url).group(1)
            page =int(curpage)+10
            url = re.sub('\d+',str(page),response.url)
            print url
            yield scrapy.Request(url, callback=self.parse)

执行:scrapy crawl tencent -L INFO

scrapy.spiders.CrawlSpider

CrawlSpider 定义了一些规则(rule)来提供跟进 link 的方便的机制。除了从 Spider 继承过来的(您必须提供的)属性外(name、allow_domains),其提供了一个新的属性:

  • rules:包含一个(或多个) 规则对象的集合(list)。 每个Rule对爬取网站的动作定义了特定操作。 如果多个 rule 匹配了相同的链接,则根据规则在本集合中被定义的顺序,第一个会被使用。
  • parse_start_url(response):当 start_url 的请求返回时,该方法被调用

爬取规则(Crawling rules)

class scrapy.contrib.spiders.Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None)

  • link_extractor:其定义了如何从爬取到的页面中提取链接。
  • callback:指定 spider 中哪个函数将会被调用。 从 link_extractor 中每获取到链接时将会调用该函数。该回调函数接受一个response 作为其第一个参数。注意:当编写爬虫规则时,请避免使用 parse作为回调函数。由于 CrawlSpider 使用 parse 方法来实现其逻辑,如果您覆盖了 parse方法,crawl spider将会运行失败。
  • cb_kwargs:包含传递给回调函数的参数 (keyword argument) 的字典。
  • follow:是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进。 如果callback为None,follow默认设置为True ,否则默认为False。
  • process_links:指定该spider中哪个的函数将会被调用,从link_extractor中获取到链接列表时将会调用该函数。该方法常用于过滤参数
  • process_request:指定该spider中哪个的函数将会被调用,该规则提取到每个request时都会调用该函数 (用来过滤request)

CrawlSpider 案例

还是以腾讯招聘为例,给出配合 rule 使用 CrawlSpider 的例子:

首先运行

scrapy shell "http://hr.tencent.com/position.php?&start=0#a"

导入匹配规则:

from scrapy.linkextractors import LinkExtractor
page_lx = LinkExtractor(allow=('position.php?&start=\d+'))

查询匹配结果:

page_lx.extract_links(response)

没有查到:

page_lx = LinkExtractor(allow=(r'position\.php\?&start=\d+'))
page_lx.extract_links(response)

[Link(url='http://hr.tencent.com/position.php?start=10', text='2', fragment='', nofollow=False),
Link(url='http://hr.tencent.com/position.php?start=20', text='3', fragment='', nofollow=False),
Link(url='http://hr.tencent.com/position.php?start=30', text='4', fragment='', nofollow=False),
Link(url='http://hr.tencent.com/position.php?start=40', text='5', fragment='', nofollow=False),
Link(url='http://hr.tencent.com/position.php?start=50', text='6', fragment='', nofollow=False),
Link(url='http://hr.tencent.com/position.php?start=60', text='7', fragment='', nofollow=False),
Link(url='http://hr.tencent.com/position.php?start=70', text='...', fragment='', nofollow=False),
Link(url='http://hr.tencent.com/position.php?start=1300', text='131', fragment='', nofollow=False)]
len(page_lx.extract_links(response))

那么,scrapy shell 测试完成之后,修改以下代码

#提取匹配 'http://hr.tencent.com/position.php?&start=\d+'的链接
page_lx = LinkExtractor(allow=('start=\d+'))

rules = [
#提取匹配,并使用spider的parse方法进行分析;并跟进链接(没有callback意味着follow默认为True)
Rule(page_lx, callback='parse',follow=True)
]

这么写对吗? callback 千万不能写 parse,一定运行有错误!!

保存代码为 tencent_crawl.py

# -*- coding:utf-8 -*-
import scrapy
from tutorial.items import RecruitItem
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class RecruitSpider(CrawlSpider):
    name = "tencent_crawl"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]
    #提取匹配 'http://hr.tencent.com/position.php?&start=\d+'的链接
    page_lx = LinkExtractor(allow=('start=\d+'))

    rules = [
        #提取匹配,并使用spider的parse方法进行分析;并跟进链接(没有callback意味着follow默认为True)
        Rule(page_lx, callback='parseContent',follow=True)
    ]

    def parseContent(self, response):
        print response.url
        for sel in response.xpath('//*[@class="even"]'):
            name = sel.xpath('./td[1]/a/text()').extract()[0]
            detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
            catalog =None
            if sel.xpath('./td[2]/text()'):
                catalog = sel.xpath('./td[2]/text()').extract()[0]

            recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
            workLocation = sel.xpath('./td[4]/text()').extract()[0]
            publishTime = sel.xpath('./td[5]/text()').extract()[0]
            #print name, detailLink, catalog,recruitNumber,workLocation,publishTime
            item = RecruitItem()
            item['name']=name.encode('utf-8')
            item['detailLink']=detailLink.encode('utf-8')
            if catalog:
                item['catalog']=catalog.encode('utf-8')
            item['recruitNumber']=recruitNumber.encode('utf-8')
            item['workLocation']=workLocation.encode('utf-8')

            item['publishTime']=publishTime.encode('utf-8')
            yield item

可以修改配置文件settings.py,添加 LOG_LEVEL='INFO'

运行: scrapy crawl tencent_crawl

process_links 参数:动态网页爬取,动态 url 的处理

在爬取 https://bitsharestalk.org 的时候,发现网站会为每一个 url 增加一个 sessionid 属性,可能是为了标记用户访问历史,而且这个 seesionid 随着每次访问都会动态变化,这就为爬虫的去重处理(即标记已经爬取过的网站)和提取规则增加了难度。

比如:General Discussion 会变成 https://bitsharestalk.org/index.phpPHPSESSID=9771d42640ab3c89eb77e8bd9e220b53&board=5.0

下面介绍几种处理方法

仅适用你的爬虫使用的是 scrapy.contrib.spiders.CrawlSpider,在这个内置爬虫中,你提取 url 要通过 Rule类来进行提取,其自带了对提取后的 url 进行加工的函数。

    rules = (
        Rule(
            LinkExtractor(
                allow=(
                    r"https://bitsharestalk\.org/index\.php\?PHPSESSID\S*board=\d+\.\d+$",
                    r"https://bitsharestalk\.org/index\.php\?board=\d+\.\d+$"
                )
            ),
            process_links='link_filtering'  # 默认函数process_links
        ),  

        Rule(
            LinkExtractor(
                allow=(
                    r" https://bitsharestalk\.org/index\.php\?PHPSESSID\S*topic=\d+\.\d+$",
                    r"https://bitsharestalk\.org/index\.php\?topic=\d+\.\d+$",
                ), 
            ),
            callback="extractPost",
            follow=True, process_links='link_filtering'
        ),

        Rule(
            LinkExtractor(
                allow=(
                    r"https://bitsharestalk\.org/index\.php\?PHPSESSID\S*action=profile;u=\d+$",
                    r"https://bitsharestalk\.org/index\.php\?action=profile;u=\d+$",
                ), 
            ),
            callback="extractUser", 
            process_links='link_filtering'
        )
    )

    def link_filtering(self, links):
        ret = []
        for link in links:
            url = link.url

        # print "This is the yuanlai ", link.url
        urlfirst, urllast = url.split(" ? ")

        if urllast:
            link.url = urlfirst + " ? " + urllast.split(" & ", 1)[1]

        # print link.url
        return links

link_filtering() 函数对 url 进行了处理,过滤掉了 sessid,关于 Rule类的 process_links 函数和 links 类,官方文档中并没有给出介绍,给出一个参考 https://groups.google.com/forum/#!topic/scrapy-users/RHGtm_2GO1M(也许需要梯子,你懂得)

如果你是自己实现的爬虫,那么 url 的处理更是可定制的,只需要自己处理一下就可以了。

process_request 参数:修改请求参数

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


class WeiboSpider(CrawlSpider):
    name = 'weibo'
    allowed_domains = ['weibo.com']

    # 不加www,则匹配不到 cookie, get_login_cookie()方法正则代完善
    start_urls = ['http://www.weibo.com/u/1876296184']
    rules = (
        Rule(
            # 微博个人页面的规则,或/u/或/n/后面跟一串数字
            LinkExtractor(allow=r'^http:\/\/(www\.)?weibo.com/[a-z]/.*'),
            process_request='process_request',
            callback='parse_item', follow=True
        ),
    )
    cookies = None

    def process_request(self, request):
        link = request.url
        page = re.search(r'page=\d*', link).group()
        tp = re.search(r'type=\d+', link).group()
        new_request = request.replace(
            cookies=self.cookies, 
            url='.../questionType?' + page + "&" + tp
        )
        return new_request

5、Logging

Scrapy 提供了 log 功能。您可以通过 logging 模块使用。

Log levels

Scrapy 提供5层 logging 级别:

  1. CRITICAL     ---  严重错误(critical)
  2. ERROR        ---  一般错误(regular errors)
  3. WARNING    ---  警告信息(warning messages)
  4. INFO             ---  一般信息(informational messages)
  5. DEBUG         ---  调试信息(debugging messages)

默认情况下 python 的 logging 模块将日志打印到了标准输出中,且只显示了大于等于 WARNING 级别的日志,这说明默认的日志级别设置为 WARNING(日志级别等级CRITICAL > ERROR > WARNING > INFO > DEBUG,默认的日志格式为DEBUG级别

如何设置 log 级别

您可以通过终端选项(command line option) --loglevel/-L 或 LOG_LEVEL 来设置log级别。

  • scrapy crawl tencent_crawl -L INFO

  • 可以修改配置文件 settings.py,添加  LOG_LEVEL='INFO'

scrapy crawl tencent_crawl -L INFO
也可以修改配置文件settings.py,添加 LOG_LEVEL='INFO'

在 Spider 中添加 log

Scrapy 为每个 Spider 实例记录器提供了一个 logger,可以这样访问:

import scrapy

class MySpider(scrapy.Spider):

    name = 'myspider'
    start_urls = ['http://scrapinghub.com']

    def parse(self, response):
        self.logger.info('Parse function called on %s', response.url)

logger 是用 Spider 的名称创建的,但是你可以用你想要的任何自定义 logging。例如:

import logging
import scrapy

logger = logging.getLogger('zhangsan')

class MySpider(scrapy.Spider):

    name = 'myspider'
    start_urls = ['http://scrapinghub.com']

    def parse(self, response):
        logger.info('Parse function called on %s', response.url)

Logging 设置

以下设置可以被用来配置logging:
LOG_ENABLED        默认: True,启用logging
LOG_ENCODING       默认: 'utf-8',logging使用的编码
LOG_FILE           默认: None,logging输出的文件名
LOG_LEVEL          默认: 'DEBUG',log的最低级别
LOG_STDOUT         默认: False。如果为 True,进程所有的标准输出(及错误)将会被重定向到log中。
                   例如,执行 print 'hello' ,其将会在Scrapy log中显示。

案例 (一) ( self.logger )

tencent_crawl.py 添加日志信息如下:

    '''
    添加日志信息
    '''
    print 'print',response.url

    self.logger.info('info on %s', response.url)
    self.logger.warning('WARNING on %s', response.url)
    self.logger.debug('info on %s', response.url)
    self.logger.error('info on %s', response.url)

完整版如下:

# -*- coding:utf-8 -*-
import scrapy
from tutorial.items import RecruitItem
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class RecruitSpider(CrawlSpider):
    name = "tencent_crawl"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]


    #提取匹配 'http://hr.tencent.com/position.php?&start=\d+'的链接
    page_lx = LinkExtractor(allow=('start=\d+'))

    rules = [
        #提取匹配,并使用spider的parse方法进行分析;并跟进链接(没有callback意味着follow默认为True)
        Rule(page_lx, callback='parseContent',follow=True)
    ]

    def parseContent(self, response):

        #print("print settings: %s" % self.settings['LOG_FILE'])
        '''
        添加日志信息
        '''
        print 'print',response.url

        self.logger.info('info on %s', response.url)
        self.logger.warning('WARNING on %s', response.url)
        self.logger.debug('info on %s', response.url)
        self.logger.error('info on %s', response.url)


        for sel in response.xpath('//*[@class="even"]'):
            name = sel.xpath('./td[1]/a/text()').extract()[0]
            detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
            catalog =None
            if sel.xpath('./td[2]/text()'):
                catalog = sel.xpath('./td[2]/text()').extract()[0]

            recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
            workLocation = sel.xpath('./td[4]/text()').extract()[0]
            publishTime = sel.xpath('./td[5]/text()').extract()[0]
            #print name, detailLink, catalog,recruitNumber,workLocation,publishTime
            item = RecruitItem()
            item['name']=name
            item['detailLink']=detailLink
            if catalog:
                item['catalog']=catalog
            item['recruitNumber']=recruitNumber
            item['workLocation']=workLocation

            item['publishTime']=publishTime
            yield item

在 settings 文件中,修改添加信息

LOG_FILE='ten.log'
LOG_LEVEL='INFO'

接下来执行:scrapy crawl tencent_crawl。或者 command line 命令行执行:

scrapy crawl tencent_crawl --logfile 'ten.log' -L INFO

输出如下

print http://hr.tencent.com/position.php?start=10
print http://hr.tencent.com/position.php?start=1340
print http://hr.tencent.com/position.php?start=0
print http://hr.tencent.com/position.php?start=1320
print http://hr.tencent.com/position.php?start=1310
print http://hr.tencent.com/position.php?start=1300
print http://hr.tencent.com/position.php?start=1290
print http://hr.tencent.com/position.php?start=1260

ten.log 文件中记录,可以看到级别大于 INFO 日志输出

2016-08-15 23:10:57 [tencent_crawl] INFO: info on http://hr.tencent.com/position.php?start=70
2016-08-15 23:10:57 [tencent_crawl] WARNING: WARNING on http://hr.tencent.com/position.php?start=70
2016-08-15 23:10:57 [tencent_crawl] ERROR: info on http://hr.tencent.com/position.php?start=70
2016-08-15 23:10:57 [tencent_crawl] INFO: info on http://hr.tencent.com/position.php?start=1320
2016-08-15 23:10:57 [tencent_crawl] WARNING: WARNING on http://hr.tencent.com/position.php?start=1320
2016-08-15 23:10:57 [tencent_crawl] ERROR: info on http://hr.tencent.com/position.php?start=1320

案例(二)( logging.getLogger )

tencent_spider.py 添加日志信息如下:logger = logging.getLogger('zhangsan')

    '''
    添加日志信息
    '''
    print 'print',response.url

    self.logger.info('info on %s', response.url)
    self.logger.warning('WARNING on %s', response.url)
    self.logger.debug('info on %s', response.url)
    self.logger.error('info on %s', response.url)

完整版如下:

import scrapy
from tutorial.items import RecruitItem
import re
import logging

logger = logging.getLogger('zhangsan')

class RecruitSpider(scrapy.spiders.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]

    def parse(self, response):
        #logger.info('spider tencent Parse function called on %s', response.url)
        '''
        添加日志信息
        '''
        print 'print',response.url

        logger.info('info on %s', response.url)
        logger.warning('WARNING on %s', response.url)
        logger.debug('info on %s', response.url)
        logger.error('info on %s', response.url)

        for sel in response.xpath('//*[@class="even"]'):
            name = sel.xpath('./td[1]/a/text()').extract()[0]
            detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
            catalog =None
            if sel.xpath('./td[2]/text()'):
                catalog = sel.xpath('./td[2]/text()').extract()[0]

            recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
            workLocation = sel.xpath('./td[4]/text()').extract()[0]
            publishTime = sel.xpath('./td[5]/text()').extract()[0]
            #print name, detailLink, catalog,recruitNumber,workLocation,publishTime
            item = RecruitItem()
            item['name']=name
            item['detailLink']=detailLink
            if catalog:
                item['catalog']=catalog
            item['recruitNumber']=recruitNumber
            item['workLocation']=workLocation
            item['publishTime']=publishTime
            yield item

        nextFlag = response.xpath('//*[@id="next"]/@href')[0].extract()
        if 'start' in nextFlag:
            curpage = re.search('(\d+)',response.url).group(1)
            page =int(curpage)+10
            url = re.sub('\d+',str(page),response.url)
            print url
            yield scrapy.Request(url, callback=self.parse)

在 settings 文件中,修改添加信息

LOG_FILE='tencent.log'
LOG_LEVEL='WARNING'

接下来执行:scrapy crawl tencent 。或者command line命令行执行:

scrapy crawl tencent --logfile 'tencent.log' -L WARNING

输出信息

print http://hr.tencent.com/position.php?&start=0
http://hr.tencent.com/position.php?&start=10
print http://hr.tencent.com/position.php?&start=10
http://hr.tencent.com/position.php?&start=20
print http://hr.tencent.com/position.php?&start=20
http://hr.tencent.com/position.php?&start=30

tencent.log 文件中记录,可以看到级别大于 INFO 日志输出

2016-08-15 23:22:59 [zhangsan] WARNING: WARNING on http://hr.tencent.com/position.php?&start=0
2016-08-15 23:22:59 [zhangsan] ERROR: info on http://hr.tencent.com/position.php?&start=0
2016-08-15 23:22:59 [zhangsan] WARNING: WARNING on http://hr.tencent.com/position.php?&start=10
2016-08-15 23:22:59 [zhangsan] ERROR: info on http://hr.tencent.com/position.php?&start=10

小试 LOG_STDOUT

settings.py

LOG_FILE='tencent.log'
LOG_STDOUT=True
LOG_LEVEL='INFO'

执行:scrapy crawl tencent

输出:空

tencent.log 文件中记录,可以看到级别大于 INFO 日志输出

2016-08-15 23:28:32 [stdout] INFO: http://hr.tencent.com/position.php?&start=110
2016-08-15 23:28:32 [stdout] INFO: print
2016-08-15 23:28:32 [stdout] INFO: http://hr.tencent.com/position.php?&start=110
2016-08-15 23:28:32 [zhangsan] INFO: info on http://hr.tencent.com/position.php?&start=110
2016-08-15 23:28:32 [zhangsan] WARNING: WARNING on http://hr.tencent.com/position.php?&start=110
2016-08-15 23:28:32 [zhangsan] ERROR: info on http://hr.tencent.com/position.php?&start=110
2016-08-15 23:28:32 [stdout] INFO: http://hr.tencent.com/position.php?&start=120
2016-08-15 23:28:33 [stdout] INFO: print
2016-08-15 23:28:33 [stdout] INFO: http://hr.tencent.com/position.php?&start=120
2016-08-15 23:28:33 [zhangsan] INFO: info on http://hr.tencent.com/position.php?&start=120
2016-08-15 23:28:33 [zhangsan] WARNING: WARNING on http://hr.tencent.com/position.php?&start=120
2016-08-15 23:28:33 [zhangsan] ERROR: info on http://hr.tencent.com/position.php?&start=120

scrapy 之 Logging 使用

#coding:utf-8
######################
##Logging的使用
######################
import logging
'''
1. logging.CRITICAL - for critical errors (highest severity) 致命错误
2. logging.ERROR - for regular errors 一般错误
3. logging.WARNING - for warning messages 警告+错误
4. logging.INFO - for informational messages 消息+警告+错误
5. logging.DEBUG - for debugging messages (lowest severity) 低级别
'''
logging.warning("This is a warning")

logging.log(logging.WARNING,"This is a warning")

#获取实例对象
logger=logging.getLogger()
logger.warning("这是警告消息")
#指定消息发出者
logger = logging.getLogger('SimilarFace')
logger.warning("This is a warning")

#在爬虫中使用log
import scrapy
class MySpider(scrapy.Spider):
    name = 'myspider'
    start_urls = ['http://scrapinghub.com']
    def parse(self, response):
        #方法1 自带的logger
        self.logger.info('Parse function called on %s', response.url)
        #方法2 自己定义个logger
        logger.info('Parse function called on %s', response.url)

'''
Logging 设置
• LOG_FILE
• LOG_ENABLED
• LOG_ENCODING
• LOG_LEVEL
• LOG_FORMAT
• LOG_DATEFORMAT • LOG_STDOUT

命令行中使用
--logfile FILE
Overrides LOG_FILE

--loglevel/-L LEVEL
Overrides LOG_LEVEL

--nolog
Sets LOG_ENABLED to False
'''

import logging
from scrapy.utils.log import configure_logging

configure_logging(install_root_handler=False)
#定义了logging的些属性
logging.basicConfig(
    filename='log.txt',
    format='%(levelname)s: %(levelname)s: %(message)s',
    level=logging.INFO
)
#运行时追加模式
logging.info('进入Log文件')
logger = logging.getLogger('SimilarFace')
logger.warning("也要进入Log文件")

6、Settings

Scrapy 设置(settings)提供了定制 Scrapy 组件的方法。可以控制包括核心(core),插件(extension),pipeline 及 spider 组件。比如 设置 Json Pipeliine、LOG_LEVEL

内置设置列表请参考内置设置参考手册

获取设置值 (Populating the settings)

设置可以通过多种方式设置,每个方式具有不同的优先级。

下面以 优先级降序 的方式给出方式列表:

  • 命令行选项(Command line Options)(最高优先级) 。命令行传入的参数具有最高的优先级。 使用选项 -s (或 --set) 来覆盖一个 (或更多) 选项。比如:scrapy crawl myspider -s LOG_FILE=scrapy.log
  • 每个 spider 的设置 ( scrapy.spiders.Spider.custom_settings )。
    class MySpider(scrapy.Spider):
      name = 'myspider'
    
      custom_settings = {
          'SOME_SETTING': 'some value',
      }
  • 项目设置模块 (Project settings module)。项目设置模块是 Scrapy 项目的标准配置文件。即 setting.py
    myproject.settings

如何访问配置 (settings)

In a spider, the settings are available through self.settings:

class MySpider(scrapy.Spider):
    name = 'myspider'
    start_urls = ['http://example.com']

    def parse(self, response):
        print("Existing settings: %s" % self.settings.attributes.keys())

Settings can be accessed through the scrapy.crawler.Crawler.settings attribute of the Crawler that is passed to from_crawler method in extensions, middlewares and item pipelines:

class MyExtension(object):
    def __init__(self, log_is_enabled=False):
        if log_is_enabled:
            print("log is enabled!")

    @classmethod
    def from_crawler(cls, crawler):
        settings = crawler.settings
        return cls(settings.getbool('LOG_ENABLED'))

案例 ( self.settings 使用 )

添加一行代码 print("Existing settings: %s" % self.settings['LOG_FILE'])

# -*- coding:utf-8 -*-
import scrapy
from tutorial.items import RecruitItem
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
import logging


class RecruitSpider(CrawlSpider):
    name = "tencent_crawl"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]
    #提取匹配 'http://hr.tencent.com/position.php?&start=\d+'的链接
    page_lx = LinkExtractor(allow=('start=\d+'))

    rules = [
        #提取匹配,并使用spider的parse方法进行分析;并跟进链接(没有callback意味着follow默认为True)
        Rule(page_lx, callback='parseContent',follow=True)
    ]

    def parseContent(self, response):
        print response.url
        print("Existing settings: %s" % self.settings['LOG_FILE'])


        self.logger.info('Parse function called on %s', response.url)
        for sel in response.xpath('//*[@class="even"]'):
            name = sel.xpath('./td[1]/a/text()').extract()[0]
            detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
            catalog =None
            if sel.xpath('./td[2]/text()'):
                catalog = sel.xpath('./td[2]/text()').extract()[0]

            recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
            workLocation = sel.xpath('./td[4]/text()').extract()[0]
            publishTime = sel.xpath('./td[5]/text()').extract()[0]
            #print name, detailLink, catalog,recruitNumber,workLocation,publishTime
            item = RecruitItem()
            item['name']=name.encode('utf-8')
            item['detailLink']=detailLink.encode('utf-8')
            if catalog:
                item['catalog']=catalog.encode('utf-8')
            item['recruitNumber']=recruitNumber.encode('utf-8')
            item['workLocation']=workLocation.encode('utf-8')

            item['publishTime']=publishTime.encode('utf-8')
            yield item

内置设置参考手册

BOT_NAME:默认: 'scrapybot'。当您使用 startproject 命令创建项目时其也被自动赋值。
CONCURRENT_ITEMS:默认: 100。Item Processor(即 Item Pipeline) 同时处理(每个response的)item的最大值。
CONCURRENT_REQUESTS:默认: 16。Scrapy downloader 并发请求(concurrent requests)的最大值。
DEFAULT_REQUEST_HEADERS 默认:
{
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
}
Scrapy HTTP Request使用的默认header。
DEPTH_LIMIT:默认: 0。爬取网站最大允许的深度(depth)值。如果为0,则没有限制。
DOWNLOAD_DELAY:默认: 0。下载器在下载同一个网站下一个页面前需要等待的时间。该选项可以用来限制爬取速度, 减轻服务器压力。同时也支持小数:
DOWNLOAD_DELAY = 0.25 # 250 ms of delay:
该设置影响(默认启用的) RANDOMIZE_DOWNLOAD_DELAY 设置。 默认情况下,Scrapy在两个请求间不等待一个固定的值, 而是使用0.5到1.5之间的一个随机值 * DOWNLOAD_DELAY 的结果作为等待间隔。
DOWNLOAD_TIMEOUT:默认: 180。下载器超时时间(单位: 秒)。
ITEM_PIPELINES:默认: {}。保存项目中启用的pipeline及其顺序的字典。该字典默认为空,值(value)任意。 不过值(value)习惯设置在0-1000范围内。
样例:
ITEM_PIPELINES = {
'mybot.pipelines.validate.ValidateMyItem': 300,
'mybot.pipelines.validate.StoreMyItem': 800,
}
LOG_ENABLED:默认: True。是否启用logging。
LOG_ENCODING:默认: 'utf-8'。logging使用的编码。
LOG_LEVEL:默认: 'DEBUG'。log的最低级别。可选的级别有: CRITICAL、 ERROR、WARNING、INFO、DEBUG 。
USER_AGENT:默认: "Scrapy/VERSION (+http://scrapy.org)"。爬取的默认User-Agent,除非被覆盖。

阳光热线问政平台( 东莞 )

目标网址:http://wz.sun0769.com/political/index/politicsNewest?id=1&type=4

items.py:添加以下代码

from scrapy.item import Item, Field

class SunItem(Item):
    number = Field()
    url = Field()
    title = Field()
    content = Field()

在 spiders 目录下新建一个自定义 SunSpider.py

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
# from tutorial.items import SunItem
import scrapy
import urllib
import time
import re


class SunSpider(CrawlSpider):
    name = 'sun0769'
    num = 0
    allow_domain = ['http://wz.sun0769.com/']
    start_urls = [
        'http://wz.sun0769.com/political/index/politicsNewest?id=1&type=4'
    ]

    rules = {
        Rule(LinkExtractor(allow='page'), process_links='process_request', follow=True),
        Rule(LinkExtractor(allow=r'/html/question/\d+/\d+\.shtml$'), callback='parse_content')
    }

    def process_request(self, links):
        ret = []

        for link in links:
            try:
                page = re.search(r'page=\d*', link.url).group()
                tp = re.search(r'type=\d+', link.url).group()
                link.url = 'http://wz.sun0769.com/index.php/question/questionType?' + page + "&" + tp
            except BaseException as e:
                print(e)
            ret.append(link)
        return ret

    def parse_content(self, response):
        item = SunItem()
        url = response.url
        title = response.xpath('//*[@class="greyframe"]/div/div/strong/text()')[0].extract().strip()
        number = response.xpath(
            '//*[@class="greyframe"]/div/div/strong/text()'
        )[0].extract().strip().split(':')[-1]
        content = response.xpath('//div[@class="c1 text14_2"]/text()').extract()[0].strip()

        item['url'] = url
        item['title'] = title
        item['number'] = number
        item['content'] = content
        print(dict(item))
        # yield item


if __name__ == '__main__':
    from scrapy import cmdline
    cmdline.execute('scrapy crawl sun0769'.split())
    pass

在 pipelines.py:添加如下代码

import json
import codecs

class JsonWriterPipeline(object):

    def __init__(self):
        self.file = codecs.open('items.json', 'w', encoding='utf-8')

    def process_item(self, item, spider):
        line = json.dumps(dict(item), ensure_ascii=False) + "\n"
        self.file.write(line)
        return item

    def spider_closed(self, spider):
        self.file.close()

settings.py 添加如下代码(启用组件)

ITEM_PIPELINES = {
    'tutorial.pipelines.JsonWriterPipeline': 300,
}

window 下调试

在项目根目录下新建 main.py 文件,用于调试

from scrapy import cmdline
cmdline.execute('scrapy crawl sun0769'.split())

7、部署、可视化 管理 爬虫

爬虫 可视化 管理平台:Gerapy、crawllab、crawlab-lite

爬虫可视化管理平台、爬虫可视化调度工具,分布式爬虫管理框架、scrapy 可视化调度工具

框架技术优点缺点
Crawlab

Golang

Vue

不限于Scrapy,适用于所有编程语言和框架。漂亮的UI界面。自然地支持分布式蜘蛛。支持蜘蛛管理,任务管理,cron作业,结果导出,分析,通知,可配置蜘蛛,在线代码编辑器等。暂不支持版本控制
ScrapydWeb

Python Flask

Vue

漂亮的UI界面,内置Scrapy日志解析器,任务执行的统计数据和图表,支持节点管理,cron作业,邮件通知,移动。全功能蜘蛛管理平台。不支持除 Scrapy 以外的其他蜘蛛。由于后端Python Flask性能有限。
Gerapy

Python Django

Vue

Gerapy是由崔庆才创建的。安装部署简单。漂亮的UI界面。支持节点管理、代码编辑、可配置抓取规则等。同样不支持除Scrapy以外的其他蜘蛛。根据用户反馈,1.0版有很多bug。期待v2.0中的改进
SpiderKeeper

Python Flask

开源Scrapyhub。简洁明了的UI界面。支持cron作业。可能太简单了,不支持分页,不支持节点管理,不支持Scrapy以外的蜘蛛。

Gerapy、crawllab、crawlab-lite

  • SpiderKeeper 可能是最早的爬虫管理平台,但功能相对来说比较局限;
  • Gerapy 虽然功能齐全,界面精美,但有不少 bug 需要处理。
  • Scrapydweb 是一个比较完善的爬虫管理平台,不过和前两者一样,都是基于 scrapyd 的,因此只能运行 scrapy 爬虫;
  • Crawlab是一个非常灵活的爬虫管理平台,可以运行 Python、Nodejs、Java、PHP、Go 写的爬虫,而且功能比较齐全,只是部署起来相对于前三者来说要麻烦一些,不过对于 Docker 使用者来说可以做到一键部署。

大多数现有的平台都依赖于 Scrapyd,这将选择限制在 python 和 scrapy 之间。当然 scrapy 是一个很棒的网络抓取框架,但是它不能做所有的事情。

对于重度 scrapy 爬虫依赖的、又不想折腾的开发者,可以考虑 Scrapydweb;而对于有各种类型的、复杂技术结构的爬虫开发者来说,可以考虑更灵活的 Crawlab。当然,不是说 Crawlab 对 scrapy 支持不友好,Crawlab 同样可以很好的集成 scrapy,也很容易使用,足够通用,可以适应任何语言和框架中的蜘蛛。它还有一个漂亮的前端界面,用户可以更容易地管理蜘蛛。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值