汽车之家口碑爬虫

1.需求分析

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

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

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

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

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

2.1问题描述

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

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

image

他们的具体做法是:

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(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代码的方法

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

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

1
2
3
4
function iq_() {
'return iq_';
return '3';
}

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

1
2
3
4
5
6
7
8
9
10
function cz_() {
function _c() {
return 'cz_';
};
if (_c() == 'cz__') {
return _c();
} else {
return '84';
}
}

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

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

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

下面给出完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# 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
,用两层循环爬取所有页面的评论:

1
2
3
4
5
6
# 两层遍历,分别遍历车型和页数
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框架,分析所需要的评论信息为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 车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中

1
2
3
4
5
6
7
8
9
10
11
# 绕过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.数据条数:

Markdown

2.数据格式

Markdown

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

分享到 评论