大众点评数据采集分析

大众点评数据采集(2019.06)

介绍

大众点评是中国领先的本地生活信息及交易平台,也是全球最早建立的独立第三方消费点评网站。大众点评不仅为用户提供商户信息、消费点评及消费优惠等信息服务,同时亦提供团购、餐厅预订、外卖及电子会员卡等O2O(Online To Offline)交易服务。

页面分析(以http://www.dianping.com/shop/5717186为例)

  1. 正常的页面如图大众点评数据采集分析_第1张图片
  2. 通过F12审查元素,却发现数字是加密的,如图大众点评数据采集分析_第2张图片
  3. 查看源码(ctrl+u)发现,数字对应的源码是诸如**""**类型的代码,且该代码代表数字0,如何找到其对应关系呢?
    大众点评数据采集分析_第3张图片
    另外发先部分文字也被加密了,如上图绿色框里的e标签,部分文字被加密

数字加密分析

通过上面的简单分析发现,大众点评的部分数字被加密,如何找到代码和数字的对应关系,成了我们的破解加密的关键,我们很容易发现,被加密的数字都是在d标签内,且具有共同的class属性:num, 那此时猜想,会不会是num通过某种规则,被js转换成数字?我们先尝试着用Google调试工具的全局搜索(ctrl+shift+f)num关键字,看会不会有什么惊喜的发现
大众点评数据采集分析_第4张图片
第一个被检索出来的是一个css文件,尝试着点进去,发现如下css片段

@font-face {
    font-family: "Microsoft YaHei";
    src: url("//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/53cfe63b.eot");
    src: url("//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/53cfe63b.eot?#iefix") format("embedded-opentype"),url("//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/53cfe63b.woff");
}

.num {
    font-family: 'Microsoft YaHei';
}

从这段css可以看出,class="num"的标签,指定了字体库地址,此时大胆猜测,是不是使用了所谓的字体加密。为了验证猜想,我们需要:

1. 下载字体库文件

下载地址 http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/53cfe63b.woff

2. 查看.woff字体文件

为了查看.woff文件内容,我们需要一个字体编辑工具FontCreator,(或者使用百度字体库的字体编辑工具:http://fontstore.baidu.com/static/editor/index.html#)发现文件内容长这样
大众点评数据采集分析_第5张图片,我们发现每个新字体上方都有个编码,咦,这个编码怎么有点熟悉,前面提到""代表数字0,
字体文件里也有个0,其上方代码是**$EC2D**,刚好是前面编码的后四位,这不正是我们要的映射关系吗?其实这个字体文件,就是web前端使用的字体库。

3. 使用python解析.woff文件

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

至此,字体库加密告一段落,拿着获得的编码和字符的对照字典,把html源码中的编码替换成对应的字符,就可以拿到正确的数据了。

文字加密

字体库完成解密之后,你会发现,有些数据还是被加密着,比如地址,工作时间,评论中的部分文字也被加密了,且不能被字体库解密
标签字体加密大众点评数据采集分析_第6张图片
通过定位元素发现,这些被加密的文字,其实是图片,图片链接是一个svg文件。svg文件的分析这里不做过多介绍了,网上有很多分析教程。关键代码如下,传入svg源码文件,返回类型svg解密的关键属性,x,y,文本,font-size

    def decrypt_svg(self,content):
    	# content 传入svg文件源码
    	# svg_data 将svg源码解析结果存入字典
        svg_data = {"fontsize": "", "decrypt": []}
        fontsize = 0
        try:
            fontsize = re.search(r'font-size:(\d+)px;', content).group(1)
        except Exception as e:
            print(e)
        svg_data.update({"fontsize": float(fontsize)})
		
        ypaths = re.findall(r'id=\"(.*?)\" d="M0 (\d+)', content)
        if ypaths:
            for (yid, y) in ypaths:
                textpath_pattern = re.compile(r'(.*?)<'.format(yid))
                # 记录x,y以及该行的文本
                svg_data["decrypt"].append({'x': 0, 'y': y, 'text': textpath_pattern.findall(content)[0]})
        else:
            paths = re.findall(r'(.*?)<', content)
            for (x, y, decrypt_str) in paths:
                svg_data["decrypt"].append({'x': x, 'y': y, 'text': decrypt_str})
        return svg_data
        
     def class2str(self,classname,x,y,tagname=''):
      rst = ''
      for k,v in self.svg_json.items():
          # print(k)
          if classname.startswith(v.get('startswith')):
              fontsize = v.get('fontsize')
              decrypt = v.get('fontsize')
              for item in v.get('decrypt'):
                  # 根据css属性y值找到svg结果对照文本
                  if float(item.get('y')) >= float(y):
                      #  根据css属性x值/fontzize值,找到文本索引,取出值即为该类映射的字符
                      xindex = math.floor(float(x)/fontsize)
                      rst = item.get('text')[xindex]
                      # print('rst:',rst)
                      break
        return rst

同样,最后形成了类名和字符的映射关系,将源码中对应类名的标签替换成相应的文字即可完成破解

执行结果
大众点评数据采集分析_第7张图片

总结

  1. 文章为首次研究时记录的分析步骤,部分分析描述可能存在错误,大方向没问题
  2. 需要注意的是,不同的类前缀使用不用的svg文件,一定要注意区分,不然会错
  3. 使用字体库加密的文字也不只有数字,字体库中的字符,理论上都有可能用于加密
  4. 相同模块的加密方式,会经常变,有时候使用字体库加密,有时使用svg加密,有时混用,甚至有时不加密,在编写程序的时候应避免写‘死’
  5. 代码是从我封装好的类里直接copy过来,应该不能直接运行,有兴趣的伙伴请自行完善程序
  6. 本人小白,强烈欢迎大牛指点,交流学习

文档仅作学习和记录使用,请不要用于商业和违法用途,如有侵权,请告知删除。

你可能感兴趣的:(技术学习与分享)