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

​scrapy 英文文档:https://docs.scrapy.org/en/latest/index.html
scrapy 中文文档:https://docs.scrapy.net.cn/en/latest/index.html

1、常用 爬虫 框架

爬虫 框架

github 爬虫 框架

scrapy & scrapy_redis

scrapy 英文文档:https://docs.scrapy.org/en/latest/index.html
scrapy 中文文档:https://docs.scrapy.net.cn/en/latest/index.html

优点:自定义程度高,需要学习的相关知识多,分布式。
缺点:非分布式框架(可以用 scrapy-redis 分布式框架)
安装:pip install Scrapy

aio-scrapy (基于 scrapy 和 scrapy_redis)

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为其提供方便的部署及调度

文档地址

Katana

Katana 是一款功能强大的网络爬虫框架,在该工具的帮助下,广大研究人员可以轻松完成资源爬取和渗透测试阶段的信息收集任务。

github:https://github.com/projectdiscovery/katana

1、快速且完全可配置的网络资源爬取;

2、支持标准模式和 Headless 模式;

3、JavaScript 解析/爬取;

4、可自定义的自动化表单填充;

5、范围控制-预配置字段/正则表达式;

6、可自定义输出-预定义字段;

7、输入数据支持STDIN、URL和列表文件形式;

8、输出数据支持STDOUT、文件和JSON格式;

安装:go install github.com/projectdiscovery/katana/cmd/katana@latest

PySpider (好久没更新)

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

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

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

可视化 爬虫 框架

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  入门

2.2  基本概念

基本概念

命令行工具

Spiders

Selectors

Items

Item Loaders

Scrapy shell

Item Pipeline 

Feed exports ( 命令行 导出 数据 )

Requests、Responses (请求、响应)

Link Extractors ( 链接提取 )

Settings ( 设置 )

Exceptions ( 异常 )

主动结束爬虫

实现原理

def __init__(self, crawler): 
    self.crawler = crawler 
     
@classmethod 
def from_crawler(cls, crawler): 
    return cls(crawler) 
# 结束爬虫 
self.crawler.engine.close_spider(spider, 'closespider')

在spider中:

self.crawler.engine.close_spider(self, 'closespider')

在 pipeline 和 Middlewares 中:

spider.crawler.engine.close_spider(spider, 'closespider')

第二种方法.添加扩展类,在settings中添加

在setting中设置

CLOSESPIDER_TIMEOUT  # 指定时间退出
CLOSESPIDER_ITEMCOUNT  # 生成了指定数量的item
CLOSESPIDER_PAGECOUNT  # 抓取了指定数量的响应
CLOSESPIDER_ERRORCOUNT  # 在发生指定数量的错误

# 打开EXTENSIONS扩展
EXTENSIONS = {
   'scrapy.extensions.closespider.CloseSpider': 500,
}

2.3 内置服务

内置服务

日志

状态统计信息

发送 Email

telnet 控制

2.4 常见问题

一些常见问题

经常被问的问题

运行、调试 (debug)

只要是运行的文件,都可以进行打断点进行调试。

最新版 Pycharm 没法调试解决方法:

参考:https://youtrack.jetbrains.com/issue/IDEA-331676/Debugger-fails-with-FastAPI-uvloop-TypeError-Task-object-is-not-callable

禁用 python.debug.asyncio.repl
操作步骤:Help ---> Find Action ---> Registry ---> python.debug.asyncio.repl 去掉后面的勾

1、编写爬虫文件 baidu.py,name = baidu

方法 1:运行爬虫:scrapy crawl baidu 
方法 2:没有创建项目的情况下运行爬虫:scrapy runspider baidu.py

2、文件中运行爬虫

方法 1:scrapy.cmdline.execute("scrapy crawl baidu".split())

方法 2:CrawlerProcess方式运行爬虫

# -*- coding: utf-8 -*-

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

class BaiduSpider(Spider):
    name = 'baidu'

    start_urls = ['http://baidu.com/']

    def parse(self, response):
        self.log("run baidu")


if __name__ == '__main__':
	# 通过方法 get_project_settings() 获取配置信息
    process = CrawlerProcess(get_project_settings())
    process.crawl(BaiduSpider)
    process.start()

示例 2:

window 环境说明:https://docs.scrapy.net.cn/en/latest/topics/asyncio.html#install-asyncio

import scrapy
from asyncio.windows_events import *
from scrapy.crawler import CrawlerProcess


class Play1Spider(scrapy.Spider):
    name = 'play1'

    def start_requests(self):
        url = 'http://testphp.vulnweb.com/'
        yield scrapy.Request(
            url=url,
            callback=self.parse,
            meta={'playwright': True, 'playwright_include_page': True}
        )

    async def parse(self, response, **kwargs):
        resp_text = response.text
        print(resp_text)
        yield {'text': resp_text}


if __name__ == "__main__":
    process = CrawlerProcess(
        settings={
            "TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
            "DOWNLOAD_HANDLERS": {
                "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
                "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
            },
            "CONCURRENT_REQUESTS": 32,
            "FEED_URI": 'Products.jl',
            "FEED_FORMAT": 'jsonlines',
        }
    )
    process.crawl(Play1Spider)
    process.start()

方法 3:通过CrawlerRunner 运行爬虫

# -*- coding: utf-8 -*-

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


class BaiduSpider(Spider):
    name = 'baidu'

    start_urls = ['http://baidu.com/']

    def parse(self, response):
        self.log("run baidu")


if __name__ == '__main__':
    # 直接运行控制台没有日志
    configure_logging(
        {
            'LOG_FORMAT': '%(message)s'
        }
    )

    runner = CrawlerRunner()

    d = runner.crawl(BaiduSpider)
    d.addBoth(lambda _: reactor.stop())
    reactor.run()

文件中运行多个爬虫

cmdline 方式不可以运行多个爬虫,如果将两个语句放在一起,第一个语句执行完后程序就退出了,执行到不到第二句

# -*- coding: utf-8 -*-

from scrapy import cmdline

cmdline.execute("scrapy crawl baidu".split())
cmdline.execute("scrapy crawl sina".split())

CrawlerProcess 方式运行多个爬虫。此方式运行,发现日志中中间件只启动了一次,而且发送请求基本是同时的,说明这两个爬虫运行不是独立的,可能会相互干扰

# -*- coding: utf-8 -*-

from scrapy.crawler import CrawlerProcess

from scrapy_demo.spiders.baidu import BaiduSpider
from scrapy_demo.spiders.sina import SinaSpider

process = CrawlerProcess()
process.crawl(BaiduSpider)
process.crawl(SinaSpider)
process.start()

通过CrawlerRunner 运行多个爬虫。此方式也只加载一次中间件,不过是逐个运行的,会减少干扰,官方文档也推荐使用此方法来运行多个爬虫

# -*- coding: utf-8 -*-

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

from scrapy_demo.spiders.baidu import BaiduSpider
from scrapy_demo.spiders.sina import SinaSpider


configure_logging()
runner = CrawlerRunner()
runner.crawl(BaiduSpider)
runner.crawl(SinaSpider)
d = runner.join()
d.addBoth(lambda _: reactor.stop())

reactor.run()

多进程方法:1个进程 -> 多个子进程 -> scrapy进程

from multiprocessing import Process
from scrapy import cmdline
import time
import logging

# 配置参数即可, 爬虫名称,运行频率
confs = [
    {
        "spider_name": "hexun_pdf",
        "frequency": 2,
    },
]


def start_spider(spider_name, frequency):
    args = ["scrapy", "crawl", spider_name]
    while True:
        start = time.time()
        p = Process(target=cmdline.execute, args=(args,))
        p.start()
        p.join()
        logging.debug("### use time: %s" % (time.time() - start))
        time.sleep(frequency)


if __name__ == '__main__':
    for conf in confs:
        process = Process(target=start_spider,
                          args=(conf["spider_name"], conf["frequency"]))
        process.start()
        time.sleep(10)

Spider 约定

一些 通用 用法

大规模 抓取

DOWNLOAD_DELAY 和 CONCURRENT_REQUESTS 都会影响 并发 请求

  • DOWNLOAD_DELAY 默认值 0
  • CONCURRENT_REQUESTS 默认值是 16

场景

  • 案例 1:CONCURRENT_REQUESTS 设置 为 5 时,理论上 可以并发 5个请求。但是  DOWNLOAD_DELAY 设置为 0.01 时,按  DOWNLOAD_DELAY 来算,可以并发 1 / 0.01 = 100 个请求,这两个取最小值 为 5,所以 并发 5个 请求。
  • 案例 2:CONCURRENT_REQUESTS 设置 为 5 时,理论上 可以并发 5个请求。但是  DOWNLOAD_DELAY 设置为 0.5 时,按  DOWNLOAD_DELAY 来算,可以并发 1 / 0.5 = 2 个请求,这两个取最小值 为 2,所以 并发 2个 请求。

测试代码

import time
import scrapy
from scrapy.http import Request, Response


class ExampleSpider(scrapy.Spider):
    name = 'douban'
    allowed_domains = ['douban.com']
    start_urls = [f'https://movie.douban.com/top250?start={i}&filter=' for i in range(10000)]

    custom_settings = {

        'DEFAULT_REQUEST_HEADERS': {
            "Accept-Encoding": "gzip, deflate",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Connection": "keep-alive",
            "Host": "movie.douban.com",
            "Upgrade-Insecure-Requests": "1",
            "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                          ' (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36',
        },
        'LOG_LEVEL': 'DEBUG',
        'CONCURRENT_REQUESTS': 5,
        'DOWNLOAD_DELAY': 0.5,
        'CONCURRENT_REQUESTS_PER_IP': 10000,
        'CONCURRENT_REQUESTS_PER_DOMAIN': 10000,
        'FEED_EXPORT_ENCODING': 'utf-8'
    }

    @staticmethod
    def parse_movie(resp: Response = None):
        data_list = resp.xpath('//ol[@class="grid_view"]/li')
        index = 0
        for data in data_list:
            data_title = data.xpath('div/div[2]/div[@class="hd"]/a/span[1]/text()').extract_first()
            data_info = data.xpath('div/div[2]/div[@class="bd"]/p[1]/text()').extract_first()
            data_quote = data.xpath('div/div[2]/div[@class="bd"]/p[2]/span/text()').extract_first()
            data_score = data.xpath('div/div[2]/div[@class="bd"]/div/span[@class="rating_num"]/text()').extract_first()
            data_num = data.xpath('div/div[2]/div[@class="bd"]/div/span[4]/text()').extract_first()
            data_pic_url = data.xpath('div/div[1]/a/img/@src').extract_first()
            index += 1
            print(f'{data_title}')
        pass

    def parse(self, response):
        # self.parse_movie(response)
        time.sleep(3)
        return


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

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

集成 无头浏览器:playwright

调试内存泄漏

下载和处理文件和图像

部署 Spider

自动负载限制爬网速度

基准测试

暂停和恢复爬行

协程

asyncio ( 异步 )

2.5 Scrapy 扩展知识

Scrapy 扩展

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

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

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

另一个框架图:

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

  1. 引擎 首先找到 自己编写的 spider,然后从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 项目

该命令将会创建包含下列内容的 myspider 目录。

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

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

修改 example.py 如下,这就是一个简单的爬虫。然后直接运行,

定义 Item

Item 对象是自定义的 python 字典,用来保存爬取到的数据,其使用方法和 python 字典类似。定义一个 Item 就是创建一个 scrapy.Item 类,然后添加 类属性,并且类属性是 scrapy.Field 类型。

提示:spider 中可以返回 dict(字典),但是官方不推荐返回数据的时候直接返回字典,因为字典没有约束。官方推荐使用 Item 来约束数据结构,用起来和字典几乎一样。

编写 爬虫 (Spider) 文件

scrapy 提供了5种 spider

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

创建一个 Spider,必须继承这5个中的任意一个,同时还需要定义以下三个属性:

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

在 myspider/spiders 目录下创建 spider_tencent.py 文件中,并将下载的 response 保存到文件

import scrapy
from scrapy import cmdline


class ExampleSpider(scrapy.Spider):
    name = "tencent"
    allowed_domains = ["tencent.com"]
    start_urls = [
        "https://careers.tencent.com/search.html"
    ]

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


if __name__ == '__main__':
    cmdline.execute(['scrapy', 'crawl', 'tencent'])
    pass

在 myspider/spiders 目录下创建 spider_xiaohua.py 文件中

xpath 相关查询:

  • //div    查询 子子孙孙 中所有 div 标签
  • /div     查询 儿子 中的第一个 div 标签
  • //div[@class='c1']     查询子子孙孙中 class="c1" 的 div 标签
  • //div[@class='c1'][@name='alex']  等价于  //div[@class='c1' and @name='alex'] 
            查询 class="c1" 并且 name="alex" 的div标签
  • //div/span/text()    查询子子孙孙中所有 div 下面的span标签中的文本内容
  • //a/@href        查询某个属性的值。示例:查询 a标签的 href 属性
import scrapy
import os
import requests
from lxml import etree


class SpiderMM(scrapy.spiders.Spider):
    name = "mei_nv_xiao_hua" 
    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': {
        #     'myspider.pipelines.MainPipelineKafka': 300,
        #     # 'myspider.pipelines.MainPipelineSQLServer': 301,
        # },

        # 'DOWNLOADER_MIDDLEWARES': {
        #     # 'myspider.middlewares.RandomUserAgentMiddleware': 90,
        #     # 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 100,
        #     # 'myspider.middlewares.ZhiMaIPMiddleware': 125,
        #     # 'myspider.middlewares.ProxyMiddleware': 126,
        # },

        # 'CONCURRENT_REQUESTS': 100,
        # 'DOWNLOAD_DELAY': 0.01,
        # '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, **kwargs):
        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:

import scrapy
import hashlib
from scrapy import cmdline
from scrapy.selector import Selector
from scrapy.http import TextResponse


class MyItem(scrapy.Item):
    img_url = scrapy.Field()
    img_title = scrapy.Field()


class ExampleSpider(scrapy.spiders.Spider):
    name = "myspider_name"
    allowed_domains = ["umeituku.com"]
    url_set = set()
    start_urls = ["https://www.umeituku.com/meinvtupian/"]

    def __init__(self):
        super(ExampleSpider, self).__init__()
        self.md5_obj = hashlib.md5()

    def parse(self, response, **kwargs):
        req_url = response.url
        print(f'{response.status} ---> {req_url}')
        self.md5_obj.update(response.url.encode('utf-8'))
        md5_url = self.md5_obj.hexdigest()

        if md5_url not in ExampleSpider.url_set:
            ExampleSpider.url_set.add(md5_url)
            # 将普通 response 转换为 Scrapy 的 TextResponse
            # text_response = TextResponse(url=response.url, body=response.body, encoding='utf-8')
            # html_xpath_select = Selector(response=text_response)
            html_xpath_select = Selector(text=response.body)

            if req_url.startswith('http://www.umeituku.com/meinvtupian/xingganmeinv/'):
                tag_a_list = html_xpath_select.xpath('//div[@class="TypeList"]//ul//li')
                list_len = len(tag_a_list)
                for index in range(list_len):
                    tag_a = tag_a_list[index]
                    a_href = tag_a.xpath('./a/@href').extract_first()
                    a_text = tag_a.xpath('./a//text()').extract_first()

                    item = MyItem()
                    item['img_url'] = a_href
                    item['img_title'] = a_text
                    print(item)
                    # yield item
            tag_a_href_list = html_xpath_select.xpath('//a/@href').extract()
            for tag_a_href in tag_a_href_list:
                a_href = tag_a_href
                yield scrapy.Request(url=a_href, callback=self.parse, dont_filter=True)



if __name__ == '__main__':
    cmdline.execute(['scrapy', 'crawl', 'myspider_name'])
    pass

此处代码的关键在于:yield Item,一旦 parse 中执行 yield Item 对象,则自动将该对象交个pipelines 的类来处理。

运行 / 调试 

方法 1:命令行中运行

执行命令:scrapy crawl tencent

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

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

方法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

scrapy shell:https://docs.scrapy.net.cn/en/latest/topics/shell.html

Scrapy shell 只是一个常规 Python 控制台,它提供了一些额外的快捷函数以方便使用。

可用快捷方式

  • shelp() - 打印包含可用对象和快捷方式列表的帮助
  • fetch(url[, redirect=True]) - 从给定的 URL 获取新的响应并相应地更新所有相关对象。您可以选择通过传递 redirect=False 来请求不遵循 HTTP 3xx 重定向
  • fetch(request) - 从给定的请求获取新的响应并相应地更新所有相关对象。
  • view(response) - 在您的本地 Web 浏览器中打开给定的响应以供检查。这会向响应正文中添加一个 <base> 标记,以便正确显示外部链接(例如图像和样式表)。但是,请注意,这会在您的计算机中创建一个临时文件,该文件不会自动删除。

可用的 Scrapy 对象

Scrapy shell 会自动从下载的页面中创建一些便捷的对象,例如 Response 对象和 Selector 对象(适用于 HTML 和 XML 内容)。这些对象是

  • crawler - 当前的 Crawler 对象。
  • spider - 已知处理该 URL 的 Spider 或 Spider 对象(如果未找到当前 URL 的 spider)
  • request - 上次抓取的页面的 Request 对象。你可以使用 replace() 修改此请求,或使用 fetch 快捷方式抓取新的请求(无需退出 shell)。
  • response - 包含上次抓取的页面的 Response 对象
  • settings - 当前的 Scrapy 设置

从命令行运行 Scrapy shell 时,用引号将 URL 括起来,否则包含参数(即 & 字符)的 URL 将无法正常工作。在 Windows 上请使用双引号。

示例:scrapy shell 'https://scrapy.net.cn' --nolog

shell 将获取 URL(使用 Scrapy 下载器)并打印可用对象和有用快捷方式的列表(您会注意到这些行都以 [s] 前缀开头)

response.xpath("//title/text()").get()
fetch("https://old.reddit.com/")
response.xpath("//title/text()").get()
request = request.replace(method="POST")
fetch(request)
from pprint import pprint
pprint(response.headers)

通过使用 scrapy.shell.inspect_response 函数,可以从代码中调用 shell 以检查响应,相当于变相的命令行断点。

import scrapy


class MySpider(scrapy.Spider):
    name = "myspider"
    start_urls = [
        "http://example.com",
        "http://example.org",
        "http://example.net",
    ]

    def parse(self, response):
        # We want to inspect one specific response.
        if ".org" in response.url:
            from scrapy.shell import inspect_response

            inspect_response(response, self)

当运行爬虫时,将获得类似于此的内容

2014-01-23 17:48:31-0400 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://example.com> (referer: None)
2014-01-23 17:48:31-0400 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://example.org> (referer: None)
[s] Available Scrapy objects:
[s]   crawler    <scrapy.crawler.Crawler object at 0x1e16b50>
...

>>> response.url        这里可以输入命令,在命令行操作
'http://example.org'

>>>response.xpath('//h1[@class="fn"]')
>>>view(response)
True

最后,按 Ctrl+D(或在 Windows 中按 Ctrl+Z)退出 shell 并恢复抓取。注意,这里无法在此处使用 fetch 快捷方式,因为 Scrapy 引擎被 shell 阻止。但是,在您离开 shell 后,爬虫将继续从停止的地方抓取

现在使用内置的 scrapy shell 来介绍 Selector 的使用方法。

进入项目的根目录,执行下列命令来启动 shell: 

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

示例 1:scrapy shell "http://hr.tencent.com/position.php"
示例 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')
response.xpath('//title').extract()
print response.xpath('//title').extract()[0]
response.xpath('//title/text()')
response.xpath('//title/text()')[0].extract()
print response.xpath('//title/text()')[0].extract()
response.xpath('//title/text()').re('(\w+):')

编写 item

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

import scrapy

class MyItem(scrapy.Item):
    name  = scrapy.Field()
    detail_link = scrapy.Field()
    catalog = scrapy.Field()
    recruit_number = scrapy.Field()
    work_location = scrapy.Field()
    publish_time = scrapy.Field()

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

import scrapy
from myspider.items import MyItem


class ExampleSpider(scrapy.spiders.Spider):
    name = "tencent"
    allowed_domains = ["tencent.com"]
    start_urls = [
        "https://careers.tencent.com/search.html"
    ]

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

执行命令:scrapy crawl tencent 即可开始执行

命令行 保存 抓取的数据

最简单存储爬取的数据的方式是使用 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

启用 Pipeline 组件

在 settings.py 文件配置 ITEM_PIPELINES ,从而启用 Pipeline 组件

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 设置 cookies

在 Scrapy 中可以通过多种方式设置 cookies。

请求时直接设置

可以在构造请求时直接设置 cookies 参数:

import scrapy

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

    def start_requests(self):
        cookies = {'key1': 'value1', 'key2': 'value2'}
        yield scrapy.Request(url='http://example.com', cookies=cookies)

请求时 headers 中设置

import scrapy

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

    def start_requests(self):
        headers = {
            'Cookie': 'key1=value1; key2=value2'
        }
        yield scrapy.Request(url='http://example.com', headers=headers)

通过中间件设置

可以创建一个自定义中间件,在其中设置 cookies。以下是一个示例中间件:

class CustomCookieMiddleware(object):

    def process_request(self, request, spider):
        request.cookies = {'key1': 'value1', 'key2': 'value2'}
        return None

然后在 settings.py 中启用这个中间件:

DOWNLOADER_MIDDLEWARES = {
    'yourproject.middlewares.CustomCookieMiddleware': 543,
}

在settings.py中设置全局默认的 Cookies

默认请求头中设置 cookies 时,需要设置 ENABLE_COOKIE = False,只有禁用才不会走默认的 cookie 中间件,这样在 默认请求头设置的Cookie信息才会生效。

DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'Cookie': 'key1=value1;key2=value2'
}

从响应中提取 Cookies 并在后续请求中使用

import scrapy

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

    def start_requests(self):
        yield scrapy.Request(url='http://example.com')

    def parse(self, response):
        cookies = response.headers.getlist('Set-Cookie')
        # 处理 cookies 字符串,将其转换为字典形式
        parsed_cookies = {}
        for cookie in cookies:
            parts = cookie.decode().split(';')[0].split('=', 1)
            if len(parts) == 2:
                parsed_cookies[parts[0]] = parts[1]
        yield scrapy.Request(url='http://example.com/otherpage', cookies=parsed_cookies)

cookiejar 管理多个 Cookie

在 Scrapy 中,cookiejar可以用于管理多个 Cookie 容器,以便在不同的请求中使用不同的 Cookie 集合。使用示例:

import scrapy

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

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url, meta={'cookiejar': 1})

    def parse(self, response):
        # 处理响应
        #...

        # 发起另一个请求,使用不同的 cookiejar
        yield scrapy.Request('http://another.example.com', meta={'cookiejar': 2})

例子中,第一个请求使用了cookiejar编号为 1 的容器来存储 Cookie。然后,第二个请求使用了编号为 2 的容器,这样两个请求的 Cookie 就不会相互干扰。注意,默认情况下 Scrapy 会自动处理 Cookie,如果你没有特殊需求,可能不需要手动使用cookiejar。但在某些情况下,比如模拟多个用户的行为或者处理需要不同登录状态的请求时,cookiejar就很有用了。

scrapy 模拟 登陆

首先抓包分析登录时的参数,然后构造 post 请求去获取数据

示例:scrapy 模拟登录

#!/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 模拟登录的示例代码:

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 模块登录

使用 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

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

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 的各个组件的源码中,首先执行的就是它

可以定制 start_requests、也可以单独设置 custom_settings。例如,如果在启动时以 POST 登录某个网站时可以这么写:

import scrapy


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

示例

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)

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)

通过 Rule类对提取到的 URL 分别指定不同的函数处理其 response。

    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/g/scrapy-users/c/RHGtm_2GO1M

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

示例:阳光热线问政平台

目标网址: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())

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

scrapy 框架中,默认的 log 设置

在 settings 文件中,修改添加信息,然后命令行执行:scrapy crawl tencent_crawl

LOG_FILE='ten.log'
LOG_LEVEL='INFO'    # 记录 级别大于INFO的日志

或者 command line 命令行执行:scrapy crawl tencent_crawl --logfile 'ten.log' -L INFO

6、Settings

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

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

设置 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

  • 方法 1:在自定义 spider 代码中,可以通过 self.settings 获取设置
    import json
    import scrapy
    from scrapy import cmdline
    
    
    class ExampleSpider(scrapy.Spider):
        name = "access_setting"
        allowed_domains = ["httpbin.org"]
        start_urls = ["https://httpbin.org/"]
    
        custom_settings = {
            'CONCURRENT_REQUESTS': 10,
            'DOWNLOAD_DELAY': 1,
            'LOG_FILE': None,
            'LOG_LEVEL': 'DEBUG',
        }
    
        def parse(self, response, **kwargs):
            scrapy_settings = self.settings
            settings_dict = dict(scrapy_settings)
            print(scrapy_settings)
            pass
    
    if __name__ == '__main__':
        cmdline.execute('scrapy crawl access_setting'.split())
        pass
  • 方法 2:通过爬虫的 scrap.crawler.Crawler.Settings 属性来访问 Settings,该 Settings 属性在 扩展(extensions)、中间件(middlewares) 和 项目管道(item pipelines) 中传递给 from_crawler 方法:
    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'))

scrapy 默认 settings

"""
This module contains the default values for all settings used by Scrapy.

For more information about these settings you can read the settings
documentation in docs/topics/settings.rst

Scrapy developers, if you add a setting here remember to:

* add it in alphabetical order
* group similar settings without leaving blank lines
* add its documentation to the available settings documentation
  (docs/topics/settings.rst)

"""

import sys
from importlib import import_module
from pathlib import Path

ADDONS = {}

AJAXCRAWL_ENABLED = False

ASYNCIO_EVENT_LOOP = None

AUTOTHROTTLE_ENABLED = False
AUTOTHROTTLE_DEBUG = False
AUTOTHROTTLE_MAX_DELAY = 60.0
AUTOTHROTTLE_START_DELAY = 5.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

BOT_NAME = "scrapybot"

CLOSESPIDER_TIMEOUT = 0
CLOSESPIDER_PAGECOUNT = 0
CLOSESPIDER_ITEMCOUNT = 0
CLOSESPIDER_ERRORCOUNT = 0

COMMANDS_MODULE = ""

COMPRESSION_ENABLED = True

CONCURRENT_ITEMS = 100

CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8
CONCURRENT_REQUESTS_PER_IP = 0

COOKIES_ENABLED = True
COOKIES_DEBUG = False

DEFAULT_ITEM_CLASS = "scrapy.item.Item"

DEFAULT_REQUEST_HEADERS = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en",
}

DEPTH_LIMIT = 0
DEPTH_STATS_VERBOSE = False
DEPTH_PRIORITY = 0

DNSCACHE_ENABLED = True
DNSCACHE_SIZE = 10000
DNS_RESOLVER = "scrapy.resolver.CachingThreadedResolver"
DNS_TIMEOUT = 60

DOWNLOAD_DELAY = 0

DOWNLOAD_HANDLERS = {}
DOWNLOAD_HANDLERS_BASE = {
    "data": "scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler",
    "file": "scrapy.core.downloader.handlers.file.FileDownloadHandler",
    "http": "scrapy.core.downloader.handlers.http.HTTPDownloadHandler",
    "https": "scrapy.core.downloader.handlers.http.HTTPDownloadHandler",
    "s3": "scrapy.core.downloader.handlers.s3.S3DownloadHandler",
    "ftp": "scrapy.core.downloader.handlers.ftp.FTPDownloadHandler",
}

DOWNLOAD_TIMEOUT = 180  # 3mins

DOWNLOAD_MAXSIZE = 1024 * 1024 * 1024  # 1024m
DOWNLOAD_WARNSIZE = 32 * 1024 * 1024  # 32m

DOWNLOAD_FAIL_ON_DATALOSS = True

DOWNLOADER = "scrapy.core.downloader.Downloader"

DOWNLOADER_HTTPCLIENTFACTORY = (
    "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
)
DOWNLOADER_CLIENTCONTEXTFACTORY = (
    "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"
)
DOWNLOADER_CLIENT_TLS_CIPHERS = "DEFAULT"
# Use highest TLS/SSL protocol version supported by the platform, also allowing negotiation:
DOWNLOADER_CLIENT_TLS_METHOD = "TLS"
DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING = False

DOWNLOADER_MIDDLEWARES = {}

DOWNLOADER_MIDDLEWARES_BASE = {
    # Engine side
    "scrapy.downloadermiddlewares.offsite.OffsiteMiddleware": 50,
    "scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware": 100,
    "scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware": 300,
    "scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware": 350,
    "scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware": 400,
    "scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": 500,
    "scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
    "scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware": 560,
    "scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware": 580,
    "scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware": 590,
    "scrapy.downloadermiddlewares.redirect.RedirectMiddleware": 600,
    "scrapy.downloadermiddlewares.cookies.CookiesMiddleware": 700,
    "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750,
    "scrapy.downloadermiddlewares.stats.DownloaderStats": 850,
    "scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware": 900,
    # Downloader side
}

DOWNLOADER_STATS = True

DUPEFILTER_CLASS = "scrapy.dupefilters.RFPDupeFilter"

EDITOR = "vi"
if sys.platform == "win32":
    EDITOR = "%s -m idlelib.idle"

EXTENSIONS = {}

EXTENSIONS_BASE = {
    "scrapy.extensions.corestats.CoreStats": 0,
    "scrapy.extensions.telnet.TelnetConsole": 0,
    "scrapy.extensions.memusage.MemoryUsage": 0,
    "scrapy.extensions.memdebug.MemoryDebugger": 0,
    "scrapy.extensions.closespider.CloseSpider": 0,
    "scrapy.extensions.feedexport.FeedExporter": 0,
    "scrapy.extensions.logstats.LogStats": 0,
    "scrapy.extensions.spiderstate.SpiderState": 0,
    "scrapy.extensions.throttle.AutoThrottle": 0,
}

FEED_TEMPDIR = None
FEEDS = {}
FEED_URI_PARAMS = None  # a function to extend uri arguments
FEED_STORE_EMPTY = True
FEED_EXPORT_ENCODING = None
FEED_EXPORT_FIELDS = None
FEED_STORAGES = {}
FEED_STORAGES_BASE = {
    "": "scrapy.extensions.feedexport.FileFeedStorage",
    "file": "scrapy.extensions.feedexport.FileFeedStorage",
    "ftp": "scrapy.extensions.feedexport.FTPFeedStorage",
    "gs": "scrapy.extensions.feedexport.GCSFeedStorage",
    "s3": "scrapy.extensions.feedexport.S3FeedStorage",
    "stdout": "scrapy.extensions.feedexport.StdoutFeedStorage",
}
FEED_EXPORT_BATCH_ITEM_COUNT = 0
FEED_EXPORTERS = {}
FEED_EXPORTERS_BASE = {
    "json": "scrapy.exporters.JsonItemExporter",
    "jsonlines": "scrapy.exporters.JsonLinesItemExporter",
    "jsonl": "scrapy.exporters.JsonLinesItemExporter",
    "jl": "scrapy.exporters.JsonLinesItemExporter",
    "csv": "scrapy.exporters.CsvItemExporter",
    "xml": "scrapy.exporters.XmlItemExporter",
    "marshal": "scrapy.exporters.MarshalItemExporter",
    "pickle": "scrapy.exporters.PickleItemExporter",
}
FEED_EXPORT_INDENT = 0

FEED_STORAGE_FTP_ACTIVE = False
FEED_STORAGE_GCS_ACL = ""
FEED_STORAGE_S3_ACL = ""

FILES_STORE_S3_ACL = "private"
FILES_STORE_GCS_ACL = ""

FTP_USER = "anonymous"
FTP_PASSWORD = "guest"
FTP_PASSIVE_MODE = True

GCS_PROJECT_ID = None

HTTPCACHE_ENABLED = False
HTTPCACHE_DIR = "httpcache"
HTTPCACHE_IGNORE_MISSING = False
HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"
HTTPCACHE_EXPIRATION_SECS = 0
HTTPCACHE_ALWAYS_STORE = False
HTTPCACHE_IGNORE_HTTP_CODES = []
HTTPCACHE_IGNORE_SCHEMES = ["file"]
HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS = []
HTTPCACHE_DBM_MODULE = "dbm"
HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
HTTPCACHE_GZIP = False

HTTPPROXY_ENABLED = True
HTTPPROXY_AUTH_ENCODING = "latin-1"

IMAGES_STORE_S3_ACL = "private"
IMAGES_STORE_GCS_ACL = ""

ITEM_PROCESSOR = "scrapy.pipelines.ItemPipelineManager"

ITEM_PIPELINES = {}
ITEM_PIPELINES_BASE = {}

JOBDIR = None

LOG_ENABLED = True
LOG_ENCODING = "utf-8"
LOG_FORMATTER = "scrapy.logformatter.LogFormatter"
LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
LOG_DATEFORMAT = "%Y-%m-%d %H:%M:%S"
LOG_STDOUT = False
LOG_LEVEL = "DEBUG"
LOG_FILE = None
LOG_FILE_APPEND = True
LOG_SHORT_NAMES = False

SCHEDULER_DEBUG = False

LOGSTATS_INTERVAL = 60.0

MAIL_HOST = "localhost"
MAIL_PORT = 25
MAIL_FROM = "scrapy@localhost"
MAIL_PASS = None
MAIL_USER = None

MEMDEBUG_ENABLED = False  # enable memory debugging
MEMDEBUG_NOTIFY = []  # send memory debugging report by mail at engine shutdown

MEMUSAGE_CHECK_INTERVAL_SECONDS = 60.0
MEMUSAGE_ENABLED = True
MEMUSAGE_LIMIT_MB = 0
MEMUSAGE_NOTIFY_MAIL = []
MEMUSAGE_WARNING_MB = 0

METAREFRESH_ENABLED = True
METAREFRESH_IGNORE_TAGS = ["noscript"]
METAREFRESH_MAXDELAY = 100

NEWSPIDER_MODULE = ""

PERIODIC_LOG_DELTA = None
PERIODIC_LOG_STATS = None
PERIODIC_LOG_TIMING_ENABLED = False

RANDOMIZE_DOWNLOAD_DELAY = True

REACTOR_THREADPOOL_MAXSIZE = 10

REDIRECT_ENABLED = True
REDIRECT_MAX_TIMES = 20  # uses Firefox default setting
REDIRECT_PRIORITY_ADJUST = +2

REFERER_ENABLED = True
REFERRER_POLICY = "scrapy.spidermiddlewares.referer.DefaultReferrerPolicy"

REQUEST_FINGERPRINTER_CLASS = "scrapy.utils.request.RequestFingerprinter"
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.6"

RETRY_ENABLED = True
RETRY_TIMES = 2  # initial response + 2 retries = 3 requests
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429]
RETRY_PRIORITY_ADJUST = -1
RETRY_EXCEPTIONS = [
    "twisted.internet.defer.TimeoutError",
    "twisted.internet.error.TimeoutError",
    "twisted.internet.error.DNSLookupError",
    "twisted.internet.error.ConnectionRefusedError",
    "twisted.internet.error.ConnectionDone",
    "twisted.internet.error.ConnectError",
    "twisted.internet.error.ConnectionLost",
    "twisted.internet.error.TCPTimedOutError",
    "twisted.web.client.ResponseFailed",
    # OSError is raised by the HttpCompression middleware when trying to
    # decompress an empty response
    OSError,
    "scrapy.core.downloader.handlers.http11.TunnelError",
]

ROBOTSTXT_OBEY = False
ROBOTSTXT_PARSER = "scrapy.robotstxt.ProtegoRobotParser"
ROBOTSTXT_USER_AGENT = None

SCHEDULER = "scrapy.core.scheduler.Scheduler"
SCHEDULER_DISK_QUEUE = "scrapy.squeues.PickleLifoDiskQueue"
SCHEDULER_MEMORY_QUEUE = "scrapy.squeues.LifoMemoryQueue"
SCHEDULER_PRIORITY_QUEUE = "scrapy.pqueues.ScrapyPriorityQueue"

SCRAPER_SLOT_MAX_ACTIVE_SIZE = 5000000

SPIDER_LOADER_CLASS = "scrapy.spiderloader.SpiderLoader"
SPIDER_LOADER_WARN_ONLY = False

SPIDER_MIDDLEWARES = {}

SPIDER_MIDDLEWARES_BASE = {
    # Engine side
    "scrapy.spidermiddlewares.httperror.HttpErrorMiddleware": 50,
    "scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
    "scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
    "scrapy.spidermiddlewares.depth.DepthMiddleware": 900,
    # Spider side
}

SPIDER_MODULES = []

STATS_CLASS = "scrapy.statscollectors.MemoryStatsCollector"
STATS_DUMP = True

STATSMAILER_RCPTS = []

TEMPLATES_DIR = str((Path(__file__).parent / ".." / "templates").resolve())

URLLENGTH_LIMIT = 2083

USER_AGENT = f'Scrapy/{import_module("scrapy").__version__} (+https://scrapy.org)'

TELNETCONSOLE_ENABLED = 1
TELNETCONSOLE_PORT = [6023, 6073]
TELNETCONSOLE_HOST = "127.0.0.1"
TELNETCONSOLE_USERNAME = "scrapy"
TELNETCONSOLE_PASSWORD = None

TWISTED_REACTOR = None

SPIDER_CONTRACTS = {}
SPIDER_CONTRACTS_BASE = {
    "scrapy.contracts.default.UrlContract": 1,
    "scrapy.contracts.default.CallbackKeywordArgumentsContract": 1,
    "scrapy.contracts.default.ReturnsContract": 2,
    "scrapy.contracts.default.ScrapesContract": 3,
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值