Python爬虫实战-使用Scrapy框架爬取土巴兔(四)
上一篇文章Python爬虫实战-使用Scrapy框架爬取土巴兔(三)我们为爬虫工程添加了下载中间件和IP代理池。接下来就要开始着手具体的爬取规则。
该系列其它文章:
- Python爬虫实战-使用Scrapy框架爬取土巴兔(一)
- Python爬虫实战-使用Scrapy框架爬取土巴兔(二)
- Python爬虫实战-使用Scrapy框架爬取土巴兔(三)
- Python爬虫实战-使用Scrapy框架爬取土巴兔(五)
该篇文章主要讲如何分析网页并通过Scrapy中Spider来定制爬取规则。
一.分析网页
当你想要爬取某个网站内容时一定是先了解网站网页的翻页URL规则和阅读网页的html源代码。了解翻页规则我们就能确定需要爬取的网页url。阅读html源代码是为了确定能够爬取的内容并且确定数据结构。
我们要爬取的是土巴兔中家居图册这个模块。那么我们就确定了起始的URL是http://xiaoguotu.to8to.com/tuce/ 。
往下拉点击第二页后可以看到,网址变为http://xiaoguotu.to8to.com/tuce/p_2.html 。 那么可以确定翻页规则匹配的正则就是"xiaoguotu.to8to.com/tuce/p_\d+.html"。
按F12调出开发者工具,通过阅读html代码我们很容易就知道在html中哪个节点位置可以获取进入详情页的url。
接下来点进某个图册,阅读源码我们很简单就能找出图片的链接。但是一个图册包括了多张图片。html源码中并没有展示所有图片的链接,那我们该怎么办,这是个疑问。
依旧利用开发者工具点击XHR这个选项,接下来一目了然,原来土巴兔是通过AJAX请求的方式来加载数据的。http://xiaoguotu.to8to.com/getxgtjson.php?a2=0&a12=&a11=10043657&a6=&a3=&a10=2 这个url返回了json格式的数据。在json数据中我们可以看到cid为c10043657的图册,id为101,共有5张图片。我们也可以看到图片的url和标题,但是剩下的字段,例如sid:"19"、hxid:"7" ,我们都无法知道它具体代表什么。
接下来就需要我们的耐心一点一点去寻找蛛丝马迹,终于在xiaoguotu_new.js这个请求中,我们看到了刚才说的sid:"19"、hxid:"7"的含义。
二.数据结构
经过一番周折,我们终于知道了该如何获取到关于图册的所有信息。
确定数据结构,并编写继承scrapy.Item的Item实体和一些常量
import scrapy
class DesignPictureItem(scrapy.Item):
fid = scrapy.Field() # 唯一标识
title = scrapy.Field() # 标题
sub_title = scrapy.Field() # 副标题
html_url = scrapy.Field() # 所爬取的网页链接
tags = scrapy.Field() # 标签
description = scrapy.Field() # 描述
img_url = scrapy.Field() # 图片URL
img_width = scrapy.Field() # 图片宽度
img_height = scrapy.Field() # 图片高度
img_name = scrapy.Field() # 图片名称
ZONE_TYPE = {'1': '客厅', '2': '卧室', '3': '餐厅', '4': '厨房', '5': '卫生间', '6': '阳台', '7': '书房', '8': '玄关', '10': '儿童房', '11': '衣帽间', '12': '花园'}
STYLE_ID = {'13': '简约', '15': '现代', '4': '中式', '2': '欧式', '9': '美式', '11': '田园', '6': '新古典', '0': '混搭', '12': '地中海', '8': '东南亚', '17': '日式',
'18': '宜家',
'19': '北欧', '20': '简欧'}
COLOR_ID = {'1': '白色', '2': '黑色', '3': '红色', '4': '黑色', '5': '绿色', '6': '橙色', '7': '粉色', '8': '蓝色', '9': '灰色', '10': '紫色', '11': '棕色', '12': '米色',
'13': '彩色', '14': '原木色'}
PART_ID = {'336': '背景墙', '16': '吊顶', '14': '隔断', '9': '窗帘', '340': '飘窗', '33': '榻榻米', '17': '橱柜', '343': '博古架', '333': '阁楼', '249': '隐形门', '21': '吧台',
'22': '酒柜', '23': '鞋柜', '24': '衣柜', '19': '窗户', '20': '相片墙', '18': '楼梯', '359': '其他'}
AREA = {'1': '60㎡以下', '2': '60-80㎡', '3': '80-100㎡', '4': '100-120㎡', '5': '120-150㎡', '6': '150㎡以上'}
HX_ID = {'1': '小户型', '7': '一居', '2': '二居', '3': '三居', '4': '四居', '5': '复式', '6': '别墅', '8': '公寓', '9': 'loft'}
三.爬取规则
Spider类定义了如何爬取、网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。更详细的Spider说明请阅读官方文档。
Scrapy也在框架中定义不同用途的Spider,如:
- CrawlSpider:定义了一些规则(rule)来提供跟进link的方便的机制
- XMLFeedSpider:被设计用于通过迭代各个节点来分析XML源
- CSVFeedSpider:该spider除了其按行遍历而不是节点之外其他和XMLFeedSpider十分类似
- SitemapSpider:使您爬取网站时可以通过 Sitemaps 来发现爬取的URL
这里显然CrawlSpider更适合我们来定义爬虫规则。
下面是爬虫的完整代码:
class DesignPictureSpider(CrawlSpider):
#定义爬虫名称
name = 'design_picture'
#定义起始域名
start_url_domain = 'xiaoguotu.to8to.com'
#定义允许的域名
allowed_domains = ['to8to.com']
#定义起始的URL
start_urls = ['http://xiaoguotu.to8to.com/tuce/']
#定义翻页规则,及回调方法
rules = (
Rule(LinkExtractor(allow="/tuce/p_\d+.html"), follow=True, callback='parse_list'),
)
#定义数据处理完后,处理Item的管道
custom_settings = {
'ITEM_PIPELINES': {
'tubatu.pipelines.DesignPicturePipeline': 302,
}
}
#Service封装具体的数据处理逻辑
design_picture_service = DesignPictureService()
#获取到网页数据后的回调方法
def parse_list(self, response):
selector = Selector(response)
#利用xpath提取元素
items_selector = selector.xpath('//div[@class="xmp_container"]//div[@class="item"]')
for item_selector in items_selector:
#拼接详情页URL,格式为:http://xiaoguotu.to8to.com/c10037052.html
cid = item_selector.xpath('div//a/@href').extract()[0][2:-6]
title = item_selector.xpath('div//a/@title').extract()[0]
#拼接获取数据的URL,格式: http://xiaoguotu.to8to.com/getxgtjson.php?a2=0&a12=&a11=10037052&a1=0
next_url = (constant.PROTOCOL_HTTP + self.start_url_domain + '/getxgtjson.php?a2=0&a12=&a11={cid}&a1=0').format(cid=cid)
#创建一个请求,反给Scrapy引擎
yield scrapy.Request(next_url, self.parse_content, meta={'cid': cid, 'title': title})#通过meta来传递数据
#解析详情方法
def parse_content(self, response):
uuid = utils.get_uuid()
cid = response.meta['cid']
title = response.meta['title']
try:
#解析json数据
data = json.loads(response.text)
except:
print("-----------------------获取到json:" + response.text + "------------------------------")
return
data_img_list = data['dataImg']
data_album_list = None
for _data_img in data_img_list:
if _data_img['cid'] == cid:
data_album_list = _data_img['album']
break
for data_album in data_album_list:
data_img = data_album['l']
#取得单张图片URL http://pic.to8to.com/case/1605/05/20160505_f0af86a239d0b02e9635a47ih5l1riuq_sp.jpg
img_url = 'http://pic.to8to.com/case/{short_name}'.format(short_name=data_img['s'])
#过滤重复URL
if self.design_picture_service.is_duplicate_url(img_url):
break
sub_title = data_img['t']
original_width = data_img['w']
original_height = data_img['h']
tags = []
try:
zoom_type = ZONE_TYPE[data_img['zid']]
if zoom_type is not None or not zoom_type.strip() == '':
tags.append(zoom_type)
except KeyError:
pass
try:
style_id = STYLE_ID[data_img['sid']]
if style_id is not None or not style_id.strip() == '':
tags.append(style_id)
except KeyError:
pass
try:
area = AREA[data_img['a']]
if area is not None or not area.strip() == '':
tags.append(area)
except KeyError:
pass
try:
color_id = COLOR_ID[data_img['coid']]
if color_id is not None or not color_id.strip() == '':
tags.append(color_id)
except KeyError:
pass
try:
house_type = HX_ID[data_img['hxid']]
if house_type is not None or not house_type.strip() == '':
tags.append(house_type)
except KeyError:
pass
try:
part = PART_ID[data_img['pid']]
if part is not None or not part.strip() == '':
tags.append(part)
except KeyError:
pass
#创建Item对象并返回
try:
design_picture_item = DesignPictureItem() # type: DesignPictureItem
design_picture_item['fid'] = uuid
design_picture_item['html_url'] = response.url
design_picture_item['img_url'] = img_url
design_picture_item['tags'] = tags
design_picture_item['title'] = title
design_picture_item['sub_title'] = sub_title
design_picture_item['img_width'] = original_width
design_picture_item['img_height'] = original_height
design_picture_item['description'] = design_picture_item['title']
yield design_picture_item
except Exception as e:
print("-----------------------获取到json:" + response.text + "------------------------------")
log.warn("%s ( refer: %s )" % (e, response.url))
if config.USE_PROXY:
proxy_pool.add_failed_time(response.meta['proxy'].replace('http://', ''))
当然这里还有一个重要的知识点是关于如何使用XPath 。XPath可以用来在XML文档中对元素和属性进行遍历,从而从网页中提取出我们需要的信息。更多XPath的语法知识可以参考http://www.w3school.com.cn/xpath/index.asp。
四.布隆过滤
好奇的同学可能会有疑问,如果我们爬到重复的URL时怎么办呢?
接下来就要引入布隆过滤。我们将爬取过的URL通过布隆算法存入Redis中,每次爬取一个新的URL时再通过布隆算法来识别是否是重复的URL,如果是重复的就不处理这个URL了。
布隆过滤的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
在URL去重上牺牲一定的准确性来降低Redis存储开销和提高爬虫整体效率也是值得的。
from redis import StrictRedis
class SimpleHash(object):
def __init__(self, cap, seed):
self.cap = cap
self.seed = seed
def hash(self, value):
ret = 0
for i in range(value.__len__()):
ret += self.seed * ret + ord(value[i])
return (self.cap - 1) & ret
class RedisBloomFilter(object):
def __init__(self, redis_client: StrictRedis):
self.bit_size = 1 << 25
self.seeds = [5, 7, 11, 13, 31, 37, 61]
self.redis = redis_client
self.hash_dict = []
for i in range(self.seeds.__len__()):
self.hash_dict.append(SimpleHash(self.bit_size, self.seeds[i]))
def is_contains(self, value, key):
if value is None:
return False
if value.__len__() == 0:
return False
ret = True
for f in self.hash_dict:
loc = f.hash(value)
ret = ret & self.redis.getbit(key, loc)
return ret
def insert(self, value, key):
for f in self.hash_dict:
loc = f.hash(value)
self.redis.setbit(key, loc, 1)
最后
爬取规则也完成了,接下来就是我们的收尾工作, Python爬虫实战-使用Scrapy框架爬取土巴兔(五)通过Item Pipeline处理Item实体。
附:
详细的项目工程在Github中,如果觉得还不错的话记得Star哦。