最近搞了个简单的文章转视频上传到youtube,需要采集点文章来生成视频,所以打算用Scrapy采集点今日头条的文章,目标是抓取其中一个分类的几十篇文章内容和下载其中的图片导出json数据文件用来生成视频,简单做一下记录。
打开头条网站,有多个分类,打开娱乐版块,查看源代码发现是没有文章内容,看来是js加载的,F12查看分析找到文章数据的接口链接
https://www.toutiao.com/api/pc/feed/?category=news_entertainment&utm_source=toutiao&widen=1&max_behot_time=0&max_behot_time_tmp=0&tadrequire=true&as=A1B5FBF1A7CA537&cp=5B172A5593E7AE1&_signature=oNvqZwAA-9u3ZjcQBbvTJ6Db6n
在浏览器里直接打开这个链接,很好,返回json结构的数据,刷新一下,返回不同的文章,嗯,这么easy,一个链接就搞定了。
那还等什么,新建Scrapy项目抓一下试试,哈哈,什么也没有,看一下Scrapy的输出 DEBUG: Forbidden by robots.txt
原来是头条禁止爬虫抓取了,需要设置Scrapy的参数 ROBOTSTXT_OBEY = False
,再试一下还是不行,直接返回
{
"message": "error",
"data": [],
"has_more": false
}
没有文章数据,构造header加上User-Agent跟Referer
headers = {
"Referer": "https://www.toutiao.com/ch/news_entertainment/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0"
}
OK,正常返回数据了,多次请求,嘿,返回的文章基本都一样,看来还是不可以哦,再加上cookie试试,用固定的 tt_webid=6563786091257824776;
,多次请求,发现跟浏览器里刷新是一样的了,会返回不同的文章,倒是基本可以满足一次抓取几十篇文章需求了。
不过既然搞了那再深入研究一下看看这几个参数是干啥的嘛
category=news_entertainment
分类,对应头条的分类,news_entertainment是娱乐
utm_source=toutiao
固定
widen=1
固定
max_behot_time=0
和max_behot_time_tmp=0
值相同,是个时间戳,初次请求是0,每次请求返回的数据中都会带有下一次请求的时间戳 "next": {"max_behot_time": 1528358256}
tadrequire=true
带广告,false
不带广告
as=A1B5FBF1A7CA537
和cp=5B172A5593E7AE1
是简单的时间加密,网上搜一下,很容易找到python的翻译代码,不加这两个参数,也能返回文章数据
def get_as_cp():
t = int(math.floor(time.time()))
e = hex(t).upper()[2:]
m = hashlib.md5()
m.update(str(t).encode(encoding='utf-8'))
i = m.hexdigest().upper()
if len(e) != 8:
as_key = "479BB4B7254C150"
cp_key = "7E0AC8874BB0985"
return as_key, cp_key
n = i[:5]
a = i[-5:]
r = ""
s = ""
for k in range(5):
s += n[k]+e[k]
r += e[k+3]+a[k]
as_key = "A1" + s + e[-3:]
cp_key = e[0:3] + r + "E1"
return as_key, cp_key
_signature=oNvqZwAA-9u3ZjcQBbvTJ6Db6n
是个签名,测试发现这个参数值的生成跟浏览器UA有关,换个UA就没有数据返回了,_signature跟UA匹配才会返回数据,网上倒是没有找到python的翻译代码,看来这个有点难搞,先用固定值,构造链接
url = 'https://www.toutiao.com/api/pc/feed/?category=news_entertainment&utm_source=toutiao&widen=1' \
'&max_behot_time={0}' \
'&max_behot_time_tmp={0}' \
'&tadrequire=true' \
'&as={1}' \
'&cp={2}' \
'&_signature=oNvqZwAA-9u3ZjcQBbvTJ6Db6n'
发现第一次请求max_behot_time=0
时正常返回文章数据,之后带上新的时间戳后,就没有数据了,看来这个签名还跟时间戳有点关系,找一下js代码看看,
function e(t) {
var e = ascp.getHoney(),
i = "";
window.TAC && (i = TAC.sign("refresh" === t ? 1 : r.params.max_behot_time_tmp)), r.params = _.extend({}, r.params, {
as: e.as,
cp: e.cp,
max_behot_time: "refresh" === t ? 0 : r.params.max_behot_time_tmp,
_signature: i
})
}
果然,签名的生成跟max_behot_time_tmp的值有关,貌似是用TAC.sign()函数生成,找一下这个函数看看,emmm~,乱码~,尴尬了,在F12控制台敲入TAC.sign
,发现还是乱码是什么鬼,这个函数还搞了一下加密?算了,算了,大道至简,还是搜搜看有没有人搞过吧,不过没有找到python的翻译代码,让我自己写,哈哈,那还是写不出来的,不费这个劲了,直接用解析js的方式来获取这个值吧,搜了一下,可以用pyExecJS这个模块来解析js,但是直接把头条的js文件加载进来发现会报错,不过倒是找到一份整理过的js代码 toutiao-TAC.sign.txt,https://bbs.125.la/thread-14115046-1-1.html,感谢nliger2015的分享,可以直接拿来用没有报错。
def get_as_cp_signature(max_behot_time=0):
with open('toutiao-TAC.sign.txt', encoding='utf-8') as f:
js = f.read()
ctx = execjs.compile(js)
s = ctx.call('get_as_cp_signature', max_behot_time)
return json.loads(s)
哈哈,很好,可以直接返回as,cp,_signature三个参数值,不错不错,之前as和cp的翻译函数不需要了,获取文章列表数据基本差不多了,接下来开始搞文章详情。
先是直接用xpath获取内容,哈哈,结果取回来的全是None,嗯?xpath写的不对?放到浏览器测试没问题的,查看源代码看一下,我去,详情页的内容也是用js加载的,想要的内容都在js里呢,都放到BASE_DATA这个变量里了,是json结构的对象,不过不能直接用json模块解析,因为不全是用的双引号,json模块解析会报错,唉,好吧,既然在js里,那直接用js解析一下,要是能直接返回BASE_DATA这个变量对象,不就是拿到了文章的结构数据,用起来不就更省事了,不就是想怎么用就怎么用了,哈哈。
data = response.xpath('//script[contains(text(),"var BASE_DATA")]/text()').extract_first()
if data:
data = data + "\nvar a = function(){return BASE_DATA;}"
ctx = execjs.compile(data)
data = ctx.call('a')
print(data['articleInfo']['title'])
先用xpath取出需要的js代码部分,再加个函数return这个变量,然后用pyExecJS解释执行一下,返回文章详情数据,执行下来,倒是没问题,也正常返回数据了,而且确实是直接可以拿来用的,不过输出一下,中文全变成了“锟斤拷锟斤拷锟叫★拷锟斤拷锟斤拷钱锟斤...”,这是什么鬼?编码有问题?尝试转码后再传递给js执行,结果报错,emmm,什么鬼,各种编码解码尝试无果,放弃js解析,改用正则,哈哈,还是正则好用,指哪拿哪
data = response.xpath('//script[contains(text(),"var BASE_DATA")]/text()').extract_first()
data = re.search(r"title:\s*'(.*?)',\s*content:\s*'(.*?)',[\s\S]*?tags:\s*(\[.*\]),", data)
if data:
title, content, tags = data.group(1, 2, 3)
直接拿到title,content,tags的数据,再对拿到的数据进行一下处理,加个下载图片的pipeline把图片也下载下来,嗯,基本搞定,采集导出的json数据文件可以直接用来生成视频。
还有点小问题,重复文章数据,因为写这个小爬虫,就是采集文章用来生成视频的,不过生成视频比较慢,嗯是挺慢的,所以一次采集个几十篇文章即可,在一次抓取中,返回的文章数据是没有重复的,不过下一次再重新启动爬虫抓取时,就会跟上次采集的文章有重复的,时间间隔不长的话,重复的还不少,也加上cookie固定ttweb_id的值试了下发现效果不大,一样会有重复的数据,所以就把每次抓取的文章链接存到文件里,每次开始抓取前,先读入存放链接的文件把已经抓取的链接放到列表里,每次发送文章详情页request前先判断一下是不是抓取过,在列表中的链接直接跳过,等到抓取完成爬虫关闭时,再把新抓取的链接追加到文件里,等抓取的链接比较多的时候,这种方式可能不太好,不过没关系,满足当前的需求就可以了,哈哈。