今日头条文章详情页抓取小记
打算把之前采集今日头条那个小站的更新搞起来,上次更新是去年还是前年来,总之,原先那个用scrapy写的爬虫代码很久没跑过了。大概流程可以看之前写的那篇文章 Scrapy 抓取今日头条文章小记,博客也差不多断更许久了,正好就借着这篇文章同时更新一下代码跟博客吧。
测试了一下之前的代码,发现之前的代码采集文章列表没问题,不过文章详情页抓不到内容了,抓到的页面只有一段js代码,没有文章的任何内容,看来头条调整详情页了,用浏览器打开看看,发现初次访问文章详情页,跟爬虫采集到的一样,是一段js代码,然后自动刷新了一下页面,文章内容才显示出来。
之前头条文章详情页没有反爬机制,直接采集就可以了,现在加上详情页也加上反爬了,大概流程是,初次访问详情页链接,会返回一个cookie值__ac_nonce
,用__ac_nonce
计算出一个__ac_signature
签名值,同时带着__ac_nonce
和__ac_signature
的cookie值再次刷新页面,就正常返回文章详情页了。不需要每次访问详情页都重新计算__ac_signature
值,只需要计算一次,后续访问详情页直接带着__ac_nonce
和__ac_signature
的cookie值就可以了,有一定的时效。
具体如何破解__ac_signature
参数值,就不深入研究了,直接搜索window.byted_acrawler
等关键词就有很多相关的js逆向分析文章,还是奔着有没有现成可用的整理过的js代码的思路找了一下,在github上找到了,感谢Bindian9710分享的代码,
https://github.com/Bindian9710/Spider-Crack_Js/blob/master/今日头条/头条.js,这个代码可以直接用pyExecJS模块解析,返回_signature计算值,不过要获取详情页的__ac_signature
还需要对这个代码简单改动一下,原代码应该是采集文章列表时用url计算来获取_signature
参数值的,详情页的__ac_signature
计算函数同列表的计算函数是相同的,只是函数传的参数稍有不同。
头条.js 14行,设置User-Agent,改成跟爬虫UA一致
window.navigator = {
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0",
}
头条.js 672行,_signature函数需要改动一下,改成详情页的通过__ac_nonce
值来计算__ac_signature
function _signature(e, t) {
var a, r;
a = window.byted_acrawler
r = a.sign
var c = r.call(a, '', e)
return c
}
function get_signature(cookie_ac_nonce) {
return _signature(cookie_ac_nonce)
}
改好后,然后另存为sign.js
,写一个测试这个js计算的__ac_signature
值是否可用的python代码
import execjs
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0'
}
_r = requests.get('https://www.toutiao.com/a6936101864131953166/', headers=headers)
_ac_nonce = _r.cookies["__ac_nonce"] # 初次访问详情页,获取__ac_nonce的值
print(f'__ac_nonce: {_ac_nonce}')
with open('./sign.js', 'r', encoding='utf-8') as f:
toutiao_js = execjs.compile(f.read()) # 加载js
_ac_signature = toutiao_js.call('get_signature', f'{_ac_nonce}') # 计算__ac_signature
print(f'__ac_signature: {_ac_signature}')
# 同时带着`__ac_nonce`和`__ac_signature`的cookie值再次访问详情页
headers.update({
'Cookie': f'__ac_nonce={_ac_nonce}; __ac_signature={_ac_signature};',
})
r = requests.get('https://www.toutiao.com/a6936101864131953166/', headers=headers)
print(r.status_code)
print(r.content.decode('utf-8'))
运行一下,正常获取文章详情页的内容了
算出来的__ac_signature
值 _02B4Z6wo00f01QMQkFQAAIBC.O9vqmPdwIkDEpTAACCEde
要比在浏览器里的看到的值短上许多,貌似长度是需要初始化一下参数控制的,window.byted_acrawler.init({ aid: 24, dfp: true })
,不过尝试在js里加上对应的长度控制代码,运行会报错 execjs._exceptions.ProgramError: TypeError: Cannot read property 'x' of undefined
。
不研究长度的问题了,虽短却也够用了,可以正常获取文章详情页的内容了,再把原先的scrapy代码改造一下,列表获取还用之前的方法,只需要在采集详情页前,先获取一下__ac_nonce
和__ac_signature
即可,又可以愉快的跑起来了。