如何獲取鬥魚直播間的彈幕信息?

如何獲取如圖紅色框中的彈幕信息呢?想做一些基於彈幕信息的二次開發。


Update 20160609 : 更新Python客戶端,修復由於鬥魚網頁版面修改帶來的小問題,直接開啟海量彈幕模式(請大家不要問我為什麼端午節這一天為什麼閑著沒事更新代碼,這個真的和情人節是同一個原因). GitHub - twocucao/danmu.fm: douyutv danmu 鬥魚TV 彈幕助手

Update 20160220 : 更新Python客戶端,增加直播視頻的Live獲取,以及Mac平台下面的Mplayer的視頻播放.代碼均放在Github上面. GitHub - twocucao/danmu.fm: douyutv danmu 鬥魚TV 彈幕助手
Update 20160214 : 更新Python和Ruby客戶端(請大家不要問我為什麼情人節這一天為什麼閑著沒事更新代碼)

由於zhihu沒有法子貼動態圖,那隻好移步到我的博客一看了.(看博客之前記得點贊╮(╯_╰)╭)

Python程序員如何優雅的看鬥魚TV

===================優雅的看鬥魚TV的分割線==================================

-1.如果不想看長文,直接使用.則在安裝好Python3或者Ruby2.0以上版本.


#安裝Python客戶端
pip3 install danmu.fm

# 比如主播的直播間
danmu.fm http://www.douyutv.com/16789
#或者
danmu.fm 16789
#安裝Ruby客戶端
gem install danmu
#使用
danmu douyu [room_id/url]
#比如
danmu douyu qiuri
danmu douyu http://www.douyutv.com/13861

就可以看到如下結果咯

0.前言

前幾天(寒假前咯)閑著無聊,看到舍友們都在看鬥魚TV,雖然我對那些網路遊戲都不是非常感興趣,但是我突然間想到,如果我可以獲取上面的彈幕內容,不就有點意思了么?

1.分析階段

如果我想要抓取網頁上面的東西,無非就是兩種方法

  1. 使用瀏覽器,手工(自己點擊)或者非手工(使用JS腳本),存取我想要的東西。
  2. 編寫HTTP客戶端(鬥魚無HTTPS通訊)

第一種方法是萬能的,但顯然是不行的, 原因如下:

  • 手動保存實在是不可行,程序員不為也。
  • 瀏覽器與本地交互有限,換而言之,也就是即使我抓取了對應的彈幕,我也沒有辦法解決持久化的問題。
  • 假設你選擇的是Chrome或者firefox瀏覽器,也不是不能實現持久化,但這需要寫擴展,Chrome擴展沒有寫過,也不是很感興趣。

第二種方法顯然是一個正常的程序員的做法。

語言選用Ruby

寫一個客戶端,也就是寫一個小爬蟲,使用的場景:

用戶在終端執行命令

gem install danmu
danmu douyu [room_id/url]
#比如
danmu douyu qiuri
danmu douyu http://www.douyutv.com/13861

然後就可以在終端欣賞彈幕咯.

回想一下抓取網站的方法

四步走:請求網頁(原始數據) - 提取數據(提純數據) - 保存數據 - 分析數據

很顯然,只要解決了請求網頁,其他的也就無非解析和SQL語句什麼的。

1.1.鬥魚TV彈幕抓取的思路確定

如果是像我上面說的那麼簡單,也就不必再寫一篇文章。畢竟,網頁小爬蟲沒有什麼技術含量。分散式爬蟲才有。

通常情況下的網頁小爬蟲無非要解決如下問題:

請求,如果對方有一定策略的反爬蟲,那需要反反爬蟲。比如,

  • header帶上host,帶上refer,帶上其他
  • 需要驗證,那就申請用戶名和密碼,然後登陸
  • 如果在登錄時期有防跨站機制,那就先獲取一次登錄頁面,然後解析出token,帶上對應的token然後登陸。
  • 在程序中加入Log,並且存到本地。防止出現各種各樣的反爬蟲機制ban掉了程序,從而方便進行下一步防反爬蟲對策。

並且,由於請求響應機制的存在,通常情況下,每一個請求對應一個響應,如果出錯了,要麼超時,要麼有狀態碼,所以普通的web爬蟲也相對而言比較容易些。

那麼,鬥魚TV的站點是不是這樣子的就能夠容易爬取呢?

你猜到了,答案是「不是」。

由於彈幕具有實時性,就決定了鬥魚TV的彈幕無法通過保存完整指定時間端彈幕的XML(比如BILIBILI的一個視頻彈幕是存在一段xml中的)或者Json數據來顯示彈幕。要不然的話,那主播操作很出色的時候,觀眾的彈幕豈不是無法實時顯示了么?

那麼,肯定就是WebSocket了,於是,我一如既往的打開F12,查看網路流量。

正如你想到的那樣,沒有任何的彈幕流量來往。一個WebSocket的消息都沒有。

那麼,消息肯定是有的,但是消息並不是通過HTTP協議或者WebSocket協議傳輸的,那麼問題會出在哪呢?

分析前端的代碼,找出獲取彈幕的JS代碼,苦於代碼太多,找了很久沒有找到。那也就是執行邏輯可能在flash裡面。

於是祭出大殺器WireShark,抓一下流量。終於看到彈幕的樣子了。

是這樣的。

原來使用的是Flash的Socket功能。

那麼,我們只需要模擬Socket的每一條消息就好了.

多分析幾組數據,但還是對發送消息內容缺乏把握,特別是在用戶認證,用戶接收彈幕這一塊。在搜索引擎上搜索了一陣,發現知乎上有個帖子,讀完終於解了我的疑惑。

地址為: 如何獲取鬥魚直播間的彈幕信息? - Python

在此基礎上,省略若干消息分析過程。

總結後得出鬥魚TV網站的伺服器分布。

1.2.房間信息和彈幕認證伺服器獲取

首先我們拿隨便一個主播房間來說,比如,qiuri

Ta的房間鏈接分為兩種

  • 直播互動贏點卡 暴雪遊戲鬧新春
  • http://www.douyutv.com/id][房間

對這個主播房間頁面請求,正常,所有的有用信息都不是放在HTML中渲染出來,而是有一條放在HTML中內置的JS腳本中,這是為了減少伺服器渲染HTML的壓力?可是渲染放在JS裡面不也一樣需要渲染?(不明白)總之,就是程序先載入沒有具體數據填充頁面,然後JS更新數據。

內置的兩段JS腳本,JS腳本中有兩個變數,該變數很容易轉換成JSON數據,也就是兩段JSON數據,一個是關於主播的個人信息,另一個是關於彈幕認證伺服器的列表(該列表中的任意一個伺服器均可以認證,但每一次請求主播頁面得到的認證伺服器列表都不一樣)

通過這步,我們就拿到了主播的信息以及彈幕伺服器的認證地址,埠。

1.3.發送Socket消息的流程簡介

我們通過抓包,分析那一大坨數據包,可以確定以下通過以下的流程便可以獲取彈幕消息。(分析過程比較繁瑣)

首先建立兩個Socket。一個用於認證(@danmu_auth_socket),另一個用戶獲取彈幕(@danmu_client)。

  • 步驟1: @danmu_auth_socket 發送消息登陸,獲取消息1解析出匿名用戶的用戶名,再獲取消息2解析出gid
  • 步驟2: @danmu_auth_socket 發送qrl消息,獲取兩個沒有什麼用的消息
  • 步驟3: @danmu_auth_socket 發送keeplive消息
  • 步驟4: @danmu_socket 發送偽登陸消息(所有匿名用戶都一樣只需要輸入步驟一中用戶名就行了,因為認證已經在上面做過了)
  • 步驟5: @danmu_socket 發送join_group消息需要步驟一中國的gid
  • 步驟6: @danmu_socket 不斷的recv消息就可以獲取彈幕消息了

後面會詳細解釋

2.1.消息Socket消息格式以及發送一條消息

既然是發消息,那麼每條消息總是有些格式的。

鬥魚的消息格式大致如下:

每一條消息並遵循下面的格式:

1.通信協議長度,後四個部分的長度,四個位元組
2.第二部分與第一部分一樣
3.請求代碼,發送給鬥魚的話,內容為0xb1,0x02, 鬥魚返回的代碼為0xb2,0x02
4.發送內容
5.末尾位元組

# -*- encoding : utf-8 -*-
class Message
# 向鬥魚發送的消息
# 1.通信協議長度,後四個部分的長度,四個位元組
# 2.第二部分與第一部分一樣
# 3.請求代碼,發送給鬥魚的話,內容為0xb1,0x02, 鬥魚返回的代碼為0xb2,0x02
# 4.發送內容
# 5.末尾位元組
#pack("c*")是位元組數組轉字元串的一種詭異的轉化方式
def initialize(content)
@length = [content.size + 9,0x00,0x00,0x00].pack("c*")
@code = @length.dup
@magic = [0xb1,0x02,0x00,0x00].pack("c*")
@content = content
@end = [0x00].pack("c*")
end

def to_s
@length + @code + @magic + @content + @end
end

end

經過封裝,我們僅僅關注那些可見的字元串,也就是Content部分就可以了。
content部分,也就是發送消息的內容,在文章後面將會詳解。

開啟兩個Socket,一個用戶認證,另一個用於彈幕的獲取。

用於用戶彈幕認證的,是2.1中所說的認證伺服器列表中任意一個。挑選出來一組ip和埠

@danmu_auth_socket = TCPSocket.new @auth_dst_ip,@auth_dst_port

用戶獲取彈幕的只要為

danmu.douyutv.com:8601
danmu.douyutv.com:8602
danmu.douyutv.com:12601
danmu.douyutv.com:12602

四組域名:埠均可以作為如下的DANMU_SERVER和PORT

@danmu_socket = TCPSocket.new DANMU_SERVER,DANMU_PORT

發送一條消息只需如此

data = "type@=loginreq/username@="+@username+"/password@=1234567890123456/roomid@=" + @room_id.to_s + "/"
all_data = message(data)
@danmu_socket.write all_data

把需要傳輸的字元串放進去就好了.

接下來,我們需處理上面說的六個步驟

2.2.發送消息詳細流程之步驟一

發送消息內容為:

type@=loginreq/username@=/ct@=0/password@=/roomid@=156277/devid@=DF9E4515E0EE766B39F8D8A2E928BB7C/rt@=1453795822/vk@=4fc6e613fc650a058757331ed6c8a619/ver@=20150929/

我們需要注意的內容如下:

type 表示消息的類型登陸消息為loginreq
username 不需要,請求登陸以後系統會自動的返回對應的遊客賬號。
ct 不清楚什麼意思,默認為0並無影響
password 不需要
roomid 房間的id
devid 為設備標識,無所謂,所以我們使用隨機的UUID生成
rt 應該是runtime吧,時間戳
vk 為時間戳+"7oE9nPEG9xXV69phU31FYCLUagKeYtsF"+devid的字元串拼接結果的MD5值(這個是參考了一篇文章,關於這一處我也不大明白怎麼探究出來的)
ver 默認

通過這一步,我們可以獲取兩條消息,並從消息中使用正則表達式獲取對應的用戶名以及gid

str = @danmu_auth_socket.recv(4000)
@username= str[//username@=(.+)/nickname/,1]
str = @danmu_auth_socket.recv(4000)
@gid = str[//gid@=(d+)//,1]

2.3.發送消息詳細流程之步驟二

發送的消息內容為

"type@=qrl/rid@=" + @room_id.to_s + "/"

無需多說,類型為qrl,rid為roomid,直接發送這條消息就好。返回的兩條消息也沒有什麼價值。

data = "type@=qrl/rid@=" + @room_id.to_s + "/"
msg = message(data)
@danmu_auth_socket.write msg
str = @danmu_auth_socket.recv(4000)
str = @danmu_auth_socket.recv(4000)

2.4.發送消息詳細流程之步驟三

發送的消息內容為

"type@=keeplive/tick@=" + timestamp + "/vbw@=0/k@=19beba41da8ac2b4c7895a66cab81e23/"

直接發送。無太大意義。

data = "type@=keeplive/tick@=" + timestamp + "/vbw@=0/k@=19beba41da8ac2b4c7895a66cab81e23/"
msg = message(data)
@danmu_auth_socket.write msg
str = @danmu_auth_socket.recv(4000)

前三步,也就是2.2-2.3-2.4三步驟,也就是使用@danmu_auth_socket 完成獲取username和gid的重要步驟。獲取這兩個欄位以後,也就完成了它存在的使命。

接下來的就是@danmu_socket獲取彈幕的時候了!

2.5.發送消息詳細流程之步驟四

消息內容為:"type@=loginreq/username@="+@username+"/password@=1234567890123456/roomid@=" +@room_id.to_s + "/"

和上面2.2中略有不同。但是,需要注意的是

username 為2.2中所得到的username
password 的值得變化

data = "type@=loginreq/username@="+@username+"/password@=1234567890123456/roomid@=" + @room_id.to_s + "/"
all_data = message(data)
@danmu_socket.write all_data
str = @danmu_socket.recv(4000)

2.6.發送消息詳細流程之步驟五

接下來就是完成認證的最後一步了,join_group的消息內容為

"type@=joingroup/rid@=" + @room_id.to_s + "/gid@="+@gid+"/"

gid為2.2中所得到的gid。

data = "type@=joingroup/rid@=" + @room_id.to_s + "/gid@="+@gid+"/"
msg = message(data)
@danmu_socket.write msg

2.7.發送消息詳細流程之步驟六

獲取彈幕,並且列印出來。

danmu_data = @danmu_socket.recv(4000)
type = danmu_data[danmu_data.index("type@=")..-3]
puts type.gsub("sui","").gsub("@S","/").gsub("@A=",":").gsub("@=",":").split("/")

後三步,則是@danmu_socket 獲取彈幕的步驟。

於是,通過這些步驟,就可以完成了簡單的danmu的核心代碼,接下來的步驟就是完善,重構這些代碼了。

總結痛點一,至今還沒有解決rtmp地址的獲取

找了很久沒有辦法解決rtmp地址的自動獲取:

路徑如下

http://www.douyutv.com/swf_api/room/301712?cdn=nofan=yes_t=24243097sign=3b2efb130cb25a85e621f477f95c7341

這一處的請求不是XHR,也就是不是JS腳本通過XMLHttpRequest非同步載入;那麼,八成是flash通過http協議獲取的。我估計八成執行邏輯應該是在flash之中。也就不方便獲取其中的sign值.故,暫時無法解析rtmp視頻流地址了

效果圖和代碼

效果圖:

我擦,居然不能傳輸gif圖片.
請移步到這裡查看吧.

記一次鬥魚TV彈幕爬蟲經歷(Ruby版本)

代碼的地址為:

GitHub - twocucao/danmu: douyutv彈幕助手

技術淺薄,還請輕拍。

參考鏈接

PS:如果有問題可以在下方留言或者發送email到twocucao@gmail.com給我。


=============2016.10.21 更新 ============
鬥魚已經開放了第三方協議和代碼,大家可以搜索看看;我的代碼也更新了下,為了性能,很多地方用的hard code,大家隨意看看就好,千萬別學我!(用golang也寫了一份)
github路徑:GitHub - fishioon/douyu: Get danmu of douyutv
代碼只在mac下測試了,其它環境大家自行修改吧
=============原文分割線 ===============
在參考頂樓的回答和評論後,正確獲取到了彈幕,來回報下社會,代碼已經放在Github上了!
代碼是C寫的,本來用的python,感覺C更好用(在處理數據的時候)。

比較喜歡看yyf的dota2直播,但是最近yyf經常吹B,說有網友彈幕問「楓哥楓哥,你的XX怎麼這麼厲害啊」,水友們紛紛表示不信,所以就想著把彈幕爬下來,同時還有那些說贏了直播吃翔的,我通通都要記錄下來!!!

回答正題,那我們該如何弄清楚這個協議呢?
彈幕屬於實時消息,第一反應應該用websocket實現,打開chrome,F12,websocket中竟然找不到!那估計是通過Flash實現了。前端不熟,找了半天也沒找到JS實現代碼,只好祭出wireshark來分析了。
看了頂樓的提示,在wireshark中查看埠為8601的數據包,如下圖所示。

前三個數據包就是TCP的三次握手啦,複習下;接著我們看第四個數據包,如下圖所示

一般來說通信協議設計「內容長度」+「內容」,我們來看tcp數據包內容,前四個位元組為0x59 = 5*16+9 = 89,再看整個數據包長度為93,正好符合長度+內容,差不多我們可以確定通信協議如下:
struct {
int len; //數據包長度
int code; //經抓包發現該欄位一直與len欄位一致
int magic; //不知道啥意思,發現請求都是0x2b1, 返回都是0x2b2
char content[0]; //消息具體內容
}
從圖中的內容中可以看到:
1. 登錄彈幕伺服器: "type@=loginreq/username@%s=/password@=%s/roomid@=%d/ct@=2/",輸入自己的賬號密碼(經測試發現,賬號密碼隨便填都能成功),要看的房間id,如yyf的房間id為52428
通過socket發送上面的內容後,你回收到這樣的一條數據,格式與發送格式一樣:
「type@=loginres/userid@=0/roomgroup@=0/pg@=0/sessionid@=0/username@=/nickname@=/live_stat@=0/is_illegal@=0/npv@=0/best_dlev@=0/cur_lev@=0/」,沒發現有啥有用的數據。
2. 收到上面的消息後,這時候要加入一個組,格式如下:
"type@=joingroup/rid@=%d/gid@=%d/",rid就是房間id,要注意的問題來了:
gid應該是group id,登錄不同房間該id都不一樣,每次我都是抓包來查看該id是多少,有知情人告訴我嗎?(一樓評論中也提到這個問題)
發送上面的消息後,我們就可以安心的接收數據了,然後從數據中提取我們想要的就可以了,其中很多數據都不懂啥意思。
最後我們看下yyf房間的彈幕哈!(節奏帶的飛起)


[多圖預警]

Update:2016.1.16
總結在博客里:抓取鬥魚直播彈幕
Github 項目地址:brucezz/DouyuCrawler
歡迎去提各種 issue PR (代碼水平不高)...


/********************************************************************************************/
Update:2016.1.14
最近幾天會把 Java 項目發布到 Github 上面,大家提提意見 :)

/********************************************************************************************/
之前看到這個問題,感覺挺好玩的,就研究了下。
照著前面的思路,抓包分析。其他幾位的回答,基本能抓到彈幕數據了,但是其中一個參數gid,需要手動抓包得到。我也查看了瀏覽器載入的js文件,沒看到相關線索。最後看到排名第一的回答評論里有人說可以通過請求頁面里server_config的ip。
然後我就順著這條線索探索下去了。
具體過程如下:

先獲取到直播頁面中的server_config相關欄位,發現是經過urlencode的。

然後進行urldecode,得到json數據,再格式化一下,
UrlEncode編碼/UrlDecode解碼
JSON在線編輯
就變成這樣:

[{"ip":"119.90.49.107","port":"8035"},{"ip":"119.90.49.102","port":"8008"},{"ip":"119.90.49.110","port":"8050"},{"ip":"119.90.49.104","port":"8020"},{"ip":"119.90.49.107","port":"8034"},{"ip":"119.90.49.92","port":"8059"},{"ip":"119.90.49.95","port":"8071"},{"ip":"119.90.49.101","port":"8001"},{"ip":"119.90.49.93","port":"8063"},{"ip":"119.90.49.91","port":"8053"}]

用wireshark對以上幾個埠進行監聽,然後發現了幾個相關的請求

前面三組四位元組數據,就不解釋了,前面的答案寫的很清楚。
相關參數:

type@=loginreq/
username@=/
ct@=0/
password@=/
roomid@=25515/ #房間id
devid@=1C01371705D8B13361396AE2FAD50D6F/ #隨機uuid 無連字元"-" 均為大寫
rt@=1451848032/ #時間戳 單位為s
vk@=c2f6b956c37d8647a8f99abeeda0d568/ #
ver@=20150929/. #版本號

其中
vk的值也可以這樣算(算出來的值和抓包請求的結果不同,但能通過伺服器驗證,沒明白原理)

MD5(timestamp + "7oE9nPEG9xXV69phU31FYCLUagKeYtsF" + uuid)

至於中間那段字元串是什麼鬼,我也沒搞懂。這個演算法是從github上的項目看到的https://github.com/reusu/DouyuAssistant

用代碼也模擬請求任意一個地址即可,之後伺服器會返回一串很長的數據,應該是幾組返回數據合在了一起的,不過沒啥影響
像這樣:

返回的type為msgrepeaterlist,裡面包含了一組可用的彈幕伺服器,其實就是http://danmu.douyutv.com:8061 /8062 /12601 /12602 四個。數據裡面帶了一堆@符號是因為它進行了編碼。在js裡面發現的一段代碼,轉化為java代碼 大概是這個樣子:

public static String deFilterStr(String str) {
if (str == null) return null;
return str.trim().replace("@A", "@").replace("@S", "/");
}

public static String filterStr(String str) {
if (str == null) return null;
return str.trim().replace("@", "@A").replace("/", "@S");
}

然後decode出來,通過"/"字元可以分割成若干個用"@="連接的鍵值對。其實也可以用正則直接提取。

後面還有一條type為setmsggroup的數據,其中攜帶了gid參數,這個就是一直在尋找的groupid啦!

這個找到之後,後面就照著請求格式,登陸彈幕伺服器

這樣就請求完畢了,後面坐等伺服器給你返回彈幕數據咯~~
彈幕數據包是這樣子的(返回的參數倒是不少,主要就是content和snick兩個):

還有一個地方,就是發送心跳包,類似這樣

經過測試,只用發送type和tick時間戳就有效了。時間間隔的話,大概40s左右一次。最終抓出來的就這樣咯


##################20160325更新##########################
最近鬥魚修改了彈幕格式,大概長這個樣子

b"x81x00x00x00x81x00x00x00xb2x02x00x00type@=chatmsg/rid@=9401/uid@=12635840/nn@=xe8xb6x85xe8xb6x8axe7xa5x9exe7x9ax84boy/txt@=xe6x89x93xe8x84xb8xe5x90xa7/cid@=20e4e3fe427e410c67c5010000000000/level@=5/x00"

其中主要的修改為:#content@=彈幕內容,sinck@=昵稱,type@=chatmessage
改為了#txt@=彈幕內容nn@=昵稱,type@=chatmsg
可能是為了減少流量吧,我的github 上有更新,大家可以去看最新的代碼。
最近還把代碼打了個包。
##################二次更新昏割線#####################
成功的勾搭上天天卡牌,這個東西可以用來幫忙分析素材出現的時間。

同時幫助解決了熊貓TV素材獲取的問題。

==================最開始的答案==============================
這幾天我按照樓上的幾位大拿的思路以及github上常式,用python寫了一個鬥魚獲取彈幕的腳本,github如下:
GitHub - lidingke/douyuTanmuGet: Get tanmu by douyuTV in Python
這個腳本添加了資料庫的功能,以便保存爬取到的信息做分析。
同時在github上fork了熊貓TV的獲取彈幕的腳本,同樣添加了資料庫功能。
GitHub - lidingke/pandatv: 熊貓tv彈幕助手
不講太多廢話,總結一下思路,如下:
1、通過requests爬取對應主播的roomid以及登錄伺服器的地址。
2、socket模擬登錄type@=loginreq向登錄伺服器發出請求。
3、返回的數據中獲取彈幕伺服器地址、彈幕伺服器埠號以及groupID
4、socket模擬登錄type@=loginreq向彈幕伺服器發出登錄請求。
5、socket模擬登錄type@=loginreq向彈幕伺服器發出加入group的請求。
6、循環接收返回請求,type@=chatmessage的是彈幕信息,其中content@=彈幕,snick@=昵稱
7、每隔40s發送keeplive請求
由於具體的步驟樓上的幾位以及寫的很詳細了,我僅僅做些補充說明:
"xb2x02"這個值可以用來對多個返回信息進行分割,在彈幕量大的時候幾個信息是合在一起一起發送過來的,這時用split進行分割非常有必要。
@天白才痴樓中,問題比較大的是gid的獲取,這個問題的原因在於他是從第4步開始,直接從彈幕伺服器上獲取彈幕,相關伺服器地址和埠都是wireshark手動抓取,如果按照上述步驟來gid的獲取不是大問題,另外幾樓都是用完整的流程獲取彈幕。不過大大的C用的確實不錯,代碼也很易讀。
彈幕伺服器地址均為danmu.douyutv.com,埠均為8601、8602、12601、12602。所以如果不是為了獲取groupID的話確實只能從第4步開始。
發送keeplive和存入資料庫的時候我是開了新的線程。

用win平台的cmd實現,總是有特殊符號由於編碼的問題不能列印出來,彈出錯誤,只能用try語句,不知道別的平台是不是都能列印。
接下來講講我的問題:
1、大家是怎麼判斷主播沒直播了呢? @Brucezz , @曹童童 , @天白才痴
我用爬蟲抓取網頁上的人氣和體重信息返回為空。
2、樓下有位匿名用戶提到反編譯flash播放器,然後再根據編碼邏輯編寫python實現,我覺得這個思路是否比wireshark抓取數據包然後分析數據好的多?
=======================================
用win平台實現的,雖然很醜,但圖還是要上的。

最後貼一些彈幕伺服器的返回信息供大家參考

msgrepeaterlist=b"wx01x00x00wx01x00x00xb2x02x00x00type@=msgrepeaterlist/rid@=16789/lis
t@=id@AA=75701@ASnr@AA=1@ASml@AA=10000@ASip@AA=danmu.douyutv.com@ASport@AA=12601
@AS@Sid@AA=75702@ASnr@AA=1@ASml@AA=10000@ASip@AA=danmu.douyutv.com@ASport@AA=126
02@AS@Sid@AA=74004@ASnr@AA=1@ASml@AA=10000@ASip@AA=danmu.douyutv.com@ASport@AA=8
602@AS@Sid@AA=74003@ASnr@AA=1@ASml@AA=10000@ASip@AA=danmu.douyutv.com@ASport@AA=
8601@AS@S/x00/x00x00x00/x00x00x00xb2x02x00x00type@=setmsggroup/rid@=1
6789/gid@=169/x00"x00x00x00"x00x00x00xb2x02x00x00type@=scl/cd@=0/maxl
@=30/x008x00x00x008x00x00x00xb2x02x00x00type@=initcl/uid@=1305506713/
cd@=6000/maxl@=30/x00x9fx00x00x00x9fx00x00x00xb2x02x00x00type@=memb
erinfores/silver@=0/gold@=0/strength@=0/weight@=300633515/exp@=0/curr_exp@=0/lev
el@=1/up_need@=1000/fans_count@=636109/fl@=0/list@=/glist@=/x00Kx00x00x00Kx00x00x00xb2x02x00x00rid@=16789/gid@=0/type@=bcrp/pt@=2/pid@=10165/pps@=9107100/rps@=0/x00"
#Sip@AA=彈幕伺服器,@ASport@AA=埠號

chatmessage=b"Ux01x00x00Ux01x00x00xb2x02x00x00type@=chatmessage/rescode@=0/sender@
=14761178/content@=xe6x88x91xe7x9ax84xe6x98xbexe5x8dxa1/snick@=xe6x82
x94xe5x88x9dxe8xb0x8ei/cd@=4/maxl@=22/chatmsgid@=e56c175b158649e558b51d0000000000
/col@=0/ct@=0/gid@=6/rid@=284584/sui@=id@A=14761178@Snick@A=xe6x82x94
xe5x88x9dxe8xb0x8ei@Srg@A=1@Spg@A=1@Sstrength@A=11120@Sver@A=20150929@S
m_deserve_lev@A=0@Scq_cnt@A=0@Sbest_dlev@A=0@Slevel@A=8@Sgt@A=0@S/x00"
#content@=彈幕內容,sinck@=昵稱

userrnter=b"`x01x00x00`x01x00x00xb2x02x00x00type@=userenter/rid@=16789/gid@=194/
userinfo@=id@A=27051443@Sname@A=qq_Qq5dQ9et@Snick@A=xe8x91xa3xe5xb0x8fxe5
xa4xabxe4xb8xab@Srg@A=1@Spg@A=1@Srt@A=1445778191@Sbg@A=0@Sweight@A=0@Sstren
gth@A=6500@Scps_id@A=0@Sps@A=1@Sver@A=20150331@Sm_deserve_lev@A=3@Scq_cnt@A=1@Sb
est_dlev@A=3@Sglobal_ban_lev@A=0@Sexp@A=188100@Slevel@A=9@Scurr_exp@A=26100@Sup_
need@A=40400@Sgt@A=0@S/x00"

dgn=b"xcdx00x00x00xcdx00x00x00xb2x02x00x00type@=dgn/gfid@=104/gs@=1/gfcn
t@=1/hits@=1/sid@=2564049/src_ncnm@=xe5x9dx8fxe5xbfx83xe8x82xa0xe7x9a
x84xe6xa9x98xe5xadx90/rid@=16789/gid@=194/lev@=0/cnt@=0/sth@=64660/level@
=6/bdl@=0/dw@=301053615/rpid@=0/slt@=0/elt@=0/srg@=1/spg@=1/x00"

upgrade=b"Px00x00x00Px00x00x00xb2x02x00x00type@=upgrade/uid@=16659562/rid@=167
89/gid@=194/nn@=sleep4007/level@=4/x00"

keeplive=b"3x00x00x003x00x00x00xb2x02x00x00type@=keeplive/tick@=1455420838/uc@=32697/x00"

donateres=b"xefx00x00x00xefx00x00x00xb2x02x00x00type@=donateres/rid@=16789/gid
@=194/ms@=100/sb@=12861/src_strength@=17400/dst_weight@=301060515/hc@=4/r@=0/gfi
d@=1/gfcnt@=0/sui@=id@A=3577140@Srg@A=1@Snick@A=xe7x81xafxe5xa1x94xe4xbc/x00"

donateres1=b"xe2x00x00x00xe2x00x00x00xb2x02x00x00type@=donateres/rid@=25515/gid
@=312/ms@=100/sb@=17/src_strength@=700/dst_weight@=199511153/hc@=1/r@=0/gfid@=1/
gfcnt@=0/sui@=id@A=38034385@Srg@A=1@Snick@A=447802922@Scur_lev@A=0@Scq_cnt@A=0@S
best_dlev@A=0@Slevel@A=2@S/x00"
#dst_weight@=體重

bc_buy_deserve=b"x97x01x00x00x97x01x00x00xb2x02x00x00type@=bc_buy_deserve/level@=9/
lev@=3/rid@=16789/gid@=194/cnt@=1/hits@=1/sid@=27072146/sui@=id@A=27072146@Sname
@A=auto_l1AafOnuG6@Snick@A=xe5x8dx88xe5xa4x9cxe4xb8xb6Slaughter@Srg@A=1
@Spg@A=1@Srt@A=1445793083@Sbg@A=0@Sweight@A=0@Sstrength@A=3900@Scps_id@A=0@Sps@A
=1@Sver@A=20150929@Sm_deserve_lev@A=0@Scq_cnt@A=0@Sbest_dlev@A=0@Sglobal_ban_lev
@A=0@Sexp@A=193500@Slevel@A=9@Scurr_exp@A=31500@Sup_need@A=35000@Sgt@A=0@S/x00"

onlinegift=b"lx00x00x00lx00x00x00xb2x02x00x00type@=onlinegift/rid@=16789/uid@=216
25213/gid@=194/sil@=130/if@=6/ct@=0/nn@=xe4xbax91xe5xb8x95/ur@=1/level@=10
/x00"


用python的話有第三方包:第三方包地址
兼容py23,鬥魚、熊貓、戰旗、全民、Bilibili多平台。
pip安裝一下danmu,就可以用了
彈幕二次開發只要這樣:

from danmu import DanMuClient

dmc = DanMuClient("http://www.douyu.com/lslalala")
if not dmc.isValid(): print("Url not valid")

@dmc.danmu
def danmu_fn(msg):
print("[%s] %s" % (msg["NickName"], msg["Content"]))
@dmc.gift
def gift_fn(msg):
print("[%s] sent a gift!" % content["NickName"])
@dmc.other
def other_fn(msg):
print("Other message received")

dmc.start(blockThread = True)


鬥魚已經開放第三方介面了,獲取彈幕的流程變得簡單了許多:
《鬥魚彈幕伺服器第三方接入協議v1.4.1》發布信息 鬥魚開發者論壇
以下為大概流程
第三方接入彈幕伺服器:
IP 地址:http://openbarrage.douyutv.com 埠:8601
1.客戶端向伺服器端發送登錄請求,格式為;

type@=loginreq/roomid@=****/

(再也不用想方設法的獲取房間gid了,直接輸入房間號就行(* ̄? ̄*))
2.伺服器將登陸成功的消息返回給客戶端,單純的相應而已
3.客戶端收到登錄成功消息後發送進入彈幕分組請求給伺服器,格式為:

type@=joingroup/rid@=****/gid@=-9999/

(gid為固定的-9999,該組成員將接受對應直播間全部彈幕~rid也是當前房間號)
4.伺服器持續將新的彈幕響應發送給客戶機
*客戶端需要每隔 45 秒發送心跳信息給彈幕伺服器


那我說說?
是這樣的,鬥魚的彈幕信息有個自己的伺服器
http://danmu.douyutv.com 可以抓包看到,他通過TCP連接到鬥魚彈幕伺服器,然後獲取的信息,那麼想要對它進行二次開發只需要模擬用戶連接並登入彈幕伺服器即可。

彈幕連接 OnConn
(program):1 彈幕 UserLogin [type@=loginreq/username@=xxxx/password@=1234567890123456/roomid@=58718/]
(program):1 彈幕 網路數據 [type@=loginres/userid@=0/roomgroup@=67108896/pg@=0/sessionid@=6/username@=/nickname@=/is_signined@=432003648/signin_count@=32756/s@=% ?/live_stat@=125387584/npv@=32756/]
(program):1 彈幕登錄成功
VM160:1 彈幕分組 [type@=joingroup/rid@=58718/gid@=0/]
2VM171:1 FMPNetStream連接狀態:NetStream.Buffer.Full
VM175:1 彈幕 網路數據 [type@=userenter/rid@=58718/gid@=0/userinfo@=id@A=4295897@Sname@A=auto_DJUziSYiJo@Snick@A=芙蘇不語丶怪力亂神@Srg@A=1@Spg@A=1@Srt@A=1416480352@Sbg@A=0@Sweight@A=12800@Sstrength@A=51800@Scps_id@A=0@Sps@A=1@Sver@A=0@Sm_deserve_lev@A=0@S/]
VM176:1 新用戶進入信息: [id@=4295897/name@=auto_DJUziSYiJo/nick@=芙蘇不語丶怪力亂神/rg@=1/bg@=0/pg@=1/rt@=1416480352/weight@=12800/strength@=51800/cps_id@=0/]

大概就是這麼個數據包


既然沒人發nodejs版,那我就發一個吧

var net = require("net");
var uuid = require("node-uuid");
var md5 = require("md5");
var request = require("request");

var HOST = "danmu.douyutv.com";
var PORT = 8602;

function send(socket, payload)
{
var data = new Buffer(4 + 4 + 4 + payload.length + 1)
data.writeInt32LE(4 + 4 + payload.length + 1, 0); //length
data.writeInt32LE(4 + 4 + payload.length + 1, 4); //code
data.writeInt32LE(0x000002b1, 8); //magic
data.write(payload, 12); //payload
data.writeInt8(0, 4 + 4 + 4 + payload.length); //end of string
socket.write(data)
}

function login(socket, roomid, user, password)
{
var req = "type@=loginreq/username@=" + user + "/password@=" + password + "/roomid@=" + roomid;
send(socket, req);
}

function getGroupServer(roomid, callback)
{
request({uri:"http://www.douyutv.com/" + roomid}, function(err, resp, body) {
var server_config = JSON.parse(body.match(/room_args = (.*?)};/g)[0].replace("room_args = ", "").replace(";", ""));
server_config = JSON.parse(unescape(server_config["server_config"]));
callback(server_config[0].ip, server_config[0].port);
});
}

function getGroupId(roomid, callback)
{
var rt = new Date().now;
var devid = uuid.v4().replace(/-/g, "");
var vk = md5(rt + "7oE9nPEG9xXV69phU31FYCLUagKeYtsF" + devid)
var req = "type@=loginreq/username@=/password@=/roomid@=" +
roomid + "/ct@=0/vk@=" + vk + "/devid@=" +
devid + "/rt@=" + rt + "/ver=@20150929/";

getGroupServer(roomid, function(server, port) {
console.log("group server: " + server + ":" + port);
var socket = net.connect(port, server, function() {
send(socket, req);
});

socket.on("data", function(data) {
if (data.indexOf("type@=setmsggroup") &>= 0) {
var gid = data.toString().match(/gid@=(.*?)//g)[0].replace("gid@=", "");
gid = gid.substring(0, gid.length - 1);
socket.destroy();
callback(gid);
}
});
});
}

function monitorRoom(roomid)
{
var socket = net.connect(PORT, HOST, function() {
login(socket, "visitor1234567", "1234567890123456");
});

setInterval(function() {
send(socket, "type@=keeplive/tick@=70/"); //send keep alive message repeatly
}, 50000);

socket.on("data", function(data) {
//data is a Buffer here
if (data.indexOf("type@=loginres") &>= 0) {
getGroupId(roomid, function(gid) {
console.log("gid of room[" + roomid +"] is " + gid)
send(socket, "type@=joingroup/rid@=" + roomid + "/gid@=" + gid + "/");
});
} else if (data.indexOf("type@=chatmessage") &>= 0) {
var msg = data.toString();
var snick = msg.match(/snick@=(.*?)//g)[0].replace("snick@=", "");
var content = msg.match(/content@=(.*?)//g)[0].replace("content@=", "");

snick = snick.substring(0, snick.length - 1);
content = content.substring(0, content.length - 1);
console.log(snick + ": " + content);// 彈幕
} else if (data.indexOf("type@=userenter") &>= 0 ||
data.indexOf("type@=keeplive") &>= 0 ||
data.indexOf("type@=dgn/gfid@=131") &>= 0 ||
data.indexOf("type@=blackres") &>= 0 ||
data.indexOf("type@=dgn/gfid@=129") &>= 0 ||
data.indexOf("type@=upgrade") &>= 0 ||
data.indexOf("type@=ranklist") &>= 0 ||
data.indexOf("type@=onlinegift") &>= 0) {
//沒用的消息
} else if (data.indexOf("type@=spbc") &>= 0) {
var drid = data.toString().match(/drid@=(.*?)//g)[0].replace("drid@=", "");
drid = drid.substring(0, drid.length - 1);

console.log("rocket! room id:" + drid);
} else {
console.log(data.toString()); //在這裡顯示其它類型的消息
}
});
}

monitorRoom("&<這裡填roomid&>");


我記錄了下抓取彈幕的流程,可以參考下。
地址: 鬥魚彈幕抓取
代碼:https://github.com/ndrlslz/DouyuBarrageTool


《鬥魚彈幕伺服器第三方接入協議》發布信息 鬥魚開發者論壇
不用辛辛苦苦的去破解了,鬥魚已經公開他們的協議了。。。。
貌似是近期要上線的新版彈幕協議。


如果開著瀏覽器的話,可以用chrome插件,呵呵

require("shark/lang/observer").on("mod.chat.msg.msg", function(a) { var t; console.log("=================="); t = jQuery(a).find(".text-cont").text(); console.log(t) })


求大神給分析一下【虎牙】的彈幕如何抓取
求大神給分析一下【虎牙】的彈幕如何抓取? - 爬蟲(計算機網路)


update:2016-04-14
現在不需要先連接發出登錄請求獲得gid的那個伺服器了。可以直接登錄到彈幕伺服器。官方說明文件樓上有朋友給出了,主要更新有
1.彈幕伺服器地址:http://openbarrage.douyutv.com:8601
這是專門給第三方開發使用的。
2.gid = -9999,可以獲取到該房間的所有彈幕
登錄請求也變簡單了,詳請請見官方文檔。
我用Qt寫了個demo,有點簡陋,有bug會閃退,有興趣可以看一下。GitHub - castnime/douyu_danmu_QtDemo: qt寫的一個獲取鬥魚彈幕的demo

--------------------------------------------------------------------------------------------------------------------------------------------
謝謝 @Brucezz 的回答,我嘗試了一下,初步成功,還在繼續研究中......

不一定要vk,可以嘗試type@=loginreq/username@=/ct@=0/password@=/roomid@=138286/。
然後遍歷room_args中的伺服器ip和port。不過有時可能嘗試時間較長才能成功。
ps: @Brucezz 中的vk計算方法得到的值和抓包得到的值貌似不同。但是用這種方法計算出vk能夠很快登錄。


以前我們是做視頻聊天站的。2014年8月-9月我們一個月的時間抄了一個鬥魚,沒錯是整個站。開始的時候由於內容上的高度重合,比如蠟筆小新、RM等,房間也沒有人氣,我將鬥魚某房間的彈幕扒下來轉播到對應內容的房間中,並過濾掉一些關鍵字,足以以假亂真。雖然平台最終半死不活,且『技術本身並不可恥』,然而還是匿了。

技術核心我看大家都沒答到一點,就是反編譯flash播放器,因為所有與socket相關的實現都在flash里,反編譯出源碼後的邏輯、協議,一目了然,然後用python實現出來,很簡單,不上碼了。

這問題應該是純技術交流吧,我這麼想。


java抓包,但是不知道是什麼問題總出現「?」問題。

感覺也不像漢字編碼問題?


java 版 鬥魚直播彈幕抓取 songlijiang/2017


發一個Java的web版本,可以動態顯示彈幕數量。效果圖如下:

圖表是基於highcharts會隨彈幕動態更新五秒彈幕量

GitHub地址:DYB-鬥魚彈幕信息獲取


額...............用易語言,怎麼寫???


用php寫了下,地址:獲取鬥魚彈幕php版(原創)


我實現的鬥魚Android視頻直播app中帶有彈幕功能,彈幕實現根據鬥魚官方提供的java代碼版本修改而來。彈幕代碼Github地址,包括連接、登陸、心跳、獲取彈幕,用法簡單:

DyBulletScreenClient mDanmuClient;
int groupId = -9999;
mDanmuClient = DyBulletScreenClient.getInstance();
//設置需要連接和訪問的房間ID,以及彈幕池分組號,房間號mRoomId從API中獲取
mDanmuClient.start(mRoomId, groupId);

//有新彈幕就回調此介面
mDanmuClient.setmHandleMsgListener(new DyBulletScreenClient.HandleMsgListener() {
@Override
public void handleMessage(String txt) {
addDanmaku(true, txt);
}
});

如果對鬥魚Android視頻直播感興趣的同學,可以圍觀項目GitHub - littleMeng/video-live: 視頻直播,使用了vitamio視頻播放器和彈幕烈焰使。


推薦閱讀:

有免費的網路爬蟲軟體使用嗎?
豆瓣是如何屏蔽爬蟲的?
python爬蟲中文編碼的問題?
一份優秀的網路爬蟲工程師簡歷是怎麼樣的?
Python 爬蟲進階?

TAG:Python | 爬蟲計算機網路 | 彈幕視頻網站 | 鬥魚直播 |