背景
這是一個(gè)大數(shù)據(jù)時(shí)代怖竭,有時(shí)候有些程序想要在網(wǎng)站上爬取主流網(wǎng)站的視頻信息并記錄下來(lái)做數(shù)據(jù)分析,甚至想要通過(guò)程序自動(dòng)拿到各大視頻網(wǎng)站的視頻資源抽米,這個(gè)時(shí)候就出現(xiàn)了很多去研究這個(gè)的程序員了~
我研究的主要是移動(dòng)端的視頻播放,所以接下來(lái)講的所有,都是和移動(dòng)端的頁(yè)面相關(guān)的狡孔。
知識(shí)儲(chǔ)備
- 首先我們要了解怎樣去抓取網(wǎng)頁(yè)信息,這個(gè)可以參考我前面的一篇文章Jsoup初探
- 之所以博主選擇騰訊視頻有兩個(gè)很重要的原因蜂嗽,第一苗膝,開(kāi)始說(shuō)到了我做的是移動(dòng)端,視頻播放格式一般是采用
m3u8
或者采用最簡(jiǎn)單的mp4
格式植旧,而m3u8一般作為直播的流媒體存儲(chǔ)格式辱揭,里面放的是若干個(gè)切片的ts文件离唐,大多數(shù)視頻供應(yīng)商(youku)在生成m3u8的時(shí)候就已經(jīng)把廣告的ts切片放在里面了,我們用程序剔出來(lái)還是比較麻煩的问窃,而mp4就比較好了亥鬓,最簡(jiǎn)單的格式,跨平臺(tái)性能好域庇,在Chrome上也可以調(diào)試嵌戈,而騰訊視頻在移動(dòng)端就是統(tǒng)一轉(zhuǎn)碼(H264)后的mp4文件,這一點(diǎn)相當(dāng)nice听皿,用起來(lái)也很是方便熟呛。第二呢,騰訊視頻會(huì)識(shí)別電腦操作系統(tǒng)尉姨,網(wǎng)頁(yè)的播放的時(shí)候不是無(wú)腦的直接上flash庵朝,這讓我的Mac不會(huì)燒機(jī)然我有了好感,所以又厉,就覺(jué)得去爬你了九府。
方案和過(guò)程
做過(guò)這方面工作的同志大概會(huì)了解,騰訊視頻在播放列表中給出的播放鏈接和真實(shí)播放鏈接是不同的覆致,進(jìn)入真實(shí)播放鏈接后爬取到的視頻源文件名和真實(shí)源文件名也是不同的侄旬,它是通過(guò)js中的方法訪問(wèn)某個(gè)servlet給出加密vkey才可以播放。
博客給大家做一個(gè)展示篷朵,決定爬取一下國(guó)產(chǎn)經(jīng)典葫蘆娃片子勾怒,將把重要步驟和代碼為大家展現(xiàn):
播放列表鏈接:葫蘆兄弟
評(píng)分還是杠杠的。我們的第一個(gè)目標(biāo)就是拿下這十三集的播放鏈接声旺。
看了我前面文章的都應(yīng)該會(huì)覺(jué)得so easy了笔链。
然而,調(diào)試了一下選擇器body > div:nth-child(2) > div.site_container.container_main > div.container_inner > div > div.wrapper_main > div._playsrc_series > span > div > div.mod_bd._episodes._bd_container > div > span > a
發(fā)現(xiàn)并獲取不到a標(biāo)簽的資源腮猖,那就很可能是動(dòng)態(tài)加載的列表了鉴扫,看一下網(wǎng)頁(yè)源代碼
果然是空的,那我們就去抓下包看看是哪個(gè)servlet獲取的
果然澈缺,這種都是比較好抓到的坪创,順便訪問(wèn)一下這個(gè)鏈接,看到了列表信息
那就直接去抓這個(gè)網(wǎng)頁(yè)姐赡,然后把鏈接拿下來(lái)好了莱预。
package com.videoqq.jsoup;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class BlogSpider {
public static void main(String[] args) {
try {
String requestUrl = "http://s.video.qq.com/get_playsource?id=2iqrhqekbtgwp1s&plat=2&type=1&data_type=2&video_type=3&plname=&otype=json&num_mod_cnt=20&callback=_jsonp_0_6914&_t=1474533056089";
URL obj = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) obj.openConnection();
conn.setReadTimeout(5000);
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
conn.addRequestProperty("User-Agent", "Mozilla");
conn.addRequestProperty("Referer", "google.com");
boolean redirect = false;
int status = conn.getResponseCode();
if (status != HttpURLConnection.HTTP_OK) {
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM
|| status == HttpURLConnection.HTTP_SEE_OTHER)
redirect = true;
}
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
//拿到整個(gè)html
StringBuffer html = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
in.close();
//分詞拿下所有URL
String[] param = html.toString().split("playUrl");
//第一個(gè)不含播放鏈接,舍棄
for (int i = 1; i < param.length; i++) {
param[i] = param[i].substring(3,63);
System.out.println(param[i]);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印鏈接看看:
隨便打開(kāi)一個(gè)鏈接项滑,以移動(dòng)端的模式打開(kāi)依沮,會(huì)發(fā)現(xiàn)跳轉(zhuǎn)了一個(gè)加密的鏈接,這就是真實(shí)的播放地址。
我打開(kāi)的是
http://v.qq.com/cover/2/2iqrhqekbtgwp1s.html?vid=c01350046ds
出現(xiàn)了這個(gè)鏈接
https://m.v.qq.com/x/cover/2/2iqrhqekbtgwp1s.html?vid=c01350046ds&ptag=v_qq_com%23v.play.adaptor%233
很幸運(yùn)這個(gè)兩個(gè)鏈接的區(qū)別并不是很大危喉,我們多打開(kāi)幾個(gè)會(huì)發(fā)現(xiàn)規(guī)律都一樣宋渔,就是把前綴改成了https://m.v.qq.com/x
,所以基本不用做什么處理辜限,但不是所有鏈接都是這樣的皇拣,就算變化比較大,也可能找到其中的規(guī)律薄嫡,一般是在js中文件關(guān)鍵字的位置做了個(gè)混淆氧急,仔細(xì)分析一下還是可以得到這個(gè)真實(shí)播放地址的。
https://m.v.qq.com/x/cover/2/2iqrhqekbtgwp1s.html?vid=c01350046ds
后面的參數(shù)可以不管了岂座。
審查這個(gè)頁(yè)面态蒂,很容易會(huì)發(fā)現(xiàn)該視頻的播放地址。
在打開(kāi)這個(gè)鏈接也是有效的费什。
但不能開(kāi)心的太早,這并不是記錄在靜態(tài)頁(yè)面中的手素≡е罚看看靜態(tài)頁(yè)面,視頻源一直是
http://183.61.13.21/vhot2.qqvideo.tc.qq.com/w01243snzdg.m701.mp4?vkey=C60965F33D298C4C8FB5971288084B312AAAE216717CB5CF9EB857647A4719794B79AFBDABA7FB67&br=29&platform=2&fmt=msd&level=3&sdtfrom=v5010
這說(shuō)明我們還需要找到它的加載地址泉懦,也是通過(guò)某個(gè)servlet來(lái)傳過(guò)來(lái)的稿黍,也就是需要去抓包看看到底哪個(gè)請(qǐng)求做了這個(gè)事呢,并且這個(gè)請(qǐng)求歸根到底就是刷新了地址的vkey崩哩,獲取到更新后的vkey,后面的參數(shù)都是無(wú)關(guān)緊要的巡球。
然而,你在這個(gè)頁(yè)面抓取所有的包都找不到一個(gè)可以去post vid而獲取key的servlet邓嘹,這就是大家止步不前的地方了酣栈。中間有很多嘗試的方法,最后都失敗了汹押,講講我最后的方案矿筝。
騰訊把所有mp4播放的入口都給加密的很深,我就去嘗試看看大眾版棚贾,就是flash的swf文件窖维,例如訪問(wèn)
http://static.video.qq.com/TPout.swf?vid=c01350046ds&auto=0
,這個(gè)通用版只是vid改一下就可以,其他的都不變就可以訪問(wèn)swf的文件妙痹,然后嘗試抓包(因?yàn)槭莝wf铸史,只能在pc模式下抓),結(jié)果還真抓到個(gè)有用的東西
多么美好的結(jié)果.
我們分析一下這個(gè)request不難發(fā)現(xiàn)里面有&vid=c01350046ds
怯伊,是不是所有與這個(gè)id相同的三處換成其他視頻的vid也是可以的呢琳轿?
嘗試了一下并沒(méi)有得到想要的結(jié)果,畢竟傳了這么一大堆參數(shù),特別看到那個(gè)奇怪的文件名后面加了一堆的轉(zhuǎn)譯字符利赋,同樣我們繼續(xù)抓包看到一個(gè)
http://vv.video.qq.com/getvinfo?guid=9A37E7B5F6FF9CD2E7A35CE8E0BE7F21AAAF5423&ran=0%2E5591048267669976&platform=70902&speed=0&charge=0&newplatform=70902&vids=c01350046ds&fp2p=1&pid=70A01B432FFB78BBF0D0A26654A3756B8A7E98A9&defnpayver=1&cKey=ildvKc3Qw4McaCUlxHmeaflPX2h%5Fk8qyDOH2Js0OMgqPBp3KkkWMxH4h8p89Jm3fQsF0xWNnyk6cYm48RFTCfvgBj1tRdVTvcS3MMSM8MKyPBmRRJBnA2nYhNSvx0WwcfXIFZ7wZunSSUTVtsuMiJkARqxZDGV1VqBGgJfAS7nES%5FMBAveqW0Lq5ebdszHJFfLvVR6VfNYg5EFRf&otype=xml&encryptVer=5%2E4&dtype=3&utype=%2D1&ip=59%2E53%2E183%2E83&linkver=2&appver=3%2E2%2E19%2E358&fhdswitch=0&vid=c01350046ds&ehost=
同樣里面也帶有vkey參數(shù)水评,按照上面的方法也試一下看。結(jié)果也是讓人挺失望的媚送。并不能拿到它的值中燥。
最后,又發(fā)現(xiàn)了一個(gè)通用版的訪問(wèn)鏈接塘偎,在iPhone6模式下去抓包疗涉,發(fā)現(xiàn)一個(gè)同樣傳輸vkey的request,過(guò)程和上面差不多就不贅述了。
URL:
http://v.qq.com/iframe/player.html?vid=c01350046ds&tiny=0&auto=0
經(jīng)測(cè)試發(fā)現(xiàn)吟秩,只要修改request中的vids就都可以拿到一個(gè)一段時(shí)長(zhǎng)有效的vkey.所以呢咱扣,想要程序自動(dòng)爬取這些播放源文件就不是難事了。
package com.videoqq.jsoup;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import com.asiainfo.util.DateFormatUtil;
public class BlogSpider {
public static void main(String[] args) {
try {
String requestUrl = "http://s.video.qq.com/get_playsource?id=2iqrhqekbtgwp1s&plat=2&type=1&data_type=2&video_type=3&plname=&otype=json&num_mod_cnt=20&callback=_jsonp_0_6914&_t=1474533056089";
URL obj = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) obj.openConnection();
conn.setReadTimeout(5000);
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
conn.addRequestProperty("User-Agent", "Mozilla");
conn.addRequestProperty("Referer", "google.com");
boolean redirect = false;
int status = conn.getResponseCode();
if (status != HttpURLConnection.HTTP_OK) {
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM
|| status == HttpURLConnection.HTTP_SEE_OTHER)
redirect = true;
}
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
// 拿到整個(gè)html
StringBuffer html = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
in.close();
// 分詞拿下所有URL
String[] param = html.toString().split("playUrl");
String[] vid = new String[param.length];
// 第一個(gè)不含播放鏈接涵防,舍棄
for (int i = 1; i < param.length; i++) {
param[i] = param[i].substring(3, 63);
// System.out.println(param[i]);
vid[i] = param[i].substring(49);
// System.out.println(vid[i]);
}
// 真正播放地址
for (int i = 1; i < vid.length; i++) {
param[i] = "https://m.v.qq.com/x" + param[i].substring(15);
// System.out.println(param[i]);
}
// 手動(dòng)拿到的播放源,丟棄除vkey以外的參數(shù)
String sourceMp4 = "http://36.250.4.15/vhot2.qqvideo.tc.qq.com/y0135k2wa6w.mp4?"
+ "vkey=0AA1C91314C9B82107F68C14832A8C9C2061397B3D0A7FE65D97AD7A8981849864FD71B933E290D3B737BB040F7DDAB570A057A1EA836F5DE95ADBA6D6A098274B8460F9261CB01B904BF2DAFC6012362B8AFF2443D00D50";
// 首次需要手動(dòng)拿到每個(gè)視頻的播放源文件地址闹伪,因?yàn)榇鎯?chǔ)沒(méi)有規(guī)律,騰訊是分散服務(wù)器存的壮池,我這邊只演示一個(gè)
String[] sourceMp4Group = { sourceMp4 };
for (int i = 0; i < sourceMp4Group.length; i++) {
// 執(zhí)行更新替換vkey
requestUrl = "http://h5vv.video.qq.com/getinfo?callback=tvp_request_getinfo_callback_340380&platform=11001&charge=0&otype=json&ehost=http%3A%2F%2Fv.qq.com&sphls=0&sb=1&nocache=0&_rnd=1474896074003&"
+ "vids=" + vid[i + 1]
+ "&defaultfmt=auto&&_qv_rmt=CTWS8OLbA17867igt=&_qv_rmt2=x6oMojAw144904luQ=&sdtfrom=v3010&callback=tvp_request_getinfo_callback_"
+ DateFormatUtil.get6num();
obj = new URL(requestUrl);
conn = (HttpURLConnection) obj.openConnection();
conn.setReadTimeout(5000);
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
conn.addRequestProperty("User-Agent", "Mozilla");
conn.addRequestProperty("Referer", "google.com");
redirect = false;
status = conn.getResponseCode();
if (status != HttpURLConnection.HTTP_OK) {
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM
|| status == HttpURLConnection.HTTP_SEE_OTHER)
redirect = true;
}
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
html = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
in.close();
param = html.toString().split(",");
// for (int j = 0; j < param.length; j++) {
// System.out.println(param[j]);
// }
String fvkey = param[38].split(":")[1];
fvkey = fvkey.substring(1, fvkey.length() - 1);
sourceMp4 += "?vkey=" + fvkey;
System.out.println(sourceMp4);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
總結(jié)
因?yàn)槊總€(gè)視頻的播放源文件存儲(chǔ)在的服務(wù)器地址不一樣(同一系列視頻也是)偏瓤,所以需要第一手動(dòng)拿一下播放mp4地址,審查一下元素就可以椰憋,很快厅克。
-
我寫了一個(gè)每五分鐘訪問(wèn)鏈接測(cè)試vkey有效時(shí)常的告警,結(jié)果發(fā)現(xiàn)差不多六小時(shí)才會(huì)失效橙依,所以設(shè)個(gè)TimeTask一般四小時(shí)刷新沒(méi)問(wèn)題的证舟。
import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.TimerTask; public class DeadTimeMonitorTask extends TimerTask { public void doVisit() { try { String url = "http://36.250.4.15/vhot2.qqvideo.tc.qq.com/y0135k2wa6w.mp4?vkey=0AA1C91314C9B82107F68C14832A8C9C2061397B3D0A7FE65D97AD7A8981849864FD71B933E290D3B737BB040F7DDAB570A057A1EA836F5DE95ADBA6D6A098274B8460F9261CB01B904BF2DAFC6012362B8AFF2443D00D50?vkey=A812D55B7E0FD9E40737D9C8E245C50A52B28D5F23F6036E28E275A644D88A4E433EC673B867AC25452CC3ACBAED1452144FA57890541CB63C250F7A52F9675BC8CABA9588EEFEA53AAA0854CB6AFF3A4ABB57CE06935DF8"; URL obj = new URL(url); HttpURLConnection conn = (HttpURLConnection) obj.openConnection(); conn.setReadTimeout(5000); conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8"); conn.addRequestProperty("User-Agent", "Mozilla"); conn.addRequestProperty("Referer", "google.com"); System.out.println("Request URL ... " + url); boolean redirect = false; int status = conn.getResponseCode(); if (status != HttpURLConnection.HTTP_OK) { if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER) redirect = true; } System.out.println("Response Code ... " + status); if (status == 403) { SendMail.sendMailfunc("檢測(cè)失效時(shí)間:" + DateFormatUtil.getCurrentTime(), "失效結(jié)束時(shí)間:" + DateFormatUtil.getCurrentTime(), "wentao_wanna@126.com"); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { //doVisit(); } @Override public void run() { doVisit(); } }
知其然還需要知其所以然,大家可以去探究一下獲取vkey的request參數(shù)窗骑,經(jīng)發(fā)現(xiàn)是這個(gè)
js
https://vm.gtimg.cn/tencentvideo_v1/tvp/js/tvp.player_v2.js
放到解密js的網(wǎng)站美化一下女责,然后搜索參數(shù)關(guān)鍵詞,你會(huì)看懂的~
- 此文章只用于大家技術(shù)研究慧域,請(qǐng)不要用于違法的地方鲤竹,轉(zhuǎn)載請(qǐng)聯(lián)系我!昔榴!