V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
JCZ2MkKb5S8ZX9pq
V2EX  ›  Python

关于小米的拆机评测,顺手写了个 B 站弹幕爬虫范例。

  •  
  •   JCZ2MkKb5S8ZX9pq · 2020-01-17 16:39:28 +08:00 · 3449 次点击
    这是一个创建于 1807 天前的主题,其中的信息可能已经有所发展或是发生改变。
    • 昨天看人讨论小米拆机的事儿,就想看看数据。
    • 其实我们有弹幕评论等等的获取和分析工具,但不方便分享。
    • 刚好旧的工具不支持历史弹幕( B 站前一阵更新后需要 cookies 了)。
    • 刚才简单写了一个,避开了所有私有的工具,都引用的基本模块,有兴趣的可以玩玩。

    结构

    • 需要 3 个文件。
    • headers.py,内含一个名为 headers 的字典,因为涉及隐私,不共享。可以把自己的请求头贴进去。
    • get_danmaku.py,获取指定 oid(cid)和 date 的弹幕。
    • get_all_history_danmaku.py,获取所有历史弹幕。

    用法

    • 三个文件放到一个文件夹内,运行get_all_history_danmaku.py,会生成一个 json。
    • 然后就可以随意分析了,熟悉 pandas 的话会方便一些。
    • 我们比较常用的是检测时间密度,发送密度,内容同质化,作者分布,等等。
    • 至于结果怎么理解,这个见仁见智了。不发表意见,不站队。

    文件

    get_danmaku.py

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    # Load bilibili history danmaku, and return json.
    
    import requests
    import logging as log
    import json
    import time
    from lxml import etree
    from .headers import headers
    
    
    def get_history_danmaku(oid, date):
        dm_list = []
    
        # get data
        url = f'https://api.bilibili.com/x/v2/dm/history'
        params = {'type': 1, 'oid': oid, 'date': date}
        r = requests.get(url, params=params, headers=headers)
        content = r.content
        log.debug(content.decode('utf-8'))
    
        # read xml
        xml = etree.HTML(content)
        for d in xml.xpath('//d'):
            attrs = d.xpath('./@p')[0]
            attrs = attrs.split(',')
            text = d.xpath('./text()')[0]
            log.debug(f'{attrs}, {text}')
    
            # format data
            d = {
                'cid': int(oid),
                'time': int(float(attrs[0])),  # 发送时间点(视频播放点)
                'position': int(attrs[1]),  # 弹幕位置
                'fontsize': int(attrs[2]),  # 字体大小
                'color': ('000000' + str(hex(int(attrs[3])))[2:])[-6:],  # 弹幕颜色
                'ctime': int(attrs[4]),  # 弹幕创建时间
                'unknown': attrs[5],
                'author': attrs[6],  # 发送者编号(不同于 uid )
                'dmid': int(attrs[7]),  # 弹幕 id
                'content': text,  # 弹幕内容
                'date': date,
                'updateTime': int(time.time())
            }
            dm_list.append(d)
    
        return dm_list
    
    
    if __name__ == '__main__':
    
        log.basicConfig(level=log.DEBUG)
    
        oid = 136870419
        date = '2019-12-20'
        d = get_history_danmaku(oid, date)
        print(json.dumps(d, ensure_ascii=False, indent=4))
    
    

    get_all_history_danmaku.py

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    import requests
    import logging as log
    import os
    import re
    import json
    from datetime import datetime, timedelta
    from .get_danmaku import get_history_danmaku
    from .headers import headers
    
    
    def get_all_history_danmaku(aid):
        url = f'https://www.bilibili.com/video/av{aid}'
        body = requests.get(url).text
        log.debug(body)
    
        # get cid / oid
        pages = re.findall(r'(?<="pages":)\[.*?\]', body)[0]
        cids = re.findall(r'(?<="cid":)\d*', pages)
        log.info(f'{cids=}')
    
        # get post date
        publish = re.findall(r'(?<=Published" content=")\d{4}-\d{2}-\d{2}', body)[0]
        start_date = datetime.strptime(publish, '%Y-%m-%d')
        log.info(f'{publish=}')
    
        result = {}
        while True:
            date = start_date.strftime('%Y-%m-%d')
            log.info(f'get danmaku of {date}')
            for cid in cids:
                dms = get_history_danmaku(cid, date)  # get danmaku
                for dm in dms:  # format data
                    dmid = dm['dmid']
                    result.setdefault(dmid, dm)  # 防止重复添加
    
            # go next day or exit
            start_date += timedelta(1)
            if start_date > datetime.now():
                break
    
        here = os.path.abspath(os.path.dirname(__file__))
        output = os.path.join(here, f'av{aid}_dm.json')
        with open(output, 'w', encoding='utf-8') as f:
            f.write(json.dumps(result, ensure_ascii=False, indent=2))
    
    
    if __name__ == '__main__':
    
        log.basicConfig(level=log.INFO)
    
        aid = 79974337
        get_all_history_danmaku(aid)
    
    

    • 做广告的,代码比较菜,欢迎指正。
    • 弹幕里有一个字段不知道怎么意思,有知道的请指教下。
    • 其实评论的部分更好一些,直接关联到用户 aid,可以做得更深一点。那个我们搞过现成的轮子。
    • 不过写完就觉得没必要在这破事儿上费太多功夫,纯分享了。
    5 条回复    2020-01-19 13:04:06 +08:00
    NSAgold
        1
    NSAgold  
       2020-01-17 19:48:16 +08:00
    没记错是弹幕池,区分是否是高级弹幕用的
    JCZ2MkKb5S8ZX9pq
        2
    JCZ2MkKb5S8ZX9pq  
    OP
       2020-01-17 19:49:44 +08:00
    @NSAgold 原来如此,我等下找个视频验证一下,非常感谢。
    这个字段在我这儿躺了至少两年了……
    JCZ2MkKb5S8ZX9pq
        3
    JCZ2MkKb5S8ZX9pq  
    OP
       2020-01-17 19:53:50 +08:00
    @NSAgold
    试了下这个视频
    https://www.bilibili.com/video/av61919487

    好像并不是,高级弹幕的这个字段仍旧是 0,但内容部分是数组的形式。
    Va1n3R
        4
    Va1n3R  
       2020-01-19 01:47:09 +08:00
    弹幕 aid 被 hash 过,能彩虹表枚举出来的。
    JCZ2MkKb5S8ZX9pq
        5
    JCZ2MkKb5S8ZX9pq  
    OP
       2020-01-19 13:04:06 +08:00 via iPhone
    @Va1n3R 那可以拿评论最多的前十和弹幕最多的前十撞一下试试,请问用的是哪种 hash ?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2615 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 06:48 · PVG 14:48 · LAX 22:48 · JFK 01:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.