前情提要 爬虫具有时效性,此篇文章代码不一定长期有效,但是解决方案通用。
今日头条 web
版的请求主要参数是:as
、cp
、_signature
。
as
、cp
比较简单,直接使用 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_time
、as
、cp
、_signature
,接下来我们就对这四个变量进行分析。
max_behot_time 分析 max_behot_time
的数值看似是时间戳,但是比较发现,并不是访问链接时的真实时间戳。
推断是由特定函数生成。
我们观察一下网页请求返回的 json
数据。发现除了返回的新闻内容之外,还有一个 next
,包含 max_behot_time
的值。
通过比较发现,这个 next
中 max_behot_time
的值,正是页面下滑时,下一个请求 url
中 max_behot_time
。
由于头条没有明确的页码,于是判断由 max_behot_time
的数值充当 页码
。由于 next
的值可以直接获取,我们就不必分析其生成函数了。
as、cp 分析 按 F12
打开开发者工具,选择 Network
按 Ctrl + F
进入全局搜索,搜索 as
。
因为词太短,我们发现了上百条数据。想找 as
的生成函数犹如大海捞针。
换个思路,我们可以查一下 max_behot_time
,在关键函数周围观察一下有没有 as
、cp
的生成函数。
按 F12
打开开发者工具,选择 Network
按 Ctrl + F
进入全局搜索,搜索 max_behot_time
。只有一条函数,格式化代码后观察:
我们不必看 max_behot_time
,正好它下方有 as
、cp
的函数。为了判断是不是我们要的值,我们在函数结尾处打断点,刷新网页,查看 as
、cp
的数值。
正是我们需要的 as、cp 的值,再观察函数,由 e 函数生成,即上图画红圈部分。关键函数为 ascp.getHoney
,我们把鼠标放在 ascp.getHoney
上跳转到相关函数。
这里就是 as
、cp
的计算函数了。 i = md5(t)
使用的是 md5 加密,感兴趣的朋友可以深入研究一下。我们可以直接将 js 代码转换为 python
代码,方便调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import hashlibimport timedef 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" }
到这里我们就获取到了 as
、cp
的值了。
_signature 分析 按 F12
打开开发者工具,选择 Network
按 Ctrl + F
进入全局搜索,搜索 _signature
。
我们看到两条结果。两条都看一下:第一条是构造函数,第二条只是调用了值。我们分析第一条。
在关键函数结尾行打断点,刷新页面。等待页面解析完成后,鼠标放在 _signature
上,看到了我们想要的值。仔细观察,_signature
的值由 tacSign
函数生成。
鼠标放在 tacSign
上,点击上方的 f tacSign(e,t)
跳转到相关函数。见下图
把上一个函数打的断点取消!然后在 tacSign
函数结尾行打断点,点击下图蓝色箭头 F8
,刷新界面。
可以看到 i
是我们想要的值,由 window.byted_acrawler.sign(o)
生成,参数 o
为访问链接。
正常流程为先获取 as
、cp
值,然后构造链接作为参数 o
调用 window.byted_acrawler.sign
得到 _signature
鼠标放在 window.byted_acrawler.sign
上,点击弹出的 f e()
,跳转到目标函数。
跳转到这里,看到这个千万别蒙蔽,这只是一个超级大的函数而已,大概 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 3 4 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
Cannot read property ‘href’ of undefined 错误信息
运行 js,报错信息如下:
解决方案
用jsdom
模拟环境
安装好 node.js
后,在命令行模式下使用 npm install jsdom
安装。
安装好后,写一个最简单的界面,然后添加头条的 href
。
那么头条的 href
在哪里呢?我们打开头条页面,按 F12
打开开发者工具,选择 Console
,输入 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 ;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,报错信息如下:
解决方案
打开头条页面,按 F12
打开开发者工具,选择 Console
,输入 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 ;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 window .byted_acrawler && window .byted_acrawler .init ({ aid : 24 , dfp : true , intercept : true , 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 ;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,报错信息如下:
解决方案
打开头条页面,按 F12
打开开发者工具,选择 Console
,输入 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 ;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=/" ; } cookies = "s_v_web_id=xxxxxxxxxxxxxxxxxxxxxxxxxx" ; for (let cookie of cookies.split (";" )){ tmp = cookie.split ("=" ); setCookie (tmp[0 ],tmp[1 ],1800 ); } 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