今日头条文章详情页抓取小记
打算把之前采集今日头条那个小站的更新搞起来,上次更新是去年还是前年来,总之,原先那个用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
即可,又可以愉快的跑起来了。
博主你好,按照你的方法,爬出来后是下面的这样一个页面,请问需要怎么操作才能拿到文章内容呢
今日头条 您需要允许该网站执行 JavaScript var startTime = Date.now(); (function (i, s, o, g, r, a, m) { i["SlardarMonitorObject"] = r; (i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments) }), (i[r].l = 1 * new Date()); (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); a.async = 1; a.src = g; a.crossOrigin = "use-credentials"; m.parentNode.insertBefore(a, m); i[r].globalPreCollectError = function () { i[r]("precollect", "error", arguments) }; if (typeof i.addEventListener === "function") { i.addEventListener("error", i[r].globalPreCollectError, true) } if ('PerformanceLongTaskTiming' in i) { var g = i[r].lt = { e: [] }; g.o = new PerformanceObserver(function (l) { g.e = g.e.concat(l.getEntries()) }); g.o.observe({ entryTypes: ['longtask', 'largest-contentful-paint'] }) } })(window, document, "script", "https://i.snssdk.com/slardar/sdk.js?bid=toutiao_web_pc", "Slardar"); window.Slardar("config", { bid: 'toutiao_web_pc', pid: 'ttwid', sampleRate: 1, }); var ttwidInstance = null; var retryTime = 0; var maxRetryTime = 2; function reportError(type) { window.Slardar('emit', 'counter', { name: 'ttwid-register-error', value: 1, tags: { type: type } }) } function reportDuration() { window.Slardar('emit', 'timer', { name: 'ttwid-page-duration', value: Date.now() - startTime }) } function setQueryParams(k, v) { var searchParamsArr = window.location.search ? window.location.search.substr(1).split('&') : []; var hasDup = false; for (var i = 0; i < searchParamsArr.length; i++) { var paramArr = searchParamsArr[i].split('=') if (paramArr[0] === k) { searchParamsArr[i] = k + '=' + v; hasDup = true; break; } } if (!hasDup) { searchParamsArr.push(k + '=' + v); } window.location.search = searchParamsArr.join('&'); } function end() { reportDuration(); setQueryParams('wid', Date.now()); } function init() { if (!TTWidInstance) { reportError('init'); end(); return; } if (!ttwidInstance) { ttwidInstance = new TTWidInstance({ aid: 24, service: 'www.toutiao.com', region: 'cn', union: true, needFid: false, }); } ttwidInstance.registerUnionWebId({}, function (err, res) { if (err || !res || res.status_code !== 0) { if (retryTime < maxRetryTime) { init(); retryTime++; return; } reportError('register'); } end(); }); } try { init(); } catch (error) { console.error(error); end(); }