Hack On Douyu -- 1

660 查看

距离上次更新又有一段时间了,毕业答辩之后,确实和同学们一起出去嗨了一段时间,由于还没入职,在家清净的环境中可以好好学一下一直感兴趣的东西啦。

一直对网络爬虫很感兴趣,所以就开始学习很想学的python,用了之后也是感觉非常棒。期间抓过包括知乎、豆瓣、煎蛋还有个壁纸网站的数据,而抓去最多的还是直播网站斗鱼。数据抓下来之后如何使用是个问题,我的办法是用这些数据通过python的web框架flask搭建一个网站,也算是这段时间的学习成果。网站的构建自然少不了前端,也是硬着头皮学习了bootstrap,了解了一些css、javascript的知识。这段时间的学习成果主要是LearningFlaskBeautifulPicsDanmuDouyuFan这四个项目(由于也是刚接触python,代码质量可能不是太高 -。-)。而最后这个DouyuFan算是对前边几个项目的总结。DouyuFan主要是通过斗鱼网站弹幕信息的抓取,获取直播礼物的分布情况,历史数据记录以及当前最热门房间信息。
接下来我就用三次分别介绍我在数据抓取、后台搭建以及前后端数据通讯中学到的知识和遇到的问题。

开播房间数据获取

使用python抓取过数据的同学肯定对requestBeautiful Soup这两个库不陌生。
号称http for humans的requests缺失不是沽名钓誉,他在页面数据的抓取上确实简单明了。
通常情况下,requests和Beautiful Soup配合使用。以对斗鱼当前直播房间的抓取为例:

# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup  # 导入BeautifulSoup,提取网页中目标元素
import re                        # re 正则表达式,在快速查找和过滤元素中有出色表现
import requests                    # reqeusts 用以获取页面数据
from datetime import datetime
from pymongo import MongoClient

首先导入上述这几个库,然后可以伪造http请求header,这样可以减少爬虫被服务器ban掉的可能。

HOST = "http://www.douyu.com"
Directory_url = "http://www.douyu.com/directory?isAjax=1"
Qurystr = "/?page=1&isAjax=1"

agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.86 Safari/537.36'
accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
connection = "keep-alive"
CacheControl = "no-cache"
UpgradeInsecureRequests = 1
headers = {
    'User-Agent': agent,
    'Host': HOST,
    'Accept': accept,
    'Cache-Control': CacheControl,
    'Connection': connection,
    'Upgrade-InsecureRequests': UpgradeInsecureRequests
}

然后就是开播房间数据的获取和入库:

cli = MongoClient(host="ip",port=xxx)
db = cli["Douyu"]
col = db["Roominfo"]


def get_roominfo(data):
    if data:
        firstpage = BeautifulSoup(data)
        roomlist = firstpage.select('li')
        print len(roomlist)
        if roomlist:
            for room in roomlist:
                try:
                    roomid = room["data-rid"]
                    roomtitle = room.a["title"]
                    roomtitle = roomtitle.encode('utf-8')
                    roomowner = room.select("p > span")
                    roomtag = room.select("div > span")
                    roomimg = room.a
                    roomtag = roomtag[0].string
                    date = datetime.now()
                    # now = datetime.datetime(
                    # date.year, date.month, date.day, date.hour, date.minute)
                    if len(roomowner) == 2:
                        zbname = roomowner[0].string
                        audience = roomowner[1].get_text()
                        audience = audience.encode('utf-8').decode('utf-8')
                        image = roomimg.span.img["data-original"]
                        word = u"万"    # 在页面中获取的房间人数以万为单位的str需要转换为int型,以便入库
                        if word in audience:
                            r = re.compile(r'(\d+)(\.?)(\d*)')
                            data = r.match(audience).group(0)
                            audience = int(float(data) * 10000)
                        else:
                            audience = int(audience)
                        roominfo = {
                            "roomid": int(roomid),
                            "roomtitle": roomtitle,
                            "anchor": zbname,
                            "audience": audience,
                            "tag": roomtag,
                            "date": date,
                            "img" : image
                        }
                        col.insert_one(roominfo)
                    # print roomid,":",roomtitle
                except Exception, e:
                    pass


def insert_info():
    session = requests.session()
    pagecontent = session.get(Directory_url).text
    pagesoup = BeautifulSoup(pagecontent)
    games = pagesoup.select('a')
    col.drop()
    for game in games:
        links = game["href"]
        gameurl = HOST + links + Qurystr
        print gameurl
        gamedata = session.get(gameurl).text
        get_roominfo(gamedata)

我平常习惯使用mongodb作为数据存储,首先建立与数据库的连接,然后通过获取斗鱼当前所有房间分类,接着逐一获取每个分类中开播的房间数据,并记录每个房间的roomid(房间号,斗鱼直播间唯一标识)、roomtitle(房间标题)、anchor(主播id)、audience(观众人数)、tag(房间所属分类)、date(数据获取时间)、img(直播间封面图片)。通过定时执行此脚本,可以获取当前观众人数最多的房间(通常大都是lol的直播 =。=),也可以在之后通过roomid查询到关于对应直播间必要的信息。

弹幕数据获取

经常看斗鱼直播的同学肯定知道“弹幕大神”这个词,我最初想要抓取弹幕的目的是想通过大量的获取直播间弹幕数据进行一些自然语言分析,由于那些东西一直没学习,也就没再弄,但是,通过弹幕,可以获取到在全频道广播的火箭信息,长时间监测这些数据应该也是一件有意思的事情。
说干就干,想要获取到直播间的弹幕数据不同于上边所说的页面数据抓取,好在斗鱼官方也提供了一个获取弹幕的途径斗鱼弹幕服务器第三方接入协议,文档中对如何获取弹幕数据、以及弹幕信息类型有具体的说明,这也大大降低了获取弹幕数据的难度。
看过这个协议之后,通过建立与弹幕服务器的tcp连接,可以不断的获取到弹幕数据,我使用的是socket这个库。

HOST = 'openbarrage.douyutv.com'
PORT = 8601
RID = 97376
LOGIN_INFO = "type@=loginreq/username@=qq_aPSMdfM5" + \
    "/password@=1234567890123456/roomid@=" + str(RID) + "/"
JION_GROUP = "type@=joingroup/rid@=" + str(RID) + "/gid@=-9999" + "/"
ROOM_ID = "type@=qrl/rid@=" + str(RID) + "/"
KEEP_ALIVE = "type@=keeplive/tick@=" + \
    str(int(time.time())) + "/vbw@=0/k@=19beba41da8ac2b4c7895a66cab81e23/"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

在这里,需要注意几个变量: host port roomid gid 。其中HOST是弹幕服务器地址,port是对外开放的端口,roomid则是主播间对应的id,gid是要加入的弹幕频道,-9999频道可以获取到所有弹幕,也就是“海量弹幕”频道。

def get_Hotroom():
    hotroom = roomcol.find().limit(1).sort(
        [("audience", pymongo.DESCENDING), ("date", pymongo.DESCENDING)])
    for item in hotroom:
        return item["roomid"]

def create_Conn():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    RID = get_Hotroom()
    print "当前最热房间:", RID
    LOGIN_INFO = "type@=loginreq/username@=qq_aPSMdfM5" + \
        "/password@=1234567890123456/roomid@=" + str(RID) + "/"
    print LOGIN_INFO
    JION_GROUP = "type@=joingroup/rid@=" + str(RID) + "/gid@=-9999" + "/"
    print JION_GROUP
    s.sendall(tranMsg(LOGIN_INFO))
    s.sendall(tranMsg(JION_GROUP))
    return s

之后,通过get_Hotroom()获取到当前最热门房间(人数最多的房间),通过create_Conn()建立与服务器的连接。连接建立之后就可以开心的获取并保存弹幕数据了:

def insert_msg(sock):
    sendtime = 0
    while True:
        if sendtime % 20 == 0:
            print "----------Keep Alive---------"
            try:
                sock.sendall(tranMsg(KEEP_ALIVE))
            except socket.error:
                print "alive error"
                sock = create_Conn()
                insert_msg(sock)
        sendtime += 1
        print sendtime
        try:
            data = sock.recv(4000)
            if data:
                strdata = repr(data)
                if "type@=spbc" in strdata:
                    get_rocket(data)
                if "type@=chatmsg" in strdata:
                    get_chatmsg(data)
        except socket.error:
            print "chat error"
            sock = create_Conn()
            insert_msg(sock)
        time.sleep(1)

每20秒向服务器发送一条KEEP_ALIVE用以使连接保活,通过获取到的数据特点,将普通聊天弹幕和火箭广播弹幕区分开来,并且保存在不同的数据库中从而为之后提供不同的用途。
弹幕数据获取大致是这样的:


获取弹幕

后续内容

至此,此项目的数据获取工作已经完成,在接下来两篇内容会分别介绍如何使用这些数据构建页面,以及在此过程中遇到的问题。