其他分享
首页 > 其他分享> > 爬取大众点评数据的血泪

爬取大众点评数据的血泪

作者:互联网

公司最近致力于实现餐饮行业的AI发展模式,领导希望采集一些餐饮数据来提供理论支持。所以没多少头发的我 ,被喊过来做数据收集。
想到餐饮数据的收集,第一反应是去爬取美团/大众点评的数据,对比了下美大众点评的数据,发现两者差不多,没什么太大的不同,但大众点评的数据更符合我们的需求(其实是听说大众点评的反爬没有那么狠),就果断选择爬取大众点评的数据。很怀念大众点评没有被美团收购的时光,那个时候数据是随便爬的。

在爬取大众点评之前,已经想到会遇到反爬,只是没想到反爬措施这么狠。
爬取的关键问题主要是2点:

  1. IP被封
  2. 网页内容加密

关于ip的问题,可以使用代理来解决,网上有一些开源的代理ip,当然如果你是有钱人家的孩子,可以购买代理。

因为我要爬取每个商户页面的详细信息,需要进入到每个商户的主页,url类似这种:http://www.dianping.com/shop/66755991.
后面那一串数字是代表商户的id,我只要获取到所有的shopid就可以进行店铺详情页爬取。这里的问题是如何获取到shopid。

如何获取shopid?

大众点评只显示前50页的内容,这也是为了反爬,为了获取全部的内容,采取组合美食分类和区域id的方式,这样基本上就可以限制页数小于50。
url类似这种:
http://www.dianping.com/shenzhen/ch10/g117r1949
其中g117代表美食分类,r1949代表区域id。

  1. 先获取大众点评的美食分类的标签
  2. 按照行政区获取各区域的id
  3. 将美食分类和区域组合就可以获取到该链接下的shopid。

这里获取到所有的shopid之后,我尝试进行了店铺详情的爬取,在成功爬取几个店铺详情页后,楼主的IP就被封了(大概封了2周的时间),果断采用代理的方式,但是发现开源的代理中本来就没有几个能 用的ip,而一个ip在爬取2页面差不多就要被封了,这样导致效率非常低。这个时候想到爬取移动端(APP)的数据。在进行一番验证后,发现shopid在网页端和移动端是一致的。爬取移动端的数据,代理ip的有效时间会好很多。最后就果断采取爬取app端的数据。
说明下:我也尝试了使用自动化测试框架selenium去模拟人为操作,但是大众点评可以识别出selenium框架,直接就进入到了验证中心,输入验证码后仍然会报服务拒绝。 就果断弃之(貌似有阉割版的浏览器驱动,可以跳过大众点评的验证)。

采用代理ip和爬取移动端的数据解决了反爬的第一个问题。

下面来讲述一下破解页面加密的血泪史。
打开某个店铺的页面,以地址为例,我们看到的内容是这样的,
在这里插入图片描述
按下F12,源码中是这样的。
在这里插入图片描述
(备注:图中不是同一个门店的页面和源码,因为我的ip被封了,就找了本地文件截图,但意思明白就好。)
这里看到源码中的白色框框就是大众点评加密的内容,需要破解的也就是这部分。这是什么鬼东西,关键信息全部乱码。验证了一个道理,你看到的不一定都是真的。
经过一番查找博客,才搞明白这是css的一种机制,之所以显示白色的方框,是因为在开发网页时指定了字体。这里的乱码是由于unicode编码导致,查看源文件可以看到具体的编码信息。
在这里插入图片描述
这些字体文件是跳过css加载的,我们在源码搜索svgtext可以看到
在这里插入图片描述
复制href后面的内容,加上http:(http://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/aa8cc71119730f22aed80ecd0ca67b7d.css)
我们打开该css链接,内容如下:

在这里插入图片描述

其中网页不同的地方使用了不同的字体,绿色框的部分就是我们要找的字体网址,输入该链接(加上http:),会下载该字体,使用百度在线字体编译器(http://fontstore.baidu.com/static/editor/index.html)
打开后,下载的woff文件如下:
在这里插入图片描述
绿色部分对应的就是该字体的编码,与源码中的unicode编码相比可知,编码的后四位是一样的,只要找到源码中unicode后四位一致的字符编码,该编码对应的汉字就是网页源码中要显示的信息。这里只要建立编码—汉字字典,取对应的编码的汉字就可以破解网页源码中的白色框框。这里的难点就是建立编码—汉字字典。暂时没有成熟的技术实现。这里我采用比较笨拙的办法,打开下载不同的woff文件,发现woff文件中的汉字和顺序都是一样,不同的是汉字编码。所以就手动按照顺序把汉字复制下来。然后再建立编码—汉字字典。(如果有大佬实现了自动建立,多多指教),具体代码会在后面给出。
获取shopid的代码,这里使用的是本机ip,没有使用代理ip

import requests
import re
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
from time import sleep
import random

# 获取食品类别
def get_classfy():
    classfy_list = []
    url = 'http://www.dianping.com/shenzhen/ch10'
    user_agent = UserAgent().random
    headers = {'User-Agent': user_agent}
    res = requests.get(url, headers=headers)
    print(res.text)
    soup = BeautifulSoup(res.text, 'html')
    classfy = soup.find('div', id='classfy')
    for i in range(len(classfy.find_all('a'))):
        classfy_list.append(int(classfy.find_all('a')[i]['data-cat-id']))
    return classfy_list

regionList = [
    # 福田区
    ('futian', 'http://m.dianping.com/shenzhen/ch10/r29'),
    # 南山区
    ('nanshan', 'http://m.dianping.com/shenzhen/ch10/r31'),
    # 罗湖区
    ('luohu', 'http://m.dianping.com/shenzhen/ch10/r30'),
    # 盐田区
    ('yantian', 'http://m.dianping.com/shenzhen/ch10/r32'),
    # 龙华区
    ('longhua', 'http://m.dianping.com/shenzhen/ch10/r12033'),
    # 龙岗区
    ('longgang', 'http://m.dianping.com/shenzhen/ch10/r34'),
    # 宝安区
    ('baoan', 'http://m.dianping.com/shenzhen/ch10/r33'),
    # 坪山区
    ('pingshan', 'http://m.dianping.com/shenzhen/ch10/r12035'),
    # 光明区
    ('guangming', 'http://m.dianping.com/shenzhen/ch10/r89951')
]

# 获取各行政区单位id
def get_region_list(regionUrl):
    region_id_name = []
    user_agent = UserAgent().random
    headers = {'User-Agent': user_agent}
    res = requests.get(regionUrl, headers=headers)
    print(res.text)
    soup = BeautifulSoup(res.text, 'lxml')
    region = soup.find('div', class_='menu sub')
    for i in range(1, len(region.find_all('a', class_="item Fix"))):
        region_id_name.append(
            (int(region.find_all('a')[i]['data-itemid']), str(region.find_all('a')[i]['data-itemname'])))
    return region_id_name
def get_all_area_list(regionurlList):
    all_area = []
    for regionname, regionurl in regionurlList:
        region_dict={}
        region_id_name = get_region_list(regionurl)
        region_dict[regionname]=region_id_name
        all_area.append(region_dict)
    return all_area
#组合分类和区域id获得URL
def recostution_url(classfy_list, all_area):
    Reurl = []
    for classfy in classfy_list:
        for data in all_area:
            for region,regiondata in data.items():
                for area_id, area_name in regiondata:
                    Reurl.append((region,area_name, area_id,'http://m.dianping.com/shenzhen/ch10/' + 'g' + str(classfy) + 'r' + str(area_id)))
    return Reurl
    # 
    def get_shopContent(Reurl):
    user_agent = UserAgent().random
    headers = {'User-Agent':user_agent}
    res=requests.get(Reurl,headers=headers)
    return res.text
def get_shopId(Reurl):
    resource=get_shopContent(Reurl)
    while '验证中心' in resource:
        print('出现验证码')
        sleep(random.randint(1,3))
        resource=get_shopContent(Reurl)
    shopidList=[]
    soup=BeautifulSoup(resource,'lxml')
    p2 = re.compile(r'{.*}', re.S)
    string=soup.find_all('script')[2].string.strip()
    string=string.replace('true','True')
    string=string.replace('false','False')
    content=eval(re.findall(p2,string)[0])
    for adshop in content['mapiSearch']['data']['list']:
        shopidList.append(adshop['shopUuid'])
    return shopidList
shopList=[]
for i in range(len(Reurl)):
    print(i)
    shopidList=get_shopId(Reurl[i][3])
    for shopid in set(shopidList):
        shop_location={}
        shop_location['shopid']=shopid
        shop_location['region']=Reurl[i][0]
        shop_location['area']=Reurl[i][1]
        shopList.append(shop_location)
    sleep(random.randint(1,3))

这里我为了实现分布式爬虫,使用redis构建队列,将爬取下来的shopid加入到队列中。
爬取店铺详情页的代码,这里我保存店铺详情页源码到hdfs中,页面的解析是下一步工作:

import requests
import json
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
import time
import re
import redis
import random
from hdfs.client import Client
#获取开源代理ip
def getListProxies():
    ip_list = []
    session = requests.session()
    headers = {'User-Agent': UserAgent().random}
    page = session.get("http://www.xicidaili.com/nn", headers=headers)
    soup = BeautifulSoup(page.text, 'lxml')
    taglist = soup.find_all('tr', attrs={'class': re.compile("(odd)|()")})
    for trtag in taglist:
        tdlist = trtag.find_all('td')
        proxy = {'http': 'http://' + tdlist[1].string + ':' + tdlist[2].string}
        ip_list.append(proxy)
    return ip_list


ip_list=getListProxies()
client = Client("http://192.168.31.51:50070")

q = RedisQueue('ftshop')
r = redis.Redis(host='localhost', port=6379)
region_dict = {'市中心区': 1949,
 '车公庙': 7475,
 '上沙/下沙': 12322,
 '梅林': 1560,
 '华强北': 1556,
 '欢乐海岸': 30824,
 '皇岗': 1559,
 '景田': 12321,
 '新洲': 12225,
 '香蜜湖': 1951,
 '荔枝公园片区': 1573,
 '石厦': 12226,
 '八卦岭/园岭': 1557,
 '竹子林': 12324,
 '市民中心': 12323,
 '华强南': 3138,
 '岗厦': 12320,
 '福田保税区': 12319}


def save_page_hdfs(ipPort, file_path, contents):
    """保存网页源码到hdfs

    :param ipPort: hdfs连接地址
    :param file_path: 文件路径
    :param contents: 网页内容
    :return: None
    """
    client = Client(ipPort)
    with client.write(file_path) as writer:
        writer.write(bytes(contents, encoding='utf8'))


proxy = ip_list[0]
error=0
while 1:
    queue_len = r.llen('queue:allshop')
    queue_index = 0
    s = requests.session()
    n = str(q.get_nowait(), encoding='utf8')
    data = json.loads(n)
    shopid = data['shopid']
    region = data['region']
    area = data['area'].encode("utf-8").decode("utf-8")
    headers = {'User-Agent': UserAgent().random,
               'Referer': 'https://m.dianping.com/shenzhen/ch10/r{0}'.format(region_dict[area])}
    url = 'https://m.dianping.com/shop/' + shopid
    try:
        respon = s.get(url, headers=headers, proxies=proxy)
    except Exception as e:
        error = 1
    i = 0
    while '验证中心' in respon.text or '抱歉!页面暂' in respon.text or respon.status_code != 200 or error == 1:
        i = i + 1
        if i < len(ip_list):
            proxy = ip_list[i]
            try:
                respon = s.get(url, headers=headers, proxies=proxy)
            except Exception as e:
                error == 1
        else:
            q.put(n)
            break

    # 用来判断队列是否循环一遍,更新ip池
    queue_index += 1
    if queue_index == queue_len:
        ip_list = getListProxies()

    if '验证中心' not in respon.text:
        if '抱歉!页面暂' not in respon.text:
            print('success')
            print(n)
            filepath = '/dazhongdianping/sz/{0}/{1}/{2}.html'.format(region, area, shopid)
            try:
                save_page_hdfs('http://192.168.31.51:50070', filepath, respon.text)
                #r.rpush('metadata', json.dumps(
                 #   {'shopid': shopid, 'url': url, 'region': region, 'area': area, 'filepath': filepath}))
            except Exception as e:
                pass
    time.sleep(1)

建立编码—汉字字典的代码:

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

woffs = [i for i in woff_string if i != '\n' and i != ' ']
#获得svgtext的url
def get_svg_url(soup):
    svgtextcss = re.search(r'href="([^"]+svgtextcss[^"]+)"', str(soup), re.M)
    woff_url = 'http:' + svgtextcss.group(1)
    return woff_url
#获取woff文件url,并下载  
def get_address_woff(woff_url):
    svg_html = requests.get(woff_url).text
    lines = svg_html.split('PingFangSC-')
    partern = re.compile(r',(url.*address)')
    for line in lines:
        out = partern.findall(line)
        if len(out) > 0:
            woff = re.compile('\((.*?)\)')
            address_url = 'http:' + woff.findall(out[0])[0].replace('"', '')
    with open('/home/woff/address.woff', 'wb') as writer:
        writer.write(requests.get(address_url).content)
    return None
    
    # 使用TTfont将woff文件转xml
    from fontTools.ttLib import TTFont
    addressfont = TTFont('/home/tao/woff/address.woff')
    addressfont.saveXML('/home/tao/woff/address.xml')
    
    address_TTGlyphs = addressfont['cmap'].tables[0].ttFont.getGlyphOrder()[2:]
    
    address_dict = {}
    for i, x in enumerate(number_TTGlyphs):
        address_dict[x] = i
#wofflist就是要网页源码中的乱码信息,TTG就是woff文件的编码列表,woffdict就是编码---汉字字典。
def woff_change(wofflist, TTG, woffdict):
    woff_content = ''
    for char in wofflist:
        text = str(char.encode('raw_unicode_escape').replace(b'\\u', b'uni'), 'utf-8')
        if text in TTG:
            content = woffs[woffdict[str(char.encode('raw_unicode_escape').replace(b'\\u', b'uni'), 'utf-8')]]
        else:
            content = char
        woff_content += ''.join(content)
    return woff_content

完整的代码稍后会给出git链接

标签:http,get,血泪,ip,woff,点评,爬取,url,region
来源: https://blog.csdn.net/Tracy_LeBron/article/details/95199012