纵有疾风起
人生不言弃

2.7 汽车之家口碑爬虫

2.7 汽车之家口碑爬虫

1.需求分析

因项目需求,要爬取汽车之家的口碑数据进行下一步分析。

但是普通的爬虫软件(如八爪鱼、火车头、神箭手)无法爬取评论(该公司采取了反爬虫措施)。

经分析,发现该公司的的反爬虫措施主要是用前端js去替换显示的字体,为一些标签。并且封住鼠标右键导致不好观察源代码。

本文以解决各个问题为顺序。

2.前端js反爬虫措施分析

声明:爬取汽车之家的主要难点在于第一步:破解前端js替换。破解方法的来源是博客园上大神Mr.Dolphin的文章反爬虫破解系列-汽车之家利用css样式替换文字破解方法:https://www.cnblogs.com/dyfblog/p/6753251.html 这一部分的问题大家可以移步前去获取更精确的答案。

2.1问题描述

以任意车型(奥迪A4L)为例:http://k.autohome.com.cn/692/

我们可以看到,表面上各个评论都由文字组成,但是打开F12开发者模式。我们就发现:一些形容词被替换成了span标签,如图:

2.7 汽车之家口碑爬虫插图

他们的具体做法是:

发布的口碑正文中随机抽取某几个字使用span标签代替,标签内容位空,但css样式显示为所代替的文。

这样不会影响正常用户的阅读,只是在用鼠标选择的时候是选不到被替换的文字的,对爬虫则会造成采集内容不全的影响。

这些是用JS实现的,这是一段js代码:

(function(hZ_) { 

    functionEW_() { = DV_()[decodeURIComponent]('%E3%80%81%E3%80%82%E4%B8%80%E4%B8%8A%E4%B8%8B%E4%B8%8D%E4%BA%86%E4%BA%94%E5%92%8C%E5%9C%B0%E5%A4%9A%E5%A4%A7%E5%A5%BD%E5%B0%8F%E5%BE%88%E5%BE%97%E6%98%AF%E7%9A%84%E7%9D%80%E8%BF%9C%E9%95%BF%E9%AB%98%EF%BC%81%EF%BC%8C%EF%BC%9F'Ÿ yc_()); 
    = la_((yc_() 23; 3; 19; 17; 9; 1; 8; 12; 18; 13; 2; 4; 16; 5; 6; 21; 15; 11; 22; 14; 24; 0; 10; 7; 20), lf_(;)); 
    = la_((10 _7, 6 _0; 2 _33, 14 _18; 8 _45, 8 _36; 0 _71, 16 _54; 13 _76, 3 _72; 0 _107, 16 _90; 15 _110, 1 _108; 4 _139, 12 _126; 9 _152, 7 _144; 10 _169, 6 _162; 4 _193, 12 _180; 11 _204, 5 _198; 3 _230, 13 _216; 1 _250, 15 _234; 13 _256, 3 _252; 6 _281, 10 _270; 9 _296, 7 _288; 13 _310, 3 _306; 6 _335, 10 _324; 7 _352, 9 _342; 6 _371, 10 _360; 5 _390, 11 _378; 5 _408, 11 _396; 7 _424, 9 _414; 6 _443, 10 _432lf_(;)), yc_(;));
            Uj_();
            return;;
        }
    function mS_() { 
        for (Gx_ = 0; Gx_ < nf_.length; Gx_++) {
            var su_ = Pn_(nf_[Gx_], ',');
            var KN_ = '';
            for (Bk_ = 0; Bk_ < su_.length; Bk_++) {
                KN_ += ui_(su_[Bk_]) + '';
            }
            Kx_(Gx_, KN_);
        }
    }
    function NH_(Gx_) { 
        return '.hs_kw' + Gx_ + '_maindC';
    }
    function Ln_() { 
        return '::before { content:'
    }
})(document);

他的逻辑是,预先定义好哪几个字要被替换,上面代码中的那个很多%的字符串就是被替换的文字串,然后定义好每个文字的序号,最后按照文字的序号对文字串进行重新排序并生成css样式,注意,最一开始的span标签的class属性中是有个序号的,这个序号就是用来定位应该对应哪个文字。

接下来要做的就是无非就是从js代码中找到这个文字串,找到文字串的顺序,然后进行重排,然后根据span标签序号对原文
进行反向替换,从而得到完整的内容。

2.2解决方法:

  1. 从js代码中找到被替换的文字串和顺序

  2. 重排文字串

  3. 对原文中span标签根据class序号进行替换

其实2、3都比较简单,重点是第一步,找到被替换的文字串和顺序,由于源代码中js代码是被混淆过的,无法直接看出哪个

是文字串,所以首先应该对js代码进行反混淆,这个反混淆也不是说非得完整的还原所有的js代码,其实只要能反混淆到能

让我们看出文字串和顺序是什么就行了。

说一下反混淆的思路,其实很简单。就是执行起来比较麻烦而已,混淆是利用将一个简单的变量定义成复杂的js代码的方法

实现的,但这种混淆方式其实是有限的(这个有限指的是混淆用的工具在生成混淆代码时肯定是人为预先定义好了几种模式

,人为定义的肯定是有限的,只要你把所有的模式找出来,就可以还原了)。举个例子

function iq_() { 
        'return iq_';
        return '3';
    }

这段代码其实你可以简单的认为就是变量iq()等于’3’,使用正则匹配这样的代码模式,然后提取关键字:函数名和最后一个return的值,然后将提取到的信息保存起来用于对js代码进行全文替换。

function cz_() { 
        function _c() { 
            return 'cz_';
        };
        if (_c() == 'cz__') {
            return _c();
        } else {
            return '84';
        }
    }

这段代码复杂了一些,增加了判断,不过也简单,利用正则匹配这样的模式,然后提取关键字:函数名、第一个return的值,判断中==后面的值,最后一个return的值,然后自己进行判断来确定cz_()的值应该是多少,保存起来进行全文替换。

以此类推,每种模式都可以使用正则来提取关键字并进行全文替换来反混淆,最后我们会得到一个大概被还原的js代码,其中的文字串和顺序都清晰可见,再使用正则匹配出来就可以了。

需要注意的一点是有时候被替换的不是单个文字,而是一些词语,这是找到的顺序是”3,1;23,5”这样的,不过这些小伎俩应该不算什么,很好解决。

下面给出完整的代码:

# coding:utf8
import re
import urllib
import urllib.parse
import requests


def get_char(js):
    all_var = {}
    # 判断混淆 无参数 返回常量 函数
    if_else_no_args_return_constant_function_functions = []
    """ function zX_() { function _z() { return '09'; }; if (_z() == '09,') { return 'zX_'; } else { return _z(); } } """
    constant_function_regex4 = re.compile(""" function\s+\w+\(\)\s*\{\s* function\s+\w+\(\)\s*\{\s* return\s+[\'\"][^\'\"]+[\'\"];\s* \};\s* if\s*\(\w+\(\)\s*==\s*[\'\"][^\'\"]+[\'\"]\)\s*\{\s* return\s*[\'\"][^\'\"]+[\'\"];\s* \}\s*else\s*\{\s* return\s*\w+\(\);\s* \}\s* \} """,
                                          re.X)
    l = constant_function_regex4.findall(js)
    # print("l 38",l)
    for i in l:
        function_name = re.search(""" function\s+(\w+)\(\)\s*\{\s* function\s+\w+\(\)\s*\{\s* return\s+[\'\"]([^\'\"]+)[\'\"];\s* \};\s* if\s*\(\w+\(\)\s*==\s*[\'\"]([^\'\"]+)[\'\"]\)\s*\{\s* return\s*[\'\"]([^\'\"]+)[\'\"];\s* \}\s*else\s*\{\s* return\s*\w+\(\);\s* \}\s* \} """, i,
                                  re.X)
        if_else_no_args_return_constant_function_functions.append(function_name.groups())
        js = js.replace(i, "")
        # 替换全文
        a, b, c, d = function_name.groups()
        all_var["%s()" % a] = d if b == c else b

    # 判断混淆 无参数 返回函数 常量
    if_else_no_args_return_function_constant_functions = []
    """ function wu_() { function _w() { return 'wu_'; }; if (_w() == 'wu__') { return _w(); } else { return '5%'; } } """
    constant_function_regex5 = re.compile(""" function\s+\w+\(\)\s*\{\s* function\s+\w+\(\)\s*\{\s* return\s+[\'\"][^\'\"]+[\'\"];\s* \};\s* if\s*\(\w+\(\)\s*==\s*[\'\"][^\'\"]+[\'\"]\)\s*\{\s* return\s*\w+\(\);\s* \}\s*else\s*\{\s* return\s*[\'\"][^\'\"]+[\'\"];\s* \}\s* \} """,
                                          re.X)
    l = constant_function_regex5.findall(js)
    # print("l 87",l)
    for i in l:
        function_name = re.search(""" function\s+(\w+)\(\)\s*\{\s* function\s+\w+\(\)\s*\{\s* return\s+[\'\"]([^\'\"]+)[\'\"];\s* \};\s* if\s*\(\w+\(\)\s*==\s*[\'\"]([^\'\"]+)[\'\"]\)\s*\{\s* return\s*\w+\(\);\s* \}\s*else\s*\{\s* return\s*[\'\"]([^\'\"]+)[\'\"];\s* \}\s* \} """, i,
                                  re.X)
        if_else_no_args_return_function_constant_functions.append(function_name.groups())
        js = js.replace(i, "")
        # 替换全文
        a, b, c, d = function_name.groups()
        all_var["%s()" % a] = b if b == c else d

    # var 参数等于返回值函数
    var_args_equal_value_functions = []
    """ var ZA_ = function(ZA__) { 'return ZA_'; return ZA__; }; """
    constant_function_regex1 = re.compile(
        "var\s+[^=]+=\s*function\(\w+\)\{\s*[\'\"]return\s*\w+\s*[\'\"];\s*return\s+\w+;\s*\};")
    l = constant_function_regex1.findall(js)
    # print("l 119",l)
    for i in l:
        function_name = re.search("var\s+([^=]+)", i).group(1)
        var_args_equal_value_functions.append(function_name)
        js = js.replace(i, "")
        # 替换全文
        a = function_name
        js = re.sub("%s\(([^\)]+)\)" % a, r"\1", js)

    # var 无参数 返回常量 函数
    var_no_args_return_constant_functions = []
    """ var Qh_ = function() { 'return Qh_'; return ';'; }; """
    constant_function_regex2 = re.compile(""" var\s+[^=]+=\s*function\(\)\{\s* [\'\"]return\s*\w+\s*[\'\"];\s* return\s+[\'\"][^\'\"]+[\'\"];\s* \}; """,
                                          re.X)
    l = constant_function_regex2.findall(js)
    # print("l 144",l)
    for i in l:
        function_name = re.search(""" var\s+([^=]+)=\s*function\(\)\{\s* [\'\"]return\s*\w+\s*[\'\"];\s* return\s+[\'\"]([^\'\"]+)[\'\"];\s* \}; """,
                                  i,
                                  re.X)
        var_no_args_return_constant_functions.append(function_name.groups())
        js = js.replace(i, "")
        # 替换全文
        a, b = function_name.groups()
        all_var["%s()" % a] = b

    # 无参数 返回常量 函数
    no_args_return_constant_functions = []
    """ function ZP_() { 'return ZP_'; return 'E'; } """
    constant_function_regex3 = re.compile(""" function\s*\w+\(\)\s*\{\s* [\'\"]return\s*[^\'\"]+[\'\"];\s* return\s*[\'\"][^\'\"]+[\'\"];\s* \}\s* """,
                                          re.X)
    l = constant_function_regex3.findall(js)
    # print("l 176",l)
    for i in l:
        function_name = re.search(""" function\s*(\w+)\(\)\s*\{\s* [\'\"]return\s*[^\'\"]+[\'\"];\s* return\s*[\'\"]([^\'\"]+)[\'\"];\s* \}\s* """,
                                  i,
                                  re.X)
        no_args_return_constant_functions.append(function_name.groups())
        js = js.replace(i, "")
        # 替换全文
        a, b = function_name.groups()
        all_var["%s()" % a] = b

    # 无参数 返回常量 函数 中间无混淆代码
    no_args_return_constant_sample_functions = []
    """ function do_() { return ''; } """
    constant_function_regex3 = re.compile(""" function\s*\w+\(\)\s*\{\s* return\s*[\'\"][^\'\"]*[\'\"];\s* \}\s* """,
                                          re.X)
    l = constant_function_regex3.findall(js)
    # print("l 206",l)
    for i in l:
        function_name = re.search(""" function\s*(\w+)\(\)\s*\{\s* return\s*[\'\"]([^\'\"]*)[\'\"];\s* \}\s* """,
                                  i,
                                  re.X)
        no_args_return_constant_sample_functions.append(function_name.groups())
        js = js.replace(i, "")
        # 替换全文
        a, b = function_name.groups()
        all_var["%s()" % a] = b

    # 字符串拼接时使无参常量函数
    """ (function() { 'return sZ_'; return '1' })() """
    constant_function_regex6 = re.compile(""" \(function\(\)\s*\{\s* [\'\"]return[^\'\"]+[\'\"];\s* return\s*[\'\"][^\'\"]*[\'\"];? \}\)\(\) """,
                                          re.X)
    l = constant_function_regex6.findall(js)
    # print("l 236",l)
    for i in l:
        function_name = re.search(""" \(function\(\)\s*\{\s* [\'\"]return[^\'\"]+[\'\"];\s* return\s*([\'\"][^\'\"]*[\'\"]);? \}\)\(\) """,
                                  i,
                                  re.X)
        js = js.replace(i, function_name.group(1))

    # 字符串拼接时使用返回参数的函数
    """ (function(iU__) { 'return iU_'; return iU__; })('9F') """
    constant_function_regex6 = re.compile(""" \(function\(\w+\)\s*\{\s* [\'\"]return[^\'\"]+[\'\"];\s* return\s*\w+; \}\)\([\'\"][^\'\"]*[\'\"]\) """,
                                          re.X)

    l = constant_function_regex6.findall(js)
    # print("l 264",l)
    for i in l:
        function_name = re.search(""" \(function\(\w+\)\s*\{\s* [\'\"]return[^\'\"]+[\'\"];\s* return\s*\w+; \}\)\(([\'\"][^\'\"]*[\'\"])\) """,
                                  i,
                                  re.X)
        js = js.replace(i, function_name.group(1))
    print("275",js)
    # 获取所有变量
    var_regex = "var\s+(\w+)=(.*?);\s"
    var_find = re.findall(var_regex, js)
    print("var_find",var_find)
    for var_name, var_value in var_find:
        var_value = var_value.strip("\'\"").strip()
        # print(var_name,"---",var_value)
        if "(" in var_value:
            var_value = ";"
        all_var[var_name] = var_value
    print("all var",all_var)
    # 注释掉 此正则可能会把关键js语句删除掉
    # js = re.sub(var_regex, "", js)

    for var_name, var_value in all_var.items():
        js = js.replace(var_name, var_value)
    print("----282",js)
    js = re.sub("[\s+']", "", js)
    print("----284",js)
    string_m = re.search("(%\w\w(?:%\w\w)+)", js)
    # string = urllib.parse.unquote(string_m.group(1)).encode("utf-8").decode("utf8")
    print("string_m",string_m.groups())
    string = urllib.parse.unquote(string_m.group(1)).encode("utf-8").decode("utf8")
    print(string)
    index_m = re.search("([\d,]+(;[\d,]+)+)", js[string_m.end():])
    print(index_m.group())
    string_list = list(string)
    print("str",len(string_list))
    # print("string_list",string_list)
    index_list = index_m.group(1).split(";")
    # print("index_list",index_list)
    _word_list = []
    # print(type(_word_list))
    # print(_word_list)
    i = 1
    exflag = 0;
    # deal exception

    # print("--max ",type(int(max(index_list))))
    max_index=0;
    for word_index_list in index_list:
        _word = ""
        if "," in word_index_list:
            word_index_list = word_index_list.split(",")
            word_index_list = [int(x) for x in word_index_list]
        else:
            word_index_list = [int(word_index_list)]
        for word_index in word_index_list:
            # print(word_index)
            if(word_index>max_index):
                max_index=word_index
            try:
                string_list[word_index]
            except Exception as e:
                exflag=1;
    print(max_index)
    print("exflag",exflag)
    less = max_index - len(string_list)
    print(less)
    for word_index_list in index_list:
        _word = ""
        if "," in word_index_list:
            word_index_list = word_index_list.split(",")
            # print("word_index_list",word_index_list)
            word_index_list = [int(x) for x in word_index_list]
            # print("word_index_list", word_index_list)
        else:
            word_index_list = [int(word_index_list)]
        j = 1;
        for word_index in word_index_list:
            # print("for",j)
            j += 1
            # print("word_index",word_index)
            # print("string_list[word_index]",string_list[word_index])
            try:
                _word += string_list[word_index-1-less]
            except Exception as e:
                print(e)

        # print(_word)
        _word_list.append(_word)
        # print("----------")
        # print(i)
        # print(_word_list)

        i += 1

    return _word_list


def get_complete_text_autohome(text):
    #print("text0",text)
    text = text.replace(r"\u0027","'").replace(r"\u003e",">").replace(r"\u003c","<")
    #print("text1",text)
    js = re.search("<!--@HS_ZY@--><script>([\s\S]+)\(document\);</script>", text)
    #print("find : %s" % js.group())
    if not js:
        print(" if not js:")
        return text
    try:
        #print("try0")
        char_list = get_char(js.group(1))
        print("try111")

    except Exception as e:
        print(e)
        print("except222")
        return text

    def char_replace(m):
        index = int(m.group(1))
        char = char_list[index]
        return char

    text = re.sub("<span\s*class=[\'\"]hs_kw(\d+)_[^\'\"]+[\'\"]></span>", char_replace, text)
    # print(text)
    return text


# resp = requests.get("http://k.autohome.com.cn/FrontAPI/GetFeelingByEvalId?evalId=1538569")
resp = requests.get("http://k.autohome.com.cn/FrontAPI/GetFeelingByEvalId?evalId=1585634")
resp.encoding = "gbk"
text = get_complete_text_autohome(resp.text)


print(re.search("<!--@HS_BASE64@-->.*<!--@HS_ZY@-->", text).group())
print("2")
# print(re.search("<div\s*class=[\'\"]text-con[^\'\"]*?[\'\"]>([\s\S]+?)</div>", text).group(1))

前一个函数是核心,用于解析js

3.爬虫框架

3.1 获取所有车型的id

首先利用爬虫软件爬取了 汽车列表 里面的所有汽车id
,用两层循环爬取所有页面的评论:

# 两层遍历,分别遍历车型和页数
        for i in car_id_list:  # i代表从车型的遍历
            for j in range(1,101): # j代表评论页数,range(1,3)表示1到2页
                req = scrapy.Request("http://k.autohome.com.cn/"+str(i)+"/index_"+str(j)+".html#dataList")
                reqs.append(req)
        return reqs

3.2本爬虫采用scrapy框架,分析所需要的评论信息为:

# 车ID
    CAR_ID = scrapy.Field()
    # 车名
    CAR_NAME = scrapy.Field()

    # 用户ID
    USER_ID = scrapy.Field()
    # 用户名
    USER_NAME = scrapy.Field()

    # 购买地点
    PURCHASE_PLACE = scrapy.Field()
    # 购买时间
    PURCHASE_TIME = scrapy.Field()
    # 裸车购买价
    CAR_PRICE = scrapy.Field()
    # 购车目的
    PURCHASE_PURPOSE = scrapy.Field()

    # 评分- 空间
    SCORE_SPACE = scrapy.Field()
    # 评分- 动力
    SCORE_POWER = scrapy.Field()
    # 评分- 操控
    SCORE_CONTROL = scrapy.Field()
    # 评分- 油耗
    SCORE_FUEL_CONSUMPTION = scrapy.Field()
    # 评分- 舒适性
    SCORE_COMFORT = scrapy.Field()
    # 评分- 外观
    SCORE_EXTERIOR = scrapy.Field()
    # 评分- 内饰
    SCORE_INTERIOR = scrapy.Field()
    # 评分- 性价比
    SCORE_COST_EFFECTIVE = scrapy.Field()

    # 评论的url
    COMMENT_URL = scrapy.Field()
    # 评论的内容
    COMMENT_CONTENT = scrapy.Field()

    # 有多少人支持这条口碑
    COMMENT_SUPPORT_QUANTITY = scrapy.Field()
    # 有多少人看过这条口碑
    COMMENT_SEEN_QUANTITY = scrapy.Field()

将其写进item中。

3.3将常用设置写入sttings中

# 绕过robots.txt
ROBOTSTXT_OBEY = False

#记录日志
LOG_FILE = "scrapy_autohome_log.log"

# 保存文件编码类型
FEED_EXPORT_ENCODING = 'GBK'

# 伪装chrome
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'

这样利用我们前面学习的scrapy框架的知识,再加上破解的js。我们成功爬取了汽车之家的数据。经过试验爬取了 将近22万条评论。

4.结果展示

1.数据条数:

2.7 汽车之家口碑爬虫插图1

2.数据格式

2.7 汽车之家口碑爬虫插图2

3.完整代码参见我饿github:
https://github.com/xqtbox/AutoHomeSpider_Scrapy

原文链接:https://lookme.blog.csdn.net/article/details/72810037

本站声明:网站内容来源于网络,如有侵权,请联系我们,我们将及时处理。

未经允许不得转载:起风网 » 2.7 汽车之家口碑爬虫
分享到: 生成海报

评论 抢沙发

评论前必须登录!

立即登录