(数据科学学习手札56)利用机器学习破解大众点评文字反爬

1、引言css

  爬取过大众点评的朋友应该会遇到这样的问题,在网页中看起来正常的文字,在其源代码中变成了下面这样:html

  究其缘由,是由于大众点评在内容上设置的特别的反爬机制,与某些网站替换底层字体文件不一样,大众点评使用随机替换的SVG图片来替换对应位置的汉字内容,使得咱们使用常规的手段没法获取其网页中完整的文字内容,通过观察我发现,全部能够被SVG图像替换的文字都保存在下图所示的地址中:web

打开该页面后能够发现其包含了全部能够被SVG替换的文字:算法

  在查阅了他人针对该问题提出的相关文章后,获悉他们使用的方法是先找到源代码中SVG图像对应的<span>标签,其属性class与下图红框中所示第一个以及第二个px值存在一一映射关系,且该关系全量保存在旁边对应的css中:数组

右键该连接,选择open in new tab,在跳转的新页面中便隐藏了全量的class属性与两个对应的px之间的映射关系:浏览器

  按照前人的经验,这两个px经过一个公式与以前的SVG界面中全部汉字的行列位置构建起一一对应关系,但他们的作法是本身去手动猜想规则,创建公式从而破解从class属性到SVG文字行列位置的一一映射关系,但这样的方式显然已经被大众点评后台人员知晓,因而乎,更变态的是,他们这套映射规则几乎天天都会发生一次更新,至少我在写这篇文章的前一天遇到的状况,与今天所遭遇的状况彻底不一样,这就使得前人总结的那套靠脑力去猜想的方法吃力不讨好,因而我摒弃了去猜想规则,而是选择去学习规则,即利用机器学习算法来解决这个看起来较为棘手的问题;app

 

2、基于决策树分类器的破解方法less

  这里我选择使用较为经典的CART分类树来训练算法,从而实现对其映射规则的学习,在训练算法前,咱们须要收集适量的样本数据来构造带标签的训练集,从而支撑以后的有监督学习过程;dom

2.1 收集训练数据机器学习

  经过观察,我发现大众点评的页面中被SVG替换的文字并不肯定,即每一次刷新页面,均可能有新的文字被替换成SVG,旧的SVG图像被还原为文字,借助这个机制,咱们能够经过对某一肯定的页面屡次刷新,每次用正则提取评论内容标签下,全部符合单个汉字格式条件和<span class="xxxxx"></span>格式条件的片断,用下面的正则就能够实现:

'(<span class="[a-z0-9]+">)|([\u4e00-\u9fa5]{1})'

每次将符合上述任一条件的一个片断按照其在整段文字中出现的顺序拼接起来,构造位置一一对应的编码列表和文字列表,并在重复的页面刷新过程当中,经过识别从SVG图像恢复成普通文字的现象,获得汉字与其class编码的一一对应关系,再将这些已证明对应关系的汉字-编码做为咱们构造训练集的基础,自变量为经过该文字编码在CSS页面中索引到的两个px值(用正则便可轻松实现),因变量为该文字在SVG页面中对应的行列位置,由于每行的文字数量不太一致,因此这里须要写一个简单的算法从SVG页面源代码中抽取每一个汉字的行列位置并保存起来,以上步骤个人代码实现以下,这里为了跳过模拟登录,我选择了在本地Chrome浏览器登陆本身的大众点评帐号并保持登陆,再利用selenium来挂载本地浏览器配置文件,从而达到自动登陆的目的:

'''这个脚本用于对大众点评店铺评论板块下全部被SVG图像替换加密的汉字进行破解'''

from bs4 import BeautifulSoup
from tqdm import tqdm
import os
from sklearn.tree import DecisionTreeClassifier
import re
import time
import requests
from selenium import webdriver
import numpy as np
import pandas as pd

def OfferLocalBrowser(headless=False):
'''
这个函数用于提供自动登陆大众点评的Chrome浏览器
:param headless: 是否使用无头Chrome
:return: 返回浏览器对象
'''
option = webdriver.ChromeOptions()
option.add_argument(r'user-data-dir=C:\Users\hp\AppData\Local\Google\Chrome\User Data')
if headless:
option.add_argument('--headless')
browser = webdriver.Chrome(options=option)

return browser

def
CollectDataset(targetUrl,low=3,high=6,page=3,refreshTime=3): ''' :param targetUrl: 传入可翻页的任意商铺评论页面地址 :param low: 设置随机睡眠防ban的随机整数下限 :param high: 设置随机睡眠防ban的随机整数上限 :param page: 设置最大翻页次数 :param refreshTime: 设置每一个页面重复刷新的时间 :return: 返回收集到的汉字列表和编码列表 ''' '''初始化用于存放全部采集到的样本词和对应的样本词编码的列表,CL用于存放全部编码,WL用于存放全部词,两者顺序一一对应''' CL,WL = [],[] browser = OfferLocalBrowser(headless=False) for p in tqdm(range(1,page+1)): for r in range(refreshTime): '''访问目标网页''' html = browser.get(url=targetUrl.format(p)) if '3s 未完成验证,请重试。' in str(browser.page_source): ii = input() '''将原始网页内容解码''' html = browser.page_source '''解析网页内容''' obj = BeautifulSoup(html,'lxml') '''提取评论部份内容以方便以后对评论汉字和SVG图像对应编码的提取''' raw_comment = obj.find_all('div',{'class':'review-words Hide'}) '''初始化列表容器以有顺序地存放符合汉字或SVG标签格式的内容''' base_Comment = [] '''利用正则提取符合汉字内容规则的元素''' firstList = re.findall('(<span class="[a-z0-9]+">)|([\u4e00-\u9fa5]{1})',str(raw_comment)) '''构造该页面中长度守恒的评论片断列表''' actualList = [] '''按顺序将全部汉字片断和<span>标签片断拼接在一块儿''' for i in range(len(firstList)): for j in range(2): if firstList[i][j] != '': actualList.append(firstList[i][j]) '''打印当前界面全部评论片断的长度''' print(len(actualList)) '''在每一个页面的第一次访问时初始化汉字列表和编码列表''' if r == 0: wordList = ['' for i in range(len(actualList))] codeList = ['' for i in range(len(actualList))] '''将actualList中粗糙的<span>片断清洗成纯粹的编码片断,汉字部分则原封不定保留,并分别更新wordList和codeList''' for index in range(len(actualList)): if '<' in actualList[index]: codeList[index] = re.findall('class="([a-z0-9]+)"',actualList[index])[0] else: wordList[index] = actualList[index] '''随机睡眠防ban''' time.sleep(np.random.randint(low,high)) '''将结束重复采集的当前页面中发现的全部汉字-编码对应规则列表与先前的规则列表合并''' CL.extend(codeList) WL.extend(wordList) print('总列表长度:{}'.format(len(CL))) browser.quit() return WL,CL

 

2.2 数据预处理

  经过上面的步骤咱们已经获得朴素的汉字-编码样本,接下来咱们将其与SVG页面内容和CSS页面内容串联起来,从而构造可以输入决策树分类器进行训练的数据形式,这部分的主要代码以下,由于在最开始我并无肯定因变量究竟是哪几个,因而下面的代码中我采集了SVG页面中每一个文字的行下标,列下标:

 

def CreateXandY(wordList,codeList,cssUrl,SvgUrl):
    '''
    这个函数用于传入朴素的汉字列表、编码列表、CSS页面地址,SVG页面地址来输出规整的numpy多维数组格式的自变量X,以及标签Y
    :param wordList: 汉字列表
    :param codeList: 编码列表
    :param cssUrl: CSS页面地址
    :param SvgUrl: SVG页面地址
    :return: 返回自变量X,因变量Y
    '''

    def GetSvgWordIpx(SvgUrl=SvgUrl):
        '''
        这个函数用于爬取SVG页面,并返回所需内容
        :param SvgUrl: SVG页面地址
        :return: 单个汉字为键,上面所列四个属性为汉字键对应嵌套的字典中对应值的字典文件
        '''

        '''访问SVG页面'''
        SvgWord = requests.get(SvgUrl).content.decode()

        '''初始化汉字-候选因变量字典'''
        Svg2Label = {}

        '''提取SVG页面中全部汉字所在的text标签内容列表,每一个列表对应页面中一行文字'''
        rawList = re.findall('[\u4e00-\u9fa5]+', SvgWord)

        '''抽取每一个汉字及其对应的四个候选因变量'''
        for row in range(len(rawList)):
            wordPreList = re.findall('[\u4e00-\u9fa5]{1}', rawList[row])
            for word in wordPreList:
                Svg2Label[word] = {
                    'RowIndex': [],
                    'ColIndex': []
                }
                Svg2Label[word]['RowIndex'] = row + 1
                Svg2Label[word]['ColIndex'] = wordPreList.index(word) + 1

        return Svg2Label

    '''访问CSS页面'''
    CodeWithIpx = requests.get(cssUrl).content.decode()

    '''初始化编码-px值字典'''
    code2ipx = {}

    '''初始化针对样本数据的编码-汉字字典'''
    code2word = {}

    '''从样本中抽取采集到的确切的汉字-编码关系'''
    for code, word in tqdm(zip(codeList, wordList)):
        if code != '' and word != '':
            code2ipx[code] = re.search(
                '.%s{background:-(.*?).0px -(.*?).0px;}' % code, CodeWithIpx).groups()
            code2word[code] = word

    Svg2Label = GetSvgWordIpx()

    '''生成自变量和因变量'''

    X = []
    for key, value in code2ipx.items():
        X.append([int(value[0]), int(value[1])])

    X = np.array(X)

    Y = []
    for key, value in code2ipx.items():
        Y.append([Svg2Label[code2word[key]]['RowIndex'],
                  Svg2Label[code2word[key]]['ColIndex']])

    Y = np.array(Y)

    return X,Y,Svg2Label,CodeWithIpx

 

 

 

2.3 训练决策树分类模型

  经过上面的工做,咱们成功构造出规整的训练集,考虑到须要学习到的映射关系较为简单,咱们分别构造因变量为行下标、因变量为列下标的模型,并直接用所有数据进行训练(最开始我有想过过拟合的问题,但后面发现这里的映射规则很是简单,甚至多是线性的,所以这里直接这样虽然显得不严谨,但通过后续测试发现这种方式最为简单高效),具体代码以下:

def GetModels(X,Y):
    '''
    
    :param X: 因变量
    :param Y: 自变量
    :return: 用于预测行下标的模型1和预测列下标的模型2
    '''
'''这个模型的因变量为对应汉字的行下标'''
    model1 = DecisionTreeClassifier().fit(X, Y[:, 0])

    '''这个模型的因变量是对应汉字的列下标'''
    model2 = DecisionTreeClassifier().fit(X, Y[:, 1])return model1,model2

接下来咱们来写用于挂载模型并对汉字和SVG标签混杂格式的字符串进行预测解码的函数:

def Translate(s,baseDF,model1,model2):
    '''
    这个函数用于对汉字和SVG标签格式混杂的字符串进行预测解码
    :param s: 待解码的字符串
    :param baseDF: 存放全部汉字与其行列下标的数据框
    :param model1: 模型1
    :param model2: 模型2
    :return: 预测解码结果
    '''
    result = ''
    for ele in s:
        for u in range(2):
            if ele[u] != '' and '<' in ele[u]:
                row_ = model1.predict(np.array(
                    [int(re.search('.%s{background:-(.*?).0px -(.*?).0px;}' % re.search('<span class="([a-z0-9]+)">',ele[u]).group(1), CodeWithIpx).groups()[i]) for i in
                     range(2)]).reshape(1, -1))
                col_ = model2.predict(np.array(
                    [int(re.search('.%s{background:-(.*?).0px -(.*?).0px;}' % re.search('<span class="([a-z0-9]+)">',ele[u]).group(1), CodeWithIpx).groups()[i]) for i in
                     range(2)]).reshape(1, -1))


                answer = baseDF['字符'][(baseDF['Row'] == row_.tolist()[0]) & (baseDF['Col'] == col_.tolist()[0])].tolist()[0]

                result += answer
            else:
                result += ele[u]

    return result

其中baseDF是利用以前从SVG页面抽取的字典中获得的字符串,格式以下:

baseDF = pd.DataFrame({'字符': [key for key in Svg2Label.keys()],
                                       'Row': [Svg2Label[key]['RowIndex'] for key in Svg2Label.keys()],
                                       'Col': [Svg2Label[key]['ColIndex'] for key in Svg2Label.keys()]})

  至此,咱们全部须要的功能都以模块化的方式编写完成,下面咱们来对任意挑选的页面进行测试;

 

2.4 测试

  这里咱们挑选某火锅店的前三页评论,每一个页面重复刷新三次,用于采集训练数据,并在某生鲜店铺任选的某页评论上进行测试,代码以下:

'''测试'''
wordList,codeList = CollectDataset(targetUrl = 'http://www.dianping.com/shop/72452707/review_all/p{}?queryType=sortType&queryVal=latest',
                                   low = 3,
                                   high = 6,
                                   page = 3,
                                   refreshTime = 3)

'''注意,这里CSS页面地址和SVG页面地址天天都在变更''' X,Y,Svg2Label,CodeWithIpx
= CreateXandY(wordList=wordList,codeList=codeList, cssUrl = 'http://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/c26b1e06f361cadaa823f1b76642e534.css', SvgUrl = 'http://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/d6a6b2d601063fb185d7b89931259d79.svg') model1,model2 = GetModels(X,Y) browser = OfferLocalBrowser() browser.get('http://www.dianping.com/shop/124475710/review_all?queryType=sortType&&queryVal=latest') obj = BeautifulSoup(browser.page_source,'lxml') rawCommentList = obj.find_all('div',{'class':'review-words'}) baseDF = pd.DataFrame({'字符': [key for key in Svg2Label.keys()], 'Row': [Svg2Label[key]['RowIndex'] for key in Svg2Label.keys()], 'Col': [Svg2Label[key]['ColIndex'] for key in Svg2Label.keys()]}) for i in range(len(rawCommentList)): s = re.findall('(<span class="[a-z0-9]+">)|([\u4e00-\u9fa5]{1})',str(rawCommentList[i])) print(Translate(s,baseDF,model1,model2))

  解码效果以下,我特地选择在与火锅店评论相差很远的生鲜类店铺下进行测试,以免潜在的过拟合现象干扰,测试效果以下,从而证实了咱们的分类器在对规则学习上的成功(大众点评的朋友们该更新加密算法了)

 

2.5 注意事项

   须要注意的是,大众点评文字反爬中涉及到的SVG页面和CSS页面天天都会更新,我尝试过能够用正则从页面中抽取SVG地址,但CSS地址暂时不知道怎么抽取,哪位老哥若是知道还请指导一下,所以须要在爬取前填入本身手动复制下来的SVG页面和CSS页面地址。

 

  以上就是本文所有内容,若有疑问欢迎评论区讨论,本文由博客园费弗里原创,首发于博客园,转载请注明出处。

相关文章
相关标签/搜索