精华内容
下载资源
问答
  • Android下的弹幕的简单实现

    千次阅读 2015-11-19 15:26:16
    Android下的弹幕的简单实现简介 今天给大家带来一个前几天在做工程的时候写的一个有意思的Android弹幕实现,可以用来吐槽,介绍APP之类的~非常简单~几行代码~哈哈~下面是下载地址~下载地址(记得给我星哟):...

    Android下的弹幕的简单实现

    简介

    这里写图片描述
    今天给大家带来一个前几天在做工程的时候写的一个有意思的Android弹幕实现,可以用来吐槽,介绍APP之类的~非常简单~几行代码~哈哈~下面是下载地址~

    下载地址(记得给我星哟):https://github.com/dayiming/BarrageView

    实现

    首先,因为是弹幕,为了让文字浮动于要显示的层的表面,我们需要准备一个半透明渐变的背景,所以,创建drawable_barrage_background.xml文件于drawable文件夹下,内容如下:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <gradient
            android:angle="-90"
            android:startColor="#00333333"
            android:centerColor="#60333333"
            android:endColor="#c0333333"/>
    </shape>

    为了让弹幕的每一段文字可以被清晰的看见,最好给弹幕的每一个条目设置一个背景,为了优化视觉效果,这个背景最好也是有一些透明度的,创建drawable_barrage_item_background.xml与drawable文件夹下,内容如下:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"           android:shape="rectangle">
        <corners android:radius="4dp"/>
        <solid android:color="#e0ffffff"/>
        <padding android:bottom="2dp" android:left="12dp" android:right="12dp" android:top="2dp"/>
    </shape>

    准备工作做完之后,下面就是重写控件了。

    首先我们创建BarrageView继承自FrameLayout(其实继承自哪个控件都可以,只要后面的代码根据控件的不同稍作修改就可以了)。在构造函数中我们完成如下操作:

    public BarrageView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private void init() {
    
            //设置背景色
            this.setBackgroundResource(
            R.drawable.drawable_barrage_background);
    
            //创建一个文本标记用于展示,并添加一定的属性让它与底部居中
            tv_tag = new TextView(getContext());
            tv_tag.setTextSize(18);
            tv_tag.setTextColor(Color.WHITE);
            LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
            int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics());
            params.bottomMargin = margin;
            params.leftMargin = margin;
            params.rightMargin = margin;
            tv_tag.setSingleLine(false);
            tv_tag.setLayoutParams(params);
            tv_tag.setText("没错这就是弹幕~一会就没了~");
            this.addView(tv_tag);
    
            //这个类是我自己写的类,用于获取屏幕的长宽的类,在源码中有
            screenParams = new ScreenParams(getContext());
            random = new Random();
    
            //这个类主要用于检测组件是否被绘制,为了提升效果,我们在组件被绘制的时候才开始动态添加弹幕
            ViewTreeObserver observer = getViewTreeObserver();
            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    //防止这个函数被多次调用
                    if (!isMessured) {
                        isMessured = true;
                        handler.postDelayed(playRunnable, PLAY_DURATION); //用于开始生成弹幕 PLAY_DURATION = 400
                        handler.postDelayed(disappearRunnable, DISAPPEAR_DURATION); //用于停止弹幕和隐藏组件 DISAPPEAR_DURATION 这个可以随意设置时长
                    }
                    return true;
                }
            });
        }

    接着我们准备一些要显示的颜色和文字,放置成数组

    private int[] colors = new int[] {
                Color.parseColor("#e51c23"),
                Color.parseColor("#e91e63"),
                Color.parseColor("#9c27b0"),
                Color.parseColor("#673ab7"),
                Color.parseColor("#3f51b5"),
                Color.parseColor("#5677fc"),
                Color.parseColor("#ffc107"),
                Color.parseColor("#009688"),
                Color.parseColor("#259b24"),
        };
    
        private String[] texts = new String[] {
                "Android上也可以有弹幕哟~",
                "是不是还挺有意思的~",
                "我也这么觉的~",
                "我们的APP特别的有意思~",
                "我只是吐槽一下产品~",
                "这个地方不要写死,以后一定会改的~",
                "***是这个世界上最好的语言!!",
                "请叫我攻城狮~~",
                "卖个萌~O(∩_∩)O~~"
        };

    下面就是最重要的一步,动态随机的生成弹幕,放在一个Runnable中定时实现,代码如下:

    private Runnable playRunnable = new Runnable() {
            @Override
            public void run() {
                final TextView tem = new TextView(getContext());
                //设置文字大小
                tem.setTextSize(18);
                //设置背景
                tem.setBackgroundResource(                       R.drawable.drawable_barrage_item_background);
                //设置颜色
                tem.setTextColor(
                colors[random.nextInt(colors.length)]);
                String text = texts[random.nextInt(texts.length)];
                tem.setText(text);
                //让文本框居右,因为我们要从右端开始显示
                LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                params.gravity = Gravity.RIGHT;
                tem.setLayoutParams(params);
                BarrageView.this.addView(tem);
                //随机的设置弹幕文本的Y值,让它可以呈现散乱出现的效果
                tem.setY(random.nextInt(
                          screenParams.getScreenHeight() / 4) + screenParams.getScreenHeight() / 3);
                //定义一个动画,让文本框开始移动
                ObjectAnimator animator = ObjectAnimator.ofFloat(tem, "translationX", 0, 0 - screenParams.getScreenWidth());
                animator.setDuration(2400);
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        //在动画结束的时候删除这个文本框
                        BarrageView.this.removeView(tem);
                    }
                });
                animator.start();
    
                handler.postDelayed(playRunnable, PLAY_DURATION);
            }
        };

    下面是播放完毕隐藏这个组件的动画,让透明度渐变,视觉效果会好很多

    ObjectAnimator animator = ObjectAnimator.ofFloat(BarrageView.this, "alpha", 1, 0);
                animator.setDuration(300);
                animator.setInterpolator(new DecelerateInterpolator());
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        BarrageView.this.setVisibility(GONE);
                    }
                });
                animator.start();

    到这这个Android下的弹幕控件就制作完成了,如果可以的话大家还可以自己修改成自己喜欢的动画,或者每一个弹幕都是图片+文字这样的实现,都很容易修改。谢谢大家~

    下载地址(记得给我星哟):https://github.com/dayiming/BarrageView

    展开全文
  • 作者 |周志鹏 ...相对于一般电影OR电视剧评论,弹幕能够贴合剧情,进行更多有意思的脑洞分析。 每次写爬虫,耳畔都会回响起那句经典的freestyle: “你看这个碗,它又大它又圆,你看这个面,它又长...

    作者 | 周志鹏

    来源 | 数据不吹牛


     

    本文以腾讯视频(都挺好)为例,解析弹幕爬取的细节和难点,对思路感兴趣的旁友们可以跟着文章逻辑走一遍,对于想直接上手爬的同学,文末已给出完整代码。

    相对于一般电影OR电视剧评论,弹幕能够贴合剧情,进行更多有意思的脑洞分析。

     

    每次写爬虫,耳畔都会回响起那句经典的freestyle:

     

    你看这个碗,它又大它又圆,你看这个面,它又长它又宽

     

    短短四句,揭示了两种本质——碗是大和圆的,面是长亦宽的。一秒就看清事物本质的人和一辈子才看透事物本质的人自然过着不同的人生。

     

    所以,写爬虫也是一样的,理清目标数据和网址的变化规律,也就是先看到碗的大和圆,面的长和宽,随后再去解决细节的数据定位和抓取(欣赏碗的花纹细节,面的Q弹),往往事半功倍。

    #这就是我写爬虫所信奉的大碗宽面逻辑。

     

      01  子弹(弹幕)轨迹规律探究

     

    1、数据定位:

     

    打开腾讯视频的电视剧(这里以《都挺好》为例),F12审查元素,默默的等待目标猎物出现,因为弹幕是播放时不断滚动出现,所以我们先假设它在JS下。

     

    正片开始后,一群以“danmu"为开头的请求不断加载打破了短暂的平静,我们把这个疑似目标预览一下:

     

    果然,弹幕内容赫然在列,对于我们分析有用的字段还有弹幕的ID,upcount(点赞数),opername(用户名)和uservip_degree(会员等级)。

     

    到这一步,我们先不纠结于这个JSON文件要如何伪装访问,如何解析,不妨跟随那句“大碗宽面”的旋律,跳出碗来,看看这个碗是大还是圆(找规律)

     

     

    2、弹道(弹幕网址)规律分析:

     

    在找网址规律的时候,有一个小技巧,就是尝试暴力删掉目标网址中不影响最终结果的部分参数,再从最精简的网址中寻找规律。

    拿我们第一个弹幕网址来说,原网址是这样的:

    •  
    https://mfm.video.qq.com/danmu?otype=json&callback=jQuery19109123255549841207_1553922882824&timestamp=45&target_id=3753912718%26vid%3Dt00306i1e62&count=80&second_count=5&session_key=558401%2C8142%2C1553922887&_=1553922882831

    在浏览器中打开是这样的:

     

    网址最后一串数据好像是时间戳,我们删掉试试,果然,返回的内容没变。那个sessiong_key到底影不影响呢?删了试试,返回内容还是没变!

     

    删到最后,我们把原网址精简成了下面的网址:

    •  
    https://mfm.video.qq.com/danmu?otype=json&timestamp=15&target_id=3753912718%26vid%3Dt00306i1e62&count=80

     

    我们把第二页网址也精简一下:

    •  
    https://mfm.video.qq.com/danmu?otype=json&timestamp=45&target_id=3753912718%26vid%3Dt00306i1e62&count=80

     

    对比很容易找到规律,从第一页到第二页,timestamp值从15变到了45,其他部分没有任何变化,我有一个大胆的猜测,这个timestamp值是控制页数的变量,并且是30秒更新一次弹幕。

     

    那一级有多少页呢?我们把进度条拉到影片结束的边缘,发现最后一页的网址的timestamp的值变成了2565。

     

    整个过程,我们只需要构造步长为30的循环变量来替换timestamp参数就可以实现批量访问了。

     

    到这里,单集中弹幕动态更新的规律我们已经探究清楚,下面来对单个页面进行解析。

    (PS:其实大碗宽面的逻辑下,我们这个时候应该再继续对比不同集数之间网址变化规律,并找到规律本身,但考虑到内容实操性与可读性,我们不妨把这一块往后稍稍)

     

     

    02  解析单页弹幕内容

     

    以第一集第一页的弹幕为例,我们只进行简单的headers伪装,进行访问尝试:

     

    异常顺利,成功返回目标结果,而且是友好的JSON格式,我们用JSON来解析一下:

     

    纳尼?结果疯狂报错:

     

    告诉我们在35444的位置有字符问题,经过排查,发现错误的原因是解析的部分内容因为格式问题没有通过JSON语法检查,解决方法很简单,我们json.loads中strict参数变成Fasle即可:

     

    OK,接下来遍历提取我们需要的关键数据:

    #存储数据df = pd.DataFrame()#遍历获取目标字段for i in bs[ comments ]:    content = i[ content ]  #弹幕内容    name = i[ opername ]    #用户名    upcount = i[ upcount ]  #点赞数    user_degree =i[ uservip_degree ] #会员等级    timepoint = i[ timepoint ]  #发布时间    comment_id = i[ commentid ]  #弹幕ID    cache = pd.DataFrame({ 用户名 :[name], 内容 :[content], 会员等级 :[user_degree],                           评论时间点 :[timepoint], 评论点赞 :[upcount], 评论id :[comment_id]})    df = pd.concat([df,cache])

     

    大写的EASY!要进行多页爬取,只需要在外层构造一个循环,以30为步长改变timestamp的变量即可。

     

     

     

     03  不同集之间网址规律探究

     

    单页、单集的规律都搞清楚了,那不同集之间的网址有什么规律呢?

    第一集是这样的:

    •  
    https://mfm.video.qq.com/danmu?otype=json&timestamp=15&target_id=3753912718%26vid%3Dt00306i1e62&count=80

     

    我们把第二集的弹幕网址也暴力精简:

    •  
    https://mfm.video.qq.com/danmu?otype=json&timestamp=15&target_id=3753912717%26vid%3Dx003061htl5&count=80

     

    发现是target_id值和%3D后面一串ID(第一集是t00306i1e62,第二集是x003061htl5)的变化决定了不同的集数。(为了区分,我们把后面那一串ID叫做后缀ID)

     

    而难点就在于他们之间没有像timestamp那样明显的规律可循,弹幕内容所在的网址本身又没有任何关于两个ID的信息。

     

    所以,我们必须跳出碗来找线索,看看有没有又大又黑的锅装这些碗(目的在于找到存储target_id和后面不规则ID的那口大锅)。

     

     

    1、找到后缀ID

     

    这个时候,需要一些常识来开路了。我们发现播放视频的时候,在播放屏右边总会显示全部集数:

     

    点击对应的集数就会进行相应的换集跳转,所以我们有理由相信ID相关的锅藏在其中。重新刷新网页,很容易找到了他们的踪迹:

     

    可以看到,上面截图中第一集的ID“t00306i1e62”对应着我们前面找到的规律(后缀ID)。打开任意一集,发现1-30集和31-46集相关的后缀ID都分别存储在两个相邻的网页。

     

    所以,我们先尝试拿下所有的后缀ID、对应剧集名称、播放量和集数:

    ​​​​​

    #打开任意一集,1-30和31-46存储在两个网页part1_url =  https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=b0030velala,t00306i1e62,x003061htl5,b0030velala,w0030ilim7z,i0030r7v63u,z003044noq2,m0030sfinyr,c0030u884k7,k0030m5zbr7,l0030e5nglm,h0030b060vn,j003090ci7w,n0030falyoi,s00308u9kwx,p0030fohijf,g00303ob0cx,v0030960y6n,x0030bl84xw,v0030keuav1,t0030kups1i,n0030y2o52i,x0030s52mev,d0030xuekgw,o0030md1a2a,x0030peo3sk,d00303l5j4k,t0030aexmnt,a0030ybi45z,y0030wpe2wu&callback=jQuery19101240739643414368_1553238198070&_=1553238198071 part2_url =  https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=t0030epjqsi,g003035mi84,n00301fxqbh,h0030zivlrq,d0030qc1yu2,m0030q9ywxj,h0030j0eq19,j0030jks835,t0030owh5uu,e0030xbj246,a00308xw434,l0030tb319m,a0030mhntt6,t0030wnr3t9,l0030t7o64e,b0030i9bi3o,m0030yklk6j,z0030tgz3pp,r00307wgnly,o00306b4zax,k00309i6ul6,j00304eu73n,v08521l667a,u0851gzzoqi,a0852328197,k0852mb3ymt,v00308p65xf,z08527pia6g,z08520difig,z0852ybpxn0&callback=jQuery19101240739643414368_1553238198072&_=1553238198073 base_info  = pd.DataFrame()for url in [part1_url,part2_url]:    html = requests.get(url,headers = headers)    bs = json.loads(html.text[html.text.find( { ):-1])        for i in bs[ results ]:        #后缀ID        v_id = i[ id ]        #这一集的名字,比如“都挺好_01”        title = i[ fields ][ title ]        #播放量        view_count = i[ fields ][ view_all_count ]        #整型存储的集数,片花则为0        episode = int(i[ fields ][ episode ])        #去掉片花,只留下正片        if episode == 0:            pass        else:            cache = pd.DataFrame({ id :[v_id], title :[title], 播放量 :[view_count], 第几集 :[episode]})            base_info = pd.concat([base_info,cache])

     

     

    OK,非常顺利。

     

    目前来说我们拿到了所有的后缀ID,但还是缺少target_id,无法构造完整的网页进行自动循环爬取。而我们在这两个网页中找不到任何和target_id有关的信息,真让人头大!

     

     

    2、死磕target_id

     

    每当没有头绪的时候,我总是想起莎翁的那句:

     

    “一切过往,皆为序章”

     

    反之,一切序章,皆有过往,正在发生或者已经发生的万事万物一定有迹可循

     

    我们心心念念的target_id一定在某个动态网页中记录着。

     

    这个时候就需要耐心的筛选了,最后,我们发现,单集的target_id,隐藏在XHR下的一个"regist"开头的动态网址中:

     

     

    仔细观察,他是一个POST请求

     

     

    传递的参数如下:

     

     

    翻了N集来对比,我们发现不同集数之间网址变化的只有传入的这个“vecIdList”,里面的参数正是我们上一步获取的那些后缀ID。

     

    真相渐渐浮出水面。

     

    3、思路梳理:

     

    • 第一步,我们搞清楚了单集内部弹幕网址的动态变化,只需要改变timestamp的值即可循环爬取单集所有内容。

    • 第二步,发现要自动爬取每一集,必须先找到构造网址的target_id和后缀的ID

    • 第三步,任意一集网页中都能直接找到所有剧集的后缀ID(我们已经拿下了所有的后缀ID),但是却只能在一集中找到单集的一个target_id。

    • 第四步,也就是接下来的一步,我们可以基于已经爬到的后缀ID,去循环访问每一集,拿到单集对应的target_id,这样就能构造出完整的弹幕网页所需的ID们了。

     

    说干就干,循环爬取target_id:

     

    #定义爬取单集target_id的函数#只需要向函数传入v_id(后缀ID)和headersdef get_episode_danmu(v_id,headers):    #target_id所在基础网址    base_url =  https://access.video.qq.com/danmu_manage/regist?vappid=97767206&vsecret=c0bdcbae120669fff425d0ef853674614aa659c605a613a4&raw=1     #传递参数,只需要改变后缀ID    pay = {"wRegistType":2,"vecIdList":[v_id],       "wSpeSource":0,"bIsGetUserCfg":1,       "mapExtData":{v_id:{"strCid":"wu1e7mrffzvibjy","strLid":""}}}        html = requests.post(base_url,data = json.dumps(pay),headers = headers)    bs = json.loads(html.text)    #定位元素    danmu_key = bs[ data ][ stMap ][v_id][ strDanMuKey ]    #解析出target_id    target_id = danmu_key[danmu_key.find( targetid ) + 9 : danmu_key.find( vid ) - 1]    return [v_id,target_id]info_lst = []#循环获取后缀ID并传递for i in base_info[ id ]:    #得到每一集的后缀ID和target_id    info = get_episode_danmu(i,headers)    print(info)    info_lst.append(info)    time.sleep(3 + random.random())

     

    当当当当~结果如下:(截取了部分)

     

     

    我们终于集齐了构成单页弹幕网址所需的target_id,后缀ID,只需要构造两个循环就可以实现完整的弹幕爬取(第一个循环构造每一集的基础网页,第二个循环构造单集内的弹幕页数)。

     

    目前来说,对于弹幕爬取(腾讯视频),单纯的headers伪装就能够畅通无阻,但也建议大家文明爬取,理性分析 :)

     

    至此,我们锅、碗和面都已经准备到位了,再把刚才各模块写的精简一些,然后就可以酣畅淋漓的吃大碗宽面了。

     

    Skrrrrrrrrrrr~

     

    最后附上完整代码:

    import requestsimport jsonimport pandas as pdimport osimport timeimport random
    
    #页面基本信息解析,获取构成弹幕网址所需的后缀ID、播放量、集数等信息。def parse_base_info(url,headers):    df = pd.DataFrame()        html = requests.get(url,headers = headers)    bs = json.loads(html.text[html.text.find( { ):-1])        for i in bs[ results ]:        v_id = i[ id ]        title = i[ fields ][ title ]        view_count = i[ fields ][ view_all_count ]        episode = int(i[ fields ][ episode ])        if episode == 0:            pass        else:            cache = pd.DataFrame({ id :[v_id], title :[title], 播放量 :[view_count], 第几集 :[episode]})            df = pd.concat([df,cache])    return df
    
    
    #传入后缀ID,获取该集的target_id并返回def get_episode_danmu(v_id,headers):    base_url =  https://access.video.qq.com/danmu_manage/regist?vappid=97767206&vsecret=c0bdcbae120669fff425d0ef853674614aa659c605a613a4&raw=1     pay = {"wRegistType":2,"vecIdList":[v_id],       "wSpeSource":0,"bIsGetUserCfg":1,       "mapExtData":{v_id:{"strCid":"wu1e7mrffzvibjy","strLid":""}}}    html = requests.post(base_url,data = json.dumps(pay),headers = headers)    bs = json.loads(html.text)    danmu_key = bs[ data ][ stMap ][v_id][ strDanMuKey ]    target_id = danmu_key[danmu_key.find( targetid ) + 9 : danmu_key.find( vid ) - 1]    return [v_id,target_id]
    
    
    #解析单个弹幕页面,需传入target_id,v_id(后缀ID)和集数(方便匹配),返回具体的弹幕信息def parse_danmu(url,target_id,v_id,headers,period):    html = requests.get(url,headers = headers)    bs = json.loads(html.text,strict = False)    df = pd.DataFrame()    for i in bs[ comments ]:        content = i[ content ]        name = i[ opername ]        upcount = i[ upcount ]        user_degree =i[ uservip_degree ]        timepoint = i[ timepoint ]        comment_id = i[ commentid ]        cache = pd.DataFrame({ 用户名 :[name], 内容 :[content], 会员等级 :[user_degree],                               弹幕时间点 :[timepoint], 弹幕点赞 :[upcount], 弹幕id :[comment_id], 集数 :[period]})        df = pd.concat([df,cache])    return df
    
    
    #构造单集弹幕的循环网页,传入target_id和后缀ID(v_id),通过设置爬取页数来改变timestamp的值完成翻页操作def format_url(target_id,v_id,end = 85):    urls = []    base_url =  https://mfm.video.qq.com/danmu?otype=json&timestamp={}&target_id={}%26vid%3D{}&count=80&second_count=5         for num in range(15,end * 30 + 15,30):        url = base_url.format(num,target_id,v_id)        urls.append(url)    return urls
    
    
    def get_all_ids(part1_url,part2_url,headers):    #分别获取1-30,31-46的所有后缀ID(v_id)    part_1 = parse_base_info(part1_url,headers)    part_2 = parse_base_info(part2_url,headers)    df = pd.concat([part_1,part_2])    df.sort_values( 第几集 ,ascending = True,inplace = True)    count = 1    #创建一个列表存储target_id    info_lst = []    for i in df[ id ]:        info = get_episode_danmu(i,headers)        info_lst.append(info)        print( 正在努力爬取第 %d 集的target_id  % count)        count += 1        time.sleep(2 + random.random())     print( 是不是发现多了一集?别担心,会去重的 )    #根据后缀ID,将target_id和后缀ID所在的表合并    info_lst = pd.DataFrame(info_lst)    info_lst.columns = [ v_id , target_id ]    combine = pd.merge(df,info_lst,left_on =  id ,right_on =  v_id ,how =  inner )    #去重复值    combine = combine.loc[combine.duplicated( id ) == False,:]    return combine
    
    
    #输入包含v_id,target_id的表,并传入想要爬取多少集def crawl_all(combine,num,page,headers):    c = 1    final_result = pd.DataFrame()    #print( Bro,马上要开始循环爬取每一集的弹幕了 )    for v_id,target_id in zip(combine[ v_id ][:num],combine[ target_id ][:num]):        count = 1        urls = format_url(target_id,v_id,page)        for url in urls:            result = parse_danmu(url,target_id,v_id,headers,c)            final_result = pd.concat([final_result,result])            time.sleep(2+ random.random())            print( 这是 %d 集的第 %d 页爬取..  % (c,count))            count += 1        print( ------------------------------------- )        c += 1    return final_result
    
    
    if __name__ ==  __main__ :        #《都挺好》1-30集的网址,31-46集的网址    #如果要爬取其他电视剧,只需要根据文章的提示,找到存储后缀ID的原网址即可    part1_url =  https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=x003061htl5,t00306i1e62,x003061htl5,b0030velala,w0030ilim7z,i0030r7v63u,z003044noq2,m0030sfinyr,c0030u884k7,k0030m5zbr7,l0030e5nglm,h0030b060vn,j003090ci7w,n0030falyoi,s00308u9kwx,p0030fohijf,g00303ob0cx,v0030960y6n,x0030bl84xw,v0030keuav1,t0030kups1i,n0030y2o52i,x0030s52mev,d0030xuekgw,o0030md1a2a,x0030peo3sk,d00303l5j4k,t0030aexmnt,a0030ybi45z,y0030wpe2wu&callback=jQuery191020844423583354543_1554200358596&_=1554200358597     part2_url =  https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=t0030epjqsi,g003035mi84,n00301fxqbh,h0030zivlrq,d0030qc1yu2,m0030q9ywxj,h0030j0eq19,j0030jks835,a00308xw434,l0030tb319m,x0030xogl32,g0030fju3w3,a0030vrcww0,l0030jzi1mi,c0030mq8yjr,u00302fdo8v,a0030w9g57k,n0030wnj6i8,j0030h91ouj,j00304eu73n,t00305kc1f5,i0030x490o2,u0030jtmlj2,d003031ey5h,w0850w594k6,l0854pfn9lg,f08546r7l7a,d0854s0oq1z,m08546pcd9k,p0854r1nygj&callback=jQuery191020844423583354543_1554200358598&_=1554200358599     headers = { User-Agent : Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 }        #得到所有的后缀ID,基于后缀ID爬取target_id    combine = get_all_ids(part1_url,part2_url,headers)        #设置要爬取多少集(num参数),每一集爬取多少页弹幕(1-85页,page参数),这里默认是爬取第一集的5页弹幕    #比如想要爬取30集,每一集85页,num = 30,page = 85    final_result = crawl_all(combine,num = 1,page = 5,headers = headers)    #final_result.to_excel( xxx.xlsx ) 可以输出成EXCEL格式的文件
    
    展开全文
  • Android弹幕01

    2016-03-02 15:41:34
    在B站或者其他视频网站看视频时,常常会打开...弹幕看起来很有意思,今天我们就来实现一个简单的弹幕效果。  从直观上,弹幕效果就是在一个ViewGroup上增加一些View,然后让这些View移动起来。所以,整体

    转:http://blog.csdn.net/goodlixueyong/article/details/50734551

    在B站或者其他视频网站看视频时,常常会打开弹幕效果,边看节目边看大家的吐槽。弹幕看起来很有意思,今天我们就来实现一个简单的弹幕效果。


           从直观上,弹幕效果就是在一个ViewGroup上增加一些View,然后让这些View移动起来。所以,整体的实现思路大概是这样的:

    1、定义一个RelativeLayout,在里面动态添加TextView。

    2、这些TextView的字体大小、颜色、移动速度、初始位置都是随机的。

    3、将TextView添加到RelativeLayout的右边缘,每隔一段时间添加一个。

    4、对每个TextView做平移动画,使得TextView从右向左移动。

    5、当TextView从左边移动出屏幕,将TextView从RelativeLayout中移除。

           有了思路下面就来看具体的代码。

           首先定义BarrageItem,用来存储每一个弹幕项的相关信息,包括字体内容、字体大小颜色、移动速度、垂直方向的位置、字体占据的宽度等。

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public class BarrageItem {  
    2.     public TextView textView;  
    3.     public int textColor;  
    4.     public String text;  
    5.     public int textSize;  
    6.     public int moveSpeed;//移动速度  
    7.     public int verticalPos;//垂直方向显示的位置  
    8.     public int textMeasuredWidth;//字体显示占据的宽度  
    9. }  

           然后定义BarrageView,由于弹幕的字体颜色大小和移动速度都是随机的,需要定义最大最小值来限定它们的范围,然后通过产生随机数来设置它们在这个范围内的值。另外还需要定义弹幕的文本内容,这里是直接写死的一些固定值。

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1.     private Context mContext;  
    2.     private BarrageHandler mHandler = new BarrageHandler();  
    3.     private Random random = new Random(System.currentTimeMillis());  
    4.     private static final long BARRAGE_GAP_MIN_DURATION = 1000;//两个弹幕的最小间隔时间  
    5.     private static final long BARRAGE_GAP_MAX_DURATION = 2000;//两个弹幕的最大间隔时间  
    6.     private int maxSpeed = 10000;//速度,ms  
    7.     private int minSpeed = 5000;//速度,ms  
    8.     private int maxSize = 30;//文字大小,dp  
    9.     private int minSize = 15;//文字大小,dp  
    10.   
    11.     private int totalHeight = 0;  
    12.     private int lineHeight = 0;//每一行弹幕的高度  
    13.     private int totalLine = 0;//弹幕的行数  
    14.     private String[] itemText = {"是否需要帮忙""what are you 弄啥来""哈哈哈哈哈哈哈""抢占沙发。。。。。。""************""是否需要帮忙","我不会轻易的狗带""嘿嘿""这是我见过的最长长长长长长长长长长长的评论"};  
    15.     private int textCount;  
    16. //    private List<BarrageItem> itemList = new ArrayList<BarrageItem>();  
    17.   
    18.     public BarrageView(Context context) {  
    19.         this(context, null);  
    20.     }  
    21.   
    22.     public BarrageView(Context context, AttributeSet attrs) {  
    23.         this(context, attrs, 0);  
    24.     }  
    25.   
    26.     public BarrageView(Context context, AttributeSet attrs, int defStyleAttr) {  
    27.         super(context, attrs, defStyleAttr);  
    28.         mContext = context;  
    29.         init();  
    30.     }  

           如果弹幕显示的垂直位置是随机的,就会出现垂直方向上弹幕重叠的情况,所以需要根据高度对垂直方向按照弹幕高度的最大值等分,然后让弹幕在这些指定的垂直位置随机分布。这个值在onWindowFocusChanged里计算,因为在这个方法中通过View的getMeasuredHeight()得到的高度不为空。

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. @Override  
    2. public void onWindowFocusChanged(boolean hasWindowFocus) {  
    3.     super.onWindowFocusChanged(hasWindowFocus);  
    4.     totalHeight = getMeasuredHeight();  
    5.     lineHeight = getLineHeight();  
    6.     totalLine = totalHeight / lineHeight;  
    7. }  

           通过Handler的sendEmptyMessageDelayed每隔随机的时间产生一个弹幕项。下面的代码设置弹幕项的属性。

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. class BarrageHandler extends Handler {  
    2.     @Override  
    3.     public void handleMessage(Message msg) {  
    4.         super.handleMessage(msg);  
    5.         generateItem();  
    6.         //每个弹幕产生的间隔时间随机  
    7.         int duration = (int) ((BARRAGE_GAP_MAX_DURATION - BARRAGE_GAP_MIN_DURATION) * Math.random());  
    8.         this.sendEmptyMessageDelayed(0, duration);  
    9.     }  
    10. }  
    11.   
    12. private void generateItem() {  
    13.     BarrageItem item = new BarrageItem();  
    14.     String tx = itemText[(int) (Math.random() * textCount)];  
    15.     int sz = (int) (minSize + (maxSize - minSize) * Math.random());  
    16.     item.textView = new TextView(mContext);  
    17.     item.textView.setText(tx);  
    18.     item.textView.setTextSize(sz);  
    19.     item.textView.setTextColor(Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256)));  
    20.     item.textMeasuredWidth = (int) getTextWidth(item, tx, sz);  
    21.     item.moveSpeed = (int) (minSpeed + (maxSpeed - minSpeed) * Math.random());  
    22.     if (totalLine == 0) {  
    23.         totalHeight = getMeasuredHeight();  
    24.         lineHeight = getLineHeight();  
    25.         totalLine = totalHeight / lineHeight;  
    26.     }  
    27.     item.verticalPos = random.nextInt(totalLine) * lineHeight;  
    28.     showBarrageItem(item);  
    29. }  

           将每一个弹幕项添加到视图上,并给View添加一个TranslateAnimation动画,当动画结束时,将View从视图上移除。


    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. private void showBarrageItem(final BarrageItem item) {  
    2.   
    3.         int leftMargin = this.getRight() - this.getLeft() - this.getPaddingLeft();  
    4.   
    5.         LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);  
    6.         params.addRule(RelativeLayout.ALIGN_PARENT_TOP);  
    7.         params.topMargin = item.verticalPos;  
    8.         this.addView(item.textView, params);  
    9.         Animation anim = generateTranslateAnim(item, leftMargin);  
    10.         anim.setAnimationListener(new Animation.AnimationListener() {  
    11.             @Override  
    12.             public void onAnimationStart(Animation animation) {  
    13.   
    14.             }  
    15.   
    16.             @Override  
    17.             public void onAnimationEnd(Animation animation) {  
    18.                 item.textView.clearAnimation();  
    19.                 BarrageView.this.removeView(item.textView);  
    20.             }  
    21.   
    22.             @Override  
    23.             public void onAnimationRepeat(Animation animation) {  
    24.   
    25.             }  
    26.         });  
    27.         item.textView.startAnimation(anim);  
    28.     }  
    29.   
    30.     private TranslateAnimation generateTranslateAnim(BarrageItem item, int leftMargin) {  
    31.         TranslateAnimation anim = new TranslateAnimation(leftMargin, -item.textMeasuredWidth, 00);  
    32.         anim.setDuration(item.moveSpeed);  
    33.         anim.setInterpolator(new AccelerateDecelerateInterpolator());  
    34.         anim.setFillAfter(true);  
    35.         return anim;  
    36.     }  
           这样就完成了弹幕的功能,实现原理并不复杂。可以根据具体的需求来增加更多的控制,如控制每一行弹幕不重复,控制弹幕移动的Interpolator产生不同的滑动效果等等。


    源码下载 


    0
    展开全文
  • Android弹幕效果实现

    万次阅读 热门讨论 2016-02-25 15:08:42
    弹幕看起来很有意思,今天我们就来实现一个简单的弹幕效果。  从直观上,弹幕效果就是在一个ViewGroup上增加一些View,然后让这些View移动起来。所以,整体的实现思路大概是这样的: 1、定义一个RelativeLayout...

           在B站或者其他视频网站看视频时,常常会打开弹幕效果,边看节目边看大家的吐槽。弹幕看起来很有意思,今天我们就来实现一个简单的弹幕效果。

           从直观上,弹幕效果就是在一个ViewGroup上增加一些View,然后让这些View移动起来。所以,整体的实现思路大概是这样的:

    1、定义一个RelativeLayout,在里面动态添加TextView。

    2、这些TextView的字体大小、颜色、移动速度、初始位置都是随机的。

    3、将TextView添加到RelativeLayout的右边缘,每隔一段时间添加一个。

    4、对每个TextView做平移动画,使得TextView从右向左移动。

    5、当TextView从左边移动出屏幕,将TextView从RelativeLayout中移除。

           有了思路下面就来看具体的代码。

           首先定义BarrageItem,用来存储每一个弹幕项的相关信息,包括字体内容、字体大小颜色、移动速度、垂直方向的位置、字体占据的宽度等。

     

    public class BarrageItem {
        public TextView textView;
        public int textColor;
        public String text;
        public int textSize;
        public int moveSpeed;//移动速度
        public int verticalPos;//垂直方向显示的位置
        public int textMeasuredWidth;//字体显示占据的宽度
    }

     

           然后定义BarrageView,由于弹幕的字体颜色大小和移动速度都是随机的,需要定义最大最小值来限定它们的范围,然后通过产生随机数来设置它们在这个范围内的值。另外还需要定义弹幕的文本内容,这里是直接写死的一些固定值。

     

     

        private Context mContext;
        private BarrageHandler mHandler = new BarrageHandler();
        private Random random = new Random(System.currentTimeMillis());
        private static final long BARRAGE_GAP_MIN_DURATION = 1000;//两个弹幕的最小间隔时间
        private static final long BARRAGE_GAP_MAX_DURATION = 2000;//两个弹幕的最大间隔时间
        private int maxSpeed = 10000;//速度,ms
        private int minSpeed = 5000;//速度,ms
        private int maxSize = 30;//文字大小,dp
        private int minSize = 15;//文字大小,dp
    
        private int totalHeight = 0;
        private int lineHeight = 0;//每一行弹幕的高度
        private int totalLine = 0;//弹幕的行数
        private String[] itemText = {"是否需要帮忙", "what are you 弄啥来", "哈哈哈哈哈哈哈", "抢占沙发。。。。。。", "************", "是否需要帮忙","我不会轻易的狗带", "嘿嘿", "这是我见过的最长长长长长长长长长长长的评论"};
        private int textCount;
    //    private List<BarrageItem> itemList = new ArrayList<BarrageItem>();
    
        public BarrageView(Context context) {
            this(context, null);
        }
    
        public BarrageView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public BarrageView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mContext = context;
            init();
        }


           如果弹幕显示的垂直位置是随机的,就会出现垂直方向上弹幕重叠的情况,所以需要根据高度对垂直方向按照弹幕高度的最大值等分,然后让弹幕在这些指定的垂直位置随机分布。这个值在onWindowFocusChanged里计算,因为在这个方法中通过View的getMeasuredHeight()得到的高度不为空。

     

     

        @Override
        public void onWindowFocusChanged(boolean hasWindowFocus) {
            super.onWindowFocusChanged(hasWindowFocus);
            totalHeight = getMeasuredHeight();
            lineHeight = getLineHeight();
            totalLine = totalHeight / lineHeight;
        }

     

           通过Handler的sendEmptyMessageDelayed每隔随机的时间产生一个弹幕项。下面的代码设置弹幕项的属性。

     

        class BarrageHandler extends Handler {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                generateItem();
                //每个弹幕产生的间隔时间随机
                int duration = (int) ((BARRAGE_GAP_MAX_DURATION - BARRAGE_GAP_MIN_DURATION) * Math.random());
                this.sendEmptyMessageDelayed(0, duration);
            }
        }
        
        private void generateItem() {
            BarrageItem item = new BarrageItem();
            String tx = itemText[(int) (Math.random() * textCount)];
            int sz = (int) (minSize + (maxSize - minSize) * Math.random());
            item.textView = new TextView(mContext);
            item.textView.setText(tx);
            item.textView.setTextSize(sz);
            item.textView.setTextColor(Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            item.textMeasuredWidth = (int) getTextWidth(item, tx, sz);
            item.moveSpeed = (int) (minSpeed + (maxSpeed - minSpeed) * Math.random());
            if (totalLine == 0) {
                totalHeight = getMeasuredHeight();
                lineHeight = getLineHeight();
                totalLine = totalHeight / lineHeight;
            }
            item.verticalPos = random.nextInt(totalLine) * lineHeight;
            showBarrageItem(item);
        }


           将每一个弹幕项添加到视图上,并给View添加一个TranslateAnimation动画,当动画结束时,将View从视图上移除。

     

    private void showBarrageItem(final BarrageItem item) {
    
            int leftMargin = this.getWidth();
    
            LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
            params.topMargin = item.verticalPos;
            this.addView(item.textView, params);
            Animation anim = generateTranslateAnim(item, leftMargin);
            anim.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
    
                }
    
                @Override
                public void onAnimationEnd(Animation animation) {
                    item.textView.clearAnimation();
                    BarrageView.this.removeView(item.textView);
                }
    
                @Override
                public void onAnimationRepeat(Animation animation) {
    
                }
            });
            item.textView.startAnimation(anim);
        }
    
        private TranslateAnimation generateTranslateAnim(BarrageItem item, int leftMargin) {
            TranslateAnimation anim = new TranslateAnimation(leftMargin, -item.textMeasuredWidth, 0, 0);
            anim.setDuration(item.moveSpeed);
            anim.setInterpolator(new AccelerateDecelerateInterpolator());
            anim.setFillAfter(true);
            return anim;
        }

           这样就完成了弹幕的功能,实现原理并不复杂。可以根据具体的需求来增加更多的控制,如控制每一行弹幕不重复,控制弹幕移动的Interpolator产生不同的滑动效果等等。

     

     

    源码下载 

           欢迎关注我的公众号一起交流学习

         

    展开全文
  • 弹幕看起来很有意思,今天我们就来实现一个简单的弹幕效果。  从直观上,弹幕效果就是在一个ViewGroup上增加一些View,然后让这些View移动起来。所以,整体的实现思路大概是这样的: 1、定义一个RelativeLayout,...
  • Android直播中弹幕效果实现

    万次阅读 2016-11-17 15:02:17
    弹幕看起来很有意思,今天我们就来实现一个简单的弹幕效果。  从直观上,弹幕效果就是在一个ViewGroup上增加一些View,然后让这些View移动起来。所以,整体的实现思路大概是这样的:1、定义一个RelativeLayout,在...
  • python大作业——B站弹幕数据爬取与分析

    千次阅读 多人点赞 2020-07-07 14:37:29
    前段时间要写一个Python大作业,选题为B站弹幕数据分析,由于是Python新手,所以参考了以下的文档,再次感谢分享技术的人 ...B站弹幕数据分析,首先我们需要抓取到B站视频的弹幕数据,才能进行数据分析 选取分析的.
  • 仿唔哩头条弹幕

    2017-11-23 09:22:15
    最近项目遇到个有意思的需求,将评论当弹幕在屏幕上划过,并且实现可以...其中遇到过一个问题 因为我的DMView是一个 HorizontalScrollView所以在很长的弹幕超过一屏幕的时候 屏幕可以左右滑动 ,解决思路是 int view
  • 爬虫爬取弹幕思路分析

    千次阅读 2019-04-11 10:09:39
    相对于一般电影 OR 电视剧评论,弹幕能够贴合剧情,进行更多有意思的脑洞分析。图源《让子弹飞》作者 | 周志鹏责编 | 仲培艺注:“前传”《都挺好》弹幕分析文章所有数据(39W+)均基于本文代码爬取。每次...
  • Unity模拟弹幕效果(一)

    千次阅读 2016-10-11 11:45:35
    最近看到好多平台都有弹幕,觉得挺有意思,就做试试看,但也有不少问题。 1.制作弹幕字体预制 新建一个unity工程,新建了一个CanvasText,然后制作脚本,主要用来字体移动效果和文本输入,建立脚本叫TextItem...
  • 如果你去抖音只是为了看小视频就少了一大乐趣,评论区才是最有趣的地方,边看视频边看评论的弹幕是不是更有意思 首先看下效果图 预览效果截图 实现思路 把最新的评论以弹幕的方式展示出来,随机生成字体颜色 局部...
  • 最近做项目做的火热,自己的学习反而有些落后了,但是没关系,毕竟现在学习的东西都是以后要一直使用...然后就发现了一个用JQuery技术实现的弹幕网页。  首先在VS默认的MVC网站中加了点东西,把弹出弹幕要用的标签都写
  • Chrome插件-《一叶》在任意网页开启实时弹幕 聊天窗口 留言板.zip。一叶是一款很有想法产品,但目前用户量还是很少, 对此,我个人也有一些想法,如果官方可以效仿pokemongo这类寻宝游戏,在各大网站主页对应留言板...
  • 前言 相信大家在网页上看电影话,都开着弹幕看吧?我觉得这样确实比较有意思,有时候可以看到网友各种神评论,有趣又好玩,下面就来给大家介绍一下爬取弹幕数据保存在txt文件中然后在...
  • 简介:为球员、球迷和粉丝提供体育图片直播和交流互动体育社区 弹幕的形式让球迷互动交流更有趣! 总结:既提供体育赛事实时照片,又提供给体育密们一个同好交流平台,还有赛事直播,生动、有趣! 此小程序已经...
  • 前言 大家好,我是J哥。 综艺,是我们劳累了一天放松方式,...本文通过爬取《令人心动offer》第二季13万+弹幕,进行可视化分析和情感分析,完整代码后台回复「offer」即可免费获取。 数据获取 《令人心动offer》
  • 这不是马上到2.14情人节了吗,寻思着做了一个比较有意义的网站,网站功能很简单,就是实现用户留言,并将留言的内容以弹幕的形式展示在网页上,项目很简单,但是挺有意思的,网站目前已经搭建在服务器上了,可以在我...
  • 刚刚在看一本书,里面有个很有意思的描述:学日语是“笑着进去,哭着出来”,为什么呀?日语中存在着一些汉字,一个完全不懂日语的人走到日本的一个城市的街道上,看着招牌大概能猜出来那些商店卖什么,这样一些人...
  • “艺术活” 于是就写了爬虫寻思着爬些热门视频信息学习学习命名 “技术” ,也寻思着现在可以使用 NLP 技术写古诗,那我可不可以做整个给视频命名,这需要大量样本数据,因为是个人感觉有意思,所以优先级比较...
  • 该标签不是html3.2的一部分,并且只支持MSIE3以后内核,所以如果你使用非IE内核浏览器(如:Netscape)可能无法看到下面一些很有意思的效果,该标签是个容器标签。 语法: <marquee></marquee> 以下是一...
  • 发现在B站学习还是非常有意思的,主要的功劳应该就是弹幕。 趣味性我觉得是现在我国教育缺失的一部分,而当打开弹幕的时候,你就会发现,很多跟你一起学习的同学会说一些俏皮话,让你走神的时候不会出现困顿,当...
  • 抓取B站所有用户信息(11/29更新数据)

    千次阅读 热门讨论 2019-04-14 10:31:07
    目前我所想到的一些感兴趣的任务有,B站所有用户、B站视频下载、B站弹幕和评论分析(这个就有意思了,弹幕和评论是可以知道是谁发的,那么当你有了B站所有弹幕和评论的时候,你就可以从一个人发的弹幕和评论分析这个...
  • 我就义无反顾地报了,周四晚上第一节课,整节课听下来感觉收获还是蛮多,学姐本身知识经验都过硬,再加上她讲课也很有意思弹幕里大佬时不时讲两句,还是挺欢乐,到后面甚至还出现了拖堂现象(哈哈哈哈)。...
  • 说起来之前也推荐了不少的小程序了,不过有很多网友后台私信我,希望能够再推荐一些有意思的小程序,那今天微小程就给大家再安利几款实用有趣的小程序吧。 第一个推荐:手持弹幕,还记得那个地铁上一个男孩手持...
  • 安洵杯 RE CrackMeWP

    2019-12-01 19:30:17
    然后开始动调,到达红色位置出现了神奇的一出的窗口不是Exception而是hook,挺有意思的,让后只能F7进去看看了。 hook技术类似于patch,将系统函数的调用执行流程给改掉了,这里似乎用的不是inline hook,用...
  • 曾经有个人,叫MC天佑,...突然心血来潮搜索了下——MC天佑,顺便听了听他喊麦,边听边看弹幕一片嘲讽喊麦声音。人真有意思,认知也不理性。生于此,灭于此,成也平台,败也平台。时崎慕意刚开始接触互联网...
  • 弹幕飞一会儿——“要有风,要有肉”;“要有火锅,要有雾”;...相对于一般电影 OR 电视剧评论,弹幕能够贴合剧情,进行更多有意思的脑洞分析。 图源《让子弹飞》 作者 |周志鹏 责编| 仲培艺 注:“...

空空如也

空空如也

1 2
收藏数 40
精华内容 16
关键字:

有意思的弹幕