1. 程式人生 > 實用技巧 >爬取大眾點評評論-字型加密解析!這個網站很難搞出來

爬取大眾點評評論-字型加密解析!這個網站很難搞出來

獲取頁面資料

首先寫一個簡單的爬蟲, 來獲取頁面資料

class DaZhongDianPing:

    def __init__(self):
        self.s = requests.session()

        self.url = "http://www.dianping.com/shop/k9oYRvTyiMk4HEdQ/review_all"

        self.headers = {
            'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
            "Cookie": "xxxxxx"
        }

        self.html = None

    def get_html(self):
        """獲取資料"""
        response = self.s.get(self.url, headers=self.headers)
        self.html = response.text

記得新增最關鍵的user-agent和Cookie, 否則你連最基本的網頁都獲取不到, 這裡我就不把自己的cookie放出來了, 自己登陸後隨便開啟一條請求後複製即可

使用xpath來獲取使用者評論資訊

    def get_data_by_xpath(self):
        """使用xpath獲取使用者資訊"""
        html_xpath = etree.HTML(self.html)
        # 獲取評論以及使用者
        user_data_list = html_xpath.xpath(
            "//li/div[@class='main-review']//a[@class='name']/text()|"
            "//li/div[@class='main-review']//span[@class='name']/text()")
        user_data_list = [x.strip() for x in user_data_list]
        user_comment_list = html_xpath.xpath(
       		"//ul/li/div[@class='main-review']/div[@class='review-words']|"
            "//ul/li/div[@class='main-review']/div[@class='review-words Hide']")
        if len(user_data_list) == len(user_comment_list):
            print("以使用xpath獲取資訊...")
        else:
            print("獲取使用者名稱個數為: ", len(user_data_list))
            print("獲取評論資訊個數為: ", len(user_comment_list))
            raise SyntaxError("使用者評論資訊不匹配, 請檢查xpath...")
        # pprint(name) ",口味"
        # 獲取使用者評論div 轉html程式碼為漢字
        comments_html_list = [html.unescape(tostring(x).decode('utf-8')) for x in user_comment_list]
        # pprint(comments_html_list)
        return comments_html_list, user_data_list

分別在網頁中取出使用者名稱和他所發的評論, 因為要直接獲取div裡的所有內容, 使用xpath的text()就明顯不夠用了, 直接使用tostring()方法將xpath物件轉換為字串格式, 在使用unescape()將html程式碼中的字元轉換為漢字, 達到能將div中標籤和內容一起取出來的效果

解密特殊標籤

獲取檔案, 提取css類

首先獲取css檔案內容, 類的座標還有.svg檔案都在css檔案中

    def get_css_file(self):
        """獲取css檔名"""
        # 獲取字型檔案
        css_file_name = re.search(r'<link rel="stylesheet" type="text/css" href="(//s3plus.+?\.css)', self.html).group(
            1)
        self.css_file_name = css_file_name[css_file_name.rfind('/') + 1:]
        print("下載檔案...", css_file_name)
        # 下載css檔案
        self.css = self.s.get("http:" + css_file_name).text
        with open(os.path.join("dz_decode", self.css_file_name), "w") as f:
            f.write(self.css)

使用正則取出css檔案連結, 提取出三個svg檔案


根據css檔案中的三個含有.svg檔案連結的類, 提取出有用的資訊, 黃框為.svg連結, 第一個紅框為該字型檔案所對應的css類座標的字首, 所有以xe為字首的類所對應的字都存放在這個svg中


另外兩個margin就是之前提到的偏移問題, 在最終座標加去這個值就好了

    def get_csv_file_name(self):
        # 獲取3個csv檔案
        self.file_data_list = re.findall(
            r'\[class\^="(\w+)"\]\{width: (\d+)px;height: (\d+)px;margin-top: (-?\d+)px;'
            r'background-image: url\((.+?\.svg)\);background-repeat: no-repeat;display: '
            r'inline-block;vertical-align: middle;(margin-left: (-?\d+)px;)?\}',
            self.css)
        # pprint(self.file_data_list)
        # 獲取所有的css類
        coordinate_dict = {}
        for data in self.file_data_list:
            css_tuple = re.findall(r'\.(%s\w+)\{background:(-?\d+\.0)px (-?\d+\.0)px;\}' % data[0], self.css)
            css_list = [{"name": x[0], "x": x[1], "y": x[2]} for x in css_tuple]
            coordinate_dict[data[0]] = css_list
        # pprint(coordinate_dict)
        return coordinate_dict

    def get_csv_file(self):
        # 下載三個檔案
        font_data_list = []
        for x in self.file_data_list:
            csv = self.s.get("http:" + x[4]).text
            # 儲存
            print("正在下載檔案...", x[4][x[4].rfind("/") + 1:])
            with open(os.path.join("dz_decode", x[4][x[4].rfind("/") + 1:]), "w", encoding='utf-8') as f:
                f.write(csv)
            # 組合字典
            font_data_list.append({
                "class": x[0],
                "width": x[1],
                "height": x[2],
                "top": x[3],
                "name": x[4][x[4].rfind("/") + 1:],
                "left": x[6] if x[6] else "0",
            })

        return font_data_list

使用正則匹配出三個svg檔案的重要資訊, 在匹配出所有的css類和x, y的值組成字典返回, 將三個檔案進行下載儲存, 將類的名稱,x, y值提取出來

獲取文字座標

經過多次嘗試, svg檔案內容分兩種境況
格式A:


一種是這種使用<text></text>標籤包裹起來,y座標在text標籤中, 使用font-size來計算x值
格式B:


另一種是使用textPath標籤包裹, 將y座標藏在path標籤內, 通過id進行對映, 使用font-size來計算x值

    @staticmethod
    def get_relationship_by_css(ui_jo_rm, font_data_list):
        """根據css檔案來獲取文字座標"""
        csv_coordinate_dict = {}
        for name_, dict_ in ui_jo_rm.items():
            # 找到對應的檔案及引數
            data = [x for x in font_data_list if x["class"] == name_][0]
            # 開啟檔案
            with open(os.path.join("dz_decode", data["name"][data["name"].rfind("/") + 1:]), "r",
                      encoding="utf-8") as f:
                csv_file_list = f.readlines()
            # 提取字型大小
            font_size = int(re.search(r'font-size:(\d+)px;', csv_file_list[3]).group(1))  # 14px
            # 判斷檔案是什麼格式
            if "defs" not in csv_file_list[4]:
                # 格式A
                # 篩選出所需的資料
                csv_font_list = [x for x in csv_file_list if "/text" in x]
                # pprint(csv_font_list)
                # 構築簡短列表 [('41', '太恆燙蓬革益闖禁牲配禿葡席決宣瘋雨平榆真漿儲紡忠洞挖錘尼爆廊傻悲板造邪圓子喂妹體疆藍'),
                csv_font_list = [re.search(r'y="(\d+)">(\w+)</text>', x).group(1, 2) for x in csv_font_list]
            else:
                # 格式B
                # 先獲取所有行的y值
                ys = re.findall(r'<path id="(\d+)" d="M0 (\d+) H600"/>', csv_file_list[4])
                # 對y進行排序整理
                # ys.sort(key=lambda x: x[0])
                new_ys = [x[1] for x in ys]
                # 獲取所有行的字
                fonts_line = [x for x in csv_file_list if "/textPath" in x]
                fonts_list = [re.search(r'textLength="\d+">(\w+)</textPath>', x).group(1) for x in fonts_line]
                # print(new_ys)
                if len(new_ys) != len(fonts_list):
                    raise SyntaxError(f"正則匹配失敗, {len(fonts_line)}, {len(fonts_list)}")
                # 組合為行列資料
                csv_font_list = list(zip(new_ys, fonts_list))
                # pprint(csv_font_list)
            # pprint(csv_font_list)
            # 構築座標列表  [{'font': '太', 'x': '', 'y': 27},
            csv_coordinate_list = [
                {"font": y, "x": i * font_size + abs(int(data['left'])), "y": int(x[0]) + int(data["top"])} for x in
                csv_font_list for i, y in enumerate(list(x[1]))]
            # pprint(csv_coordinate_list)
            # 組合最後的字典
            csv_coordinate_dict[name_] = csv_coordinate_list
        return csv_coordinate_dict

這裡我使用了正則來匹配出需要的資訊, 將一列資訊和y值匹配出來, 再通過font-size計算x座標, 減去偏移值後組合成小字典, 將它們放在列表中返回

生成對照表

得到兩個關係表後之後的事情就變的很簡單了, 只需要判斷一個字型資料和css類中的x, y值相近, 這個字就是對應著這個類, 在進行替換之後就能得到解密後的資料了

        for name_, font_list in ui_jo_rm.items():
        	# 提取出字型的xy座標
            data = font_coordinate_dict[name_]
            # 格式化css座標字典
            for font in font_list:
                font['x'] = int(abs(float(font['x'])))
                font['y'] = int(abs(float(font['y'])))
            for font_info in data:
                for font_name in font_list:
                    # 計算偏差的值是否小於設定的值
                    if abs(font_name["x"] - font_info["x"]) <= self.deviation_value and abs(
                            font_name["y"] - font_info["y"]) <= self.deviation_value:
                        # print(font_name["name"], font_info["font"])
                        new_font_coordinate_dict[font_name['name']] = font_info['font']

遍歷兩個列表, 進行計算差值, 如果差值小於或等於設定的最大偏移量就忽略, 反之將其進行儲存, 最終返回這個字典

儲存對照資訊

進行嘗試後發現, 反爬資訊是一天進行一次更換, 雖然檔案的是動態的但是檔名是固定的, 沒有必要每次執行程式都進行一次下載和遍歷對照表, 會影響再次訪問的速度, 只要判斷我是否分析過這個css檔案, 如果分析過直接將分析後的對照表返回, 沒有在進行對照對映.

        if len(new_font_coordinate_dict) != all_num:
            print("出現匹配異常資料, 建議加大偏差值...")
            print("以匹配列表個數", len(new_font_coordinate_dict))
            print("匹配列表總個數", all_num)
        else:
            print("匹配完成, 無異樣資料...")
            # 將資料儲存到檔案中, 以加快下次解析
            with open(os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdwj"), 'w', encoding="utf-8") as f:
                f.write(str(new_font_coordinate_dict))

在匹配無異常時進行資料的儲存, 這裡是直接將字典進行str之後, 存放在一個檔案中, 檔名為原css檔案的名字方便下次尋找, 檔案的字尾就隨便你了

        if read_file:
            # 判斷這次有沒有已經對映的檔案
            if os.path.exists(os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdnmd")):
                print("以使用之前的對映關係進行對映...", os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdwj"))
                # 開啟檔案讀取
                with open(os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdnmd"), 'r', encoding="utf-8") as f:
                    file_data = f.read()
                return eval(file_data)
            else:
                print("未找到對映檔案, 自動進行對映...")
        else:
            print("重新整理對映關係")

判斷是否開啟讀取舊檔案, 根據有沒有這個檔案來決定是否進行字型對映, 直接讀取檔案後使用eval()函式將字串型別的字典重新格式化為字典

進行文字替換

回到開始, 將最開始獲得到的整個評論的div資料進行處理, 提取出文字和類名, 如下圖


之後進行對五位編碼進行替換, 就能得到包含這篇評論的所有文字, 將他們進行組合就得到了最後完整的評論

    def data_decode(self, comments_html_list):
        """解密字型檔案"""
        dzd = DzDecode(self.s, self.html)
        # 獲取解密字典
        csv_decode_dict = dzd.get_font_map(True)  # True 使用對映檔案
        # print(comments_html_list)
        # print(comments_tuple_list)
        # 使用正則匹配出所需資訊
        comments_tuple_list = [re.findall(r'>?>?(\s*?.*?)<\w{7} class="(.{5})"|>?>(.*?)\n', x) for x in
                               comments_html_list]
        # pprint(comments_tuple_list[0])
        # 過濾資料 re.sub(r'</svgmtsi', '', x) 
        # pprint(comments_tuple_list[1])
        comments_tuple_list = eval(re.sub(r"</svgmtsi>|</i>|</div>", '', str(comments_tuple_list)))
        # 對編碼字元進行替換
        comments_tuple_decrypted_list = []
        num = 0
        for comment in comments_tuple_list:
            char = ""
            for font in comment:
                char += font[0]
                if font[1]:
                    for key, value in csv_decode_dict.items():
                        if key == font[1]:
                            char += value
                            break
                char += font[2]
            comments_tuple_decrypted_list.append(char)
        if num:
            print(f"出現異常值, 匹配異常個數為 {num}")
        comments_tuple_decrypted_list = [x.strip() for x in comments_tuple_decrypted_list]
        return comments_tuple_decrypted_list

將使用者和評論進行對應


最後對應使用者和他所發的評論, 爬取評論的需求就完成了

結語

這次程式碼雖然只打印了使用者和評論, 但核心問題已經解決了, 已經得到了三個字型的對映字典, 店鋪的手機號, 地址, 評論都可以使用這個字典進行對照, 也沒什麼需求, 就沒去細摳這些東西

歷時近3天時間終於把程式碼寫完並完善好了, 剛開是也是處處碰壁, 慢慢的才摸到門路, 天知道我第二天試程式碼時發現反爬是動態的表情, 世上無難事, 只怕有心人, 我相信只要下定決心就沒有解決不了的問題, 看著跑起來的程式碼還是蠻開心的

本文僅供交流學習,嚴禁用於商業或著任何違法用途

如有侵權聯絡刪除

完整的視訊教程加群:1136192749