大衆點評店鋪詳情頁評論採集(破解css文字映射反爬,包含項目github 可用時間至2020-01-21)

在店鋪詳情頁下拉 點擊更多點評 纔會進入真正的詳情頁。
在這個頁面,使用的是css的文字映射反爬。

分析一下頁面 可以看到部分評論的數據是缺失的,跟每個節點的class屬性可能有關係
在這裏插入圖片描述
可以打開這個文件,找到節點class屬性 映射的像素值。這個url可以用正則找到

svg_text_url = re.findall('<link rel="stylesheet" type="text/css" href="(.*?svg.*?)">', html)[0]    #獲得節點名對應座標 css文件地址

在這裏插入圖片描述
可以看到 css文件中 都是每個節點的class屬性 對應的地址。接下來我們要找到地址所對應的文字
**在這裏插入圖片描述
在css文件中,有一些 包含svg的url 將它打開
在這裏插入圖片描述
這個就是文字表,可以根據css座標提取出相應的文字。
在這裏插入圖片描述
注意:有三個不同的文字座標文件。其中只有兩個座標文件有用,點評網會在不同時間使用這兩個座標文件。如果進行長時間採集,需要對這兩個座標文件進行判斷,來選擇不同的座標文件進行使用。

接下來我們的思路已經確定了

1.請求詳情頁數據,使用正則表達式找到css文件地址。
2.在css文件中 使用正則提取出所有節點名稱以及對應的座標。並找到文字座標文件svg的地址
3.根據座標 提取出.svg文件中對應的文字。形成節點名稱對應文字的字典。
4.將請求詳情頁中返回的html,根據節點class屬性替換爲對應的文字。
5.正常解析html即可

在.svg文件根據座標提取文字時,不同的時間段,點評網會使用不同的.svg文件進行匹配。

當.svg文件內容如下時 匹配規則如下:
在這裏插入圖片描述
例:
在這裏插入圖片描述
在html中 “午”字 對應的節點class屬性爲 “xucya”
“xucya” 對應的座標爲 “-14 -959”
其中 -959確定文字所在行 -14確定文字橫向偏移量

可以看到svg返回數據中,每行的y值爲一個個區間,根據959數字的大小確定區間,即可確定所在行。
確定行之後,橫向便宜量爲 數字/14 以14爲例 14/14=1 則橫向偏移量爲1,確定的文字爲當前行的第二個字。

當.svg文件內容如下時 匹配規則如下:
在這裏插入圖片描述

將每個節點前的 #數字 替換爲上方的值 如下圖所示
替換完成後 匹配規則和上面svg文件的匹配規則相同。在這裏插入圖片描述

注:不可能每次啓動採集都手動修改匹配方式,我們需要對svg文件做一個判斷,完成自動化的匹配。可以根據svg內容信息來判斷,如行數。每次解析svg文件,若行數小於20,則跳出繼續解析下一個svg文件。如下圖
在這裏插入圖片描述

相關代碼:

def get_node_dict(css_url, cookie):
    """
    獲取座標值 對應 文字 字典
    :param background_image_link:
    :return:
    """
    res = requests.get(css_url, headers=css_headers)
    node_data_ls = re.findall(r'\.([a-zA-Z0-9]{5,6}).*?round:(.*?)px (.*?)px;', res.text)  # 提取節點名與對應座標
    background_image_link = re.findall("background-image: url\((.*?)\)", res.text)         # 提取 座標對應數字 css文件地址
    word_coordinate_dict, y_ls = get_word_coordinate_dict(background_image_link)           #
    node_data_dict = {}
    for i in node_data_ls:
        """構造成{節點名: 數字, .........}"""
        x = -int(i[1][:-2]) // 14
        for index in range(len(y_ls)):
            if -int(i[2][:-2]) <= int(y_ls[index]):
                y = y_ls[index]
                break
        try:
            node_data_dict[i[0]] = word_coordinate_dict[(int(x), str(y))]
        except Exception as e:
            pass
    return node_data_dict

def get_word_coordinate_dict(background_image_link):
    """
    獲取座標值 對應 文字 字典
    :param background_image_link:
    :return:
    """
    word_coordinate_dict = {}
    y_ls = []
    for svg_url in background_image_link:
        url = "http:" + svg_url
        res = requests.get(url, headers=css_headers)
        # time.sleep(random.uniform(30, 120))    # 隨機休眠
        Text = res.text
        if 'x=' in Text:
            font_list = re.findall(r'<text x="(.*?)" y="(.*?)">(.*?)</text>', Text)     # 提取座標對應數字
            if len(font_list) < 20:
                continue
            for i in font_list:
                if i[1] not in y_ls:
                    y_ls.append(i[1])
                for j in range(len(i[2])):
                    word_coordinate_dict[(j, i[1])] = i[2][j]

        elif 'textPath' in Text:
            Y_ls = re.findall(r'<path id="(.*?)" d="M0 (.*?) H600"/>', Text)
            if len(Y_ls) < 20:
                continue
            font_list = re.findall(r'<textPath xlink:href="(.*?)" textLength="(.*?)">(.*?)</textPath>', Text)
            font_list = [(i[1], j[1], i[2]) for i, j in zip(font_list, Y_ls)]
            for i in font_list:
                if i[1] not in y_ls:
                    y_ls.append(i[1])
                for j in range(len(i[2])):
                    word_coordinate_dict[(j, i[1])] = i[2][j]

    return word_coordinate_dict, y_ls

def replace_html(html, css_url, cookie):
    """
    提取全部節點,根據節點名 替換數據 返回真實html數據
    :param html:
    :param css_url:
    :param cookie:
    :return:
    """
    node_data_dict = get_node_dict(css_url, cookie)
    node_names = set()
    for i in re.findall('<svgmtsi class="([a-zA-Z0-9]{5,6})"></svgmtsi>', html):  # 提取所有節點名
        node_names.add(i)
    for node_name in node_names:
        try:
            html = re.sub('<svgmtsi class="%s"></svgmtsi>' % node_name, node_data_dict[node_name], html)  # 替換html節點爲數字
        except KeyError as e:
            # print(e)
            pass

    return html


def parse_html(html, shop_id):
    """
    解析html 第一次採集 提取好評差評數量
    後續採集 提取口味,環境,服務,食材,星級,評論
    :param html:
    :param i:
    :return:
    """
    sel = etree.HTML(html)
    save_data = []


    for node_num in range(1, 16):
        comment_content_3 = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[3]/text()'.format(node_num))   # 評論數據存在於 兩個節點  div3號節點數據
        comment_content_4 = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[4]/text()'.format(node_num))   # div4號節點數據
        if len(''.join(comment_content_3)) > len(''.join(comment_content_4).replace(' ', '').replace(r'\n', '')):
            comment_content = comment_content_3
        else:
            comment_content = comment_content_4
        taste_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[1]/text()'.format(node_num))        # 口味評分
        environment_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[2]/text()'.format(node_num))  # 環境評分
        service_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[3]/text()'.format(node_num))      # 服務評分
        food_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[4]/text()'.format(node_num))         # 食物評分
        food_score = ''.join(food_score).strip().replace('食材:', '')
        per_capita = ''
        if "人均" in food_score:    #
            per_capita = food_score.replace('人均:', '').replace('元', '')
            food_score = ''
        star_level = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[1]/@class'.format(node_num))

        item = {
            'taste_score': ''.join(taste_score).strip().replace('口味:', ''),
            'environment_score': ''.join(environment_score).strip().replace('環境:', ''),
            'service_score': ''.join(service_score).strip().replace('服務:', ''),
            'food_score': food_score,
            'per_capita': per_capita,
            'shop_id': shop_id,
            'star_level': ''.join(star_level).strip().replace('sml-rank-stars sml-str', '').replace(' star', ''),
            'comment_content': ''.join(comment_content).strip(),
        }
        save_data.append(item)
    return save_data



def parse_action(cookie, main_url, proxy, Thread_name, progress):
    """
    詳情頁採集主函數
    :param cookie:
    :param url:
    :return:
    """
    # cookie = "__mta=51347094.1571707475917.1571707475917.1571707475917.1; _lxsdk_cuid=16ddc82c62cc8-0c2da24cdecceb-7373e61-15f900-16ddc82c62dc8; _lxsdk=16ddc82c62cc8-0c2da24cdecceb-7373e61-15f900-16ddc82c62dc8; _hc.v=63c163cf-d75d-89b9-0dc1-74a9b2026548.1571362621; ua=sph; ctu=cdff7056daa2405159990763801b14e0b74443eb8302ce0928ea5e3d95696905; s_ViewType=10; aburl=1; _dp.ac.v=da84abba-5c47-4040-ac30-40e09e04162f; uamo=15292060685; cy=1; cye=shanghai; dper=ae518422253841cb8382badd9f84a3d471e237b43479f8d74f098a5ea02eabda1d22f0912bc506af3ff0f9dfc1cabd260be8824e9a0abeff2b67e50524f5875806e64becebd3923d9b402b395373f85a7f4228bc2e2dfc6533237b19446496e3; ll=7fd06e815b796be3df069dec7836c3df; _lx_utm=utm_source%3DBaidu%26utm_medium%3Dorganic; _lxsdk_s=16e353fda1a-7ba-0ce-cef%7C%7C124"
    # url = "http://www.dianping.com/shop/128001304/review_all"
    headers["Cookie"] = cookie
    headers['Host'] = 'www.dianping.com'
    # ip_list = requests.get(
    #     url="http://route.xiongmaodaili.com/xiongmao-web/api/glip?secret=8c1fe70d7ceb3a4e77284561df11f0d5&orderNo=GL20191111103706QQa4ktHu&count=1&isTxt=1&proxyType=1").text.split("\r\n")
    # ip_pool = [{"http": "http://{}".format(ip)} for ip in ip_list if ip != ""]
    # main_url = "http://www.dianping.com/shop/100034705"
    shop_id = main_url.replace("http://www.dianping.com/shop/", "")   # 提取shop_id

    if progress == None:
        progress = 1

    for page_num in range(int(progress)+1, 1000):
        url = "http://www.dianping.com/shop/{}/review_all/p{}?queryType=isAll&queryVal=true".format(shop_id, page_num)   # 生成url
        time.sleep(random.uniform(85, 120))  # 每次隨機休眠50-100秒
        i = 0
        while i < 2:
            try:
                rep = requests.get(url, headers=headers, timeout=25, proxies=proxy)
                break
            except requests.exceptions.ConnectionError as e:
                i += 1
                time.sleep(300)
                print('{}: 請求失敗 休眠300秒 url:{}'.format(Thread_name, url))
                continue

        Text = rep.text

        sel = etree.HTML(Text)
        go_on_flag = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[2]/div[3]/text()')
        if go_on_flag == ["暫無點評"]:     # 解析出暫無電影 則退出採集
            print("{}: {}店鋪 採集完畢".format(Thread_name, shop_id, ))
            Mysql.modify_statue(main_url, page_num, 1)
            break

        css_url = "http:" + re.findall('<link rel="stylesheet" type="text/css" href="(.*?svg.*?)">', Text)[0]   # 提取cssurl地址
        html = replace_html(Text, css_url, cookie)         # 原始html 替換節點後 生成帶有原文的html
        save_data = parse_html(html, shop_id)              # 解析
        Mysql.save_comment(url, save_data, Thread_name)    # 存儲
        Mysql.modify_statue(main_url, page_num)

項目github:https://github.com/sph116/dazhong_spider_font_svg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章