当前位置:   article > 正文

爬虫基础篇之Scrapy抓取京东

爬虫基础篇之Scrapy抓取京东

虚拟环境

同一台服务器上不同的项目可能依赖的包不同版本,新版本默认覆盖旧版本,可能导致其他项目无法运行,通过虚拟环境,完全隔离各个项目各个版本的依赖包,实现运行环境互不影响。

virtualenv

pip install virtualenv		安装virtualenv
python -m pip install --upgrade pip  升级pip
pip install -i https://pypi.doubanio.com/simple/ --trusted-host pypi.doubanio.com scrapy
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple  使用清华源
pip uninstall scrapy    卸载django 
virtualenv scrapytest   默认环境创建虚拟环境
cd scrapytest/Scripts &&  activate.bat && python 进入3.7虚拟环境
virtualenv -p D:\Python27\python.exe scrapytest
cd scrapytest/Scripts &&  activate.bat && python 进入2.7虚拟环境
deactivate.bat          退出虚拟环境

apt-get install python-virtualenv       安装虚拟环境
virtualenv py2 && cd py2 && cd bin && source activate && python 进入2.7虚拟环境
virtualenv -p /usr/bin/python3 py3 && && cd py3 && cd bin && source activate && python  进入3.7虚拟环境
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

virtualenvwrapper

pip install virtualenvwrapper
pip install virtualenvwrapper-win	解决workon不是内部指令
workon  列出所有虚拟环境
新建环境变量   WORKON_HOME=E:\envs
mkvirtualenv py3scrapy  新建并进入虚拟环境
deactivate          退出虚拟环境
workon py3scrapy        进入指定虚拟环境
    pip install -i https://pypi.douban.com/simple scrapy    安装scrapy源
    若缺少lxml出错https://www.lfd.uci.edu/~gohlke/pythonlibs/寻找对应版本的lxml的whl源
    python -m pip install --upgrade pip     更新pip
    pip install lxml-4.1.1-cp35-cp35m-win_amd64.whl
    若缺少Twisted出错http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml搜对应版本Twisted
    pip install Twisted‑17.9.0‑cp35‑cp35m‑win_amd64.whl
mkvirtualenv --python=D:\Python27\python.exe py2scrapy      一般不会出问题
    pip install -i https://pypi.douban.com/simple scrapy
    
    
pip install virtualenvwrapper    
    find / -name virualenvwrapper.sh
    vim ~/.bashrc
        export WORKON_HOME=$HOME/.virtualenvs
        source /home/wj/.local/bin/virtualenvwrapper.sh
    source ~/.bashrc    
mkvirtualenv py2scrapy          指向生成~/.virtualenv
deactivate          退出虚拟环境
mkdirtualenv --python=/usr/bin/python3 py3scrapy
rmvirtualenv py3scrapy  删除虚拟环境
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

Scrapy

在这里插入图片描述

pip install -i https://pypi.douban.com/simple/ scrapy  安装scrapy
scrapy startproject mall_spider  创建mall_spider项目
scrapy genspider jd_category https://dc.3.cn/category/get  创建分类爬虫
scrapy genspider --list  查看爬虫生成模板
scrapy genspider -t crawl lagou www.lagou.com   创建全站爬虫
pip freeze > requirements.txt 生成依赖到文件
pip install -r requirements.txt 一键安装依赖
scrapy shell http://blog.jobbole.com/       可以在脚本中调试xpath或者chrome浏览器右键copy xpath,chrome浏览器右键copy selector
scrapy shell -s USER_AGENT="Mozilla/5.0 (Windows NT 6.1; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0" https://www.zhihu.com/question/56320032
view(response)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

需求

1.1 抓取首页的分类信息

  • 抓取数据: 各级分类的名称 和 URL

在这里插入图片描述

1.2 抓取商品信息

  • 抓取: 商品名称, 商品价格, 商品评论数量, 商品店铺, 商品促销, 商品选项, 商品图片的URL
    在这里插入图片描述

  • 由于全网爬虫, 抓取页面非常多, 为了提高抓的速度, 选择使用scrapy框架 + scrapy_redis分布式组件

  • 由于京东全网的数据量达到了亿级, 存储又是结构化数据, 数据库, 选择使用MongoDB;

实现

我们采用广度优先策略, 我们把类别和商品信息的抓取分开来做.

在这里插入图片描述

模型

类别模型
class Category(scrapy.Item):
    """商品类别"""
    # 大分类名称
    b_category_name = scrapy.Field()
    # 大分类URL
    b_category_url = scrapy.Field()
    # 中分类名称
    m_category_name = scrapy.Field()
    # 中分类URL
    m_category_url = scrapy.Field()
    # 小分类名称
    s_category_name = scrapy.Field()
    # 小分类URL
    s_category_url = scrapy.Field()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
数据模型
class Product(scrapy.Item):
    # 商品类别
    product_category = scrapy.Field()
    # 商品ID
    product_sku_id = scrapy.Field()
    # 商品名称
    product_name = scrapy.Field()
    # 商品图片URL
    product_img_url = scrapy.Field()
    # 商品店铺
    product_shop = scrapy.Field()
    # 图书信息, 作者,出版社
    product_book_info = scrapy.Field()
    # 商品选项
    product_option = scrapy.Field()
    # 商品评论数量
    product_comments = scrapy.Field()
    # 商品促销
    product_ad = scrapy.Field()
    # 商品价格
    product_price = scrapy.Field()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

分类爬虫

分析, 分类信息的URL
  • 目标: 确定分类信息的URL

  • 步骤:

    1. 进入到京东首页
    2. 右键检查, 打开开发者工具, 搜索 家用电器
    3. 确定分类的URL
  • 图解:
    在这里插入图片描述

  • 结论:

    • 分类URL: https://dc.3.cn/category/get
创建爬虫, 抓取数据
  • 目标: 抓取分类数据, 交给引擎
  • 步骤:
    1. 创建类别爬虫
    2. 指定起始URL
    3. 解析数据, 交给引擎
创建爬虫
  • 进入项目目录: cd mall_spider
  • 创建爬虫: scrapy genspider category_spider jd.com
指定起始URL
  • 修改起始URL: https://dc.3.cn/category/get
解析数据, 交给引擎
  • 分析数据格式:
    • 整体数据
      在这里插入图片描述

    • 各级分类位置
      在这里插入图片描述

    • 分类信息格式

      • 格式1:
        • jiadian.jd.com|家用电器||0
        • 特点: 第一项分类URL,第二项分类名称
      • 格式2:
        • 652-654|摄影摄像||0
        • 对应的URL: https://channel.jd.com/652-654.html
        • 特点:第一项是频道ID, 包含一个 -
      • 格式3:
        • 1318-2628-12131|户外风衣||0
        • 对应URL: https://list.jd.com/list.html?cat=1318,2628,12131
        • 特点: 第一项为分类ID, 包含两个 -
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

    def get_category_name_url(self, 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

保存分类数据

# 在settings.py开启, 类别的Pipeline
ROBOTSTXT_OBEY = False  不遵守网络协议
ITEM_PIPELINES = {
   'mall_spider.pipelines.CategoryPipeline': 300,
}
  • 1
  • 2
  • 3
  • 4
  • 5

步骤:

  1. open_spider方法中, 链接MongoDB数据库, 获取要操作的集合
  2. process_item 方法中, 向MongoDB中插入类别数据
  3. close_spider 方法中, 关闭MongoDB的链接
"""
实现保存分类的Pipeline类
- open_spider方法中, 链接MongoDB数据库, 获取要操作的集合
- process_item 方法中, 向MongoDB中插入类别数据
- close_spider 方法中, 关闭MongoDB的链接
"""

class CategoryPipeline(object):

    def open_spider(self, spider):
        """当爬虫启动的时候执行1次"""
        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))

        return item

    def close_spider(self, spider):
        # close_spider 方法中, 关闭MongoDB的链接
        if isinstance(spider, JdCategorySpider):
            self.client.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

商品爬虫

总体设计:

  1. 把MongoDB中存储的分类信息, 放到redis_key指定列表中
  2. 支持分布式爬虫, 当然也可以在一台电脑上运行多次, 以启动多个进程,充分使用CPU的多核.
  3. 所以这里的爬虫, 先从一个分类开始抓就可以了, 后面再改造为分布式
分析
  • 列表页

    • 提取商品 skuid
      在这里插入图片描述

    • 实现翻页

      • 获取下一页URL
        在这里插入图片描述

      • 没有下一页的情况
        在这里插入图片描述

  • 详情页
    由于PC和手机页面商品信息, 在js中, 且比较分散, 并且每次请求数量页比较大, 我们这里使用手机抓包, 抓到json数据.

  • 商品基本信息

    • 图:
      在这里插入图片描述

    • URL: https://cdnware.m.jd.com/c1/skuDetail/apple/7.3.0/32426231880.json; 最后一部分是商品skuid

    • 可以获取到的信息: 商品名称, 商品店铺信息 , 商品类别id, 商品品牌id, 商品选项

      {
    "code": "0",
    "wareInfo": {
    	"recommendInfo": {
    		"recommendList": null
    	},
          // 商品店铺信息
    	"shopInfo": {
    		"shop": {
    			"shopId": 1000000127,
    			"name": "京东Apple产品专营店",
    		    ...
    		},	
    	"basicInfo": {
    		"gift": false,
    		"bookInfo": {
                  // 如果是书, 这里是书的选项信息
    			"display": false
    		},
    	
    		"colorSizeInfo": {
                  // 商品选项信息列表 有的没有
    			"colorSize": [{
    				"buttons": [{
    					"no": "1",
    					"skuList": ["100000177738", "100000287117", "100000287145", "100000309448", "100000309450", "100000375233", "100000435832", "100000458753", "100000458755", "100001860767", "100001860773"],
    					"text": "金色"
    				}, {
    					"no": "2",
    					"skuList": ["100000177764", "100000287113", "100000287135", "100000435780", "100000435816", "100000435818", "100000569049", "100000602206", "100000602208", "100001860765", "100002539302"],
    					"text": "深空灰色"
    				}, {
    					"no": "3",
    					"skuList": ["100000177740", "100000177784", "100000287147", "100000435834", "100000458737", "100000458739", "100000602174", "100000602176", "100000602204", "100001860789", "100002539304"],
    					"text": "银色"
    				}],
    				"title": "颜色"
    			}, {
    				"buttons": [{
    					"no": "1",
    					"skuList": ["100000177738", "100000177740", "100000177764", "100000177784", "100000287113", "100000287117", "100000287135", "100000287145", "100000287147"],
    					"text": "公开版"
    				},
                      ...
                      ],
    				"title": "版本"
    			}, {
    				"buttons": [{
    					"no": "1",
    					"skuList": ["100000177764", "100000287145", "100000287147", "100000375233", "100000435818", "100000458739", "100000458755", "100000602204", "100000602208", "100001860765", "100001860773", "100001860789"],
    					"text": "64GB"
    				}, 
                      ...
                      ],
    				"title": "内存"
    			}],
    			"colorSizeTips": "#与其他已选项无法组成可售商品,请重选"
    		},
    	    ...
              // 品牌ID
    		"brandID": "14026",
              ...
              // 商品图片
    		"wareImage": [{
    			"small": "https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/3/15/4536/138660/5b997bf8Ed72ebce7/819dcf182d743897.jpg!q70.jpg.webp",
                  ...
    		  }
                ...
              ],
              ...
              // 商品名称
    		"name": "Apple iPhone XS Max (A2104) 256GB 深空灰色 移动联通电信4G手机 双卡双待",
              // 商品类别id
    		"category": "9987;653;655"
              }
          }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
  • 商品促销信息(PC端):

    • 图:
      在这里插入图片描述

    • URL: https://cd.jd.com/promotion/v2?skuId=4749506&area=1_72_4137_0&cat=737%2C794%2C798

      • 参数
        • skuId=4749506: 商品sku_id
        • area=1_72_4137_0: 购买者区域, 固定的
        • cat=737%2C794%2C798: 类别
    • 数据

    {
      ...
      // 商品促销信息
      "ads": [{
          "id": "AD_4749506",
          "ad": "【即刻预约,21号秒杀到手价2999】\n1、前100名晒单送腾讯企鹅影院季卡,联系客服领取!!\n2、曲面爆款,5.5万好评推荐!<a target=\"_blank\" href=\"https://item.jd.com/7055876.html\">升级55Q1D超清全面屏电视</a>"
      }],
      ...
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 商品评论信息(PC端)

    • 图:
      在这里插入图片描述

    • URL: https://club.jd.com/comment/productCommentSummaries.action?referenceIds=4749506

      • 参数
        • referenceIds=4749506: 商品sku_id
    • 数据:

      {"CommentsCount":[
          {
              "CommentCountStr":"10万+", 
              "CommentCount":100000, //评论数量
              "AverageScore":5,
              "GoodRate":0.98, //好评率
              "PoorCountStr":"600+", 
              "PoorCount":600, // 差评数量
              ...
          }]}
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
  • 商品价格信息:

    • 图:
      在这里插入图片描述

    • URL: https://p.3.cn/prices/mgets?skuIds=J_4749506

      • 参数:
        • skuIds=J_4749506 商品的sku_id
    • 数据:

      [
        {
            "op": "5499.00",
            "m": "5999.00",
            "id": "J_4749506", //商品skuid
            "p": "3299.00" // 商品价格
            }
        ]
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
代码实现
  • 步骤:
    1. 重写start_requests方法, 根据分类信息构建列表页的请求
    2. 解析列表页, 提取商品的skuid, 构建商品基本的信息请求; 实现翻页
    3. 解析商品基本信息, 构建商品促销信息的请求
    4. 解析促销信息,构建商品评价信息的请求,
    5. 解析商品评价信息, 构建价格信息的请求
    6. 解析价格信息
class JdProductSpider(scrapy.Spider):
    name = 'jd_product'
    allowed_domains = ['jd.com', 'p.3.cn']

    def start_requests(self):
        category = {  "b_category_name" : "家用电器",
                      "b_category_url" : "https://jiadian.jd.com",
                      "m_category_name" : "洗衣机",
                      "m_category_url" : "https://list.jd.com/list.html?cat=737,794,880",
                      "s_category_name" : "洗衣机配件",
                      "s_category_url" : "https://list.jd.com/list.html?cat=737,794,877" }

        yield scrapy.Request(category['s_category_url'], self.parse, meta={'category': category})

    def parse(self, response):
        # 获取类别信息
        category = response.meta['category']
        # 获取类别的URL
        category_url = response.url.split('&')[0]
        # 获取所有商品的sku_ids
        sku_ids = response.xpath('//div[contains(@class, "j-sku-item")]/@data-sku').extract()
        # 遍历sku_ids, 构建基本详情信息的请求
        for sku_id in sku_ids:
            item = {
                 'product_category': category,
                 'product_sku_id':sku_id
            }
            product_url = 'https://cdnware.m.jd.com/c1/skuDetail/apple/7.3.0/{}.json'.format(sku_id)
            yield scrapy.Request(product_url, callback=self.parse_product, meta={'item': item})


        # 获取下一页的URL
        next_url = response.xpath('//a[@class="pn-next"]/@href').extract_first()
        if next_url:
            # 补全URL
            next_url = response.urljoin(next_url)
            # 构建下一页请求
            yield scrapy.Request(next_url, callback=self.parse, meta={'category': category})

    def parse_product(self, response):
        # 取出传递过来的数据
        item = response.meta['item']
        # 把响应数据数据转为字典
        product_dic = json.loads(response.text)

        # 获取商品名称
        item['product_name'] = product_dic['wareInfo']['basicInfo']['name']
        if  item['product_name']:
            # 获取类别id, 把 `;` 替换为 ,
            item['product_category_id'] = product_dic['wareInfo']['basicInfo']['category'].replace(';', ',')
            # 获取店铺信息
            product_shop = jsonpath(product_dic, '$..shop')
            if product_shop:
                product_shop = product_shop[0]
                if product_shop is None:
                    item['product_shop'] = {'name':'京东自营'}
                else:
                    item['product_shop'] = {
                        "shopId": product_shop['shopId'],
                        "name": product_shop['name'],
                        "score": product_shop['score'],
                        "url": product_shop['url'],
                    }

            # 如果是书, 记录书的信息
            if product_dic['wareInfo']['basicInfo']['bookInfo']['display']:
                item['product_book_info'] = product_dic['wareInfo']['basicInfo']['bookInfo']
                # 删除display
                del item['book_info']['display']
            # 获取商品选购信息
            color_sizes = jsonpath(product_dic, '$..colorSize')
            product_option = {}
            if color_sizes:
                for color_size in color_sizes[0]:
                    title = color_size['title']
                    texts = jsonpath(color_size, '$..text')
                    product_option.update({title:texts})
                    # print(product_option)
            item['product_option'] = product_option
            # 商品图片
            item['product_img_url'] = jsonpath(product_dic, '$..wareImage[0].small')[0]

            # 构建促销信息的请求
            ad_url = 'https://cd.jd.com/promotion/v2?skuId={}&area=1_72_4137_0&cat={}'.format(item['product_sku_id'], item['product_category_id'])
            yield scrapy.Request(ad_url, callback=self.parse_ad, meta={'item': item})

    def parse_ad(self, response):
        """获取商品促销"""
        item = response.meta['item']
        ad_dic = json.loads(response.body.decode('GB18030'))
        ad =  ad_dic['ads'][0]['ad']
        item['product_ad'] = ad

        # for key, value in item.items():
        #     print('{} = {}'.format(key, value))

        # 构建平均信息请求
        comments_url = 'https://club.jd.com/comment/productCommentSummaries.action?referenceIds={}'.format(item['product_sku_id'])
        yield scrapy.Request(comments_url, callback=self.parse_comments, meta={'item': item})

    def parse_comments(self, response):
        """解析商品评论信息"""
        item = response.meta['item']
        comments_dic = json.loads(response.text)
        comments = {
            'comment_count': jsonpath(comments_dic, '$..CommentCount')[0],
            'good_rate': jsonpath(comments_dic, '$..GoodRate')[0],
            'poor_count': jsonpath(comments_dic, '$..PoorCount')[0],
        }
        item['product_comments'] = comments
        # print(item)
        # 构建价格请求
        price_url = 'https://p.3.cn/prices/mgets?skuIds=J_{}'.format(item['product_sku_id'])
        yield scrapy.Request(price_url, callback=self.parse_price, meta={'item': item})

    def parse_price(self, response):
        """解析价格"""
        item = response.meta['item']
        item['product_price'] = json.loads(response.text)[0]['p']
        # print(item)
        yield item
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
分布式
  • 步骤:
    1. 修改爬虫类
    2. 在settings文件中配置scrapy_redis
    3. 写一个程序用于把MongoDB中分类信息, 放入到爬虫redis_key指定的列表中

修改爬虫类

  • 步骤:
    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})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

注意: 在make_request_from_data不能使用 yield 必须使用 return

在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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

把MongoDB中分类信息, 放入到爬虫redis_key指定的列表中

  • 步骤:

      1. 在项目文件夹下创建 add_category_to_redis.py
      1. 实现方法 add_category_to_redis:
        1. 链接MongoDB
        2. 链接Redis
        3. 读取MongoDB中分类信息, 序列化后, 添加到商品爬虫redis_key指定的list
        4. 关闭MongoDB
      1. if __name__ == '__main__':中调用add_category_to_redis方法
  • 代码

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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

保存商品数据

步骤

  • 在 open_spider方法, 建立MongoDB数据库连接, 获取要操作的集合

  • 在 process_item方法, 把数据插入到MongoDB中

  • 在close_spider方法, 关闭数据库连接

  • 代码

class ProductPipeline(object):

    def open_spider(self, spider):
        if isinstance(spider, JdProductSpider):
            # 建立MongoDB数据库链接
            self.client = MongoClient(MONGO_URL)
            # 获取要操作集合
            self.category = self.client['jd']['product']

    def process_item(self, item, spider):
        if isinstance(spider, JdProductSpider):
            # 把数据插入到mongo中
            self.category.insert_one(dict(item))

        return item

    def close_spider(self, spider):
        """关闭"""
        if isinstance(spider, JdProductSpider):
            self.client.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在settings.py中开启这个管道

ITEM_PIPELINES = {
   'mall_spider.pipelines.CategoryPipeline': 300,
    # 开启商品管道
   'mall_spider.pipelines.ProductPipeline': 301,
}
  • 1
  • 2
  • 3
  • 4
  • 5

反爬

为了避免IP反爬, 我们实现随机User-Agent和代理IP的中间件

  • 步骤:
    1. 实现随机User-Agent的中间件
    2. 实现代理IP中间件
    3. 在settings.py 文件开启, 下载器中间件
实现随机User-Agent的中间件
  • 步骤

    • 准备User-Agent列表
    • 在middlewares.py中, 实现RandomUserAgent类
    • 实现process_request方法
      • 如果是请求是 https://cdnware.m.jd.com 开头的, 就是设置一个iPhone的user-agent
      • 否则从User-Agent列表中随机取出一个
  • 代码

import requests
import random

# 准备请求头
USER_AGENTS = [
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
    "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
    "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
    "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
    "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
    "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 LBBROWSER",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SV1; QQDownload 732; .NET4.0C; .NET4.0E; 360SE)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
    "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
    "Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; zh-cn) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
    "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre",
    "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
    "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10"
]

class RandomUserAgent(object):
    def process_request(self, request, spider):
        if request.url.startswith('https://cdnware.m.jd.com'):
            # 如果使用手机抓包, 获取到商品信息; 生成请求请求头
            request.headers['user-agent'] = 'JD4iPhone/164880 (iPhone; iOS 12.1.2; Scale/2.00)'
        else:
            # 随机获取一个请求头, 进行设置
            request.headers['user-agent'] = random.choice(USER_AGENTS)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
实现代理IP中间件
  • 步骤:

    • 在middlewares.py中, 实现ProxyMiddleware类
    • 实现process_request方法
      • 从代理池中获取一个随机的代理IP, 需指定代理IP的协议, 和访问的域名
      • 设置给request.meta[‘proxy’]
    • 实现process_exception方法
    • 当请求出现异常的时候, 代理池哪些代理IP在本域名下是不可以用的
  • 代码

"""
9.2. 实现代理IP中间件
步骤:
    在middlewares.py中, 实现ProxyMiddleware类
    实现process_request方法
    从代理池中获取一个随机的代理IP
    设置给request.meta['proxy']
"""
from twisted.internet import defer
from twisted.internet.error import TimeoutError, DNSLookupError, \
        ConnectionRefusedError, ConnectionDone, ConnectError, \
        ConnectionLost, TCPTimedOutError
from twisted.web.client import ResponseFailed
from scrapy.core.downloader.handlers.http11 import TunnelError

class ProxyMiddleware(object):

    EXCEPTIONS_TO_RETRY = (defer.TimeoutError, TimeoutError, DNSLookupError,
                           ConnectionRefusedError, ConnectionDone, ConnectError,
                           ConnectionLost, TCPTimedOutError, ResponseFailed,
                           IOError, TunnelError)

    def process_request(self, request, spider):
        """
         从代理池中获取一个随机的代理IP
         设置给request.meta['proxy']
        """
        response = requests.get('http://localhost:6868/random?protocol=https&domain=jd.com')
        request.meta['proxy'] = response.content.decode()
        request.meta['dont_redirect'] = True
        return None

   def process_exception(self, request, exception, spider):
        if isinstance(exception, self.EXCEPTIONS_TO_RETRY):
            # 获取代理IP
            proxy = request.meta['proxy']
            # 提取IP地址
            ip = re.findall('https://(.+):\d+', proxy)[0]

            params = {
              'ip': ip,
              'domain': 'jd.com'
            }

            requests.get('http://localhost:6868/disable_domain', params=params)
            # 构建请求返回
            req = request.copy()
            req.dont_filter = True
            return req
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

在settings.py中开启上面的两个下载器中间件

    # 配置下载器中间件
    DOWNLOADER_MIDDLEWARES = {
    'mall_spider.middlewares.RandomUserAgent': 500,
    'mall_spider.middlewares.ProxyMiddl eware': 543,
    }
  • 1
  • 2
  • 3
  • 4
  • 5

完整源码请关注微信公众号:ReverseCode,回复:爬虫基础

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/384538
推荐阅读
相关标签
  

闽ICP备14008679号