0.参考

https://doc.scrapy.org/en/latest/topics/item-pipeline.html?highlight=mongo#write-items-to-mongodb

20180721新增:异步版本

https://twistedmatrix.com/documents/15.3.0/core/howto/rdbms.html

https://twistedmatrix.com/documents/18.7.0/api/twisted.python.failure.Failure.html

https://twistedmatrix.com/documents/12.1.0/core/howto/time.html

1.主要实现

(1) 连接超时自动重连 MySQL server

(2) 通过 item_list 收集 item,达到阈值后批量提交至数据库

(3) 通过解析异常,自动移除存在异常的数据行,重新提交 item_list

(4) shutdown 之前在 close_spider() 中提交当前 item_list

(5) 20180721新增:异步版本

2.同步版本

保存至 /site-packages/my_pipelines.py

from socket import gethostname
import time
import re
from html import escape import pymysql
pymysql.install_as_MySQLdb()
from pymysql import OperationalError, InterfaceError, DataError, IntegrityError class MyMySQLPipeline(object): hostname = gethostname() def __init__(self, settings):
self.mysql_host = settings.get('MYSQL_HOST', '127.0.0.1')
self.mysql_port = settings.get('MYSQL_PORT', 3306)
self.mysql_user = settings.get('MYSQL_USER', 'username')
self.mysql_passwd = settings.get('MYSQL_PASSWD', 'password')
self.mysql_reconnect_wait = settings.get('MYSQL_RECONNECT_WAIT', 60) self.mysql_db = settings.get('MYSQL_DB')
self.mysql_charset = settings.get('MYSQL_CHARSET', 'utf8') #utf8mb4
self.mysql_item_list_limit = settings.get('MYSQL_ITEM_LIST_LIMIT', 30) self.item_list = [] @classmethod
def from_crawler(cls, crawler):
return cls(
settings = crawler.settings
) def open_spider(self, spider):
try:
self.conn = pymysql.connect(
host = self.mysql_host,
port = self.mysql_port,
user = self.mysql_user,
passwd = self.mysql_passwd,
db = self.mysql_db,
charset = self.mysql_charset,
)
except Exception as err:
spider.logger.warn('MySQL: FAIL to connect {} {}'.format(err.__class__, err))
time.sleep(self.mysql_reconnect_wait)
self.open_spider(spider)
else:
spider.logger.info('MySQL: connected') self.curs = self.conn.cursor(pymysql.cursors.DictCursor)
spider.curs = self.curs def close_spider(self, spider):
self.insert_item_list(spider)
self.conn.close()
spider.logger.info('MySQL: closed') def process_item(self, item, spider):
self.item_list.append(item)
if len(self.item_list) >= self.mysql_item_list_limit:
self.insert_item_list(spider) return item def sql(self):
raise NotImplementedError('Subclass of MyMySQLPipeline must implement the sql() method') def insert_item_list(self, spider):
spider.logger.info('insert_item_list: {}'.format(len(self.item_list)))
try:
self.sql()
except (OperationalError, InterfaceError) as err:
# <class 'pymysql.err.OperationalError'>
# (2013, 'Lost connection to MySQL server during query ([Errno 110] Connection timed out)')
spider.logger.info('MySQL: exception {} err {}'.format(err.__class__, err))
self.open_spider(spider)
self.insert_item_list(spider)
except Exception as err:
if len(err.args) == 2 and isinstance(err.args[1], str):
# <class 'pymysql.err.DataError'>
# (1264, "Out of range value for column 'position_id' at row 2")
# <class 'pymysql.err.InternalError'>
# (1292, "Incorrect date value: '1977-06-31' for column 'release_day' at row 26")
m_row = re.search(r'at\s+row\s+(\d+)$', err.args[1])
# <class 'pymysql.err.IntegrityError'>
# (1048, "Column 'name' cannot be null") films 43894
m_column = re.search(r"Column\s'(.+)'", err.args[1]) if m_row:
row = m_row.group(1)
item = self.item_list.pop(int(row) - 1)
spider.logger.warn('MySQL: {} {} exception from item {}'.format(err.__class__, err, item))
self.insert_item_list(spider)
elif m_column:
column = m_column.group(1)
item_list = []
for item in self.item_list:
if item[column] == None:
item_list.append(item)
for item in item_list:
self.item_list.remove(item)
spider.logger.warn('MySQL: {} {} exception from item {}'.format(err.__class__, err, item))
self.insert_item_list(spider)
else:
spider.logger.error('MySQL: {} {} unhandled exception from item_list: \n{}'.format(
err.__class__, err, self.item_list))
else:
spider.logger.error('MySQL: {} {} unhandled exception from item_list: \n{}'.format(
err.__class__, err, self.item_list))
finally:
self.item_list.clear()

3.调用方法

Scrapy 项目 project_name

MySQL 数据库 database_name, 表 table_name

(1) 项目 pipelines.py 添加代码:

from my_pipelines import MyMySQLPipeline

class MySQLPipeline(MyMySQLPipeline):

    def sql(self):
self.curs.executemany("""
INSERT INTO table_name (
position_id, crawl_time)
VALUES (
%(position_id)s, %(crawl_time)s)
ON DUPLICATE KEY UPDATE
crawl_time=values(crawl_time)
""", self.item_list) self.conn.commit()

(2) 项目 settings.py 添加代码:

# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
# 'project_name.pipelines.ProxyPipeline': 300,
'project_name.pipelines.MySQLPipeline': 301,
} MYSQL_HOST = '127.0.0.1'
MYSQL_PORT = 3306
MYSQL_USER = 'username'
MYSQL_PASSWD ='password'
MYSQL_RECONNECT_WAIT = 60 MYSQL_DB = 'database_name'
MYSQL_CHARSET = 'utf8' #utf8mb4
MYSQL_ITEM_LIST_LIMIT = 3 #100

4.运行结果

自动移除存在异常的数据行,重新提交 item_list:

2018-07-18 12:35:52 [scrapy.core.scraper] DEBUG: Scraped from <200 http://httpbin.org/>
{'position_id': 103, 'crawl_time': '2018-07-18 12:35:52'}
2018-07-18 12:35:52 [scrapy.core.scraper] DEBUG: Scraped from <200 http://httpbin.org/>
{'position_id': None, 'crawl_time': '2018-07-18 12:35:52'}
2018-07-18 12:35:52 [scrapy.core.scraper] DEBUG: Scraped from <200 http://httpbin.org/>
{'position_id': 104, 'crawl_time': '2018-02-31 17:51:47'}
2018-07-18 12:35:55 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://httpbin.org/> (referer: http://httpbin.org/)
2018-07-18 12:35:55 [test] INFO: insert_item_list: 3
2018-07-18 12:35:55 [test] WARNING: MySQL: <class 'pymysql.err.IntegrityError'> (1048, "Column 'position_id' cannot be null") exception from item {'position_id': None, 'crawl_time': '2018-07-18 12:35:52'}
2018-07-18 12:35:55 [test] INFO: insert_item_list: 2
2018-07-18 12:35:55 [test] WARNING: MySQL: <class 'pymysql.err.InternalError'> (1292, "Incorrect datetime value: '2018-02-31 17:51:47' for column 'crawl_time' at row 1") exception from item {'position_id': 104, 'crawl_time': '2018-02-31 17:51:47'}
2018-07-18 12:35:55 [test] INFO: insert_item_list: 1
2018-07-18 12:35:55 [scrapy.core.scraper] DEBUG: Scraped from <200 http://httpbin.org/>

提交结果:

在 self.item_list.append(item) 之后 添加代码 spider.logger.info('process_item: {}'.format(len(self.item_list))) 打印添加 item 后的当前 item_list 元素个数

连续 yield 5个 item,累计3个则触发 insert,红框 insert 部分将会阻塞绿框中后续 yield 部分:

5.异步版本

(1) 保存至 /site-packages/my_pipelines.py

# -*- coding: utf-8 -*-
from socket import gethostname
import time
import re # https://twistedmatrix.com/documents/15.3.0/core/howto/rdbms.html
# twisted.enterprise.adbapi: Twisted RDBMS support
from twisted.enterprise import adbapi
import pymysql
from pymysql import OperationalError, InterfaceError, DataError, InternalError, IntegrityError class MyMySQLPipeline(object): hostname = gethostname() def __init__(self, spider, settings):
self.spider = spider self.dbpool = adbapi.ConnectionPool('pymysql',
host = settings.get('MYSQL_HOST', '127.0.0.1'),
port = settings.get('MYSQL_PORT', 3306),
user = settings.get('MYSQL_USER', 'username'),
passwd = settings.get('MYSQL_PASSWD', 'password'),
db = settings.get('MYSQL_DB', 'test'),
charset = settings.get('MYSQL_CHARSET', 'utf8'), #utf8mb4 cursorclass = pymysql.cursors.DictCursor
)
self.mysql_reconnect_wait = settings.get('MYSQL_RECONNECT_WAIT', 60)
self.mysql_item_list_limit = settings.get('MYSQL_ITEM_LIST_LIMIT', 30)
self.item_list = [] @classmethod
def from_crawler(cls, crawler):
return cls(
spider = crawler.spider,
settings = crawler.settings
) def close_spider(self, spider):
self._sql(list(self.item_list)) def process_item(self, item, spider):
self.item_list.append(item) if len(self.item_list) >= self.mysql_item_list_limit:
spider.log('item_list: %s'%len(self.item_list))
self._sql(list(self.item_list))
self.item_list.clear() return item def sql(self, txn, item_list):
raise NotImplementedError('Subclass of MyMySQLPipeline must implement the sql() method') def _sql(self, item_list, retrying=False):
d = self.dbpool.runInteraction(self.sql, item_list)
d.addCallback(self.handle_result, item_list)
d.addErrback(self.handle_error, item_list, retrying) def handle_result(self, result, item_list):
self.spider.logger.info('{} items inserted with retcode {}'.format(len(item_list), result)) def handle_error(self, failure, item_list, retrying):
# https://twistedmatrix.com/documents/18.7.0/api/twisted.python.failure.Failure.html
# r = failure.trap(pymysql.err.InternalError) args = failure.value.args # <class 'pymysql.err.OperationalError'> (1045, "Access denied for user 'username'@'localhost' (using password: YES)")
# <class 'pymysql.err.OperationalError'> (2013, 'Lost connection to MySQL server during query ([Errno 110] Connection timed out)')
# <class 'pymysql.err.OperationalError'> (2003, "Can't connect to MySQL server on '127.0.0.1' ([WinError 10061] 由于目标计算机积极拒绝,无法连接。)")
# <class 'pymysql.err.InterfaceError'> (0, '') # after crawl started: sudo service mysqld stop
if failure.type in [OperationalError, InterfaceError]:
if not retrying:
self.spider.logger.info('MySQL: exception {} {} \n{}'.format(
failure.type, args, item_list))
self.spider.logger.debug('MySQL: Trying to recommit in %s sec'%self.mysql_reconnect_wait) # self._sql(item_list)
# https://twistedmatrix.com/documents/12.1.0/core/howto/time.html
from twisted.internet import task
from twisted.internet import reactor
task.deferLater(reactor, self.mysql_reconnect_wait, self._sql, item_list, True)
else:
self.spider.logger.warn('MySQL: exception {} {} \n{}'.format(
failure.type, args, item_list)) return # <class 'pymysql.err.DataError'> (1264, "Out of range value for column 'position_id' at row 2")
# <class 'pymysql.err.InternalError'> (1292, "Incorrect date value: '1977-06-31' for column 'release_day' at row 26")
elif failure.type in [DataError, InternalError]:
m_row = re.search(r'at\s+row\s+(\d+)$', args[1])
row = m_row.group(1)
item = item_list.pop(int(row) - 1)
self.spider.logger.warn('MySQL: {} {} exception from item {}'.format(failure.type, args, item)) self._sql(item_list)
return # <class 'pymysql.err.IntegrityError'> (1048, "Column 'name' cannot be null") films 43894
elif failure.type in [IntegrityError]:
m_column = re.search(r"Column\s'(.+)'", args[1])
column = m_column.group(1)
some_items = [item for item in item_list if item[column] is None]
self.spider.logger.warn('MySQL: {} {} exception from some items: \n{}'.format(
failure.type, args, some_items)) self._sql([item for item in item_list if item[column] is not None])
return else:
self.spider.logger.error('MySQL: {} {} unhandled exception from item_list: \n{}'.format(
failure.type, args, item_list)) return

(2) 项目 pipelines.py 添加代码:注意 dbpool.runInteraction 是自动提交的 transaction

from my_pipelines import MyMySQLPipeline

class MySQLPipeline(MyMySQLPipeline):

    def sql(self, txn, item_list):
return txn.executemany("""
INSERT INTO table_name (
position_id, crawl_time)
VALUES (
%(position_id)s, %(crawl_time)s)
ON DUPLICATE KEY UPDATE
crawl_time=values(crawl_time)
""", item_list)

(3) 项目 settings.py

见上文同步版本 3(1)

(4) 运行结果

在 self.item_list.append(item) 之后 添加代码 spider.logger.info('process_item: {}'.format(len(self.item_list))) 打印添加 item 后的当前 item_list 元素个数

连续 yield 5个 item,累计3个则触发 insert,红框 insert 部分并不会阻塞绿框中后续 yield 部分:

另外可见使用了连接池

Scrapy 扩展中间件: 同步/异步提交批量 item 到 MySQL的更多相关文章

  1. Scrapy 扩展中间件: 针对特定响应状态码,使用代理重新请求

    0.参考 https://doc.scrapy.org/en/latest/topics/downloader-middleware.html#module-scrapy.downloadermidd ...

  2. 表单(同步提交)和AJAX(异步提交)示范

    表单提交(同步提交) HTML文件: PHP文件: 这样就能接收到HTML里输入的内容,注意: FORM表头method为POST,PHP文件获取的方法就是$_POST,method为GET,PHP的 ...

  3. LoadRunner11脚本小技能之同步/异步接口分离+批量替换请求头

    最近在公司又进行了一次LoadRunner11性能测试,技能又get了一点,继续Mark起来!!! 一.异步/同步接口分离 之前在另一篇博文中有提到"事务拆分"的小节,即一个htm ...

  4. 02 . Go框架之Gin框架从入门到熟悉(数据解析和绑定,渲染,重定向,同步异步,中间件)

    数据解析和绑定 json数据解析和绑定 package main import ( "github.com/gin-gonic/gin" "net/http" ...

  5. 图片上传,支持同步/异步、预览(MVC、uploadify异步提交、js预览、ajaxSubmit异步提交)兼容大部分浏览器,含代码

    图片上传代码,支持同步/异步和图片的预览 主要用了两种方式,可兼容大部分浏览器. 第一种使用uploadify异步上传,上传后返回图片路径显示到页面. 每二种使用ajaxSubmit异步上传,为兼容I ...

  6. 使用C# HttpWebRequest进行多线程网页提交。Async httpclient/HttpWebRequest实现批量任务的发布及异步提交和超时取消

    使用线程池并发处理request请求及错误重试,使用委托处理UI界面输出. http://www.cnblogs.com/Charltsing/p/httpwebrequest.html for (i ...

  7. Django之ORM多对多表创建方式,AJAX异步提交,分页器组件等

    MTV与MVC MTV模型: ​ M:模型层(models.py),负责业务对象和数据库关系的映射(ORM) ​ T:模板层(Template),负责如何把页面展示给用户(HTML) ​ V:视图层( ...

  8. 多道技术 进程 线程 协程 GIL锁 同步异步 高并发的解决方案 生产者消费者模型

    本文基本内容 多道技术 进程 线程 协程 并发 多线程 多进程 线程池 进程池 GIL锁 互斥锁 网络IO 同步 异步等 实现高并发的几种方式 协程:单线程实现并发 一 多道技术 产生背景 所有程序串 ...

  9. {Python之进程} 背景知识 什么是进程 进程调度 并发与并行 同步\异步\阻塞\非阻塞 进程的创建与结束 multiprocess模块 进程池和mutiprocess.Poll

    Python之进程 进程 本节目录 一 背景知识 二 什么是进程 三 进程调度 四 并发与并行 五 同步\异步\阻塞\非阻塞 六 进程的创建与结束 七 multiprocess模块 八 进程池和mut ...

随机推荐

  1. 关于 redis 的 数据类型 和 内存模型

    该文章 是在读了 公众号 : java 后端技术 之后 做的一个小记录 原文网址  : https://mp.weixin.qq.com/s/mI3nDtQdlVlLv2uUTxJegA 作者文章写的 ...

  2. python计算斐波那契数列

    斐波那契数列就是黄金分割数列 第一项加第二项等于第三项,以此类推 第二项加第三项等于第四项 代码如下 这一段代码实现fib(n)函数返回第n项,PrintFN(m,n,i)函数实现输出第i项斐波那契数 ...

  3. 关于使用jwt编写接口时候对token判断时候错误的机制处理

    前言:php在使用接口时候很多时候都是需要带token的,如果不对token进行校验那么别人就能够随意编写一个token进入你的接口拿数据,应该怎样处理呢? //生成token public func ...

  4. 2018-2019-2 《Java程序设计》第9周学习总结

    20175319 2018-2019-2 <Java程序设计>第9周学习总结 教材学习内容总结 本周学习任务: 下载安装MySQL数据库管理系统. 教材介绍在官网下载安装Mysql服务,启 ...

  5. Java虚拟机—垃圾收集器(整理版)

    1.概述 如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现.Java虚拟机规范中对垃圾收集器应该如何实现并没有规定,因此不同的厂商.不同版本的虚拟机所提供的垃圾收集器都可能会有很 ...

  6. 关于APP测试的一点思考

    1  系统入口部分要细化测试用例颗粒度  充分准备好测试数据   真实覆盖线上场景 比如注册验证码的获取  国内 国外手机  一般国外手机发送短信 打电话都要加上区号 2 新版本发布   有新功能上线 ...

  7. webpack-dev-server live reloading 技术实现

    webpack-dev-server  live reloading https://github.com/webpack/webpack-dev-server Use webpack with a ...

  8. macOS Mojave配置OpenGL开发环境

    ---恢复内容开始--- 前言: 本文写作目的: 是由于本人参考 csdn原文 的方法配置环境时踩了很多坑,所以在此写一篇文防止以后用到. 工具: Xcode CMake 步骤: 准备工作: Xcod ...

  9. request redirection

    # encoding:utf-8 import reimport jsonimport randomfrom esdapi.config import BASE_URLfrom requests.se ...

  10. 【ShaderToy】抗锯齿相关函数

    *示例代码可以直接在ShaderToy中运行. *我放在这里咯ShaderToy基础学习中~欢迎交流(ノ>ω<)ノ 先上未抗锯齿的两个圆形图案,可以清楚看清图案边缘像素块,即“锯齿”. 附 ...