发现更多的B站UP主——爬虫+简易数据挖掘(1)

整个项目使用python3、PHP、和MariaDB

整个项目的具体思路是:

  1. 爬取所有bilibili的用户,筛选出level6的用户,存入数据库。
  2. 处理所有UP主的投稿倾向。
  3. 用户输入自己的B站ID。
  4. 爬取该用户的关注列表。
  5. 获取关注列表中所有level6的UP主的投稿倾向。
  6. 针对不同倾向的UP主进行分类统计。
  7. 获取各个分区中和用户口味最相近且用户尚未关注的UP主,并推荐给用户。

在所有事情开始之前,我们需要设计数据库才行。根据需求,我们就先搭建一个最简单的数据库结构。

用户信息表:user。表内三个字段含义分别为:

  • uid:用户ID
  • uname:用户名称
  • ulevel:用户等级

71m9owbngrqitp4

UP主投稿倾向表:usertendency。表内四个字段为:

  • uid:用户ID
  • tendency:用户所有投稿按分类计算平均每稿点击量
  • procount:用户所有投稿按统计各分类投稿数量
  • protendency:分析用户所擅长的投稿种类

cmcu1bp9tu2ra33iod

视频分类到B站分类ID的对照表:vediotype。表内三个字段为:

  • typeid:B站中登记的分类ID
  • typename:实际的分类名称
  • fathertypename:所属的父分类的名称

rpvfbhffh0_thef

第一步:爬取所有bilibili的用户,筛选出level6的用户,存入数据库。

如果没有反爬机制,爬虫实现其实特别简单。

首先,使用chrome的F12工具打开你想爬取的网页,选择Network栏23LWA`50{4B00J{KWVJ5XZ7

接下来,在所有的请求包中寻找包含有你需要信息的包,B站的用户信息包名字叫做GetInfo。当然,以后包名可能会改动。

我们打开这个请求包,查看请求内容SSAC6[3(([[KJ~PSBILB$`U

我们可以看出,这是个POST请求包,请求地址为http://space.bilibili.com/ajax/member/GetInfo

知道了请求的地址以及方式接下来我们查看请求的内容。IRWKVEJ8G$8H@Y))T3U)D{T

我们能看到,请求内容就是一个mid,表明你想获取的UP主的ID。

接下来我们就要使用python3的requests包来模拟请求的发送过程。

url = "http://space.bilibili.com/ajax/member/GetInfo"
data = {"mid":tuid}
r = requests.post(url,data=data)

然而这样会获取到一个错误页面,原因是因为我们没有添加请求头。

B}U_C][N}RL}`K$`FTP8A2Y

简单的办法就是将这些请求全部添加到请求头中打包发送。

url = "http://space.bilibili.com/ajax/member/GetInfo"
head = {'Accept':'application/json, text/javascript, */*; q=0.01',
        'Accept-Encoding':'gzip, deflate',
        'Accept-Language':'zh-CN,zh;q=0.8',
        'Connection':'keep-alive',
        'Content-Length':'12',
        'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
        'Cookie':'fts=1472448094; LIVE_BUVID=151be91a7d2ed257be99c4b6ed5bb997; LIVE_BUVID__ckMd5=b7b78d78ce40af58; DedeUserID=1809348; DedeUserID__ckMd5=3270394b3b7228d9; SESSDATA=8883e5f5%2C1475067975%2Cadc5c28f; sid=9hhgtulj; LIVE_LOGIN_DATA=666a9dc04baf89124aab64a1c738aa6f607eeee6; LIVE_LOGIN_DATA__ckMd5=c9b8e68487180ce2; rlc_time=1472720031273; CNZZDATA2724999=cnzz_eid%3D1606070337-1472481179-http%253A%252F%252Fwww.bilibili.com%252F%26ntime%3D1472720049; _cnt_dyn=null; _cnt_pm=0; _cnt_notify=0; uTZ=-480; _dfcaptcha=b1f95a8b3a1749d199dd7a5eec34baf3',
        'Host':'space.bilibili.com',
        'Origin':'http://space.bilibili.com',
        'Referer':'http://space.bilibili.com/11267281',
        'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36',
        'X-Requested-With':'XMLHttpRequest'
        }
data = {"mid":tuid}
r = requests.post(url,data=data,headers=head)

但是爬虫对网络的请求量很大,所以,我们需要尽可能的精简内容。

经过逐条删除检查之后,我们发现B站只会验证请求头中的Referer这一条。

所以整个请求就被精简成了

url = "http://space.bilibili.com/ajax/member/GetInfo"
head = {'Referer':'http://space.bilibili.com'}
data = {"mid":tuid}
r = requests.post(url,data=data,headers=head)

接下来,我们只要动态的修改tuid,然后发送请求,就可以获取数据了。

获取到的数据在r.text中,内容和chrome中的应该是一样的。LQL@C@A{IFCEAK$_)R1G]{Y

我们能看到数据是json格式的字符串。我们可以通过json包的loads函数将他整理成一个可以查询的dict,同样,可以使用dumps方法将dict打包成json字符串。

info = json.loads(r.text)

我们先获取100个数据看看。

很快就报错了。因为B站的用户ID是有坑的,B站没有0号ID和12号ID,所以我们要使用try…except…来过滤掉有问题的数据包。同样,我们也筛选掉所有level6以下的用户。

值得一提的是,B站中是有很多无效ID的,甚至出现了连续1000个无效ID,所以我们不能把读到的包全部无效作为判断是否已经读完所有用户的标准。

好了,我们现在获取到了数据,我们需要把它保存到数据库中。

python3连接mysql的数据库我们使用pymysql包即可。需要注意的一点就是,如果有unicode输入需求的话,连接时你需要指定编码格式,否则会显示为乱码。

这里我们建立一个名为bilibili_info的数据库,然后建立一个user的表,里面有三列,用户的id(uid),用户的昵称(uname),用户的等级(ulevel)。具体建表操作不再赘述。

操作方式如下:

#建立连接
conn = pymysql.connect(host="localhost",user="****",passwd="******",db="********",use_unicode=True, charset="utf8")
#建立指向数据库的指针
cur = conn.cursor()

#用execute()方法可以执行SQL语句
#插入数据
cur.execute('insert into user (uid, uname ,ulevel) values (%s, %s, %s)', [uid,name,level])
#提交修改
conn.commit()

#获取数据
cur.execute("select * from user;")
#取出获取到的结果
res = cur.fetchall()
#打印试试?
print(res)

#关闭指针对象 
cur.close()
#关闭数据库连接对象
conn.close()

 

需要提醒的一点是,由于爬虫常常会因为等待请求包而耗费时间,所以我们在不影响别人网站正常运作的情况下可以考虑使用多线程或者分布式。

分布式写法参照python 分布式计算

在这里我们使用多线程。

具体的思路是本地维护一个queue,每个线程运行时向队列获取一个起始值,然后爬取指定数量的数据即可。

demo:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
填好user\passwd\db即可使用
爬取所有level6的UP存入指定数据库中
'''
__author__ = 'kasora'

import requests
import json
import time,queue
import threading
import pymysql
import sys
#taskqueue为任务队列
taskqueue = queue.Queue()
#flag为备用
flag = True
#jump为跳跃值
jump = 1000;

url = "http://space.bilibili.com/ajax/member/GetInfo"
head = {'Referer':'http://space.bilibili.com'}

user = ""
passwd = ""
db =  ""

def getdata():
    #连接数据库
    conn = pymysql.connect(host="localhost",user=user,passwd=passwd,db=db,use_unicode=True, charset="utf8")
    cur = conn.cursor()
    #任务队列取空则终止
    while(flag and not taskqueue.empty()):             
        #从队列获取起始值
        x = taskqueue.get()
        #设定爬虫爬取的范围
        lid = x*jump
        #获取lid-lid+jump范围的数据
        for i in range(lid,lid+jump):
            #设定参数
            params = {"mid": i}
            try:
                r = requests.post(url,data=params,headers=head)
                info = json.loads(r.text)
                #通过分析数据包获取需要的信息
                level = info['data']['level_info']['current_level']
                name = info['data']['name']
                uid = info['data']['mid']                
                if(level==6):
                    #入库
                    cur.execute('insert into user (uid, uname ,ulevel) values (%s, %s, %s)', [uid,name,level])
                    conn.commit()
            except:
                pass
    cur.close()#关闭指针对象 
    conn.close()#关闭数据库连接对象

if __name__=='__main__':
    #动态设定爬取区间
    args = sys.argv
    datastart = 0
    dataend = 45000000//jump
    if(len(args)==2):
        dataend = args[1]//jump
    if(len(args)==3):
        datastart = args[1]//jump
        dataend = args[2]//jump
    for i in range(datastart,dataend):
        taskqueue.put(i)
    #开启线程进行爬取
    for i in range(30):
        sthread = threading.Thread(target=getdata)
        sthread.start()
    #每30秒打印当前爬取进度
    while(True):
        conn = pymysql.connect(host="localhost",user=user,passwd=passwd,db=db,use_unicode=True, charset="utf8")
        cur = conn.cursor()
        cur.execute("select count(*) from user;")
        res = cur.fetchall()
        print(res[0][0])
        print(taskqueue.qsize())
        time.sleep(30)
    

按惯例,感谢ntzyz的支持,虽然他的blog处于日常挂掉的状态(

发表评论

电子邮件地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据