前情提要

爬虫具有时效性,此篇文章代码不一定长期有效,但是解决方案通用。

版本信息:2020-07-17

今日头条 web 版的请求主要参数是:ascp_signature

  • ascp 比较简单,直接使用 js 源码,或者用 python 编译都可以
  • _signature 比较复杂

URL 分析

随便打开今日头条网页版一个界面,示例这里打开的是 热点分栏 地址:https://www.toutiao.com/ch/news_hot/

我们向下滑动页面,不断加载出新的内容

F12 打开开发者工具,选择 Network 中的 XHR 标签,继续下滑头条网页,观察网页请求链接

以下为三个示例链接,我们分析一下:

1
2
3
https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=0&max_behot_time_tmp=0&tadrequire=true&as=A1E51F21B0A055D&cp=5F10201525DD4E1&_signature=_02B4Z6wo00f01jcKhsgAAIBAdPSMZ6-fGcI3D4JAANLfaIBd69iVqrqwt-Kzkp68yjCiTBebZn4bKtxcot5cz26TAvNJxqWymSmizGkrEL3-TkzTvjaW14sJJpUdGO-qtIjt.n.qWnE26C8g79
https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594880609&max_behot_time_tmp=1594880609&tadrequire=true&as=A1F58F71E04057E&cp=5F1030A557BEAE1&_signature=_02B4Z6wo00901tH42wgAAIBAkgbRpdhpFFbR.d-AAOt8c3CZDocehB19PuHUmDrMDvCRZp9PXbVULneN4NWmDbAaPPGPWLtRA9--LfxHyF7itVXaG6r5K8bMdDlZeFZqFmVD3ExhcFH9u52b84
https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594869246&max_behot_time_tmp=1594869246&tadrequire=true&as=A1B5EF51300180F&cp=5F10A138508FDE1&_signature=_02B4Z6wo00501-pBU5QAAIBBqb9ZOOz-JLfqRFcAAKWKddCx4Y7Ps7qRC.B89m1IPx7kVtIM9Dy4i2lN8gSXryJypKZG7gVFrub3gVeiJxy8SjWeeg8O1c4-OQN2YJLbXyVanlfiHvufxjHi59

经过比较发现关键变量有:max_behot_timeascp、_signature,接下来我们就对这四个变量进行分析。

max_behot_time 分析

max_behot_time 的数值看似是时间戳,但是比较发现,并不是访问链接时的真实时间戳。

推断是由特定函数生成。

我们观察一下网页请求返回的 json 数据。发现除了返回的新闻内容之外,还有一个 next,包含 max_behot_time 的值。

max_behot_time

通过比较发现,这个 nextmax_behot_time 的值,正是页面下滑时,下一个请求 urlmax_behot_time

由于头条没有明确的页码,于是判断由 max_behot_time 的数值充当 页码。由于 next 的值可以直接获取,我们就不必分析其生成函数了。

as、cp 分析

F12 打开开发者工具,选择 NetworkCtrl + F 进入全局搜索,搜索 as

因为词太短,我们发现了上百条数据。想找 as 的生成函数犹如大海捞针。

as

换个思路,我们可以查一下 max_behot_time,在关键函数周围观察一下有没有 ascp 的生成函数。

F12 打开开发者工具,选择 NetworkCtrl + F 进入全局搜索,搜索 max_behot_time。只有一条函数,格式化代码后观察:

as

我们不必看 max_behot_time,正好它下方有 ascp 的函数。为了判断是不是我们要的值,我们在函数结尾处打断点,刷新网页,查看 ascp 的数值。

getHoney

正是我们需要的 as、cp 的值,再观察函数,由 e 函数生成,即上图画红圈部分。关键函数为 ascp.getHoney ,我们把鼠标放在 ascp.getHoney 上跳转到相关函数。

ascpmd5

这里就是 ascp 的计算函数了。 i = md5(t) 使用的是 md5 加密,感兴趣的朋友可以深入研究一下。我们可以直接将 js 代码转换为 python 代码,方便调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import hashlib
import time

def get_honey():
t = int(time.time())
e = hex(t).upper()[2:]
md = hashlib.md5()
md.update(str(t).encode('utf-8'))
i = str(md.hexdigest()).upper()
if len(e) != 8:
return {'as': "479BB4B7254C150", 'cp': "7E0AC8874BB0985"}
s = r = ''
for k in range(0, 5):
s = s + i[:5][k] + e[k]
r = r + e[k+3] + i[-5:][k]
return {'as': "A1" + s + e[-3:], 'cp': e[:3] + r + "E1"}

到这里我们就获取到了 ascp 的值了。

_signature 分析

F12 打开开发者工具,选择 NetworkCtrl + F 进入全局搜索,搜索 _signature

我们看到两条结果。两条都看一下:第一条是构造函数,第二条只是调用了值。我们分析第一条。

signature

在关键函数结尾行打断点,刷新页面。等待页面解析完成后,鼠标放在 _signature 上,看到了我们想要的值。仔细观察,_signature 的值由 tacSign 函数生成。

tacsign

鼠标放在 tacSign 上,点击上方的 f tacSign(e,t) 跳转到相关函数。见下图

tacsign_func

把上一个函数打的断点取消!然后在 tacSign 函数结尾行打断点,点击下图蓝色箭头 F8 ,刷新界面。

f8

可以看到 i 是我们想要的值,由 window.byted_acrawler.sign(o) 生成,参数 o 为访问链接。

正常流程为先获取 ascp 值,然后构造链接作为参数 o 调用 window.byted_acrawler.sign 得到 _signature

鼠标放在 window.byted_acrawler.sign 上,点击弹出的 f e(),跳转到目标函数。

arguments

跳转到这里,看到这个千万别蒙蔽,这只是一个超级大的函数而已,大概 500 行。

我们不必完全看懂,把整个 js 文件考出来即可。

自己拷贝就好,我这里不贴完整代码了,近 500 行,只放一下开头和结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
var _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(f) {
return typeof f
}
: function(f) {
return f && "function" == typeof Symbol && f.constructor === Symbol && f !== Symbol.prototype ? "symbol" : typeof f
}
;
TAC = function() {
function f(f, a, b, d, c, r) {
...
}
}(),
TAC("484e4f4a4......", []);

我们把上述代码保存为单独的文件,比如 sign.js

在结尾加上两行代码测试一下输出:

1
2
sign = window.byted_acrawler.sign({url:"https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594869246&max_behot_time_tmp=1594869246&tadrequire=true&as=A1B5EF51300180F&cp=5F10A138508FDE1"});
console.log(sign);

我是在 Pycharm 中安装了 node.js 插件,所以可以在 Pycharm 中直接运行。

js 运行报错

window is not defined

错误信息

运行 js,报错信息如下:

window is not defined

解决方案

在开头添加一下 window

1
window = global;

Cannot read property ‘href’ of undefined

错误信息

运行 js,报错信息如下:

href of undefined

解决方案

jsdom 模拟环境

安装好 node.js 后,在命令行模式下使用 npm install jsdom 安装。

安装好后,写一个最简单的界面,然后添加头条的 href

那么头条的 href 在哪里呢?我们打开头条页面,按 F12 打开开发者工具,选择 Console,输入 window.location 后回车,可见下图:

window.location

我们在 window.location 中添加 href 即可,为了更安全,我们把 location 中其他参数也添加进去。

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
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
window = global;
var document = dom.window.document;
var params = {
location:{
hash: "",
host: "www.toutiao.com",
hostname: "www.toutiao.com",
href: "https://www.toutiao.com",
origin: "https://www.toutiao.com",
pathname: "/",
port: "",
protocol: "https:",
search: "",
},
};
Object.assign(window,params);
window.document = document;
// ----------这里是复制的近 500 行代码 ----------
var _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(f) {
return typeof f
}
: function(f) {
return f && "function" == typeof Symbol && f.constructor === Symbol && f !== Symbol.prototype ? "symbol" : typeof f
}
;
TAC = function() {
function f(f, a, b, d, c, r) {
...
}
}(),
TAC("484e4f4a4......", []);
// ----------------------------------------

sign = window.byted_acrawler.sign({url:"https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594869246&max_behot_time_tmp=1594869246&tadrequire=true&as=A1B5EF51300180F&cp=5F10A138508FDE1"});
console.log(sign);

Cannot read property ‘userAgent’ of undefined

错误信息

运行 js,报错信息如下:

userAgent of undefined

解决方案

打开头条页面,按 F12 打开开发者工具,选择 Console,输入 window.navigator 后回车,可见下图:

window.navigator

window.navigator中添加 userAgent 即可,为了更安全,把 navigator 中其他参数也添加进去

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
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
window = global;
var document = dom.window.document;
var params = {
location:{
hash: "",
host: "www.toutiao.com",
hostname: "www.toutiao.com",
href: "https://www.toutiao.com",
origin: "https://www.toutiao.com",
pathname: "/",
port: "",
protocol: "https:",
search: "",
},
navigator:{
appCodeName: "Mozilla",
appName: "Netscape",
appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
cookieEnabled: true,
deviceMemory: 8,
doNotTrack: null,
hardwareConcurrency: 4,
language: "zh-CN",
languages: ["zh-CN", "zh"],
maxTouchPoints: 0,
onLine: true,
platform: "Win32",
product: "Gecko",
productSub: "20030107",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
vendor: "Google Inc.",
vendorSub: "",
},
};
Object.assign(window,params);
window.document = document;
// ----------这里是复制的近 500 行代码 ----------
var _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(f) {
return typeof f
}
: function(f) {
return f && "function" == typeof Symbol && f.constructor === Symbol && f !== Symbol.prototype ? "symbol" : typeof f
}
;
TAC = function() {
function f(f, a, b, d, c, r) {
...
}
}(),
TAC("484e4f4a4......", []);
// ----------------------------------------

sign = window.byted_acrawler.sign({url:"https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594869246&max_behot_time_tmp=1594869246&tadrequire=true&as=A1B5EF51300180F&cp=5F10A138508FDE1"});
console.log(sign);

运行一下,成功输出结果如下:

1
_02B4Z6wo00f0122Q2eAAAIBAkOB99iCRDv9tkt1AAIR91a

但是,这只是 _signature 的一部分。是不是遗漏了什么?

再全局搜索 window.byted_acrawler,在网页源码中发现有一段 js 生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
window.byted_acrawler && window.byted_acrawler.init({
aid: 24,
dfp: true,
intercept: true, // 开启拦截器后,所有符合下面列表条件的 url 都会自动加上 _signature 参数
// SDK 会拦截所有使用 XMLHTTPRequest 发送的请求,包括第三方库发出的,所以请严格设置 enablePathList
enablePathList: [
'/c/ugc/video/publish/'
],
urlRewriteRules: [
['/c/ugc/video/publish/', 'https://cdn.jsdelivr.net/gh/Sitoi/cdn/img/toutiao/c/ugc/video/publish/']
]
}
)

把上述代码 sdk 拦截去掉,然后插入 sign.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
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
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
window = global;
var document = dom.window.document;
var params = {
location:{
hash: "",
host: "www.toutiao.com",
hostname: "www.toutiao.com",
href: "https://www.toutiao.com",
origin: "https://www.toutiao.com",
pathname: "/",
port: "",
protocol: "https:",
search: "",
},
navigator:{
appCodeName: "Mozilla",
appName: "Netscape",
appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
cookieEnabled: true,
deviceMemory: 8,
doNotTrack: null,
hardwareConcurrency: 4,
language: "zh-CN",
languages: ["zh-CN", "zh"],
maxTouchPoints: 0,
onLine: true,
platform: "Win32",
product: "Gecko",
productSub: "20030107",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
vendor: "Google Inc.",
vendorSub: "",
},
};
Object.assign(window,params);
window.document = document;
// ----------这里是复制的近 500 行代码 ----------
var _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(f) {
return typeof f
}
: function(f) {
return f && "function" == typeof Symbol && f.constructor === Symbol && f !== Symbol.prototype ? "symbol" : typeof f
}
;
TAC = function() {
function f(f, a, b, d, c, r) {
...
}
}(),
TAC("484e4f4a4......", []);
// ----------------------------------------

window.byted_acrawler && window.byted_acrawler.init({
aid: 24,
dfp: true,
})

sign = window.byted_acrawler.sign({url:"https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594869246&max_behot_time_tmp=1594869246&tadrequire=true&as=A1B5EF51300180F&cp=5F10A138508FDE1"});
console.log(sign);

Cannot read property ‘width’ of undefined

错误信息

运行 js,报错信息如下:

width of undefined

解决方案

打开头条页面,按 F12 打开开发者工具,选择 Console,输入 window.screen 后回车,可见下图:

window.screen

window.screen 中添加 width 即可,为了更安全,把 screen 中其他参数也添加进去。

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
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
window = global;
var document = dom.window.document;
var params = {
location:{
hash: "",
host: "www.toutiao.com",
hostname: "www.toutiao.com",
href: "https://www.toutiao.com",
origin: "https://www.toutiao.com",
pathname: "/",
port: "",
protocol: "https:",
search: "",
},
navigator:{
appCodeName: "Mozilla",
appName: "Netscape",
appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
cookieEnabled: true,
deviceMemory: 8,
doNotTrack: null,
hardwareConcurrency: 4,
language: "zh-CN",
languages: ["zh-CN", "zh"],
maxTouchPoints: 0,
onLine: true,
platform: "Win32",
product: "Gecko",
productSub: "20030107",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
vendor: "Google Inc.",
vendorSub: "",
},
"screen":{
availHeight: 1040,
availLeft: 0,
availTop: 0,
availWidth: 1920,
colorDepth: 24,
height: 1080,
pixelDepth: 24,
width: 1920,
}
};
Object.assign(window,params);
window.document = document;
// ----------这里是复制的近 500 行代码 ----------
var _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(f) {
return typeof f
}
: function(f) {
return f && "function" == typeof Symbol && f.constructor === Symbol && f !== Symbol.prototype ? "symbol" : typeof f
}
;
TAC = function() {
function f(f, a, b, d, c, r) {
...
}
}(),
TAC("484e4f4a4......", []);
// ----------------------------------------

window.byted_acrawler && window.byted_acrawler.init({
aid: 24,
dfp: true,
})

sign = window.byted_acrawler.sign({url:"https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594869246&max_behot_time_tmp=1594869246&tadrequire=true&as=A1B5EF51300180F&cp=5F10A138508FDE1"});
console.log(sign);

再运行一下,没有报错了。返回值如下:

1
_02B4Z6wo00f01erPHLwAAIBCF7-4qJivPAXqzRgAACWl0b

_signature 长度不一致

多方调查发现:是真实网页是带 cookie 访问的,我们的模拟环境没有 cookie

解决方案

在模拟环境中添加 cookie

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
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
window = global;
var document = dom.window.document;
var params = {
location:{
hash: "",
host: "www.toutiao.com",
hostname: "www.toutiao.com",
href: "https://www.toutiao.com",
origin: "https://www.toutiao.com",
pathname: "/",
port: "",
protocol: "https:",
search: "",
},
navigator:{
appCodeName: "Mozilla",
appName: "Netscape",
appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
cookieEnabled: true,
deviceMemory: 8,
doNotTrack: null,
hardwareConcurrency: 4,
language: "zh-CN",
languages: ["zh-CN", "zh"],
maxTouchPoints: 0,
onLine: true,
platform: "Win32",
product: "Gecko",
productSub: "20030107",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
vendor: "Google Inc.",
vendorSub: "",
},
"screen":{
availHeight: 1040,
availLeft: 0,
availTop: 0,
availWidth: 1920,
colorDepth: 24,
height: 1080,
pixelDepth: 24,
width: 1920,
}
};
Object.assign(window,params);
window.document = document;

function setCookie(name, value, seconds) {
seconds = seconds || 0;
var expires = "";
if (seconds != 0 ) {
var date = new Date();
date.setTime(date.getTime()+(seconds*1000));
expires = "; expires="+date.toGMTString();
}
document.cookie = name+"="+escape(value)+expires+"; path=/";
}
// 把自己浏览器的真实 cookie 复制过来即可
cookies = "s_v_web_id=xxxxxxxxxxxxxxxxxxxxxxxxxx";
for(let cookie of cookies.split(";")){
tmp = cookie.split("=");
setCookie(tmp[0],tmp[1],1800);
}

// ----------这里是复制的近 500 行代码 ----------
var _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(f) {
return typeof f
}
: function(f) {
return f && "function" == typeof Symbol && f.constructor === Symbol && f !== Symbol.prototype ? "symbol" : typeof f
}
;
TAC = function() {
function f(f, a, b, d, c, r) {
...
}
}(),
TAC("484e4f4a4......", []);
// ----------------------------------------

window.byted_acrawler && window.byted_acrawler.init({
aid: 24,
dfp: true,
})

sign = window.byted_acrawler.sign({url:"https://www.toutiao.com/api/pc/feed/?category=news_hot&utm_source=toutiao&widen=1&max_behot_time=1594869246&max_behot_time_tmp=1594869246&tadrequire=true&as=A1B5EF51300180F&cp=5F10A138508FDE1"});
console.log(sign);

运行一下,终于!得到了完整的 _signature 值:

1
_02B4Z6wo00f01jc-omQAAIBByk4GctL5ZYo3PKbAANLr40OHsHg8RRe1BK03uca1smyI5DA3wElBPDGI.KcAotMiY1IOIhstbtN3bZIM9xRX0NzP.PoYAaq0JjXmU5cIgLSE03L.57r1BQkJe6