一、效果图
动态页(这个刷屏的是谁啊)
视频页(嵌入iframe):
播放页:
二、一些说明
为什么做了这个?
很早的时候,在高中就对破解很有兴趣,大学开始学习相关知识。其中最想做的就是把噼哩噼哩的客户端改了。天天给你推一些卖肉,标题党这些垃圾东西,属实让我气的慌。当时就一直想,自己整一个客户端,屏蔽掉自己不想要的东西。
思路1:通过网络代理实现
其实一开始我的思路是这样的,不就是要去掉那些广告信息吗,我参考通过酸酸乳实现的广告过滤,我想,只要手机设置一个代理,让数据全部通过代理。由服务端对数据进行识别,把过滤后的数据返回给手机,这样就能实现了。
但是因为某些网络安全的原因,这方面的资料特别少,我甚至连关键词都不知道,最后没有实现,但是到目前为止,我觉得这个方案是最优雅的,也是最好的,就是一直没找到学习资料和实现思路,有大佬知道的告诉我一些。
思路2: 直接修改客户端
修改bilibili客户端,hook相关方法,在获取数据的时候进行过滤判断。虽然定制性是相当的高,但是,技术门槛也相当的高。又要反编译。又要写模块hook,手机也不支持root。没办法,只能放弃了。
思路3:自己制作客户端
因为客户端是自己制作的,所以想要什么就展示什么。我最终就采用这种方式。
思路很简答: 抓到数据,展示到自己的客户端上。这里选择用html作为客户端。写起来方便
三、难点
针对第三种思路,就是抓数据,展示到客户端。有以下几个难点:
数据从哪来?
抓包。我本次采用的是抓取bilibili移动端网页和pc端网页的接口。
根据我的经验,抓包难度一般是这样的:
网页< ios < android.
网页好抓,因为代码是js,直接运行在你的浏览器上。f12就可以看见发送了哪些请求,可以直接看到响应头和请求头。
其次是ios,为什么呢?我也不太清除,我在ios12和 android上分包抓包过,感觉android的监测严格很多。发现你使用了代理也就是维批嗯 来抓包后,它就不发数据了。但是在ios上,监测没那么严格,可能是使用人数少?
最后是android端,随着root权限越来越不开放,抓包难度大了许多。另外就是,大家都知道android定制性高,容易做黑产,所以检测都比较严格。
综上所述,最后从网页上抓取数据。
我分别在移动端和pc端网页抓取了数据。因为,移动端没有动态页面,而pc端又不好内嵌。反正两者互相辅助。如果接口不够首先我会选择去ios端继续抓取接口。
怎么请求接口获得数据?
最核心的一点就是cookie,bilibili的网页都是通过cookie来鉴定用户的,抓包的时候可以看到有很多请求头,有些是不重要的,比如 accept这种,可以去掉,有些是比较重要的,比如 Origin,Referer,Cookie,User-Agent,Host 这几个都是需要的。
因为为了防止爬虫,现在很多网站都会对这些进行分析,一旦判断你是程序,就会禁止你的ip访问。
需要注意的是,每个接口的 origin可能不同,需要单独配置。另外cookie也可能不太一样。android端限制的比较严格。
请求时的跨域问题
虽然接口拿到了,在postman上测试也是通过的,可是一旦你在浏览器上发送请求,就会出现跨域。
在平时的开发中,都是由后端给我们配置允许跨域,比如Spring 就是在Mvc配置中编写允许跨域。
但是,现在我们是拿着别人的端口用,不可能做到让哔哩哔哩的服务器允许我们跨域发送请求。
我昨天试了比较多的办法,都没能成功,最后看了网上一篇回到豁然开悟。跨域是浏览器限制,只要越过浏览器,不就可以实现跨域了吗?
于是我打算通过代理接口的方式进行:
客户端请求我自己编写的服务端接口,由服务端向bilibili请求数据,然后将数据下发给客户端。
好处是:
我自己编写的服务端,肯定是允许我自己跨域。
cookie等敏感数据,可以直接托管到服务端,客户端请求时不需要发送cookie。
因为是服务端发送请求,越过了浏览器,想请求什么接口就请求什么接口,不需要面对跨域问题。而客户端访问自己的服务端,自然是允许跨域,所以这个跨域问题,就通过代理解决了。
服务端的编写
其实比较简单,就是写一个接口供客户端调用。调用时服务端自己用什么工具去发请求。请求结束后把数据解析出来,直接返回给客户端。
简单是因为,现在用的都是json字符串,返回一个字符串遇到的问题相当的少。调整下编码就可以了
客户端的制作
客户端只负责解析和展示数据,那么客户端需要展示什么页面呢?
目前我只制作了动态页面,总体页面预计有:
- 推荐页
- 动态页
- 搜索页
- 登录页
- 播放页
其中,播放页可以直接用iframe嵌入。因为播放页面是比较复杂,要获取播放接口并设置一个播放器比较麻烦。
推荐页自己制作,这个也是这个客户端最大的意义,就是过滤自己不想要的东西。
搜索页面自己制作,并且调用的数据接口必须是 android或者 ios或者pc网页,不能是移动端网页。因为移动端网页能搜到的数据比较少,估计是?移动端访问的人都是一些游客?
登录页,打算使用bilibili自己的网页。因为登录时有个人机验证,这个做起来是比较麻烦的,要越过bilibili的限制不是那么容易
iframe注入js(难点)
上面我们的播放页和登录页面,都是使用iframe嵌入网页。但是这样一来就只能看,不能动里面的内容。
比如播放页下面会有一堆的推荐,我想去掉。又比如登录之后,我想获得cookie,这些都需要通过js脚本。
但是,万恶的浏览器不允许对非同域名下的iframe网页进行js操作。估计是为了防止攻击。比如你访问一个博客,它偷偷嵌入了一个京东的网页,如果你登录过,显然iframe中会有你的信息。只要操作ifram,就可以获得你的cookie信息,非常的不安全。
但是为了达成我们的目的,必须要往iframe中嵌入js,怎么办呢?
对于android的webview,这个在一定程度上可以实现。我对webview使用的比较少,但是看过类型的app。登录百度网页,然后app就可以获得百度的相关token信息。在第三方贴吧类app上常见。
既然使用的html客户端,那就绕不过浏览器的限制了。我的思路是这样的,直接嵌入一个script标签,然后从自己的服务器上下载js脚本。
具体代码是这样的:(伪代码示例)
let iframeChildNode = document.getElementByClassName('xxx')
let child = document.createNode("script")
child.setAttr("src","js地址")
iframeChildNode.appendChild(child)
思路就是:找到一个iframe中的节点,然后往里添加一个子标签script,然后浏览器会自动下载子标签,从而js脚本就注入到iframe中了
比较麻烦的是,有时候找不到iframe中的子元素。
另一方面,可以通过浏览器来强行注入js脚本,类型greasyfork。现在的不少手机浏览器也支持添加js脚本了。
四、代码实现
只提供了动态页面的代码实现(其他页面没做完),服务端也没啥好说的。大家自己简单发个请求就可以。
动态页面的接口是:
//最新动态
https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=你的uid&type_list=xxxx&from=weball&platform=web
//请求头如下:
'Host': 'api.vc.bilibili.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0',
'Origin': ' https://t.bilibili.com',
'Connection': 'keep-alive',
'Referer': 'https://t.bilibili.com/',
'Cookie': cookie,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-site',
'TE': 'trailers'
//历史动态
https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=你的uid&offset_dynamic_id=动态偏移
动态偏移就是你最后一条动态的id,它根据这个id计算出需要发送的历史动态是哪些
//请求头同上
页面代码实现如下:
(因为bilibili动态有很多种类型的卡片,每个卡片需要单独适配,比较麻烦,需要自己甄别接口返回的数据是什么类型的)
<template>
<div class="container">
<div class="dongtai-list">
<div class="dongtai-item" v-for="card in cards" :key="card.dynamic_id_str">
<div class="header">
{{card.desc.user_profile.info.uname}}
</div>
<div class="timediff">{{calcDiffTime(card.desc.timestamp)}}</div>
<!--视频卡片-->
<div class=" videcard" v-if="card.card.pic" @click="toVideo(card.desc.bvid)">
<div class="dynamic" v-if="card.card.dynamic">
{{card.card.dynamic}}
</div>
<div class="content">
<div class="left">
<img referrer="no-referrer|origin|unsafe-url" v-if="card.card.pic" :src="card.card.pic"/>
</div>
<div class="right">
<div class="video-title">
{{card.card.title}}
</div>
<div class="video-info" v-if="card.card.stat">
<span class="iconfont icon-bofang"> {{card.card.stat.view}}</span>
<span class="iconfont icon-danmu">{{card.card.stat.danmaku}}</span>
</div>
</div>
</div>
</div>
<!--直播卡片-->
<div class="content livecard" v-if="card.card.live_play_info">
<div class="left">
<img referrer="no-referrer|origin|unsafe-url" v-if="card.card.live_play_info"
:src="card.card.live_play_info.cover"/>
</div>
<div class="right">
<div class="video-title">
{{card.card.live_play_info.title}}
</div>
<div class="video-info">
<span class="iconfont icon-bofang">{{card.card.live_play_info.watched_show}}</span>
</div>
</div>
</div>
<!--转发动态卡片-->
<div class="replaycard" v-if="card.card.origin_user">
<div class="dynamic">
{{card.card.item.content}}
</div>
<div class="content">
<!--card.card.origin_user-->
<div class="originvidecard" @click="toVideo(card.desc.origin.bvid)">
<div class="header">
{{card.card.origin_user.info.uname}}
</div>
<div class="dynamic">
{{toObject(card.card.origin).dynamic}}
</div>
<div class="content">
<div class="left">
<img referrer="no-referrer|origin|unsafe-url"
:src="toObject(card.card.origin).pic"/>
</div>
<div class="right">
<div class="video-title">
{{toObject(card.card.origin).title}}
</div>
<div class="video-info" v-if="toObject(card.card.origin).stat">
<span class="iconfont icon-bofang"> {{toObject(card.card.origin).stat.view}}</span>
<span class="iconfont icon-danmu">{{toObject(card.card.origin).stat.danmaku}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!--自己发动态的卡片-->
<div class="mydongtaicard" v-if="card.card.item && card.card.item.description">
<div class="dynamic">
{{card.card.item.description}}
</div>
<div class="picture-list" v-if="card.card.item.pictures">
<div class="picture-item" v-for="pic in card.card.item.pictures" :key="pic.img_src">
<img :src="pic.img_src" style="width: 100px;height: 100px;">
</div>
</div>
</div>
<!--纯文字自己发的动态-->
<div class="mydongtaicard" v-if="card.card.item && card.card.item.content">
<div class="dynamic">
{{card.card.item.content}}
</div>
</div>
<!--音乐卡片-->
<div class="musiccard" v-if="card.card.intro" >
<div class="dynamic" >
{{card.card.intro}}
</div>
<div class="content">
<div class="left">
<img referrer="no-referrer|origin|unsafe-url" v-if="card.card.cover" :src="card.card.cover"/>
</div>
</div>
</div>
<!--文章卡片-->
<div class="articlecard" v-if="card.card.summary" >
<div class="dynamic" >
{{card.card.title}}
</div>
<div class="content">
<img style="width: 100%" referrer="no-referrer|origin|unsafe-url" v-if="card.card.origin_image_urls" :src="card.card.origin_image_urls[0]"/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import api from '../../api/index'
import moment from "moment"
export default {
name: "Home",
data() {
return {
//动态列表数据
cards: [],
//是否在底部
isBottom:false,
//定时器id
tId:''
}
},
mounted() {
this.getDongTai()
window.onscroll = ()=>{
//变量scrollTop是滚动条滚动时,距离顶部的距离
var scrollTop = document.documentElement.scrollTop||document.body.scrollTop;
//变量windowHeight是可视区的高度
var windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
//变量scrollHeight是滚动条的总高度
var scrollHeight = document.documentElement.scrollHeight||document.body.scrollHeight;
//滚动条到底部的条件
if(scrollTop+windowHeight>=(scrollHeight-20)){
//写后台加载数据的函数
console.log("距顶部"+scrollTop+"可视区高度"+windowHeight+"滚动条总高度"+scrollHeight);
//加载历史动态数据
clearTimeout(this.tId);
this.tId=setTimeout(()=>{
this.getHistoryVideo()
},300)
}
}
},
methods: {
/**
* 获取动态数据
*/
async getDongTai() {
let result = await api.getDongTaiList();
this.cards = result.data.cards
for (let i = 0; i < this.cards.length; i++) {
let tempcard = this.cards[i]
tempcard.card = this.toObject(tempcard.card)
}
},
/**
* 计算距离现在的时间
* @param timestamp
*/
calcDiffTime(timestamp) {
let cardTime = moment.unix(timestamp).format();
let diffMinutes = moment().diff(cardTime, "minutes")
if (diffMinutes > 60) {
let diffHours = moment().diff(cardTime, "hours")
if (diffHours > 24) {
let diffDays = moment().diff(cardTime, "days")
return diffDays + "天前"
} else {
return diffHours + "小时前"
}
} else {
return diffMinutes + "分钟前"
}
},
/**
* 将json转换为对象
* @param jsonstr
*/
toObject(jsonstr) {
return JSON.parse(jsonstr)
},
/**
* 跳转到视频播放界面
*/
toVideo(bvid){
console.log(bvid)
this.$router.push({
path: '/video',
query: {bvid: bvid}
})
},
/**
* 滚动到底部了,此时获取历史动态
*/
async getHistoryVideo(){
console.log("发送历史请求")
let lastCard = this.cards[this.cards.length-1]
let result = await api.getHistoryDongTaiList(lastCard.desc.dynamic_id_str);
result = result.data.cards
for (let i = 0; i < result.length; i++) {
let tempcard = result[i]
tempcard.card = this.toObject(tempcard.card)
}
this.cards.push(...result)
},
log(obj) {
console.log('log:')
console.log(obj)
}
}
}
</script>
<style scoped>
.container {
padding: 10px 20px;
}
.dongtai-item {
margin-bottom: 20px;
}
.content {
display: flex;
flex-direction: row;
}
.header {
font-size: 18px;
padding: 10px 0;
}
.dynamic {
padding: 10px 0;
}
.content .left {
flex: 2;
}
.left img {
width: 150px;
height: 100px;
}
.timediff {
color: #99a2aa;
padding-bottom: 5px;
}
.right {
position: relative;
margin-left: 5px;
flex: 3;
}
.right .video-title {
font-size: 16px;
margin-left: 10px;
}
.right .video-info {
position: absolute;
bottom: 5px;
}
.right .video-info span {
padding-left: 10px;
}
.replaycard .content {
background: #f4f5f7;
}
</style>