Python爬虫 | 某点评网站商铺信息采集

1,068 阅读10分钟

这是我参与8月更文挑战的第 9 天,活动详情查看:8月更文挑战

1.简述

今天,咱们就以滑雪为关键字,演示一下如何用Python爬虫采集大众点评的商铺信息吧。

在搜索结果以翻页的形式通过 request.get() 即可获取页面数据,然后再对网页数据进行相关解析即可获得我们需要的商铺信息。

不过在爬虫过程中,我们会发现比如商铺评价数、人均消费以及商铺的地址等信息在网页上显示为,在get的数据中是类似 &#xf622 ,咋一看不知道是什么。这里其实是一种字体反爬,接下来我们就将其个个击破吧。

以下是我们需要采集的数据字段:

字段说明获取方式字体
shop_id商铺ID直接获取
shop_name商铺名称直接获取
shop_star商铺星级直接获取
shop_address商铺地址直接获取
shop_review商铺评价数字体反爬shopNum
shop_price商铺人均消费字体反爬shopNum
shop_tag_site商铺所在区域字体反爬tagName
shop_tag_type商铺分类字体反爬tagName

2.字体反爬处理

打开大众点评,搜索 滑雪,我们在搜索结果页面 按F12进入到开发者模式,选到评价数可以看到其 class 为 shopNum且内容为□,在右侧styles中可见其字体font-family为PingFangSC-Regular-shopNum。其实,点击右侧.css链接可以找到其字体文件链接。考虑到其他涉及到字体反爬的字段信息对应的字体文件链接可能有差异,我们采集另外一种方式进行一次性获取(具体请看下一段)。

字体反爬(评价数)

2.1.获取字体文件链接

我们在网页的head部分,可以找到 图文混排css,其对应的css地址就包含了后续会用到的全部字体文件链接,直接用requess.get()请求改地址即可返回全部字体名称及其字体文件下载链接。

字体反爬(字体链接)

- 定义获取网页数据的函数方法get_html()

# 获取网页数据
def get_html(url, headers):   
    try:
        rep = requests.get(url ,headers=headers)
    except Exception as e :
        print(e)
    text = rep.text
    html = re.sub('\s', '', text) #去掉非字符数据
    
    return html

- 获取网页数据

import re
import requests
# Cookie部分,直接复制浏览器里的即可
headers = {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36",
        "Cookie":"你的浏览器Cookie",
        }
# 搜索关键字
key = '滑雪'
# 基础url
url = f'https://www.dianping.com/search/keyword/2/0_{key}'
# 获取网页数据
html = get_html(url, headers)

- 获取字体文件链接

# 正则表达式获取head里图文混排css中的字体文件链接
text_css = re.findall('<!--图文混排css--><linkrel="stylesheet"type="text\/css"href="(.*?)">', html)[0]  
# 'http://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/29de4c2bc5d95d1e147c3c25a5f4aad8.css'
# 组合成css链接
css_url = 'http:' + text_css
# 获取字体文件链接的网页数据
font_html = get_html(css_url, headers)
# 正则表达式获取 字体信息列表
font_list = re.findall(r'@font-face{(.*?)}', font_html)

# 获取使用到的字体及其链接
font_dics = {}
for font in font_list:
    # 正则表达式获取字体文件名称
    font_name = re.findall(r'font-family:"PingFangSC-Regular-(.*?)"', font)[0]
    # 正则表达式获取字体文件对应链接
    font_dics[font_name] = 'http:' + re.findall(r',url\("(.*?)"\);', font)[0]

- 下载字体文件到本地

# 由于我们用到的只有shopNum、tagName和address,这里只下载这三类字体
font_use_list = ['shopNum','tagName','address']
for key in font_use_list:
    woff = requests.get(font_dics[key], headers=headers).content
    with open(f'{key}.woff', 'wb')as f:
        f.write(woff)
  • 字体文件(存于本地,安装FontCreator可以打开字体文件,查看字体内容,回复FontCreator可以获取安装包下载地址)

字体文件

2.2.创建三类字体与实际字符映射关系

我们先看请求的网页数据中评价数的html内容如下:

<b>
    <svgmtsi class="shopNum">&#xf8a1;</svgmtsi>
    <svgmtsi class="shopNum">&#xee4c;</svgmtsi>
    <svgmtsi class="shopNum">&#xe103;</svgmtsi>
    <svgmtsi class="shopNum">&#xe62a;</svgmtsi>
</b>条评价

其对应的网页显示评价数为4576条评价,我们知道对应关系为4=&#xf8a15=&#xee4c7=&#xe1036=&#xe62a

我们用FontCreator打开shopNum字体文件如下:

shopNum

我们对比可以发现,4在shopNum中对应的是uniF8A1,5对应的是uniEE4C...等等。于是,找到规律,我们知道了在请求的数据中对应数据信息如 &#xf8a1 其实是 uniF8A1真实对应的数字或者文字需要对应于字体文件中的某个字符(4)即可。

这里需要引入python的字体处理第三方库fontTools,修改三类字体的映射关系:

from fontTools.ttLib import TTFont

# 修改三类字体映射关系
real_list = {}
for key in font_use_list:
    # 打开本地字体文件
    font_data = TTFont(f'{key}.woff')
    # font_data.saveXML('shopNum.xml')
    # 获取全部编码,前2个非有用字符去掉 
    uni_list = font_data.getGlyphOrder()[2:]
    # 请求数据中是 "&#xf8a1" 对应 编码中为"uniF8A1",我们进行替换,以请求数据为准
    real_list[key] = ['&#x' + uni[3:] for uni in uni_list]

real_list

我们通过打开这三类字体文件,发现其对应的字符顺序都是一致的(顺序和字符内容),拷贝如下:

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

对于数字类的(其实只有10个,位于字体映射和words字符串前10个),当我们获取反爬字符为 &#xf8a1 其实是 uniF8A1时,我们先在找到其在shopNum对应位置,然后替换同等位置的words字符串中的字符即可

for i in range(10):
	s.replace(real_list['shopNum'][i], words[i])

对于中文字符类的(最多就是len(real_list['tagName'])),其替换逻辑和数字类类似,都是替换同等位置即可。

for i in range(len(real_list['tagName'])):
    s.replace(real_list['tagName'][i], words[i])

3.单页店铺信息解析

通过第2部分对字体反爬的处理,结合可以直接获取的店铺信息字段,我们并可以完成全部店铺信息解析采集。这里我们用re正则表达式进行解析哈,有兴趣的同学也可以用xpath、bs4等其他工具库处理。

我们创建获取单页全部店铺信息数据的函数get_items(html, real_list, words)

# 获取单页全部信息
def get_items(html, real_list, words):    
    # 获取单页全部商铺html整体信息
    shop_list = re.findall(r'<divclass="shop-listJ_shop-listshop-all-list"id="shop-all-list">(.*)<\/div>',html)[0]
    # 获取单页全部商铺html信息组成的列表
    shops = re.findall(r'<liclass="">(.*?)<\/li>', shop_list)
    
    items = []
    for shop in shops:
        # 解析单个商铺信息
        # shop = shops[0]
        item = {}
        # 商铺id(唯一性,用于数据清洗阶段去重)
        item['shop_id'] = re.findall(r'<divclass="txt"><divclass="tit">.*data-shopid="(.*?)"', shop)[0]
        # 商铺名称
        item['shop_name'] = re.findall(r'<divclass="txt"><divclass="tit">.*<h4>(.*)<\/h4>', shop)[0]
        # 商铺星级,由于是二位数,需要除以10.0转化为浮点数
        item['shop_star'] = re.findall(r'<divclass="nebula_star"><divclass="star_icon"><spanclass="starstar_(\d+)star_sml"><\/span>', shop)[0]
        item['shop_star'] = int(item['shop_star'])/10.0
        
        # 其实关于商铺地址信息,在class="operate J_operate Hide"中的data-address是有的
        # 因此,我们不需要用到 字体反爬,直接正则获取吧
        # 商铺地址
        item['shop_address'] = re.findall('<divclass="operateJ_operateHide">.*?data-address="(.*?)"', shop)[0]
        
        shop_name = item['shop_name']
        # 评价数和人均价格,用的是shopNum
        try:
            shop_review = re.findall(r'<b>(.*?)<\/b>条评价', shop)[0]
        except:
            print(f'{shop_name} 无评价数据')
            shop_review = ''
            
        try:
            shop_price = re.findall(r'人均<b>¥(.*?)<\/b>', shop)[0]
        except:
            print(f'{shop_name} 无人均消费数据')
            shop_price = ''
            
        for i in range(10):
            shop_review = shop_review.replace(real_list['shopNum'][i], words[i])
            shop_price = shop_price.replace(real_list['shopNum'][i], words[i])
        # 评价数和人均价格,只取数字,然后组合起来
        item['shop_review'] = ''.join(re.findall(r'\d',shop_review))
        item['shop_price'] = ''.join(re.findall(r'\d',shop_price))
        
        # 商铺所在区域和商铺分类用的是tagName
        shop_tag_site = re.findall(r'<spanclass="tag">.*data-click-name="shop_tag_region_click"(.*?)<\/span>', shop)[0]
        # 商铺分类
        shop_tag_type = re.findall('<divclass="tag-addr">.*?<spanclass="tag">(.*?)</span></a>', shop)[0]
        for i in range(len(real_list['tagName'])):
            shop_tag_site = shop_tag_site.replace(real_list['tagName'][i], words[i])
            shop_tag_type = shop_tag_type.replace(real_list['tagName'][i], words[i])
        # 匹配中文字符的正则表达式: [\u4e00-\u9fa5]
        item['shop_tag_site'] = ''.join(re.findall(r'[\u4e00-\u9fa5]',shop_tag_site))
        item['shop_tag_type'] = ''.join(re.findall(r'[\u4e00-\u9fa5]',shop_tag_type))
        items.append(item)
    
    return items

以下为我们以滑雪为例,获取的首页全部店铺信息数据:

某页全部店铺信息

4.全部页数据获取

大多数情况下,我们搜索结果是多页数据组成的,除了采集单页数据外,我们需要获取全部页的数据。这种情况下,一般先获取页数,然后再进行翻页循环去爬取全部页的数据即可。

4.1.获取数据页数

对于单页数据来说,没有总页数;对于多页数据来说,我们拖到页面最下方,选择最后一页的控件即可找到该值所在的html节点,然后这里用正则表达式获取该值即可。

页数

# 获取页数
def get_pages(html):
    try:
        page_html = re.findall(r'<divclass="page">(.*?)</div>', html)[0]
        pages = re.findall(r'<ahref=.*>(\d+)<\/a>', page_html)[0]
    except :
        pages = 1
    
    return pages

4.2.采集全部数据

我们在解析第一页网页数据的时候,可以获取数据页数、下载反爬字体、获取实际字体real_list映射关系和真实字符组成的字符串words,同时我们也获取了第一页的全部商铺数据构成列表。然后,我们可以从第二页开始遍历到最后一页,将获取的单页数据加到第一个列表中即可。

# 第一页商铺数据构成的列表
shop_data = get_items(html, real_list, words)
# 从第二页开始遍历到最后一页
for page in range(2,pages+1):
    aim_url = f'{url}/p{page}'
    html = get_html(aim_url, headers)
    items = get_items(html, real_list, words)
    shop_data.extend(items)
    print(f'已爬取{page}页数据')
# 转化为dataframe类型
df = pd.DataFrame(shop_data)

获取的全部数据组成的dataframe类型如下:

全部结果

5.总结

在对大众点评及类型的字体反爬机制下,我们先获取字体文件解析出其字符code对应的真实字符映射关系,然后替换code为真实字符即可。

但其实,在Python爬取大众点评商铺信息的实际操作过程中,我们可能会遇到更多复杂的情况,比如 提示要验证中心验证或者提示账号ip限制等等,这种情况下通过设置Cookie、添加ip代理等操作可以进行处理。

完整代码在公众号后台回复0104即可获取~

以上全部内容仅有技术交流,请勿用作任何非法商业活动!