京东全网爬虫项目

一.确定项目需求

1.1 抓取首页的分类信息

·抓取数据:各级分类的名称 和 url

1.2 商品信息的抓取

·抓取:商品名称,商品价格,商品评论数量,商品店铺,商品促销,商品选项,商品图片和URL
京东全网爬虫项目_第1张图片

二.开发环境

·平台:linux
·开发语言:python3
·开发工具:pycharm
·技术选择: 由于全网爬虫,抓取页面非常的多,为了提高抓取的速度,选择使用scrapy框架+scrapy_redis分布式组件。
·由于京东全网的数据量达到了亿级,存储又是结构化数据,数据库选择使用Mongodb。

三.京东全网爬虫实现步骤

采用广度优先策略,把类别和商品信息的抓取分开来做。
·好处:可以提高程序的稳定性

3.1 总体设计

京东全网爬虫项目_第2张图片

3.2实现步骤

1.创建爬虫项目
2.根据需求,定义数据的数据模型
3.实现分类爬虫
4.保存分类信息
5.实现商品爬虫
6.保存商品信息
7.实现随机user-agent和代理IP下载中间件,解决IP反爬

3.3 创建爬虫项目
· scrapy startproject mall_spider

四.明确需要抓取的数据(定义数据模型)

爬虫数据模型,根据需求定义一个大概,随着项目的实现可能会对数据模型做出相应的修改。

4.1类别数据模型

·类别数据模型:用于存储类别信息(Category)- 字段:
。b_category_name:大类别名称
。b_category_url:大类别URL
。m_category_name:中分类名称
。m_category_url:中分类URL
。s_category_name:小分类名称
。s_category_url: 小分类URL

·代码

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()
    
4.2商品的数据模型

·商品数据模型类:用于储存商品信息(Product)
·字段:
。product_category: 商品类别
。product_sku_id:商品ID
。product_name:商品名称
。product_img_url:商品图片url
。product_book_info:图书信息,作者,出版社
。product_option:商品选项
。product_shop:商品店铺
。product_comments:商品评论数量
。product_ad :商品促销
。product_price:商品价格

·代码

class Product(scrapy.Item):
    product_category=scrapy.Field()#商品类别
    product_sku_id=scrapy.Field()#商品ID
    product_name=scrapy.Field()#商品名称
    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()#商品价格

五.商品分类爬虫

·目标:抓取各级分类信息
·步骤:
1.分析页面,确定分类信息的URL
2.创建类别爬虫,抓取数据

5.1 分析,分类信息的URL

·目标:确定分类信息的URL
·目标:
1.进入京东首页
2右键检查,打开开发者工具,搜索 家用电器
3.确定分类的URL

5.2创建爬虫,抓取数据

·目标:抓取分类数据,交个引擎
·步骤:
1.创建类别爬虫
2.指定其实URL
3.解析数据交给引擎

5.2.1创建爬虫

·进入项目目录:cd mall_spider
·创建爬虫: scrapy genspider categpry_spider jd.com

5.2.2指定起始URL

·修改起始URL:https://dc.3.cn/category/get

class JdCategorySpider(scrapy.Spider):
    name = 'jd_category'
    allowed_domains = ['3.cn']
    start_urls = ['https://dc.3.cn/category/get']

5.2.3 解析数据,交给引擎

·分析数据格式
整体数据
004
005
·分析数据格式(三类数据格式)
·book.jd.com/library/life.html|生活||0
·1713-3270|育儿家教||0 构造url:https://chanel.jd.com/1713-3270.html
·4938-12420-12423|会议周边||0 构造URL: https://list.jd.com/list.html?cat=4398,12420,12423

代码实现:

    def parse(self, response):
        # print(response.body.decode('GBK'))
        result=json.loads(response.body.decode('GBK'))
        datas=result['data']
        #遍历数据列表
        for data in datas:
            #创建一个category对象
            item=Category()

            #拿取大分类
            b_category=data['s'][0]
            #大分类的信息
            b_category_info=b_category['n']
            item['b_category_name'],item['b_category_url']=self.get_category_name_url(b_category_info)

            # print('大分类:{}'.format(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
        """
        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-3270|育儿家教||0
            category_url='https://channel.jd.com/{}.html'.format(category_url)
        else:
            #4938-12420-12423|会议周边||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

6.保存分类信息

·目标:把分类信息保存到mongodb中
·步骤:
1.实现保存分类的Pipeline类
2.在settings.py开启,类别的pipeline

ITEM_PIPELINES = {
   'mall_spider.pipelines.CategoryPipeline': 300,
}
6.1 实现保存分类的Pipeline类

·步骤:
1.open_spider方法中,链接mongodb数据库,获取要操作的集合
2.process_item 方法中,像mongodb中插入类别数据
3.close_spider 方法中,关闭mongodb的链接

代码:

from mall_spider.spiders.jd_category import JdCategorySpider
from pymongo import MongoClient
from mall_spider.settings import MONGODB_URL
"""
1.open_spider方法中,链接mongodb数据库,获取要操作的集合
2.process_item  方法中,像mongodb中插入类别数据
3.close_spider 方法中,关闭mongodb的链接
"""

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))
        return item
    def close_spider(self,spider):
        #close_spider 方法中,关闭mongodb的链接
        if isinstance(spider,JdCategorySpider):
            self.client.close()
6.2在settings.py中开启,类别的pipeline
ITEM_PIPELINES = {
  'mall_spider.pipelines.CategoryPipeline': 300,
}

7.实现商品爬虫

思路:
1.把MongoDB中存储的分类信息,放到redis_key指定类表中
2.支持分布式爬虫,可以再一台电脑上运行多次,以启动多个进程,充分使用cpu的多核
3.从一个分类开始抓取,然后在改造成分布式

7.1 分析确定数据所在的URL

·解析列表页,提取商品 skuid,实现翻页,确定翻页的URL

·获取商品的基本信息,通过手机抓包,抓手机app的包,确定url
·PC详情页面,确定评论信息的url
·PC详情页,确定商品价格信息URL

7.2代码实现

步骤:
1.重写start_request方法,根据分类信息构建列表页的请求
2.解析列表页,提取商品的skuid,构建商品的基本信息请求,实现列表页翻页

# -*- coding: utf-8 -*-
import scrapy
import json
from jsonpath  import jsonpath
from mall_spider.items import  Product
class JdProductSpider(scrapy.Spider):
    name = 'jd_product'
    allowed_domains = ['jd.com']

    def start_requests(self):
        """重写start_request方法,根据分类信息构建列表页的请求"""
        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,798",
                  "s_category_name":"超薄电视",
                  "s_category_url":"https://list.jd.com/list.html?cat=737,794,798&ev=4155_76344&sort=sort_rank_asc&trans=1&JL=2_1_0#J_crumbsBar"}
        #根据小分类的url,构建列表页的请求
        yield scrapy.Request(category['s_category_url'],callback=self.parse,meta={'category':category})
    def parse(self, response):
        category=response.meta['category']
        print(category)
        #解析列表页,提取商品的skuid
        sku_ids=response.xpath('//div[contains(@class,"j-sku-item")]/@data-sku').extract()
        for sku_id in sku_ids:
            #创建Product,用于保存商品信息
            item=Product()
            #设置商品类别
            item['product_category']=category
            item['product_sku_id']=sku_id
            #构建商品基本信息的请求
            product_base_url='https://cdnware.m.jd.com/c1/skuDetail/apple/7.3.0/{}.json'.format(sku_id)
            yield  scrapy.Request(product_base_url,callback=self.parse_product_base,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)
            print(next_url)
            #构建下一页的请求
            yield scrapy.Request(next_url,callback=self.parse,meta={'category':category})


    def parse_product_base(self,response):
        #取出传递过来的数据
        item=response.meta['item']
        # print(item)
        # print(response.text)  
        

3.解析商品基本信息,构建商品促销信息的请求
解析商品基本信息

    def parse_product_base(self,response):
        #取出传递过来的数据
        item=response.meta['item']
        # print(item)
        # print(response.text)
        #把json字符串转换为字典
        result=json.loads(response.text)
        #提取相关数据信息
        #product_name:商品名称
        item['product_name']=result['wareInfo']['basicInfo']['name']
        #product_img_url:商品图片url
        item['product_img_url']=result['wareInfo']['basicInfo']['wareImage'][0]['small']

        #product_book_info:图书信息,作者,出版社
        item['product_book_info'] = result['wareInfo']['basicInfo']['bookInfo']
        #product_option:商品选项
        color_size=jsonpath(result,'$..colorSize')
        if color_size:
            #注意:coloeSize:值是列表,但是jsonpath返回的是列表,coloe_size 是两层列表
            color_size=color_size[0]
            product_option={}
            for option in color_size:
                title=option['title']
                value=jsonpath(option,'$..text')
                product_option[title]=value
            item['product_option']=product_option

        #product_shop:商品店铺
        shop=jsonpath(result,'$..shop')
        if  shop:
            shop=shop[0]
            if not shop:
                # print(response.url)
                item['product_shop']={'shop_id':shop['shopId'],
                                      'shop_name':shop['name'],
                                      'shop_score':shop['score']}
            else:
                item['product_shop']={'shop_name':'京东自营'}

        #product_category_id  商品类别id
        item['product_category_id']=result['wareInfo']['basicInfo']['category']
        #需要把Category里面的数据进行相应的修改
        item['product_category_id']=item['product_category_id'].replace(';',",")
        # print(item)

构建商品促销信息的请求
方法:GET
参数:
1.skuid=100000020845 商品SKU_ID
2.&area1_72_4137_0区域,固定值
3.cat=737%2C94%2C798 类别

#准备促销信息的url
        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_product_ad,meta={'item':item})

    def parse_product_ad(self,response):
        item=response.meta['item']
        print(item)
        print(response.text)

4.解析促销信息,构架商品评价信息的请求
1.准备评价信息请求
url:https://club.jd.com/comment/productCommentSummaries.action?referenceIds=100000020845
2.方法:GET
3.参数:referencelds=100000020845 商品的skuid

    def parse_product_ad(self,response):
        item=response.meta['item']
        # print(item)
        # print(response.text)
        # 把数据转换为字典
        result=json.loads(response.body)
        # 商品促销
        item['product_ad']=jsonpath(result,'$..ad')[0]
        # print(item)
        #构建评价信息的请求
        comments_url='https://club.jd.com/comment/productCommentSummaries.action?referenceIds={}'.format(item['product_sku_id'])
        yield scrapy.Request(comments_url,callback=self.parse_product_comments,meta={'item':item})


    def parse_product_comments(self,response):
        item=response.meta['item']
        print(item)
        print(response.text)

5.解析商品评价信息,构建价格信息的请求
a.product_comments 商品评评论数量
b评论数量,好评数量,差评数量,好评率

 #解析商品评价信息
        result=json.loads(response.text)
        item['product_comments']={
            'CommentCount':jsonpath(result,'$..CommentCount'),
            'GoodCount':jsonpath(result,'$..GoodCount'),
            'PoorCount':jsonpath(result,'$..PoorCount'),
            'GoodRate':jsonpath(result,'$..GoodRate')
            }

6.解析价格信息

  #构建价格请求
        price_url='https://p.3.cn/prices/mgets?skuIds=J_{}'.format(item['product_sku_id'])
        yield scrapy.Request(price_url,callback=self.parse_product_price,meta={'item':item})

    def parse_product_price(self,response):
        item=response.meta['item']
        # print(response.text)
        result=json.loads(response.text)
        #product_price: 商品价格
        item['product_price']=result[0]['p']
        #把商品数据交给引擎
        yield item
7.3 商品爬虫实现分布式

·步骤:
1.修改爬虫类
2.在settings文件中配置scrapy_redis
3.编写程序,用于把mongodb中的数据进行分类,放入redis_key中的指定列表中

1.修改爬虫类
步骤:
1.修改继承,继承RedisSpider
2.指定redis_key
3.把重写start_requests改为 重写 make_request_from_data

from scrapy_redis.spiders import RedisSpider
import pickle
class JdProductSpider(RedisSpider):
    name = 'jd_product'
    allowed_domains = ['jd.com','3.cn']
    #1.指定起始url列表,在redis数据库中key
    redis_key = 'jd_product:category'
    
    # def start_requests(self):
    #     """重写start_request方法,根据分类信息构建列表页的请求"""
    #     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,798",
    #               "s_category_name":"超薄电视",
    #               "s_category_url":"https://list.jd.com/list.html?cat=737,794,798&ev=4155_76344&sort=sort_rank_asc&trans=1&JL=2_1_0#J_crumbsBar"}
    #     #根据小分类的url,构建列表页的请求
    #     yield scrapy.Request(category['s_category_url'],callback=self.parse,meta={'category':category})
    def make_request_from_data(self, data):
        """
        根据redis中读取的二进制数据,构建请求
        :param data: 分类信息的二进制数据
        :return: 根据小分类url构建的请求对象
        """
        #分类信息的二进制数据,转换为字典
        category=pickle.loads(data)
        #根据小分类的url,构建列表页的请求
        yield scrapy.Request(category['s_category_url'],callback=self.parse,meta={'category':category})

2.在settings文件中配置scrapy_redis

#redis数据连接
REDIS_URL='redis://127.0.0.1:6379/0'
#去重容器类:用于把已爬取的指纹存储到基于redis的set集合中
DUPEFILTER_CLASS='scrapy_redis.dupefilter.RFPDupeFilter'
#调度器:用于把待爬取的请求存储到基于redis的队列中
SCHEDULER='scrapy_redis.scheduler'
#是不是进行调度持久化
#true 当程序结束时,会保留redis中已经爬取的和待爬取的请求
#flase 当程序结束时,不会保留redis中已经爬取的和待爬取的请求
SCHEDULER_PERSIST=True

3.编写程序,用于把mongodb中的数据进行分类,放入redis_key中的指定列表中
步骤:
1.在项目文件夹下创建add_category_to_redis.py
1.实现方法 add_category_to_redis:
a.链接mongodb
b.链接redis
c.读取mongodb中分类信息,序列化后,添加到商品爬虫redis_key 指定的list中
d.关闭mongodb

from mongo import MongoClient
from redis import StrictRedis
import pickle
from mall_spider.settings import MONGODB_URL,REDIS_URL
from mall_spider.spiders.jd_product import JdProductSpider
"""步骤:
1.在项目文件夹下创建add_category_to_redis.py
1.实现方法  add_category_to_redis:
a.链接mongodb
b.链接redis
c.读取mongodb中分类信息,序列化后,添加到商品爬虫redis_key 指定的list中
d.关闭mongodb"""

def add_category_to_redis():
    #a.链接mongodb
    mongo=MongoClient(MONGODB_URL)
   #b.链接redis
    redis=StrictRedis.from_url(REDIS_URL)
    #c.读取mongodb中分类信息,序列化后,添加到商品爬虫redis_key指定的list中
    #获取分类信息
    collection=mongo['jd']['category']
    #读取分类信息
    cursor=collection.find()
    for category in cursor:
        #序列化字典数据
        data=pickle.dumps(category)
        #添加到商品爬虫redis_key指定的list中
        redis.lpush(JdProductSpider.redis_key,data)
    #d.关闭mongodb
    mongo.close()
if __name__ == '__main__':
    add_category_to_redis()

8. 保存商品数据

8.1实现存储商品pipeline类

步骤:
在open_spider 方法,建立mongodb数据库链接,获取要操作的集合
在 process_item 方法,把数据插入到mongodb中
在close_spider 方法,关闭数据库连接

from mall_spider.spiders.jd_product import JdProductSpider
class ProductPipeline(object):


    def  open_spider(self,spider):
        """当爬虫启动的时候执行"""
        if isinstance(spider,JdProductSpider):
            #open_spider方法中,链接mongodb数据库,获取要操作的集合

            self.client=MongoClient(MONGODB_URL)
            self.db=self.client['jd']
            self.collection=self.db['product']
            # self.collection=self.client['jd']['category']


    def process_item(self, item, spider):
        #process_item  方法中,像mongodb中插入类别数据
        if isinstance(spider,JdProductSpider):
            self.collection.insert_one(dict(item))
            print("插入成功")
        return item
    def close_spider(self,spider):
        #close_spider 方法中,关闭mongodb的链接
        if isinstance(spider,JdProductSpider):
            self.client.close()
8.2在settings.py中开启管道
ITEM_PIPELINES = {
   'mall_spider.pipelines.CategoryPipeline': 300,
   'mall_spider.pipelines.ProductPipeline': 301,
   
}

实现下载中间件

避免反爬,使用user-agent 和代理ip

9.1 随机user-agent的中间件

步骤:
准备 user-agent列表
在middlewares.py中,实现RandomUserAgent类
实现process_request 方法

#准备useragent列表
USER_AGENTS=[
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.0.11) Gecko/2009060215 Firefox/3.0.11 (.NET CLR 3.5.30729)",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 GTB5",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E)",

]
class RandomUserAgent(object):
    def process_request(self,requset,spider):
        #1.如果请求是手机抓包,就设置一个iphone的user-agent
        if requset.url.startswith('https://cdnware.m.jd.com'):
            requset.headers['user-agent']='JD4iphone/164880 (iphone; iOS 12.1.2; Scale/2.00'
        # 否则从useragent中选取一个
        else:
            requset.headers['user-agent']=random.choice(USER_AGENTS)
9.2 实现代理ip中间件

步骤:
1.在middleware中 实现ProxyMiddleware类
2.实现process_request方法
从代理池中获取一个随机的代理ip,需要指定代理ip协议,和访问的域名
设置给request.meta[‘proxy’]
3.设置request_exception方法
当请求出现异常的时候,代理池哪些代理是在当前域名下不可用的

import requests
from scrapy.downloadermiddlewares.retry import RetryMiddleware
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.exceptions import NotConfigured
from scrapy.utils.response import response_status_message
from scrapy.core.downloader.handlers.http11 import TunnelError
import re
class ProxyMiddleware(object):
    
    EXCEPTIONS_TO_RETRY = (defer.TimeoutError, TimeoutError, DNSLookupError,
                           ConnectionRefusedError, ConnectionDone, ConnectError,
                           ConnectionLost, TCPTimedOutError, ResponseFailed,
                           IOError, TunnelError)

    def process_request(self, request, spider):
        #实现process_request方法

        #从代理池中获取一个随机的代理ip,需要指定代理ip协议,和访问的域名
        response= requests.get('http://localhost:6868/random?protocol=https&domain=jd.com')
        #设置给request.meta['proxy']
        request.meta['proxy']=response.content.decode()
        return None



    def process_exception(self, request, exception, spider):
        if isinstance(exception,self.EXCEPTIONS_TO_RETRY):
            #当请求出现异常的时候,代理池哪些代理是在当前域名下不可用的
            url='http://localhost:6868/disable_domain'
            proxy=request.meta['proxy']
            ip=re.findall('https?://(.+?):\d+',proxy)[0]
            
            params={
                'ip':'',
            'domain':'jd.com'
            }
            #发送请求
            requests.get(url,params=params)
        


9.3 开启中间件
DOWNLOADER_MIDDLEWARES = {
   'mall_spider.middlewares.ProxyMiddleware': 300,
   'mall_spider.middlewares.RandomUserAgent': 301,
}

京东全网爬虫项目_第3张图片

你可能感兴趣的:(python爬虫)