·抓取数据:各级分类的名称 和 url
·抓取:商品名称,商品价格,商品评论数量,商品店铺,商品促销,商品选项,商品图片和URL
·平台:linux
·开发语言:python3
·开发工具:pycharm
·技术选择: 由于全网爬虫,抓取页面非常的多,为了提高抓取的速度,选择使用scrapy框架+scrapy_redis分布式组件。
·由于京东全网的数据量达到了亿级,存储又是结构化数据,数据库选择使用Mongodb。
采用广度优先策略,把类别和商品信息的抓取分开来做。
·好处:可以提高程序的稳定性
1.创建爬虫项目
2.根据需求,定义数据的数据模型
3.实现分类爬虫
4.保存分类信息
5.实现商品爬虫
6.保存商品信息
7.实现随机user-agent和代理IP下载中间件,解决IP反爬
· scrapy startproject mall_spider
爬虫数据模型,根据需求定义一个大概,随着项目的实现可能会对数据模型做出相应的修改。
·类别数据模型:用于存储类别信息(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()
·商品数据模型类:用于储存商品信息(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.创建类别爬虫,抓取数据
·目标:确定分类信息的URL
·目标:
1.进入京东首页
2右键检查,打开开发者工具,搜索 家用电器
3.确定分类的URL
·目标:抓取分类数据,交个引擎
·步骤:
1.创建类别爬虫
2.指定其实URL
3.解析数据交给引擎
·进入项目目录:cd mall_spider
·创建爬虫: scrapy genspider categpry_spider jd.com
·修改起始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']
·分析数据格式
整体数据
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
·目标:把分类信息保存到mongodb中
·步骤:
1.实现保存分类的Pipeline类
2.在settings.py开启,类别的pipeline
ITEM_PIPELINES = {
'mall_spider.pipelines.CategoryPipeline': 300,
}
·步骤:
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()
ITEM_PIPELINES = {
'mall_spider.pipelines.CategoryPipeline': 300,
}
思路:
1.把MongoDB中存储的分类信息,放到redis_key指定类表中
2.支持分布式爬虫,可以再一台电脑上运行多次,以启动多个进程,充分使用cpu的多核
3.从一个分类开始抓取,然后在改造成分布式
·解析列表页,提取商品 skuid,实现翻页,确定翻页的URL
·获取商品的基本信息,通过手机抓包,抓手机app的包,确定url
·PC详情页面,确定评论信息的url
·PC详情页,确定商品价格信息URL
步骤:
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
·步骤:
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()
步骤:
在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()
ITEM_PIPELINES = {
'mall_spider.pipelines.CategoryPipeline': 300,
'mall_spider.pipelines.ProductPipeline': 301,
}
避免反爬,使用user-agent 和代理ip
步骤:
准备 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)
步骤:
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)
DOWNLOADER_MIDDLEWARES = {
'mall_spider.middlewares.ProxyMiddleware': 300,
'mall_spider.middlewares.RandomUserAgent': 301,
}