玩點兒好玩的,抓取鬥魚彈幕

玩點兒好玩的,抓取鬥魚彈幕

來自專欄動物也要學編程4 人贊了文章

現象

說明

本文涉及到的一些知識點:

  • tcp協議

  • python的socket模塊用法

  • python的struct模塊用法

  • python的re模塊用法

  • python的threading模塊用法

  • python的requests庫用法

正文

先上代碼:

douyu_dm

我來說下我的思路,我想完成這麼個功能,輸入某個主播uid(房間的url上的),然後控制台就開始刷刷刷的出現彈幕。但經過wireshark分析發現,發給彈幕伺服器的是roomid(一串純數字),所以我們要找出,uid和roomid的對應關係,經過觀察,這個對應關係隱藏在房間的網頁源碼中,我們用requests請求網頁,然後用正則提取它,順便提取了房間名,可以用作提示,讓程序更人性化,這部分代碼如下:

def get_room_info(uid): """根據主播的uid(房間url上的), 獲取純數字的room_id和主播中文名. :param uid: str. :return room_id: str, 房間id. name: str, 主播中文名. """ url = http://www.douyu.com/{}.format(uid) r = requests.get(url) # 提取規則可能隨時間變動 id_pattern = re.compile(r"room_id":(d+)) name_pattern = re.compile(r<a class="zb-name"><h1>(.*?)</h1></a>) room_id = id_pattern.findall(r.text)[0] name = name_pattern.findall(r.text)[0] return room_id, name

我們已經獲取了roomid了,可以向彈幕伺服器發送請求了,我們通過socket模塊來發送TCP請求包,首先要連接彈幕伺服器(通過wireshark分析得到地址),通過以下代碼實現:

cfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)SERVER_ADDR = (223.111.12.101, 12601) # 隨時可能失效sk_client.connect(SERVER_ADDR)

接下來開始發送數據包,發完以下兩個包,鬥魚伺服器才會持續返回數據包:

# loginreq中的req指requests 還需要緊接著發下面一個包,伺服器會返回loginresmsg_login = type@=loginreq/username@=/password@=/roomid@={}/x00.format(room_id)send_msg(msg_login)# 直覺認為這裡暫停一秒比較好。time.sleep(1)# gid=-9999 代表接收海量彈幕,發完下面這個包,伺服器才會向客戶端發送彈幕msg_join = type@=joingroup/rid@={}/gid@=-9999/x00.format(room_id)send_msg(msg_join)

發送的數據包格式必須遵循鬥魚規定格式,鬥魚已經開放了彈幕協議,可百度鬥魚彈幕協議找到。下面這個函數,用來構造鬥魚要求數據包頭部:

def send_msg(cfd, msg): """發給鬥魚伺服器所有的包都得加上消息頭, 格式見鬥魚彈幕手冊. :param msg: str. """ content = msg.encode() # 消息長度, 這裡加8而不是加12.所以實際tcp數據部分長度會比鬥魚協議頭長度大4 length = len(content) + 8 # 689代表客戶端向伺服器發送的數據, 690代表伺服器向客戶端發送的數據 code = 689 head = struct.pack(i, length) + struct.pack(i, length) + struct.pack(i, code) cfd.sendall(head + content)

我們用以下函數,接收鬥魚伺服器返回的數據包,並提取彈幕(返回的數據包中不僅有彈幕信息,還有禮物信息等,按需求提取):

def get_dm(cfd): """接受伺服器消息, 並提取彈幕信息.""" pattern = re.compile(btype@=chatmsg/.+?/nn@=(.+?)/txt@=(.+?)/.+?/level@=(.+?)/) while True: # 接收的包有可能被分割, 需要把它們重新合併起來, 不然信息可能會缺失 buffer = b while True: recv_data = cfd.recv(4096) buffer += recv_data if recv_data.endswith(bx00): break for nn, txt, level in pattern.findall(buffer): # 鬥魚有些表情會引發unicode編碼錯誤 # `error=replace`, 把其替換成? print([lv.{:0<2}][{}]: {}. format(level.decode(), nn.decode(), txt.decode(errors=replace).strip()))

到這裡,基本的都已經完成了,程序也已經能夠正常運行,並看到彈幕,但是鬥魚伺服器不會一直返回數據包,你還需要每隔一段時間(45s),向鬥魚伺服器發送一個包,稱之為心跳包,來證明你還與伺服器保持著連接。可以看到,我們的程序不僅要接受數據包,同時還要發送心跳包,顯然這裡需要多線程來實現,以下是心跳包函數實現:

def keep_live(cfd): """每隔40s發送心跳包.""" while True: time.sleep(40) msg_keep = type@=mrkl/x00 send_msg(cfd, msg_keep)

接下來完成多線程代碼:

# daemon參數設為True, 則主線程結束後會直接退出,# 而不會等待子線程結束後再退出,# 並且此時子線程也會結束.t = Thread(target=keep_live, args=(cfd,), daemon=True)t.start()get_dm(cfd)

這樣,整個程序就完整了,當然,如果僅僅在控制台看彈幕的話,感覺還是太low了,正確姿勢應該是把彈幕都保存到資料庫中(如mongodb),日後翻出來再分析分析,這樣應該挺有意思的。

保存到mongodb代碼在mongodb分支。

感謝觀看。


推薦閱讀:

Manager進程之間共享數據
日常 Python 編程優雅之道
來自詞法分析的啟發——使用狀態機改寫控制結構
與孩子一起學編程(Python讀書筆記3)
如何查看Python函數的源代碼

TAG:Python | 爬蟲計算機網路 | 鬥魚直播 |