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頁面地址。
以上就是本文所有內容,若有疑問歡迎評論區討論,本文由博客園費弗裏原創,首發於博客園,轉載請註明出處。