豆瓣阅读上有许多精彩小说,想要把一些小说下载下来,但是一个个页面复制粘贴未免显得麻烦,所以自然想到利用爬虫爬取文章咯。但是不爬取不知道,一分析才发现豆瓣阅读的反爬也是十分厉害了。
请求分析
以栗木村拐卖事件这个页面为例,分析获取文章的全过程。
打开谷歌的调试工具F12
,刷新页面,可以看到刷新过程中,页面会出现如下载入界面,随后再出现真正的文章内容。
这意味着网页内容是很可能是ajax动态加载的,也意味着有请求接口可以直接获得数据。
但是还是以常规的办法开局,查看文本内容是否直接在页面html中,但是
Elements
界面定位的文字内容竟然是一个文字在一个<span>
标签内,如果文本真的在html内,其实就算这样,也不是很难提取,但是Elements
界面看到的都是渲染过后的,这非常重要,对静态网页无所谓,但是尤其要注意动态网页,不要以为直接右键定位就可以爬取数据。对动态加载的网页,保险的方式还是查看网页源代码Ctrl+U
,看到的页面基本没有任何文章内容。这意味着数据一定是另外请求得到的,对我而言,我还是很喜欢数据在单独的请求中的,这样就不用解析网页了,但是豆瓣会轻易的给出数据吗?
由于基本确定是动态加载,那么是XHR
请求的概率也是很大了。Network
界面中的XHR
请求如下,除了get_reader_data
外,其他大小size都非常小。
进一步
preview
看其他请求,只有一些评论,文章信息的json
数据,但是get_reader_data
不一样,其data
部分有许多编码后的数据,而且量是非常的大,非常有可能是编码后的文章内容。至此,请求分析基本完成,可以确定文本数据在
get_reader_data
中,但是我们需要的数据经过编码,看看编码后的文本,也应该不是常见的编码方式,很有可能是豆瓣自定义的一种编码,下面要做的就是找到解码方式,就要深入到JS文件中了,这也是最难最繁琐的地方。
JS分析
给数据请求打上断点,刷新页面。
Call Stack给出了发起请求的过程函数,可能由于豆瓣JS代码混淆没有做好,一眼就被
getArticleData
吸引了,这个名字极可能意味着这里就是获取文本数据的地方。在函数中部打上断点,至于过程多试几次,我也是打了好几次才找到解码函数位置,刷新后观察如下界面
其中变量
l
是get_reader_data
请求中返回的data数据,变量i
是文章id,变量r
是解码后的JSON对象。找到解码函数之后,剩下的就很简单,移动到函数上方进入函数定义部分,一点点分析,这需要一些JavaScript语法知识,我也将解码部分抽离出来了。
var o = ["A", "b", "H", "P", "Q", "X", "V", "p", "r", "I", "$", "7", "F", "z", "o", "K", "_", "S", "6", "a", "T", "C", "t", "j", "5", "n", "D", "e", "x", "U", "R", "y", "4", "N", "Y", "9", "v", "0", "3", "W", "l", "u", "1", "i", "q", "s", "O", "J", "G", "E", "w", "f", "B", "m", "L", "2", "d", "h", "k", "8", "c", "g", "Z", "M"];
function n(t) {
return function(t) {
if (Array.isArray(t)) {
for (var e = 0, i = new Array(t.length); e < t.length; e++)
i[e] = t[e];
return i
}
}(t) || function(t) {
if (Symbol.iterator in Object(t) || "[object Arguments]" === Object.prototype.toString.call(t))
return Array.from(t)
}(t) || function() {
throw new TypeError("Invalid attempt to spread non-iterable instance")
}()
}
var k = function(t, e) {
return function(t, e, i) {
var o = {},
s = String.fromCharCode("}".charCodeAt(0) + 1),
r = e.length;
e = function(t, e, i) {
return e ? (t = t.slice(), e.split("").forEach(function(e) {
var o = e.charCodeAt(0) % i;
t = [].concat(n(t.slice(o)), n(t.slice(0, o)))}), t) : t
}(e, i, r);
for (var a = 0; a < r; ++a)
o[e[a]] = a;
for (var h, l, c, u, d = [], f = 0, p = 0; f < t.length;)
h = o[t[f++]],
l = o[t[f++]],
c = o[t[f++]],
u = o[t[f++]],
d[p++] = h << 2 | l >> 4,
d[p++] = (15 & l) << 4 | c >> 2,
d[p++] = (3 & c) << 6 | u;
var g = t.slice(-2);
return g[0] === s ? d.length = d.length - 2 : g[1] === s && (d.length = d.length - 1),
function(t) {
for (var e = "", i = 0; i < t.length; ++i) {
var n = t[i];
e += String.fromCharCode(256 * n + t[++i])
}
return e
}(d)
}(t, o, e)
}
var N = function(t) {
return parseInt(("" + t).slice(0, 10), 36).toString().slice(0, 5)
}
function result(l, i){
var r = k(l, N(i));
return r
}
使用时只要调用result
函数即可,参数分别为编码后的data数据和文章id字符串。返回的是JSON字符串,可在python中转换为dict对象。
代码实现
分析完成后,就需要代码实现,由于涉及到js代码,当然可以将其用python语言描述,但是耗时费力,同时需要对js语法比较了解,不推荐。可以使用python中一些执行js代码的库,比如execjs
,js2py
等,由于结果涉及中文,推荐使用js2py
。将上述抽离出来的JS代码保存为douban.js
文件。
import js2py
import json
import requests
def run_js(l, i):
with open('douban.js') as fp:
js = fp.read()
context = js2py.EvalJs()
context.execute(js)
r = context.result(l, i)
return json.loads(r)
def post_url(url, aid):
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36"}
data = {"aid": aid, "reader_data_version": "v15"}
r = requests.post(url, data=data, headers=headers, timeout=3)
return r.json()["data"]
if __name__ == '__main__':
data =post_url("https://read.douban.com/j/article_v2/get_reader_data", "108019536")
result = run_js(data, "108019536")["posts"][0]["contents"]
content = ""
for i in result:
d = i["data"]["text"][0]["content"]
content+=d.strip()
print(content)
代码内的一些函数使用不深入讲解,可自行深入了解。
参考文章:
1、豆瓣阅读的文字解码 - Blog