某东全网爬虫——scrapy_redis分布式
爬取京东的商品信息,从外层的分类,一步步深入获取商品的详情页信息。
环境:Python3.7
需求:
1、首页的分类信息:各级分类的名称和URL
2、商品信息:商品名称, 商品价格, 商品评论数量, 商品店铺, 商品促销, 商品选项, 商品图片等等
技术选择:
由于全网爬虫, 抓取页面非常多, 为了提高抓的速度, 选择使用scrapy框架 + scrapy_redis分布式组件
由于京东全网的数据量达到了亿级, 存储又是结构化数据, 所以数据库选择使用MongoDB
实现步骤
- 创建爬虫项目
- 根据需求, 定义数据数据模型
- 实现scrapy分类爬虫
- 保存分类信息
- 实现scrapy商品爬虫
- 保存商品信息
- 实现随机User-Agent和代理IP下载器中间件, 解决IP反爬
- 替换成scrapy_redis组件
- 部署到docker容器中,构建dockerfile,实现多服务器快速部署
采用广度优先策略, 我们把类别和商品信息的抓取分开来做
好处: 可以提高程序的稳定性
明确爬取数据
1、分类爬虫:
class Category(scrapy.Item):
b_category_name = scrapy.Field() # 大类别名称
b_category_url = scrapy.Field() # 大类别URL
m_category_name = scrapy.Field() # 中分类名称
m_category_url = scrapy.Field() # 中分类URL
s_category_name = scrapy.Field() # 小分类名称
s_category_url = scrapy.Field() # 小分类URL
2、商品爬虫:
class Product(scrapy.Item):
product_category = scrapy.Field() # 商品类别
product_category_id = scrapy.Field() # 商品类别ID
product_sku_id = scrapy.Field() # 商品ID
product_name = scrapy.Field() # 商品名称
product_url = scrapy.Field() # 商品详情页URL
product_img_url = scrapy.Field() # 商品图片URL
product_book_info = scrapy.Field() # 图书信息, 作者, 出版社
product_option = scrapy.Field() # 商品选项
product_shop = scrapy.Field() # 商品店铺
product_comments = scrapy.Field() # 商品评论数量
product_ad = scrapy.Field() # 商品促销
product_price = scrapy.Field() # 商品价格
分类爬虫
分析过程简单,不做说明
# -*- coding: utf-8 -*-
import scrapy
import json
from mall_spider.items import Category
class JdCategorySpider(scrapy.Spider):
name = 'jd_category'
allowed_domains = ['3.cn']
start_urls = ['https://dc.3.cn/category/get']
def parse(self, response):
# print(response.body.decode('GBK'))
result = json.loads(response.body.decode('GBK'))
datas = result['data']
# 遍历数据列表
for data in datas:
item = Category()
b_category = data['s'][0]
b_category_info = b_category['n']
# print('大分类: {}'.format(b_category_info))
item['b_category_name'], item['b_category_url'] = self.get_category_name_url(b_category_info)
# 中分类信息列表
m_category_s = b_category['s']
# 遍历中分类列表
for m_category in m_category_s:
# 中分类信息
m_category_info = m_category['n']
# print('中分类: {}'.format(m_category_info))
item['m_category_name'], item['m_category_url'] = self.get_category_name_url(m_category_info)
# 小分类数据列表
s_category_s = m_category['s']
for s_category in s_category_s:
s_category_info = s_category['n']
# print('小分类: {}'.format(s_category_info))
item['s_category_name'], item['s_category_url'] = self.get_category_name_url(s_category_info)
# print(item)
# 把数据交给引擎
yield item
@staticmethod
def get_category_name_url(category_info):
"""
根据分类信息, 提取名称和URL
:param category_info: 分类信息
:return: 分类的名称和URL
分析数据格式(三类数据格式)
- book.jd.com/library/science.html|科学技术||0
- 1713-3287|计算机与互联网||0
- Https://channel.jd.com/{}.html
- 9987-12854-12856|屏幕换新||0
- Https://list.jd.com/list.html?cat={}
- 把 - 替换为逗号, 然后填充到占位的地方.
"""
category = category_info.split('|')
# 分类URL
category_url = category[0]
# 分类名称
category_name = category[1]
# 处理第一类分类URL
if category_url.count('jd.com') == 1:
# URL进行补全
category_url = 'https://' + category_url
elif category_url.count('-') == 1:
# 1713-3287|计算机与互联网||0
category_url = 'https://channel.jd.com/{}.html'.format(category_url)
else:
# 9987-12854-12856|屏幕换新||0
# 把URL中 `-` 替换为 `,`
category_url = category_url.replace('-', ',')
# 补全URL
category_url = 'https://list.jd.com/list.html?cat={}'.format(category_url)
# 返回类别的名称 和 URL
return category_name, category_url
分类信息数据先存储到MongoDB
from pymongo import MongoClient
from mall_spider.spiders.jd_category import JdCategorySpider
from mall_spider.settings import MONGODB_URL
class CategoryPipeline(object):
def open_spider(self, spider):
"""当爬虫启动的时候执行"""
if isinstance(spider, JdCategorySpider):
# open_spider方法中, 链接MongoDB数据库, 获取要操作的集合
self.client = MongoClient(MONGODB_URL)
self.collection = self.client['jd']['category']
def process_item(self, item, spider):
# process_item 方法中, 向MongoDB中插入类别数据
if isinstance(spider, JdCategorySpider):
# self.collection.insert_one(dict(item))
# 以s_category_name过滤更新
self.collection.update({'s_category_name': item['s_category_name']}, dict(item), True)
return item
def close_spider(self, spider):
# close_spider 方法中, 关闭MongoDB的链接
if isinstance(spider, JdCategorySpider):
self.client.close()
商品爬虫
商品详情页的基本信息可以用fiddler抓包手机端数据获取,因为是json格式,解析简单。
对应的请求头要带上手机端的UA。
代码不放了
商品爬虫实现分布式
1、修改爬虫类
步骤:
1、修改继承关系: 继承RedisSpider
2、指定redis_key
3、把重写start_requests 改为 重写 make_request_from_data
from scrapy_redis.spiders import RedisSpider
import pickle
# 1. 修改继承关系: 继承RedisSpider
class JdProductSpider(RedisSpider):
name = 'jd_product'
allowed_domains = ['jd.com', 'p.3.cn']
# 2. 指定redis_key
redis_key = 'jd_product:start_category'
# 3. 把重写start_requests 改为 重写 make_request_from_data
def make_request_from_data(self, data):
# 把从Redis中读取到分类信息, 转换为字典
category = pickle.loads(data)
return scrapy.Request(category['s_category_url'], self.parse, meta={'category': category})
注意: 在make_request_from_data不能使用 yield 必须使用 return
2、在settings文件中配置scrapy_redis
# MongoDB数据库的URL
MONGO_URL = 'mongodb://127.0.0.1:27017'
# REDIS数据链接
REDIS_URL = ' redis://127.0.0.1:6379/0'
# 去重容器类: 用于把已爬指纹存储到基于Redis的set集合中
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 调度器: 用于把待爬请求存储到基于Redis的队列
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 是不进行调度持久化:
# 如果是True, 当程序结束的时候, 会保留Redis中已爬指纹和待爬的请求
# 如果是False, 当程序结束的时候, 会清空Redis中已爬指纹和待爬的请求
SCHEDULER_PERSIST = True
3、redis_key脚本程序
写一个程序用于把MongoDB中分类信息, 放入到爬虫redis_key指定的列表中
from redis import StrictRedis
from pymongo import MongoClient
import pickle
from mall_spider.settings import MONGO_URL, REDIS_URL
from mall_spider.spiders.jd_product import JdProductSpider
# 把MongoDB中分类信息, 添加到Redis中
def add_category_to_redis():
# 链接MongoDB
client = MongoClient(MONGO_URL)
# 链接Redis
redis = StrictRedis.from_url(REDIS_URL)
cursor = client['jd']['category'].find()
# 读取MongoDB中分类信息, 序列化后, 添加到商品爬虫redis_key指定的list
for category in cursor:
redis.rpush(JdProductSpider.redis_key, pickle.dumps(category))
# 关闭MongoDB的链接
client.close()
if __name__ == '__main__':
add_category_to_redis()
爬虫措施
在middlewares.py中间件中实现RandomUserAgent类和ProxyMiddleware类
User-Agent列表网上随便找
IP池根据需求,可以使用相关redis+flask实现的proxy pool,增加付费代理爬虫
或者使用阿布云之类的第三方动态http隧道代理
scrapy_redis空跑问题可以定义扩展类来解决
部署
推荐使用docker部署到服务器,利用k8s进行管理
使用gerapy实现多服务器调度管理
最后
正常使用scrapy调整并发数,多服务器运行,实现分布式增量爬虫。
爬取速度受限于IP池,一天百万数据问题不大。
千万和上亿级数据,要考虑对接布隆过滤器,以及对数据库进行调优。
代码
GitHub: