Lingmoumou's Blog

きっといつかって愿うまま

0%

爬大众点评的商铺列表

斗智斗勇,不同时间不同地点的页面内容不同,反爬需要解析的内容也不同。大众点评中商铺列表的最大页数是50页,所以根据分类进行搜索。爬取的时候不需要登录。这次使用的是vscode+anconda+mysql+redis,前期试着用scrapy不通过模拟网页去爬取,需要看运气什么时候能正常获取到页面,这个方法等回头再试试。后来就用request+selenium模拟网页进行爬取,虽然慢,但是不会跳出验证码页面,还是能够接受的。

前期准备

访问大众点评的页面时,若没有user-agent与cookie会自动跳转至美团的验证页面,让用户输入验证码。因此访问页面时,需要带着user-agent,先进入首页获取cookie,再跳转至其他页面中。

User-Agent

可以获取浏览器中的user-agent,也可以百度出user-agent的列表,自己轮换着使用。在scrapy框架下,可以使用scrapy-redis中新建一个user-agent的文件,随机读取。

1
2
3
4
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
}
res = current_session.get("http://www.dianping.com/zhengzhou/ch10/g112r2038", headers=headers)

使用request库时请求链接,可以从selenium打开的浏览器中获取cookie,也可以直接用request请求主页,然后记录cookie传递给下一个request。

1
2
3
4
5
6
7
8
9
10
current_session = requests.session()

browser = webdriver.Chrome()
browser.get("http://www.dianping.com/zhengzhou")
cookie = browser.get_cookies() #获取浏览器cookies
c = requests.cookies.RequestsCookieJar()
for i in cookie: #添加cookie到CookieJar
c.set(i["name"], i["value"])

current_session.cookies.update(c) #更新session里的cookie

延时

1
time.sleep(1)

顺藤摸瓜

解析页面的时候发现使用图片代替文字和使用文字字体两种进行反爬。使用图片代替文字的解决思路主要是通过css找到文字出现在svg图片中的位置(x,y)。字体的话,就只能依靠辅助方式来把文字识别出来了。

图片代替文字

数字反爬

商铺列表1
chrome里按F12,在调试页面可以看到css中引用了背景图片,先找到这个css文件和svg文件。
css中记录的相对位置:
css文件

再看看svg的源码如下:
svg文件1

现将css中这个svg相关的样式都提出来,形成各字典(样式名称,x,y),解析svg文件,将text标签中的y值和文字提取出来。由于svg中使用的字的大小都是12px,所以先确定css中的样式在svg的哪一行,再确定是第几个就行了。

1
2
3
4
5
6
7
8
9
10
11
12
def svg_num_dict(css_list,svg_html):
# css中的样式进行格式转换
css_list = [[i[0], abs(float(i[1])), abs(float(i[2]))] for i in css_list ]

digits_list = re.findall(r'>(\d+)<', svg_html) # 解析svg中的数字
ys=re.findall(r'y="(\d+)">',svg_html) # 解析svg中的y值
for i in css_list:
for j in range(len(ys)):
if i[2]<= int(ys[j]):
other_dic[i[0]]=digits_list[j][int(i[1])//12] # 存入other_dic全局字典中
# redis_con.set(i[0], digits_list[j][int(i[1])//12]) # 存入redis中
break

文字反爬

目前在大众点评中见到的是两种,在页面中体现的方式都一样,用svgmtsi包裹着,用class中的前缀来区分是使用哪个svg,css中样式提取与形成字典的方式与数字的差不多。主要区别在于svg的解析方式不太一样。一种svg中使用的标签是text,另一种使用的是path和textPath。
商铺列表2

text标签

svg文件2

1
2
3
4
5
6
7
8
9
10
11
12
def svg_font_dict(css_list,svg_html):
# css中的样式进行格式转换
css_list = [[i[0], abs(float(i[1])), abs(float(i[2]))] for i in css_list ]

digits_list = re.findall(r'<text x="\d+" y="\d+">(.*?)<', svg_html)
ys=re.findall(r'y="(\d+)">',svg_html)
for i in css_list:
for j in range(len(ys)):
if i[2]<= int(ys[j]):
other_dic[i[0]]=digits_list[j][int(i[1])//12] # 存入other_dic全局字典中
# redis_con.set(i[0], digits_list[j][int(i[1])//12]) # 存入redis中
break

textPath标签

通过path知道x与y的值

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
def svg_dict(css_list,svg_html):
# css中的样式进行格式转换
css_list = [[i[0], abs(float(i[1])), abs(float(i[2]))] for i in css_list ]

# 解析textPath标签
svg_text_r = r'<textPath xlink:href="(.*?)" textLength="(.*?)">(.*?)</textPath>'
svg_text_re = re.findall(svg_text_r, svg_html)

dict_avg = {}
for data in svg_text_re:
dict_avg[int(data[0].replace("#", ""))-1] = list(data[2])

# 解析path标签
svg_y_r = r'<path id="(.*?)" d="(.*?) (.*?) (.*?)"/>'
svg_y_re = re.findall(svg_y_r, svg_html)
list_y = []
for data in svg_y_re:
list_y.append(data[2])

for i in css_list:
for j in range(len(list_y)):
if i[2]<= int(list_y[j]):
other_dic[i[0]]=dict_avg[j][int(i[1])//12] # 存入other_dic全局字典中
# redis_con.set(i[0], dict_avg[j][int(i[1])//12]) # 存入redis中
break

字体代替文字

字体反爬

商铺列表3
查看源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<span class="addr">
<svgmtsi class="address">&#xe457;</svgmtsi>
<svgmtsi class="address">&#xf0c8;</svgmtsi>
<svgmtsi class="address">&#xe0e4;</svgmtsi>
<svgmtsi class="address">&#xf392;</svgmtsi>

<svgmtsi class="address">&#xea3a;</svgmtsi>
<svgmtsi class="address">&#xeae4;</svgmtsi>
<svgmtsi class="address">&#xe0e4;</svgmtsi>
<svgmtsi class="address">&#xe0ce;</svgmtsi>
<svgmtsi class="address">&#xe057;</svgmtsi>
<svgmtsi class="address">&#xf230;</svgmtsi>
<svgmtsi class="address">&#xe235;</svgmtsi>
<svgmtsi class="address">&#xf616;</svgmtsi>
100
<svgmtsi class="address">&#xe2cd;</svgmtsi>
<svgmtsi class="address">&#xe0e4;</svgmtsi>
<svgmtsi class="address">&#xf03d;</svgmtsi>
</span>

唯一突破口就是css中的address,发现他的唯一属性就是有个字体。所以去css的文件中搜索这个字体,发现使用了自定义字体。
css文件

在新的 @font-face 规则中,您必须首先定义字体的名称(比如 myFirstFont),然后指向该字体文件。

然后下载字体,通过FontCreator或者FontEditor打开字体文件,我这里用了百度AI里开放API,将打开后的字体文件截图后进行文字识别,获取下面代码中的list数组。
将字体与编号组成字典。

1
2
3
4
5
6
7
def init_font():
font2 = TTFont('./font/f1c26632.woff')
keys = font2['glyf'].keys()
values =list(' .1234567890店中美家馆小车大市公酒行国品发电金心业商司超生装园场食有新限天面工服海华水房饰城乐汽香部利子老艺花专东肉菜学福饭人百餐茶务通味所山区门药银农龙停尚安广鑫一容动南具源兴鲜记时机烤文康信果阳理锅宝达地儿衣特产西批坊州牛佳化五米修爱北养卖建材三会鸡室红站德王光名丽油院堂烧江社合星货型村自科快便日民营和活童明器烟育宾精屋经居庄石顺林尔县手厅销用好客火雅盛体旅之鞋辣作粉包楼校鱼平彩上吧保永万物教吃设医正造丰健点汤网庆技斯洗料配汇木缘加麻联卫川泰色世方寓风幼羊烫来高厂兰阿贝皮全女拉成云维贸道术运都口博河瑞宏京际路祥青镇厨培力惠连马鸿钢训影甲助窗布富牌头四多妆吉苑沙恒隆春干饼氏里二管诚制售嘉长轩杂副清计黄讯太鸭号街交与叉附近层旁对巷栋环省桥湖段乡厦府铺内侧元购前幢滨处向座下県凤港开关景泉塘放昌线湾政步宁解白田町溪十八古双胜本单同九迎第台玉锦底后七斜期武岭松角纪朝峰六振珠局岗洲横边济井办汉代临弄团外塔杨铁浦字年岛陵原梅进荣友虹央桂沿事津凯莲丁秀柳集紫旗张谷的是不了很还个也这我就在以可到错没去过感次要比觉看得说常真们但最喜哈么别位能较境非为欢然他挺着价那意种想出员两推做排实分间甜度起满给热完格荐喝等其再几只现朋候样直而买于般豆量选奶打每评少算又因情找些份置适什蛋师气你姐棒试总定啊足级整带虾如态且尝主话强当更板知己无酸让入啦式笑赞片酱差像提队走嫩才刚午接重串回晚微周值费性桌拍跟块调糕')

for k, v in zip(keys, values):
font_dic[re.sub(r'uni', r'\\u', k).encode().decode('unicode_escape')]=v

获取CSS和对应的SVG

由于css是随机的,因此每次打开的打开的页面自动获取css和对应的svg的样式。我这里虽然把目前见到的解析都写了,但是需要指定哪一种前缀用哪种解析方式,后续可以改进成为自动判定。
然后把css中的每个样式形成个列表,传到需要解析的方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def init_dic(html):
# 获取页面中使用的css样式的地址
css_url = "http:" + re.search(r'(//.+svgtextcss.+\.css)', html).group()
css_res = current_session.get(css_url, headers=headers)

# 根据svgmtsi标签,确定svg对应的css样式的前缀和地址
css_list = re.findall(r'svgmtsi(.+)','\n'.join(css_res.text.split('}')))
for c in css_list:
svg_prefix = re.findall(r'class\^="(\w+)"', '\n'.join(c.split('}')))[0]
svg_url = "http:" + re.findall(r'class\^="\w+".+(//.+svgtextcss.+\.svg)', '\n'.join(c.split('}')))[0]

dic[svg_prefix]=svg_url
svg_res = current_session.get(svg_url, headers=headers)

# 根据前缀合成css样式的字典列表
pattern='('+svg_prefix+r'\w+){background:(.+)px (.+)px;'
c_list = re.findall(pattern, '\n'.join(css_res.text.split('}')))

# 前缀解析
if svg_prefix=='gvj' :
svg_num_dict(c_list,svg_res.text)

if svg_prefix=='bwh':
svg_font_dict(c_list,svg_res.text)

替换

由于使用图片代替文字的css翻译的字典存成了一个,所以用一个方法进行解析。而字体的class都是一个,主要根据内容不同来解析,所以单独写了一下。可以使用全局字典存储这些字典,也可以使用redis存储这些字典。替换成redis后,会面临这些值取出来以后是byte类型的,需要转换,经过byte、str、urf-8、unicode来回折腾后,我还是决定用全局字典方便点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用图片代替文字的替换
def replace_keyword(html):
review_kw=re.findall(r'<svgmtsi class="(.*?)">',html)
for kw in review_kw:
html=html.replace('<svgmtsi class="'+kw+'"></svgmtsi>',other_dic[kw])
return str(html)

# 使用文字字体的替换
def replace_keyword_font(html):
tag=re.findall(r'<svgmtsi class="(.*?)">',html)[0]
address_kw=re.findall(r'<svgmtsi class="\w+">(.*?)<',html)
for kw in address_kw:
html=html.replace('<svgmtsi class="'+tag+'">'+kw+'</svgmtsi>',font_dic[kw])
return str(html)

解决问题

空值

搜索结果为空

搜索结果为空

在点评中表现为整个content-wrap中只有div[@class=not-found]一个div,所以直接判断是否这个div是否存在。

解析的某个内容为空

商铺列表

在解析页面内容的时候会有很多判断,大部分都是判断某个标签是否存在即可。

获取页数

由于进入页面后,首先获取当前搜索结果的页数,因此把搜索结果为空的判断加在获取页数的前面了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def parse_page_size():
content=browser.find_element_by_xpath('//*[@class="content-wrap"]').get_attribute('innerHTML')
# 判断是否搜索结果为空
if re.search(r'class="not-found"',str(content))==None:
# 若仅有一页,没有页码条
if re.search(r'class="page"',str(content))==None:
return 1
else:
pages=browser.find_elements_by_xpath('//*[@class="page"]/a[@class="PageLink"]')
list=[]
for page in pages:
list.append(int(page.text))
# 若有页码条,找出页码的最大值
return max(list)
else:
return 0

去重

通过redis的一个set对重复数据进行过滤。如果set用这个商铺的id,则跳过解析,若没有这个商铺的id,解析保存到数据库中后,在set中新增这个id。

1
2
3
4
if redis_con.sismember('shops', sid)==False:
shop=parse_item(item)
insert_shop(shop)
redis_con.sadd("shops",sid)

整体流程

分类与地点

分类与地点

分类

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
def category_classfy():
browser.get("http://www.dianping.com/zhengzhou")
browser.get("http://www.dianping.com/zhengzhou/ch10/g112")
classfies=browser.find_elements_by_xpath('//*[@id="classfy"]/a')
print("len:"+str(len(classfies)-1))
for i in range(14,len(classfies)-1):

if i==18:
browser.find_element_by_xpath('//a[@class="more J_packdown"]').click()

classfy=browser.find_elements_by_xpath('//*[@id="classfy"]/a')[i]
cat_id=classfy.get_attribute("data-cat-id")
cid='g'+cat_id
name=classfy.get_attribute("data-click-name").split('_')[2]
url=classfy.get_attribute("href")
insert_category_classfy(cat_id,cid,name,url,None)
classfy.click()
flag=is_element_by_xpath_exist('//*[@id="classfy-sub"]/a')

if flag==True:
classfies_sub=browser.find_elements_by_xpath('//*[@id="classfy-sub"]/a')
# if classfies_sub!=None and len(classfies_sub)>0:
for sub in classfies_sub:
sub_name=sub.get_attribute("data-click-name").split('_')[3]
if 'all'!=sub_name:
cat_sub_id=sub.get_attribute("data-cat-id")
sub_cid='g'+cat_sub_id
sub_url=sub.get_attribute("href")
insert_category_classfy(cat_sub_id,sub_cid,sub_name,sub_url,cid)
time.sleep(10)

地点

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
def category_region(n,root_url,tip,xpath_p,xpath_s):
browser.get("http://www.dianping.com/zhengzhou")
browser.get(root_url)
bussi=browser.find_elements_by_xpath(xpath_p) #('//*[@id="bussi-nav"]/a')
time.sleep(10)

browser.find_elements_by_xpath('//*[@id="J_nav_tabs"]/a')[n].click()
print("len:"+str(len(bussi)))
for i in range(len(bussi)):
browser.get(root_url)
browser.find_elements_by_xpath('//*[@id="J_nav_tabs"]/a')[n].click()
bussi=browser.find_elements_by_xpath(xpath_p)[i]
cat_id=bussi.get_attribute("data-cat-id")
cid='r'+cat_id
name=bussi.get_attribute("data-click-title")
url=bussi.get_attribute("href")
insert_category_region(cat_id,cid,name,url,None,tip)
bussi.click()
time.sleep(1)
if i==2:
# if i==34:
browser.find_element_by_xpath(xpath_s+'[@class="more J_packdown"]').click()
flag=is_element_by_xpath_exist(xpath_s) #('//*[@id="bussi-nav-sub"]/a')
print(name+":"+str(flag))
if flag==True:
bussi_sub=browser.find_elements_by_xpath(xpath_s)
# if classfies_sub!=None and len(classfies_sub)>0:
for sub in bussi_sub:
cat_sub_id=sub.get_attribute("data-cat-id")
if '0'!=cat_sub_id and cat_sub_id!=None:
sub_name=sub.text
sub_cid='r'+cat_sub_id
sub_url=sub.get_attribute("href")
insert_category_region(cat_sub_id,sub_cid,sub_name,sub_url,cid,tip)
time.sleep(5)

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def init():
browser.get("http://www.dianping.com/zhengzhou")
time.sleep(3)

cookie = browser.get_cookies() #获取浏览器cookies
c = requests.cookies.RequestsCookieJar()
for i in cookie: #添加cookie到CookieJar
c.set(i["name"], i["value"])
current_session.cookies.update(c) #更新session里的cookie

res = current_session.get("http://www.dianping.com/zhengzhou/ch10/g112r2038", headers=headers)
# 初始化css字典
init_dic(res.text)
init_font()

解析页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def parse_page(base_url):
browser.get(base_url)
# 获取页码
pageSize=parse_page_size()
if pageSize!=0:

for i in range(pageSize):
print(i)
# 翻页
if i!=0:
url=base_url+"p"+str(i+1)
browser.get(url)
items=browser.find_elements_by_xpath('//*[@id="shop-all-list"]/ul/li')

# 遍历内容信息
for item in items:
tit=item.find_element_by_xpath('./div[2]/div[@class="tit"]/a')
sid=tit.get_attribute('data-shopid')
# 重复的不再解析
if redis_con.sismember('shops', sid)==False:
shop=parse_item(item)
insert_shop(shop)
redis_con.sadd("shops",sid)

解析信息

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

def parse_item(item):
shop={}
# 商铺ID,商铺名称,商铺URL
tit=item.find_element_by_xpath('./div[2]/div[@class="tit"]/a')
shop['id']=tit.get_attribute('data-shopid')
shop['name']=tit.get_attribute('title')
shop['url']=tit.get_attribute('href')

# 是否有团购、外卖信息
promoes=item.find_elements_by_xpath('./div[2]/div[@class="tit"]/div[@class="promo-icon J_promo_icon"]/a')
for promo in promoes:
data_click_name=promo.get_attribute('data-click-name')
if data_click_name=='shop_group_icon_click':
group_title=promo.get_attribute('title')
shop['groupdeal'] = re.findall(r'.*(\d+).*', group_title)[0] # 若有团购,记录团购信息的个数
if data_click_name=='shop_icon_takeaway_click':
shop['takeway']='1'

# 评分
comment=item.find_element_by_xpath('./div[2]/div[@class="comment"]')
stars=comment.find_element_by_xpath('./span')
shop['stars']=stars.get_attribute('title')
star_num_class=stars.get_attribute('class')
shop['stars_num']=re.findall(r'.*-str(\d+).*',star_num_class)[0]

# 评价条数
review_btn=comment.find_element_by_xpath('./a[@data-click-name="shop_iwant_review_click"]')
if re.search(r'<(.*)>',str(review_btn.get_attribute('innerHTML')))!=None:
review=review_btn.find_element_by_xpath('./b').get_attribute('innerHTML')
shop['reviews']=replace_keyword(review)
else:
shop['reviews']=None

# 人均消费
avgprice_btn=comment.find_element_by_xpath('./a[@data-click-name="shop_avgprice_click"]')
if re.search(r'<(.*)>',str(avgprice_btn.get_attribute('innerHTML')))!=None:
avgprice=avgprice_btn.find_element_by_xpath('./b').get_attribute('innerHTML')
shop['avgprice']=replace_keyword(avgprice)
else:
shop['avgprice']=None

tag_addr=item.find_element_by_xpath('./div[2]/div[@class="tag-addr"]')

# 所属分类
classfy_href=tag_addr.find_element_by_xpath('./a[@data-click-name="shop_tag_cate_click"]').get_attribute('href')
shop['classfy']=classfy_href[int(classfy_href.rfind('/'))+1:int(len(classfy_href))]

# 所属地区
region_href=tag_addr.find_element_by_xpath('./a[@data-click-name="shop_tag_region_click"]').get_attribute('href')
shop['region']=region_href[int(region_href.rfind('/'))+1:int(len(region_href))]

# 地址
address=tag_addr.find_element_by_xpath('./span[@class="addr"]').get_attribute('innerHTML')
shop['address']=replace_keyword_font(address)

# 推荐菜
recommends=item.find_elements_by_xpath('./div[2]/div[@class="recommend"]/a')
recommend={}
for rc in recommends:
recommends_href=rc.get_attribute('href')
recommends_idx=recommends_href[int(recommends_href.rfind('/'))+1:int(len(recommends_href))]
recommend[recommends_idx]=rc.text
shop['recommend']=str(recommend)

# 评价:口味、环境、服务
if re.search(r'class="comment-list"',str(item.find_element_by_xpath('./div[2]').get_attribute('innerHTML')))!=None:
comment_lists=item.find_elements_by_xpath('./div[2]/span[@class="comment-list"]/span')
favor=comment_lists[0].find_element_by_xpath('./b').get_attribute('innerHTML')
shop['comment_favor']=replace_keyword(favor)
env=comment_lists[1].find_element_by_xpath('./b').get_attribute('innerHTML')
shop['comment_env']=replace_keyword(env)
service=comment_lists[2].find_element_by_xpath('./b').get_attribute('innerHTML')
shop['comment_service']=replace_keyword(service)
else:
shop['comment_favor']=None
shop['comment_env']=None
shop['comment_service']=None

print("--- shop "+shop['id']+"---")
return shop

主程序

1
2
3
4
5
6
7
8
def main():
# 从redis中取分类组合
list=redis_con.lrange("category",0,999)
base="http://www.dianping.com/zhengzhou/ch10/"
for i in list:
parse_page(base+str(i,encoding="utf-8"))
redis_con.lrem("category",1,i)
time.sleep(2)

结果

商铺分类

商铺地点

商铺结果

辅助工具

Xpath Helper

Xpath Helper

FontCreator

FontCreator

百度字体编辑器FontEditor
FontEditor


参考链接:
[1] 小白进阶之Scrapy第三篇(基于Scrapy-Redis的分布式以及cookies池)