获取抖音用户作品列表信息
# 需求
获取指定用户的作品信息,包含标题、时间、播放地址等
# 分析
获取用户信息的方式:
- 星图达人
星图达人只有一小部分作品,不满足需求
- 哪里有列表,分析列表接口
# 实践
# 抖音app抓包
- 使用Charles进行抓包
https://aweme-hl.snssdk.com/aweme/v1/search/item/?iid=91648520549&aid=1128&mcc_mnc=&os_api=18&app_name=aweme&channel=App%20Store&idfa=3911688C-3A52-4237-BC0C-84255F48E141&device_platform=iphone&build_number=83101&vid=614D7CD9-AE0B-4E29-91CB-D06791916C23&openudid=bf7f9afd8bc01da17253c34849f70701a2e985a3&device_type=iPhone9,4&app_version=8.3.1&js_sdk_version=1.17.4.1&device_id=68291934124&ac=WIFI&version_code=8.3.1&os_version=10.2&screen_width=1242&source=video_search&search_id=201911071144490100260780891712893F&offset=120&query_correct_type=1&count=12&keyword=%E5%B0%8F%E4%BB%99%E7%88%B1%E7%94%9F%E6%B4%BB&hot_search=0
很容易就获取到了用户列表滚动加载的接口,设置offset和count 进行获取数据!!! 然而现实很骨感,返回的接口是空数据!! 哪里不对呢?(此时不知道抖音接口有加密) 是不是哪里设置错了呢? 进过一番折腾(网页请求、postman请求等),都不得解,最后知道抖音接口加密了,继续抓包查看请求头部header header信息
"x-tt-token":"000c2f72735ade685f370d832c3ea4d2549d057cbeb7b24350b15572702d25077074f2540c27f20a6cca49dab3b899cc6135",
"x-tt-trace-id": "00-80c302f19427f59a8e0ff4926a9cab1d-80c302f19427f59a-01",
"x-ss-dp": 1128,
"user-Agent": "Aweme 8.3.1 rv:83101 (iPhone; iOS 10.2; zh_CN) Cronet",
"x-gorgon": "830000000000a3122894b28e071f1455c00e85d18b4967ba1e67",
"x-khronos": 1573116669,
"sdk-version": 1
ok,那就把头部信息带入请求中。 结果还是得不到数据?why?一头闷逼
为什么抓包就能获取数据,而我模拟请求就获取不到数据呢?有什么差异呢,对比一下
经过对比,发现每次的请求这些请求头部的参数值不一样
x-gorgon
x-khronos
这个值是怎么来的呢? 不知道,于是百度搜索!!!
从这里才开始知道抖音接口有加密这个功能!!!
用什么算法加密,不清楚? 怎么样获取x-gorgon的值, 可以模拟动态计算吗? 不知道
经过网上的一番折腾,明白了抖音是故意这样的,就是防止接口被刷吧!
解决方式有2种:
- 破解app,逆推算法
- 有第三方人在卖相应的接口
难道除了以上方法就没有别的了吗?不甘心,继续github和百度、Google
爬虫很多人用了python,有的时候需要下载别人的工程去试试可不可以,于是学习python,边学边用
在诸多的信息反馈中,得知抖音有用户m站,于是打开
https://www.iesdouyin.com/share/user/99007818191
这里包含了用户作品列表、喜欢列表、粉丝量、点赞量
也是滚动加载,可以通过接口去拿,不过有了抖音App接口的经验,并没有高兴起来,先看接口长啥样 果然不出所料,有签名信息,肯定是动态的
_signature: sC0QzBAU7e5XWujK0PgnC7AtEN
dytk: 241ee964ddd0156f2efe4cdc664654bf
对比了一下不同用户的签名,发现是不一样的,那么就不能批量去获取用户信息了
怎么办!!! 难点又来了
好在这是在web端,比客户端好解决多了
放出大招!!!puppeteer
之前看过一篇文章讲述使用puppeteer进行爬虫的文章,有感而发,模拟用户行为,签名不用自己造,觉得可行,于是行动起来
在puppeteer中用到了以下技术
- 监听requestfinished事件,获取请求返回的数据,把数据放入数组中
- 模拟滑动屏幕,每500毫秒让页面下滑100像素的距离,获取了所有作品
- 通过串行的方式把分id获取视频列表,并把结果写入文件
最后把代码进行整理,封装!!
# 总结:
- 第一次碰到这么复杂的接口,抖音做的应该是很厉害了, 防刷做的很好,加密算法运用的好
- 未知事物总是很难, 要一步步去分析找到解决方案,也许结果很简单,但是没有找到思路或方法,就像一座大山,难以迈过
- 学会搜索,(百度+github),百度搜不了github,搜索能给人很多帮助
- 学会交流,找一些群或者人,主动去询问难点,你的难点对于别人来说未必是难点,也许是一句话的事情
- 相信自己可以去解决问题
# 相关
# 附上代码
const fs = require('fs');
const puppeteer = require('puppeteer');
const moment= require('moment')
// 获取抖音用户作品列表~
const douyinList = (id) => {
return new Promise(async resolve => {
const browser = await (puppeteer.launch({ headless: false }));
const page = await browser.newPage();
let allInfo = [];
page.on('requestfailed', request => {
console.log(request.url() + ' ' + request.failure().errorText);
});
page.on('requestfinished', request => {
// 查看所有请求地址
if (request.resourceType() === "xhr") {
// 匹配所需数据的请求地址
if(request.url().indexOf('https://www.iesdouyin.com/web/api/v2/aweme/post') !== -1) {
const waitResult = async () => {
try {
// 获取数据并转为json格式
console.log('等待请求结果')
let res = await request.response();
let result = await res.json();
const awemeList = result.aweme_list
const {min_cursor, max_cursor} = result
const startDate = moment(min_cursor).format('YYYY-MM-DD')
const endDate = moment(max_cursor).format('YYYY-MM-DD')
const list = awemeList.map(item => {
const {aweme_id, desc, video,statistics} = item
const video_src = `https://www.iesdouyin.com/share/video/${aweme_id}/?region=CN&mid=&u_code=&titleType=title&utm_source=copy_link&utm_campaign=client_share&utm_medium=android&app=aweme`
return {
aweme_id, desc, video_src,video,statistics,startDate,endDate
}
})
allInfo.push(...list)
} catch(err){
console.log(err)
}
}
waitResult()
}
}
})
// 进入页面
await page.goto(`https://www.iesdouyin.com/share/user/${id}`);
const title = await page.$eval('.nickname', el => el.innerHTML);
console.log(title);
console.log('开始滚动到底部')
await autoScroll(page);
console.log(`共获取到${allInfo.length}个作品信息`);
// 将作品信息写入文件
let writerStream = fs.createWriteStream(`douyin-${id}.json`);
writerStream.write(JSON.stringify(allInfo, undefined, 2), 'UTF8');
writerStream.end();
await page.waitFor(5000);
browser.close();
resolve()
// 滑动屏幕,滚至页面底部
function autoScroll(page) {
return page.evaluate(() => {
return new Promise((resolve) => {
var totalHeight = 0;
var distance = 100;
// 每500毫秒让页面下滑100像素的距离
var timer = setInterval(() => {
var scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 500);
})
});
}
})
}
const arr = ['102269017585', '58146392181']
let index = 0
const doTask = async (arr) => {
console.log('index', index)
await douyinList(arr[index++])
if(index <= arr.length -1){
doTask(arr)
return
}
console.log('task is done')
}
doTask(arr)