抓包
地址https://live.kuaishou.com/profile/3x72uw4df7wdq8q?fid=43648984
这里我们抓包
可以发现地址都是https://**live.kuaishou.com**/live_graphql
通过请求内容的operationName的内容控制获取的数据
根据测试可知,首屏数据的名字为privateFeedsQuery,其他为publicFeedsQuery
返回的数据我大概看了下,只有封面图片,没有视频,点开
发现也是存在id的,所以这里我们应该另想办法
这时候我们发现通过手机分享的单个视频url是无水印的
宝贝 看完了吗~ "夹子音 "高能夹子音 "夹子音变装 https://v.kuaishou.com/dPkfN2 复制此消息,打开【快手】直接观看!
所以抓包开干!
通过抓包分析可知
的返回了无水印视频
为data.visionVideoDetail.photo.photoUrl
提交数据如下
{"operationName":"visionVideoDetail","variables":{"photoId":"3xqmst68mjpue66","page":"detail"},"query":"query visionVideoDetail($photoId: String, $type: String, $page: String, $webPageArea: String) {\n visionVideoDetail(photoId: $photoId, type: $type, page: $page, webPageArea: $webPageArea) {\n status\n type\n author {\n id\n name\n following\n headerUrl\n __typename\n }\n photo {\n id\n duration\n caption\n likeCount\n realLikeCount\n coverUrl\n photoUrl\n liked\n timestamp\n expTag\n llsid\n viewCount\n videoRatio\n stereoType\n croppedPhotoUrl\n manifest {\n mediaType\n businessType\n version\n adaptationSet {\n id\n duration\n representation {\n id\n defaultSelect\n backupUrl\n codecs\n url\n height\n width\n avgBitrate\n maxBitrate\n m3u8Slice\n qualityType\n qualityLabel\n frameRate\n featureP2sp\n hidden\n disableAdaptive\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n tags {\n type\n name\n __typename\n }\n commentLimit {\n canAddComment\n __typename\n }\n llsid\n danmakuSwitch\n __typename\n }\n}\n"}
这里可以看到photoid是变化的,其他应该没有什么变化,photoid在我们之前的第一个页面可以看到,那我们可以先放下来,构造一个获取无水印的获取函数。
function fuckkuaishouvideo(id,index){
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url:"https://www.kuaishou.com/graphql",
method :"POST",
data:'{"operationName":"visionVideoDetail","variables":{"photoId":"'+id+'","page":"detail"},"query":"query visionVideoDetail($photoId: String, $type: String, $page: String, $webPageArea: String) {\\n visionVideoDetail(photoId: $photoId, type: $type, page: $page, webPageArea: $webPageArea) {\\n status\\n type\\n author {\\n id\\n name\\n following\\n headerUrl\\n __typename\\n }\\n photo {\\n id\\n duration\\n caption\\n likeCount\\n realLikeCount\\n coverUrl\\n photoUrl\\n liked\\n timestamp\\n expTag\\n llsid\\n viewCount\\n videoRatio\\n stereoType\\n croppedPhotoUrl\\n manifest {\\n mediaType\\n businessType\\n version\\n adaptationSet {\\n id\\n duration\\n representation {\\n id\\n defaultSelect\\n backupUrl\\n codecs\\n url\\n height\\n width\\n avgBitrate\\n maxBitrate\\n m3u8Slice\\n qualityType\\n qualityLabel\\n frameRate\\n featureP2sp\\n hidden\\n disableAdaptive\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n tags {\\n type\\n name\\n __typename\\n }\\n commentLimit {\\n canAddComment\\n __typename\\n }\\n llsid\\n danmakuSwitch\\n __typename\\n }\\n}\\n"}'
,headers: {
"Content-type": "application/json"
},
onload:function(xhr){
let obj=JSON.parse(xhr.responseText)
let res=obj.data.visionVideoDetail.photo
if(res===null){
console.log('失败的id"',id,index)
resolve('success')
}
downloadurl.push(res.photoUrl)
resolve('success')
}
});
})
}
这里需要注意的是如果我们把获取的数据直接放到xhr内\n会出现换行的情况,所以我们需要对这类字符进行转义,在记事本里把\n改成\\n,这样提交的时候会把\\n转义成\n。
function(data){
let list=JSON.parse(data)
console.log('获取了list',list)
let target=null
if(list.data.privateFeeds!==undefined){
target=list.data.privateFeeds
}
if(list.data.publicFeeds!==undefined){
target=list.data.publicFeeds
}
if(target!==null){
for(let index=0;index<target.list.length;index++){
if(target.list[index].id===null){
savelistid.push('null')
zhibo++
}
else{
if(target.list[index].imgUrls!==undefined&&target.list[index].imgUrls.length!==0){
for(let imgindex=0;imgindex<target.list[index].imgUrls.length;imgindex++){
imglist.push(target.list[index].imgUrls[imgindex])
}
savelistid.push('null')
}else{
savelistid.push(target.list[index].id)
}
}
}
}
let enddownload=document.querySelector('.enddownload')
console.log('zhibo',zhibo,savelistid.length,savelistid.length-zhibo)
if(enddownload!==null){
enddownload.innerHTML=''+(savelistid.length-zhibo)
}
resolve(data)
}
然后继续像知乎劫持一样,打一波fetch劫持,这里因为名字不同,但是内容是一致的,所以我先判断前缀部分是什么,然后再进行统一处理。
并将所有的id以及图片地址(获取列表的时候,图片会直接返回给我们)都保存起来。
然后写当我们点击按钮的时候的代码
async function StatToGetVideo(){
alert('已开始,不要重复点击!')
let imgnumber=0;
for(let index=0;index<savelistid.length;index++){
let id=savelistid[index]
let result='success'
if(id!=='null'){
result= await fuckkuaishouvideo(id,index)
}else{
imgnumber++;
}
if(result==='success'){
let startdownload=document.querySelector('.startdownload')
if(startdownload!==null){
startdownload.innerHTML=''+(index+1)
}
}
}
GM_setClipboard([...downloadurl,...imglist].join('\n'))
alert(savelistid.length+'个共成功'+(downloadurl.length+imgnumber)+'可能存在图片')
downloadurl=[]
}
代码也是非常简单的,就是通过同步promise,一个一个进行post获取无水印地址,然后存储到数组中,最后通过Gm_setClipboard设置到剪辑板上
...downloadurl是一个解构数组的运算符,join是将数组中每个元素拼接起来。然后清空下载数组。
那么快手批量下载的脚本就做完了
结语
撒花~