大众点评爬虫文档
一,开发环境
1, Scrapy-redis爬虫框架
2, pycharm开发工具
二,项目创建
1,创建项目:scrapy startproject +项目名称
2,创建爬虫:scrapy genspider +爬虫文件名 + 允许爬取的网站域名
三,修改配置文件
1,在配置文件settings.py文件中添加USER_AGENT参数,不添加UA参数无法获取到页面,先复制使用本机浏览器的UA,在会出现滑块验证码时只需在本地浏览器滑动解锁即可。
2,scrapy-redis的配置信息在完成整体爬虫流程之后配置修改。
四,爬取页面,获取页面数据
1,因为大众点评最重要的是对文字进行了加密,使用xpath无法获取到数据,所以先使用response.text将响应数据转换成unicode 型的文本数据
2,请求页面会出现验证中心的滑块验证界面,所以首先判断页面是否是验证中心,如果是验证界面则回调函数到本函数重新请求(后面会添加UA代理中间件与IP代理中间件),如果不是验证界面即可获取数据。
3,由于大众点评只能显示50页数据,所以我们需要将爬取范围缩小。因此先获取分类名称与分类地址(url)数据。遍历请求该分类地址,然后回调至下一个函数。
4,获取行政区的名称与地址(url)数据,遍历请求该行政区地址,然后回调至下一个函数。
5,获取当前分类下的当前行政区下的景区列表数据,使用正则匹配到需求数据,并获取下一页的地址,回调到本函数。遍历列表景区地址回调到详情数据获取函数。
6,编写详情页数据获取函数,使用正则匹配所需的数据。
五,文字解密
1,首先匹配到当前页面的加密css文件地址,然后请求该css地址,使用正则匹配到加密文件的地址,请求该加密文件地址,将加密文件下载到本地。
2,景区列表页与景区详情页面的字体加密文件(.woff)不同,每个景区列表页与每个景区详情页面的字体加密文件可能不同。
3,使用正则匹配我们需要的已经被加密的字符串,例如地址数据使用正则匹配到下面的字符串:
地址:</span> <div id="J_map-show" class="map_address" > <span class="item" itemprop="street-address" id="address">
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
<e class="address">&
4,对正则匹配出来的字符串进行抽取分离出来,然后对部分字符串进行替换(
&
['unie16f', 'uniefb4', 'unie2f8', '1', 'unie2db', 'unied47',
'unie79c', 'unie484', '(', 'unieddc', 'unif8d0', 'unie6b0',
'unif708', 'unie037', 'unie23d', '妇', 'unif4e9', 'unif7c5',
'unie16f', 'unie35b', 'unie90e', 'unie559', 'uniefb4', 'unif10e', ') ', ' ']
5,将获取到的
.woff字体文件生成
.xml文件,然后对
.xml文件生成
{编码
:索引
} 的字典格式,如下:
{'unie136': 0, 'unie79c': 1, 'uniee2b': 2, 'unif711': 3, 'unif597': 4, ......
6,根据字体文件位置对应生成字符串如下:
woff_string
= '''
1234567890店中美家馆
小车大市公酒行国品发电金心业商司
超生装园场食有新限天面工服海华水
房饰城乐汽香部利子老艺花专东肉菜
学福饭人百餐茶务通味所山区门药银
农龙停尚安广鑫一容动......'''
7,将上述的woff_string字符串切割成列表形式,如下所示:
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '店', '中', '美', '家',
'馆', '小', '车', '大', '市', '公', '酒', '行', '国', '品', '发', '电', '金',
'心', '业', ......]
8,将正则匹配切割好的加密数据列表(上述
4步骤)分别对应在
.woff生成的字典中(上述
5步骤)找到对应的索引,然后在上述
7步骤中按照索引找到对应的数据。
9,将解密的数据进行拼接成最终的解密数据
六,地址数据解密
地址数据的加密方式与其他的加密方式稍有不同,其中地址数据中的中文(汉字)使用的加密字体文件与数字使用的字体加密文件不同,使用的是两种字体加密的混合。
1,使用正则获取地址数据
2,对字符串进行切割与替换,其中给定三个列表,
text_list列表是用来存放每一位地址信息的,如下:[{'belong': 'address_list', 'index': 0, 'value': 'uniebf7'}, {'belong': 'address_list', 'index': 1, 'value': 'unie530'}...]
chinese_list列表是存放的是中文切割替换出来的字符串,如下:['uniebf7', 'unie530', 'unie8f4', 'unie49b', 'unie252', 'unie593', 'unie9b6', 'unie384', 'unie980']
num_list列表是存放的是数字切割替换出来的字符串,如下:['unif036', 'unif132', 'unif132']
3,将中文与数字分开进行解密,解密完成后根据text_list中对应的字典与索引进行匹配各位解密的数字,然后进行拼接。
4,注意:地址数据的切割与替换函数需要重写,与其他数据的切割与替换步骤不同。
5,地址的数据解密跟其他数据的解密方式不同,因为涉及到两个字体文件的处理。
七,将Scrapy更改为Scrapy-redis
1,导入模块:from scrapy_redis.spiders import RedisSpider
2,修改spiders文件中类的继承类
3,动态获取允许的域
def __init__(self, *args, **kwargs):
domain = kwargs.pop("domains", "")
self.alllowed_domains = filter(None, domain.split(','))
super(DzdpSpider, self).__init__(*args, **kwargs)
4,添加存放起始请求url的键:redis_key='start_url'
5,settings中添加以下配置信息
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True
REDIS_URL = "redis://127.0.0.1:6379"
DOWNLOAD_DELAY = 1
八,代理中间件(由于代理收费,该中间件未启用)
1,UA中间件
(1)安装模块 fake_useragent
(2)导入模块 from fake_useragent import UserAgent
(3)编写UA中间件代码
class UserAgentMiddleware(object):
def process_request(self, request, spider):
user_agent = UserAgent().chrome
print('当前UA是{}'.format(user_agent))
request.headers['User-Agent'] = user_agent
(4)settings.py中激活UA中间件
2,IP代理中间件
我们使用的是阿布云代理(收费),代码如下:
proxyServer = "http://http-dyn.abuyun.com:9020"
proxyUser = "HPX8KNGF4B4R17GD"
proxyPass = "F7B4421CB49EE54C"
proxyAuth = "Basic " + base64.urlsafe_b64encode(bytes((proxyUser + ":" + proxyPass), "ascii")).decode("utf8")
class ProxyMiddleware(object):
def process_request(self, request, spider):
request.meta["proxy"] = proxyServer
print('IP{}'.format(request.meta["proxy"]))
request.headers["Proxy-Authorization"] = proxyAuth
def process_response(self, request, response, spider):
if response.status != '200':
request.dont_filter = True
return request
九,注意事项
1,数据获取中,评分数据是在列表页面进行获取,但是部分景区的评分数据是在详情页面,经过对比发现,虽然评分的名称不同,但是评分规则相同,因此我们在景区列表页面获取评分数据,在详情页面也获取评分数据,加入逻辑判断,在主页为获取到评分数据,而详情页面获取到评分数据时使用详情页获取的评分数据。
2,景区列表页面的加密字体匹配与详情页面的加密字体匹配机制略有不同,并不是同一个匹配函数。
3,景区列表页获取到的加密字体的切割替换与景区详情的加密字体的切割替换是不同的,是单独的函数。
4,景区列表页的界面函数与景区详情页面的数据界面函数是不同的,是单独的函数。
5,加密的方式大同小异,短时间内可能会发生更改,加密字体文件会发生改变,只要匹配机制完善就可以。
十,启动爬虫文件
1,cmd连接redis客户端,lpush start_url http://.....(起始url)
2,cmd打开项目目录,到spiders目录下执行命令:scrapy runspider 爬虫文件名.py
十一,数据的正则匹配代码示例
name = re.findall('(.*?)
', i, re.S)
print(name[0])
dianping = re.findall('(.*?).*?条点评', i, re.S)
price = re.findall('¥(.*?)', i, re.S)
total_points = re.findall('总分(.*?)', i, re.S)
environment = re.findall('环境(.*?)', i, re.S)
serve = re.findall('服务(.*?)', i, re.S)
十二,主页的加密字体文件的匹配与下载
def get_svg_url(self, soup):
try:
svgtextcss = re.search(r'href="([^"]+svgtextcss[^"]+)"', soup, re.M)
woff_url = 'http:' + svgtextcss.group(1)
svg_html = requests.get(woff_url).text
lines = svg_html.split('PingFangSC-')
partern = re.compile(r',(url.*shopNum)')
for line in lines:
out = partern.findall(line)
if len(out) > 0:
woff = re.compile('\((.*?)\)')
list_page_url = 'http:' + woff.findall(out[0])[0].replace('"', '')
path = r'D:\code\DZDP\DZDP\spiders\woff\list_page.woff'
with open(path, 'wb') as f:
f.write(requests.get(list_page_url).content)
print('当前列表页加密字体文件已下载完成!')
else:
pass
except Exception as e:
pass
十三,获取到的加密字符串的分离与替换
def split_str(self, str):
if str:
text_split = str.split('')
try:
text_split.remove('')
except ValueError:
pass
text_list = []
for i in text_split:
try:
dex = i.index('<')
except ValueError:
text_list.append(i)
continue
if dex != 0:
text_list.append(i[:dex])
tag = re.findall('.*?">(.*?);', i[dex:])
if tag:
text_list.append('uni' + tag[0])
final_list = []
for t in text_list:
if t.startswith(''):
new_s = 'uni' + re.findall('(.*?);', t)[0]
final_list.append(new_s)
else:
final_list.append(t)
return final_list
else:
return
十四,主页数据的解密函数
def woff_change(self, woffdict):
woff_string = '''
1234567890店中美家馆
小车大市公酒行国品发电金心业商司
超生装园场食有新限天面工服海华水
房饰城乐汽香部利子老艺花专东肉菜
学福饭人百餐茶务通味所山区门药银
农龙停尚安广鑫一容动南具源兴鲜记
时机烤文康信果阳理锅宝达地儿衣特
产西批坊州牛佳化五米修爱北养卖建
材三会鸡室红站德王光名丽油院堂烧
江社合星货型村自科快便日民营和活
童明器烟育宾精屋经居庄石顺林尔县
手厅销用好客火雅盛体旅之鞋辣作粉
包楼校鱼平彩上吧保永万物教吃设医
正造丰健点汤网庆技斯洗料配汇木缘
加麻联卫川泰色世方寓风幼羊烫来高
厂兰阿贝皮全女拉成云维贸道术运都
口博河瑞宏京际路祥青镇厨培力惠连
马鸿钢训影甲助窗布富牌头四多妆吉
苑沙恒隆春干饼氏里二管诚制售嘉长
轩杂副清计黄讯太鸭号街交与叉附近
层旁对巷栋环省桥湖段乡厦府铺内侧
元购前幢滨处向座下臬凤港开关景泉
塘放昌线湾政步宁解白田町溪十八古
双胜本单同九迎第台玉锦底后七斜期
武岭松角纪朝峰六振珠局岗洲横边济
井办汉代临弄团外塔杨铁浦字年岛陵
原梅进荣友虹央桂沿事津凯莲丁秀柳
集紫旗张谷的是不了很还个也这我就
在以可到错没去过感次要比觉看得说
常真们但最喜哈么别位能较境非为欢
然他挺着价那意种想出员两推做排实
分间甜度起满给热完格荐喝等其再几
只现朋候样直而买于般豆量选奶打每
评少算又因情找些份置适什蛋师气你
姐棒试总定啊足级整带虾如态且尝主
话强当更板知己无酸让入啦式笑赞片
酱差像提队走嫩才刚午接重串回晚微
周值费性桌拍跟块调糕'''
woffs = [i for i in woff_string if i != '\n' and i != ' ']
list_pagefont = TTFont(r'D:\code\DZDP\DZDP\spiders\woff\list_page.woff')
list_pagefont.saveXML(r'D:\code\DZDP\DZDP\spiders\woff\list_page.xml')
list_page_TTGlyphs = list_pagefont['cmap'].tables[0].ttFont.getGlyphOrder()[2:]
list_page_dict = {}
for i, x in enumerate(list_page_TTGlyphs):
list_page_dict[x] = i
dianping_list = woffdict['dianping']
price_list = woffdict['price']
total_points_list = woffdict['total_points']
enviroment_list = woffdict['enviroment']
serve_list = woffdict['serve']
if dianping_list:
dianping = ''
for char in dianping_list:
text = char.replace('', 'uni')
if text in list_page_dict:
content = woffs[list_page_dict[text]]
else:
content = char
dianping += ''.join(content)
else:
dianping = '暂无'
if price_list:
price = ''
for char in price_list:
text = char.replace('', 'uni')
if text in list_page_dict:
content = woffs[list_page_dict[text]]
else:
content = char
price += ''.join(content)
else:
price = '暂无'
if total_points_list:
total_points = ''
for char in total_points_list:
text = char.replace('', 'uni')
if text in list_page_dict:
content = woffs[list_page_dict[text]]
else:
content = char
total_points += ''.join(content)
else:
total_points = '暂无'
if enviroment_list:
enviroment = ''
for char in enviroment_list:
text = char.replace('', 'uni')
if text in list_page_dict:
content = woffs[list_page_dict[text]]
else:
content = char
enviroment += ''.join(content)
else:
enviroment = '暂无'
if serve_list:
serve = ''
for char in serve_list:
text = char.replace('', 'uni')
if text in list_page_dict:
content = woffs[list_page_dict[text]]
else:
content = char
serve += ''.join(content)
else:
serve = '暂无'
result = {
'dianping': dianping,
'price': price,
'total_points': total_points,
'enviroment': enviroment,
'serve': serve
}
return result
十五,切割地址数据函数
def split_address(self, str):
text_list = []
chinese_list = []
num_list = []
if str:
text_split = re.split('\S+>', str)
try:
text_split.remove('')
except ValueError:
pass
for i in text_split:
try:
dex = i.index('<')
except ValueError:
if i != ' ':
s_dict = {'belong': None,
'index': None,
'value': i}
text_list.append(s_dict)
continue
if dex != 0:
if i[:dex] == ' ':
pass
else:
s_dict = {'belong': None,
'index': None,
'value': i[:dex]}
text_list.append(s_dict)
tag = re.findall('.*?">(.*?);', i[dex:])
if tag:
val = 'uni' + tag[0]
if 'address' in i[dex:]:
chinese_list.append(val)
s_dict = {'belong': 'address_list',
'index': chinese_list.index(val),
'value': val}
elif 'num' in i[dex:]:
num_list.append(val)
s_dict = {'belong': 'num_list',
'index': num_list.index(val),
'value': val}
else:
s_dict = {'belong': None,
'index': None,
'value': val}
text_list.append(s_dict)
return text_list, chinese_list, num_list
十六,详情页数据的获取函数
def detail_page(self, response):
item = response.meta['huihui']
html = response.text
t = html.find('验证中心')
if t == -1:
print('未遇到验证模块,开始获取加密字体')
print('当前所在分类==={}===位置==={}==='.format(item['classify_name'], item['administrative_region_name']))
start = re.findall('', html, re.S)
address = re.findall('(.*?)', html, re.S)
phone = re.findall('(.*?)
', html, re.S)
huasuan = re.findall('划算: (.*?)', html, re.S)
serve_score = re.findall('服务: (.*?)', html, re.S)
huanjing = re.findall('环境: (.*?)', html, re.S)
if phone:
phone_list = self.split_list_page(phone[0])
else:
phone_list = []
if huasuan:
huasuan_list = self.split_list_page(huasuan[0])
else:
huasuan_list = []
if serve_score:
serve_score_list = self.split_list_page(serve_score[0])
else:
serve_score_list = []
if huanjing:
huanjing_list = self.split_list_page(huanjing[0])
else:
huanjing_list = []
data = {
'phone': phone_list,
'huasuan': huasuan_list,
'serve_score': serve_score_list,
'huanjing': huanjing_list
}
if address:
text_list, address_list, num_list = self.split_address(address[0])
else:
text_list = []
address_list = []
num_list = []
self.get_detailwoff_url(html)
self.get_adress_url(html)
result = self.detail_change(data)
if item['total_points'] == '暂无':
item['total_points'] = result['huasuan']
if item['environment'] == '暂无':
item['environment'] = result['huanjing']
if item['serve_number'] == '暂无':
item['serve_number'] = result['serve_score']
try:
item['start'] = start[0]
except Exception as e:
item['start'] = '该用户暂无星级'
item['phone'] = result['phone'].strip().replace(' ', '')
if text_list:
result_address, result_num = self.address_change(address_list=address_list, num_list=num_list)
end_address = ''
for code in text_list:
if code['belong'] == 'address_list':
str = result_address['address'][code['index']]
end_address += str
elif code['belong'] == 'num_list':
str = result_num['num'][code['index']]
end_address += str
elif code['belong'] is None:
str = code['value']
end_address += str
else:
end_address = '暂无'
item['address'] = end_address
print(item)
yield item
else:
print('详情页正确请求地址url:{}'.format(response.meta['redirect_urls'][0]))
print('出现验证码时的请求地址为:{}'.format(response.url))
time.sleep(10)
url = response.meta['redirect_urls'][0]
yield scrapy.Request(url=url,
callback=self.detail_page,
headers=DEFAULT_REQUEST_HEADERS,
meta={'huihui': item},
dont_filter=False
)