基于Scrapy+MySQL爬取国家药监局100w+数据踩坑记录

  • 1. 网页请求返回json数据的处理
  • 2. Scrapy的Request中回调函数间的信息交流
  • 3. MySQL报错:pymysql.err.InternalError: (1046, '')
    • 3.1 打印一下sql语句,同时放入Navicat中执行。
    • 3.2 检查程序连接mysql的设置是否正确,打印出配置信息检查:
  • 4. pymysql报错:AttributeError: 'NoneType' object has no attribute 'encoding' using pymysql
  • 5. Navicat中利用sql建表
    • 5.1 基本信息表
    • 5.2 详细信息表
  • 6. IP被封问题
    • 6.1 策略一:设置download_delay
    • 6.2 策略二:禁止cookies
    • 6.3 策略三:使用user-agent池
    • 6.4 策略四:使用IP池
    • 6.5 策略五:分布式爬取
  • 7. 保存至MySQL数据库去重问题
  • 8. 解决scrapy自动进行网页去重问题
  • 8.1 解决办法一:在Request中加入参数:dont_filter = True
  • 8.2 解决办法二:配置setting
  • 9 利用字典避免多重if...elif..else提升效率
  • 10. pymysql.err.DataError: (1406, '')
  • 11. scrapy爬虫的暂停与重启
  • 12 Todo

基于Scrapy+MySQL爬取国家药监局100w+数据踩坑记录

  • 1. 网页请求返回json数据的处理
  • 2. Scrapy的Request中回调函数间的信息交流
  • 3. MySQL报错:pymysql.err.InternalError: (1046, '')
    • 3.1 打印一下sql语句,同时放入Navicat中执行。
    • 3.2 检查程序连接mysql的设置是否正确,打印出配置信息检查:
  • 4. pymysql报错:AttributeError: 'NoneType' object has no attribute 'encoding' using pymysql
  • 5. Navicat中利用sql建表
    • 5.1 基本信息表
    • 5.2 详细信息表
  • 6. IP被封问题
    • 6.1 策略一:设置download_delay
    • 6.2 策略二:禁止cookies
    • 6.3 策略三:使用user-agent池
    • 6.4 策略四:使用IP池
    • 6.5 策略五:分布式爬取
  • 7. 保存至MySQL数据库去重问题
  • 8. 解决scrapy自动进行网页去重问题
  • 8.1 解决办法一:在Request中加入参数:dont_filter = True
  • 8.2 解决办法二:配置setting
  • 9 利用字典避免多重if...elif..else提升效率
  • 10. pymysql.err.DataError: (1406, '')
  • 11. scrapy爬虫的暂停与重启
  • 12 Todo

1. 网页请求返回json数据的处理

Scrapy的返回对象Response没有json()方法,所以如果请求结果是json数据的话,必须用json.loads()方法处理成python可以处理的json对象。即

result = json.loads(response.text)

2. Scrapy的Request中回调函数间的信息交流

Scrapy中Request的回调函数间的信息传递,在scrapy 1.7之前的版本是通过meta参数传递实现,最新的scrapy 1.7开始推荐使用cb_kwargs进行传递,无法通过return返回。比如start_requests(self)方法中有:total_pages = yield Request(url, callback=self.get_total_pages),尽管在get_total_pages(self, response)方法中有return total pages, 但是返回到start_requests(self)方法中的total_pages是None,而不是get_total_pages()返回的total_pages。示例:

# 错误的方法def start_requests(self):"""发起初始请求:return: """url = 'http://mobile.cfda.gov.cn/datasearch/QueryList?tableId=36&searchF=Quick%20Search&pageIndex=1&pageSize=15'total_pages = yield Request(url, callback=self.get_total_pages)print(total_pages)def get_total_pages(self, response):"""获取目录页总页数:param url::return: total_pages"""# 要用json.loads()将字符串转换为json数据total_items = json.loads(response.text)[0]['COUNT'] # 获取总条数# 获取总页数,每页15条数据,所以页数=总条数/15,如果巧好整除则+1,否则+2if total_items/15 == int(total_items/15):total_pages = int(total_items/15) + 1else:total_pages = int(total_items/15) + 2return total_pages# 推荐的方法def start_requests(self):"""发起初始请求:return:"""# 1.数据库实例初始化self.mysql = MysqlClient(self.settings)table_id = 25page_index = 1logger.info('开始发起请求...')content_url = self.content_base_url.format(table_id=table_id, page_index=page_index)# Request跟进我们的URL,并将response作为参数传递给self.get_total_pages,典型的回调函数yield Request(content_url, callback=self.get_total_pages, cb_kwargs=dict(table_id=table_id))def get_total_pages(self, response, table_id):"""获取目录页总页数:param url::return: total_pages"""# 要用json.loads()将字符串转换为json数据total_items = json.loads(response.text)[0]['COUNT'] # 获取总条数# print(total_items)# 获取总页数,每页15条数据,所以页数=总条数/15,如果巧好整除则+1,否则+2if total_items/15 == int(total_items/15):total_pages = int(total_items/15) + 1else:total_pages = int(total_items/15) + 2# table_id = response.meta['table_id'] # 这是原来meta的用法,现在已经不推荐这么传参数count = 0for page_index in range(1, total_pages):count += 1content_url = self.content_base_url.format(table_id=table_id, page_index=page_index)if count > 50:count = 0# time.sleep(random.uniform(1, 2)) # 请求延迟# 请求产品的目录页yield Request(url=content_url, callback=self.parse_content, cb_kwargs=dict(table_id=table_id))

3. MySQL报错:pymysql.err.InternalError: (1046, ‘’)

这个问题花了很多时间,总结一下解决的思路和流程。

3.1 打印一下sql语句,同时放入Navicat中执行。

打印sql语句的方法:

print('开始执行sql语句')
sql = self.cursor.mogrify(sql)
print(sql)

如果Navicat中执行成功,则说明sql语句没有问题,问题出在连接数据库上面。转入步骤2。
如果Navicat中执行失败,说明sql语句有问题,检查表的名字、字段名是否意义对应,以及其中是否包含特殊字符,有特殊字符的表名或者字段名需要用”``“包裹,比如有“/”。中文的建议用”`字段名`“或者"`表名`".

3.2 检查程序连接mysql的设置是否正确,打印出配置信息检查:

print('开始连接MySQL数据库')
print('打印主机号:{}'.format(self.host))
print(self.user)
print(self.password)
print(self.db)
print(self.charset)
print(self.port)

4. pymysql报错:AttributeError: ‘NoneType’ object has no attribute ‘encoding’ using pymysql

原因是:charset编码问题,pymysql世界里,“utf8”是一个有效的字符集,但“utf-8”不是。具体参考:https://stackoverflow.com/questions/49828342/attributeerror-nonetype-object-has-no-attribute-encoding-using-pymysql

self.conn = pymysql.connect(host=self.host, user=self.user, password=self.password, db=self.db,port=self.port,charset=self.charset)
# self.charset不能是'utf-8',可以是'utf8'

5. Navicat中利用sql建表

一个非常好用的测试sql语法的网站:sql测试

5.1 基本信息表

这个表通用,只要把表名替换一下就可以了。

-- 进口药品基本表
DROP TABLE IF EXISTS `jkyp_content_info`;
CREATE TABLE `jkyp_content_info` (`id` int(11) NOT NULL AUTO_INCREMENT,`product_count` INT(11) DEFAULT NULL,`product_info` VARCHAR(255) DEFAULT NULL,`product_id` INT(11) DEFAULT NULL,`url` VARCHAR(255) DEFAULT NULL,`crawl_time` VARCHAR(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5.2 详细信息表

详细表每个都需要单独建一个表和scrapy.Field()字段比较麻烦。
不过这里我写了个小脚本帮助来偷点懒。

s = """批准文号
产品名称
...
原批准文号
药品本位码
药品本位码备注
注
"""s = s.split('\n') # 转换成列表
ls_1 = list()
ls_2 = list()
# 建表用
for each in s[:-1]:ls_1.append('`' + each + '`' + ' VARCHAR(255) DEFAULT NULL,')
for each in ls_1:print(each)print('='*100)
# 建scrapy.Field()用
ls_2.append('product_id = scrapy.Field()')
for each in s[:-1]:ls_2.append(each + ' = scrapy.Field()')
ls_2.extend(['url = scrapy.Field()', 'crawl_time = scrapy.Field()'])
for each in ls_2:print(each)

屏幕会自动输出符合格式的scrapy.Field()和建表部分,直接复制粘贴完善一些就好。省事很多。

-- 国产药品详细信息
DROP TABLE IF EXISTS `gcyp_detail_info`;
CREATE TABLE `gcyp_detail_info` (`id` int(11) NOT NULL AUTO_INCREMENT,`product_id` INT(11) DEFAULT NULL,`批准文号` VARCHAR(255) DEFAULT NULL,`产品名称` VARCHAR(255) DEFAULT NULL,`英文名称` VARCHAR(255) DEFAULT NULL,`商品名` VARCHAR(255) DEFAULT NULL,`剂型` VARCHAR(255) DEFAULT NULL,`规格` VARCHAR(255) DEFAULT NULL,`上市许可持有人` VARCHAR(255) DEFAULT NULL,`生产单位` VARCHAR(255) DEFAULT NULL,`生产地址` VARCHAR(255) DEFAULT NULL,`产品类别` VARCHAR(255) DEFAULT NULL,`批准日期` VARCHAR(255) DEFAULT NULL,`原批准文号` VARCHAR(255) DEFAULT NULL,`药品本位码` VARCHAR(255) DEFAULT NULL,`药品本位码备注` VARCHAR(255) DEFAULT NULL,`注` VARCHAR(255) DEFAULT NULL,`url` VARCHAR(255) DEFAULT NULL,`crawl_time` VARCHAR(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

6. IP被封问题

参考文献:Scrapy研究探索(七)——如何防止被ban之策略大集合

6.1 策略一:设置download_delay

download_delay的作用主要是设置下载的等待时间,大规模集中的访问对服务器的影响最大,相当与短时间中增大服务器负载。
下载等待时间长,不能满足段时间大规模抓取的要求,太短则大大增加了被ban的几率。
使用注意:
download_delay可以设置在settings.py中,也可以在spider中设置。

6.2 策略二:禁止cookies

所谓cookies,是指某些网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密),禁止cookies也就防止了可能使用cookies识别爬虫轨迹的网站得逞。
使用:
在settings.py中设置COOKIES_ENABLES=False。也就是不启用cookies middleware,不想web server发送cookies。

6.3 策略三:使用user-agent池

所谓的user-agent,是指包含浏览器信息、操作系统信息等的一个字符串,也称之为一种特殊的网络协议。服务器通过它判断当前访问对象是浏览器、邮件客户端还是网络爬虫。在request.headers可以查看user agent。
scrapy本身是使用Scrapy/版本号 来表明自己身份的。这也就暴露了自己是爬虫的信息

6.4 策略四:使用IP池

web server应对爬虫的策略之一就是直接将你的IP或者是整个IP段都封掉禁止访问,这时候,当IP封掉后,转换到其他的IP继续访问即可。
可以使用Scrapy+Tor+polipo
配置方法与使用教程可参见:http://pkmishra.github.io/blog/2013/03/18/how-to-run-scrapy-with-TOR-and-multiple-browser-agents-part-1-mac/。

6.5 策略五:分布式爬取

这个,内容就更多了,针对scrapy,也有相关的针对分布式爬取的GitHub repo。可以搜一下。

7. 保存至MySQL数据库去重问题

使用sql语句,添加去重函数:

    def _select_product_id(self, query_sql=''):"""去重函数,查找product_id字段,如果存在返回[(1,1)],不存在则会返回():param query_sql::return:"""self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)self.cursor.execute("set names utf8") # utf8 字符集self.cursor.execute(query_sql)# sql = self.cursor.mogrify(query_sql)# print(sql)# print(self.cursor.fetchall())return self.cursor.fetchall() # cursor.fetchall()只能调用一次,再次调用就返回()空元组def _insert(self, insert_sql=''):"""执行插入MySQL:param insert_sql::return:"""try:self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)self.cursor.execute("set names utf8") # utf8 字符集# insert_sql = self.cursor.mogrify(insert_sql)# print(insert_sql)self.cursor.execute(insert_sql)self.conn.commit()return Trueexcept Exception as e:logger.error('err:s% : %s', insert_sql, str(e))self.conn.rollback()self.cursor.close()return Falsedef save_base_info(self, item):"""保存信息到MySQL数据库:param item::return:"""try:table = item.tableproduct_id = item['product_id']# keys = ', '.join(item.keys())keys = '`' + '`, `'.join(item.keys()) + '`' # 因为是item类名称有中文values = ', '.join(['%s'] * len(item))insert_sql = f'INSERT INTO `{table}`({keys}) VALUES ({values})'insert_sql = insert_sql % tuple([r"'" + pymysql.escape_string(value) + r"'" if isinstance(value, str) else valuefor value in item.values()])# query_sql = f'SELECT EXISTS (SELECT 1 FROM `{table}` WHERE product_id={product_id})'query_sql = f'SELECT 1 FROM `{table}` WHERE product_id={product_id}'if self._select_product_id(query_sql):logger.info(f'{product_id}已存在MySQL数据库中')return Trueelse:return self._insert(insert_sql)except Exception as e:logger.error('保存信息失败,item=%s, err=%s', item, str(e))return None

8. 解决scrapy自动进行网页去重问题

参考链接Scrapy - Filtered duplicate request
因为在获取目录页总页数时,访问过一次page_index=1的url,是scrapy有基于布隆过滤器对url进行去重,导致后面存储时,这个网址无法再次抓取。

8.1 解决办法一:在Request中加入参数:dont_filter = True

这种办法需要在每一个Request加入dont_filter=True参数。

yield Request(content_url, callback=self.get_total_pages, cb_kwargs=dict(table_id=table_id), dont_filter = True)

8.2 解决办法二:配置setting

可以一步到位,不用一个个去设置Request。

# settings.py
DUPEFILTER_CLASS = 'scrapy.dupefilters.BaseDupeFilter'

9 利用字典避免多重if…elif…else提升效率

因为涉及到很多个table_id,如果用if…elif…else…需要写的特别长,而且效率会受影响。如下:

        if table_id == 36:item = JKYPDetailItem()elif table_id == 25:item = GCYPDetailItem()elif table_id == 26:item = GCQXDetailItem()elif table_id == 27:item = JKQXDetailItem()elif table_id == 68:item = GCTSYTHZPDetailItem()elif table_id == 69:item = JKTSYTHZPDetailItem()elif table_id == 122:item = ZYYSDetailItem()elif table_id == 96:item = WSYDDetailItem()else:logger.error('表格号有误,请确认输入表格号是否正确!')return Noneprint(item.table)

采用字典优化后

 # 采用字典, 避免过多的if...elif...else,提升效率,简化代码detail_items = {36:JKYPDetailItem(), 25:GCYPDetailItem(), 26:GCQXDetailItem(), 27: JKQXDetailItem(),68: GCTSYTHZPDetailItem(), 69: JKTSYTHZPDetailItem(), 122: ZYYSDetailItem(),96: WSYDDetailItem()}item = detail_items.get(table_id)if not item:logger.error('表格号有误,请确认输入表格号是否正确!')

代码量减少了,而且执行也会变快。

10. pymysql.err.DataError: (1406, ‘’)

因为数据量大于了设置的数据类型。对于文本类型,超过了varchar(255),则需要设置为LONGTEXT类型, 把表的字段类型修改即可。

11. scrapy爬虫的暂停与重启

启动时变为:

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

然后按ctrl+C暂停,再使用启动即可,注意不同的爬虫,要使用不同的文件夹。

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

12 Todo

  1. 日志的配置不够灵活,后期需要优化。
  2. 构建一个IP池,不再设置下载延迟,提升效率。。

后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技,扫描下方二维码或者搜索每日学一技关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!

基于Scrapy+MySQL爬取国家药监局100w+数据踩坑记录相关推荐

  1. 基于Scrapy框架爬取豆瓣《复联4》影评,并生成词云

    基于Scrapy框架爬取豆瓣<复联4>影评,并生成词云 1. 介绍及开发环境 2. 爬虫实现 2.1 新建项目 2.2 构造请求 2.3 提取信息 2.4 数据存储 2.4 运行结果 3. ...

  2. Scrapy+MySQL爬取去哪儿网

    Scrapy+MySQL爬取去哪儿旅游[超详细!!!] 基于Python语言,利用Scrapy框架爬取信息,并持久化存储在MySQL 文章目录 Scrapy+MySQL爬取去哪儿旅游[超详细!!!] ...

  3. scrapy获取a标签的连接_python爬虫——基于scrapy框架爬取网易新闻内容

    python爬虫--基于scrapy框架爬取网易新闻内容 1.需求[前期准备] 2.分析及代码实现(1)获取五大板块详情页url(2)解析每个板块(3)解析每个模块里的标题中详情页信息 点击此处,获取 ...

  4. 19. python爬虫——基于scrapy框架爬取网易新闻内容

    python爬虫--基于scrapy框架爬取网易新闻内容 1.需求 [前期准备] 2.分析及代码实现 (1)获取五大板块详情页url (2)解析每个板块 (3)解析每个模块里的标题中详情页信息 1.需 ...

  5. 爬虫案例之爬取国家药监局化妆品生产许可明细(爬取动态加载数据)

    一.实验目的 爬取国家药监局(化妆品生产许可信息管理系统服务平台 (nmpa.gov.cn))化妆品生产明细(具体到每家企业的具体信息),当我们进入该网站首页时,发现其结构为每页15条的json类型数 ...

  6. 14. python爬虫——基于scrapy框架爬取糗事百科上的段子内容

    python爬虫--基于scrapy框架爬取糗事百科上的段子内容 1.需求 2.分析及实现 3.实现效果 4.进行持久化存储 (1)基于终端指令 (2)基于管道 [前置知识]python爬虫--scr ...

  7. 爬虫项目实战一:基于Scrapy+MongDB爬取并存储糗事百科用户信息

    爬虫项目实战一:基于Scrapy+MongDB爬取并存储糗事百科用户信息 一.前言 二.项目目标 三.项目的环境配置 四.项目实现 1. 创建QSBK项目 2. 实现Spider 1. 实现一页网页上 ...

  8. 基于Webmagic的爬取B站用户数据的爬虫

    基于Webmagic的爬取B站用户数据的爬虫 github: https://github.com/Al-assad/Spider-bilibiliUser-active 数据示例样本:http:// ...

  9. scrapy框架爬取王者荣耀英雄数据

    scrapy框架爬取王者荣耀英雄属性 爬虫工程 爬虫文件 import scrapy from theKingPro.items import ThekingproItemclass ThekingS ...

  10. BeautifulSoup+scrapy+MySQL爬取人人词典

    完整代码 https://github.com/CarryHJR/91dict 需求 爬取人人词典 人人词典最好的地方就是根据单词可以搜到语境,而且有对应的mp3音频,凭直觉这个可能会有版权问题,所以 ...

最新文章

  1. 图片上加动图怎么弄_用PS把千张图片拼成心爱的人的样子,只需三步!
  2. 白白浪费了这满园春色
  3. 指针应用-----链表二
  4. 在服务器托管中对流量和带宽进行限制
  5. java model.put_深入理解Java内存模型(一)——基础
  6. 大话设计模式—适配器模式
  7. Maven 中的pom.xml文件
  8. MySQL保留关键字
  9. php 手机端播放器,用JS代码适配电脑端和手机端播放器代码
  10. Too many input arguments.
  11. mac nginx 安装及PHP配置
  12. 胡萝卜,是鸡蛋,还是咖啡豆
  13. LINUX模拟键盘F5的脚本(未试过)
  14. WAS中间件服务器简介
  15. Android常用高质量框架
  16. LVGL 8.2图片缩放及旋转
  17. DSPE-PEG1-COOH可与伯胺基反应的反应性磷脂PEG共轭物之一
  18. 贪心(优先队列) - New Year Snowmen - CodeForces - 140C
  19. 正负筛选(neo正向+HSV-tk负向)原理
  20. Java 开发验证码。随机产生一个四位数的验证码,每位数可能是数字、大写字母或小写字母。

热门文章

  1. 如何通过WebEx Meeting进行远程IT协作?
  2. 从阿里投资B站看动漫IP,二次元市场蕴含了怎样的价值?
  3. php gb2312转big5 函数
  4. WPS简历模板的图标怎么修改_桌面图标怎么修改?自定义软件图标的操作方法
  5. QThread 应用浅析
  6. 中国智慧能源行业行情监测及未来动向规划预测报告2022-2028年
  7. Android11安装谷歌,Android 11正式版
  8. 写个爬虫,爬图片,mzitu
  9. 前端学习笔记-22-浏览器中的DOM操作
  10. TextCNN(文本分类)