亲眼见证明星微博“发大水”:用R爬虫记录艺人微博数据注水全过程

亲眼见证明星微博“发大水”:用R爬虫记录艺人微博数据注水全过程

上周三中午,我吃饭的时候打开微博,恰好撞见我喜欢的一位演员姐姐十几秒前发博了。电光火石之间,我马上意识到:第0分钟开始就能看到数据,这是观察这个姐姐微博真实人气的绝佳机会,也是观察这个姐姐的微博是否发水的绝佳时间窗的起点。

作为一个非著名过气网黄,我经常盯我自己文章的传播数据。根据过往经验总结,对于基于时间线传播的内容来说,从第0分钟开始的那短暂几分钟、十几分钟的数据有以下特点:

  • 内容不可能火到上热搜,所以热搜榜没有影响;
  • 机器人水军要去主动联系还要排期,因此一时半会也没法到位;
  • 粉丝数据组轮博刷评论,也要去群里和超话先布置任务什么的,因此也没法这么快就进来。

因此,刚开始那几分钟、十几分钟的数据,是最“干净”的。

当后面热搜、水军、粉丝数据组进来之后,只要一和最初那十几分钟的干净数据一对比,一下子就都能现原形了。

然而,要从第0分钟开始收集数据,靠人工盯和手抄是不太靠谱的。

一方面,你不知道明星什么时候发微博;另一方面,手刷再手记得话,会占用自己大量的时间,记录效率也低。

为了解决这两个问题,我想了想,作为一个流行病学搬砖工,我最会的就是R了,那就用R写个微博爬虫自动监视明星主页吧。

如何在R中实现微博爬虫

(对技术细节不感兴趣的观众朋友们,你们可以划过这部分,直接看下一个大标题“爬虫监视结果:娱乐圈几乎人均发水,区别只在多少”)

写爬虫,有一个基本格言,叫做“可见即可爬”。不过,微博web版对爬虫设置了登陆屏障,直接通过爬虫访问微博,只能抓回一个跳转中的网页:“Sina Visitor System”。

幸好,微博移动端是可以被爬虫直接访问的。


  • 找到要爬取的具体网址

观察微博移动端加载全过程的网络活动,可以发现:微博移动端的全部微博数据,都可以在一个json中找到。

再观察这个json的地址:m.weibo.cn/api/containe

这里面有两个参数value和containerid,有可能是动态的。

经过确认,value=后面的这个东西是固定的用户ID,也就是每个人微博主页的uid;containerid,不知道从哪儿来的,但是很幸运的是,每次加载,这个containerid都是不变的。

作为一个学流行病学而非计算机专业的人,我直觉觉得这个containerid怎么生成对我来说应该是个很难查的问题。鉴于每次加载它这个链接都不会变,那本外行就懒得去研究怎么找到这个containerid了,直接复制这个json的链接拿来爬就是。

URLs = 'https://m.weibo.cn/api/container/getIndex?type=uid&value=1757744065&containerid=1076031757744065'

取回json之后,照例美化一下格式,方便阅读和后续数据提取:

### require a package to format json file from weibo
require(jsonlite)
  
### read json file for weibo lists
json=readLines(jsonURL,encoding = 'UTF-8')
  
### format the json file fetched
json=prettify(json)
writeLines(json,'D:/desktop/weibocrawler/json.txt')

这里输出了txt,是因为jsonlite这个轮子美化格式换行用的是直接加换行符,这种换行符grep()是识别不了的,因此得弄成txt让readLines()重新读一遍,才能做成grep()能够识别的分行的对象。

观察这个json,可以发现它一次会包含10条最近的微博,其中每条微博开始和结束的时候长这样,而且用黄色标识出来的这两个字段是唯一的:

红线以上是上一条微博,红线以下是下一条微博

所以,用这两个黄色的开始、结束标志作为分段,撰写代码如下:

### separate the 10 weibos
page=readLines('json.txt')
starts=grep('                "card_type": 9,',page)
ends=grep('                "show_type": 0',page)


  • 将取回的json中含有的数据提取出来

我前面遇到的主要问题是,我不知道明星啥时候发微博。所以,爬虫要能够监测新微博的出现,就不能指定具体哪条微博。

此外,每条微博我都想监测起码两天以上的数据变化,如果只抓最新两三条微博,那么明星万一失心疯连着刷个五六条的,我的程序就不抓了,这可不行。

固然,我可以每次抓回来列表之后取一下发表时间再算算这微博还要不要抓,但是这实在是太麻烦了。既然这个json文件一次能提供个人主页全部前10条微博的数据,那就每次爬取都把这最新10条微博的数据全都记录下来好了!后边如果有空,再慢慢迭代添加算时间的代码吧。

接下来是每条微博的实时累计互动数据。观察json文件,可以发现我们要的数据在以下这些字段里:

因此,通过如下代码,把这些数据用gsub()提取出来,然后放进数据库。

#### create the data matrix for saving data: weibo ID, timestamp, repost, comment, like
  topwb=length(grep('"text": "置顶",',page))!=0
  db=matrix(nrow = length(starts),ncol = 8)
  if (topwb) {db=matrix(nrow = length(starts)-1,ncol = 8)}
  colnames(db)=c('weibo_ID', 'weibo_text', 'timestamp', 'repost', 'comment', 'like', 'isRepost', 'fromChaoHua')

#### fill in the data
for (i in 1:length(starts)) {
  weibo=page[starts[i]:ends[i]]

  # remove pinned weibo
  if (length(grep('"text": "置顶",',weibo))!=0) {next}

  # correct i for skipped weibos
  rowdb=length(db[,1])-(length(starts)-i)

  # get attributes
  wbid=gsub('                    "bid": "','',weibo[grep('                    "bid": "',weibo)])
  wbid=gsub(',|"|\\s','',wbid)
  wbtext=gsub('                    "raw_text": "','',weibo[grep('"raw_text": "',weibo)])
  wbtext=gsub('",','',wbtext)
  repo=gsub("[^0-9]",'',weibo[grep('"reposts_count": ',weibo)])
  comment=gsub("[^0-9]",'',weibo[grep('"comments_count": ',weibo)])
  like=gsub("[^0-9]",'',weibo[grep('"attitudes_count": ',weibo)])
  if (length(grep('                        "retweeted": 1,',weibo))>0) {repost=1} else {repost=0}
  if (grepl('超话',weibo[grep('                   "source": ',weibo)][1])) {fromch=1} else {fromch=0}

  # if a weibo is a repost, keep only the data from the user being observed
  db[rowdb,1]=as.character(wbid[length(wbid)])
  db[rowdb,2]=wbtext[length(wbtext)]
  db[rowdb,3]=as.numeric(Sys.time())
  db[rowdb,4]=repo[length(repo)]
  db[rowdb,5]=comment[length(comment)]
  db[rowdb,6]=like[length(like)]
  db[rowdb,7]=repost
  db[rowdb,8]=fromch
}

第二部分就是简单转移了下数据,看起来很多此一举,其实是因为,如果一条微博是转发微博,那么前面的那些字段抓回来的数据都会有两套。我们只关心明星的数据,不关心原博的,所以需要只留第二个。

由于每条微博回来既可能是转发也可能是原创,所以这些数据的长度并不一定是2,也可能是1,所以wbid[2]这种写法是要不得的。动态响应的最佳写法,是直接取所有数据里面的最后一个,这样无论一条还是两条,都能取到想要的那一个。

最后,把以上过程全部打包在一起,写成一个函数,方便调用:

wbwatcher = function(jsonURL) {
  <.........>
  return(db)
}


  • 自动监视自动存数据

光写一个函数,肯定是不够的。所以这里我们要搞一个自动轮询的循环。

既然都写爬虫了,那就干脆一次多整点明星上去吧。我刚看完浪姐,那就以30位浪姐作为样本,然后再加上肉眼确认过不买水军的何老师作为阴性对照,加上大家公认的四字水后作阳性对照。

接下来是循环本体:

while (Sys.time()<"2020-11-10 23:59:59 CEST") { # define stop time
  for (i in 1:length(urltable$URLs)) {
    URL=urltable$URLs[i]
    if (length(grep('m.weibo.cn/statuses/show?id=',URL,fixed = T))!=0) {
      try({                                        # use try() to prevent breaks from errors caused by network failure
        db=wbpostwatcher(URL)
      })  
      if (!is.na(db[1,1])) {
        print(paste('Success!', 'Checked', urltable$names[i], ' at',Sys.time()))
      }
    }
    else {
      try({
        db=wbwatcher(URL)   # another similar function, used to watch a single weibo only
      })  
      if (!is.na(db[1,1])) {
        print(paste('Success!', 'Checked', urltable$names[i], ' at',Sys.time()))
      }
    }
    db=as.table(db)
    if (!is.na(db[1,1])) {
      db=cbind(rep(urltable$names[i],length(db[,1])),db)
      colnames(db)[1]='name'
      write.table(db, "D:/desktop/weibocrawler/db_sisters.csv", sep = ",", col.names = !file.exists("D:/desktop/weibocrawler/db_sisters.csv"), row.names = F, append = T)
    }
    else {print(paste('Error! at',Sys.time()))}
    Sys.sleep(5)     # Pause query to prevent being banned by weibo
  }
}

最后这一套程序执行下来的效果是这样的:


爬虫监视结果:娱乐圈几乎人均发水,区别只在多少

正常情况下,在基本没有粉丝做数据、没有机器刷数据的时候,作为一种基于时间线的、与多个博主内容相互竞争传播的、时间越长排序越后被刷到的概率越低的内容,一条微博的转评赞数据增长曲线大体应该是这样的:

即使是靠近半夜,也只是曲线下降的速度变快而已:

请记住上面的正常的曲线长啥样。

然后,坐稳了,我们要看不正常的了~


以下是部分被监测的明星的微博评论数据变化趋势:

有午夜惊魂型的,半夜十二点前在638秒内突然集中增加了2000条评论和20000个赞:

也有光天化日公然刷量的:


当然,请务必注意,前面这些图,虽然能强力提示这些明星的微博有水分,但并不能证明水是明星本人买的——毕竟,这些水也可能是广告公司、经纪公司、粉丝买的。

比如有的姐姐的微博转发之所以有异常增长,就很可能是她粉丝干的:

而如果你问她们,为什么要给自己姐姐用机器刷数据,她们的回答一般是:为了给姐姐排面、为了让姐姐数据更好看——与此同时,却浑然不知或者假装不知,这样低劣的机刷数据,在稍微像样点的娱乐数据分析公司那里,都能一秒现原形。


后记

在这篇文章里举出的疑似发大水的曲线,还只是我观测到的所有曲线图中的一小部分。

我的程序从德国的昨天早上跑到现在,一共抓住了十几位发了新微博的明星,有了他们的微博从第0分钟开始的传播数据全程变化。

在这十几个人当中,除了我肉眼翻完过评论鉴定的何炅何老师的一条非营业博仍然保持着阴性对照的优良作风之外,其他大部分人,包括一些非常有名的演员和歌手,都有这种几分钟内几千几万的异常的增长。

唯一区别,只在这些刷的量和他们真实流量的比例多少:

有的人5/1,有的人10~20/1,有的人甚至100/1以上。


是的,数据造假是不光彩的。

然而,在这泥沙俱下的流量经济时代之中,无论是初出茅庐急需认可的年轻艺人,还是已经功成名就的圈内前辈,只要还想继续从广告投放商那里赚钱,那么每个人都得被逼着接受这样的现实。

这一风雨晦暝的时代,何时才能落下帷幕呢?

愿我们仍能见证那一天的到来。

编辑于 10-23