[数据科学]用Python看看微博上的“五个一”并对微博内容进行情感分析

五个一

Posted by Mr.X | X师傅 on May 31, 2020

前言:

身为海外党,平时没事儿就看看机票的X师傅突然被回国的票价吓住了!

  • 巴黎飞北京 : 国航经济舱 要 3W+ (单程)
  • 巴黎飞上海 : 东航经济舱 也得3W+ (单程)

随着五个一延续到10月底,X师傅上个月买的外航航班被取消,机票价格也涨价6倍。隔离期省下的票子,这把一下全梭哈了!工作大半年,资产没变,负债倒是增加了不少。

平复了一下 被g韭菜的心情(隔离期爆肥+找外航退钱无果+血亏一笔),本着亏不止我一人吃的原则, X师傅跑去微博看了看这个话题:

  • 民航局网站被爆
  • 网友嘲留学生千里投毒
  • 还有人 @战狼 带我回家

果然,吃亏的不只是我一个人(手动狗头🐕) 赶紧下载(pa)点数据一探究竟!

本片中的所有代码都在这个repo中:

(X师傅的github)[https://github.com/pickpikyup/blogs_-]

基本情况

  • 微博数据
  • python 数据处理
  • 百度AI 自然语言处理
  • Plotly数据可视化

1. 数据来源 :

  • 新浪微博

  • 知乎 (to do)

  • 头条(to do)

X师傅写了个简单的爬虫, 结果干不过新浪的反扒机制,没过多久就被封IP了! 于是决定上网搜搜优秀轮子! 结果让我找到了一个 不错的针对话题的爬虫。其中有GUI版本和普通版, 代码简单,结构清楚,普通版非常容易更改,并将之部署在本地。俗话说的好,专业的事要交给专业的人去做,看了人家的爬虫,豁然开朗,爬数据,果然还是爬取移动端更有效!! 下面附上原github链接。

Link: github 超级微博爬虫

五个一在3月16日发布,正式实行日期为3月29号到6月31日。然后5月19日宣布将延长到10月底。然后5月20日左右,社媒就炸锅了。。

几个时间点:

  • 发布日期: 3月16日
  • 第一阶段实行日期 : 3月29日到6月31日
  • 宣布延长日期:5月19日
  • 第二阶段延长日期 : 7月1日到10月31日(大概是这个日期)

所以 我们的数据时间范围为3月16日到 5月30日。一切就绪,“下载” 数据!

Tips:这里说几个部署爬虫的小技巧,

  • 为了保证爬虫的稳定性,每翻3-5页的时候最好sleep 10到15 秒。如果爬虫很温和,就不会给目标服务器太多压力,也防止被反扒机制干掉。利人利己。
  • 对于微博这种社交大平台,新注册账号的封禁率特别高,怕麻烦的同学可以用自己的微博账号 (也可以用女朋友的号 xD)
  • 反复爬取相同的日期,说不定就能找到以前遗漏的数据 。 ”温故而知新“
  • 最最重要的一点: 下载到的数据 千万-千万-千万 不要搞非法用途,不然爬虫就是程序员通向监狱的最快路径!!!

2. EDA:

2.1 数据预处理

经过一段时间的爬取,数据量看起来不错,但是有大量重复的微博,并且数据格式有误。先对原始数据进行预处理

  • 根据微博正文,微博id,发布时间,去除爬取到重复的微博。
  • 计算 微博互动数 = 点赞数+转发数+评论数+1
  • 考虑到微博上有大量官方媒体,自媒体和私人用户。采用粉丝数量,粉丝活跃程度 和交互数量来区分这些媒体。(这部分内容不够完善,有更好的建议请给X师傅留言!)
  • 处理微博正文内容,消除text中的特殊符号,提取关键tag。
    import os
    import pandas as pd
    import numpy as np

    # Prepare Data
    def remove_duplicates(df):
        size_before = df.shape[0]
        df = df.drop_duplicates(subset=['微博正文','微博id','发布时间'])
        size_after = df.shape[0]

        print(u'重复微博 数量:%d'%(size_before-size_after))
        return df
        
    def add_interativity(df):
        df['nb_interactivity'] = df[['点赞数', '转发数' ,'评论数']].sum(axis=1) + 1
        df['发布时间'] = pd.to_datetime(df['发布时间'])
        df['date'] = df['发布时间'].dt.date
        df['date'] = pd.to_datetime(df['date'])

        print(df['date'].min(), df['date'].max())
        return df  

    # 仍需改进
    def add_identity(df):
        # 如何界定大V , 50w 粉丝 
        df['if_大V'] = 0
        df.loc[df['发布者粉丝数']>=500000, 'if_大V'] = 1

        # 多粉 少互动
        df['if_官媒'] = 0
        df.loc[(df['nb_interactivity']/(df['发布者粉丝数']+1)) < 0.00002, 'if_官媒'] = 1

        # 多粉 高互动
        df['if_自媒'] = 0
        df.loc[((df['nb_interactivity']/(df['发布者粉丝数']+1))>0.0005)&(df['发布者粉丝数']>50000), 'if_自媒'] = 1

        # 身份修正
        ## 官媒 list
        l1 = ['中国民航网','']
        df.loc[df['发布者姓名'].isin(l1), 'if_官媒'] = 1
        df.loc[df['发布者姓名'].isin(l1), 'if_自媒'] = 0
        df.loc[df['发布者姓名'].isin(l1), 'if_个人用户'] = 0

        ## 自媒 list
        l2 = ['小qqq阿','胡舒立','FATIII']
        df.loc[df['发布者姓名'].isin(l2), 'if_官媒'] = 0
        df.loc[df['发布者姓名'].isin(l2), 'if_自媒'] = 1
        df.loc[df['发布者姓名'].isin(l2), 'if_个人用户'] = 0

        # 其他 
        df['if_个人用户'] = 0
        df.loc[(df['if_大V'] == 0) & (df['if_官媒'] == 0) & (df['if_自媒'] == 0),'if_个人用户'] = 1

        return df

    df = remove_duplicates(pd.read_csv('../data/data.csv'))
    df = add_interativity(df)
    df = add_identity(df)

基本处理完了之后,初步爬到3000多条相关的原创微博。 基本情况如下: | 原创微博数 | 转发数 | 评论数| 点赞数| 总互动数| |—————-|———-|————|———–|———-| | 3620 | 35927 | 90420| 497735| 627712|

这是3月16日至5月30日之间的数据,在这将近74天的日子里,总互动数将近63万。 平均每天互动数量不到1万。乍一看热度不怎么高!

那我们在查看一下每日微博数量,在官宣延长之后,5月19日话题度突然提升。。。

from plotly import express as px

data = df.groupby('date').agg({'微博正文' : 'count', '转发数':'sum'})[['微博正文','转发数']].reset_index()
data['平均转发数'] = data['转发数']/data['微博正文']
fig = px.bar(data_frame=data, y='微博正文',x='date',text='微博正文')
fig.update_layout(width=1000, title = '五个一-每日原创微博数')
fig.show()

在这里插入图片描述 但是这段时间里,5月20日,22日,这几天的微博数量异常的少,有点不对劲儿。这里我提出一个假设:

  • 在这几天里,海外航空公司预售的回国机票陆续被取消,所以大量用户发表相对过激的言论,导致被删贴,下搜索。
  • 也可能是,新浪反境外爬虫限制,导致数据下载不齐全。

我们再看看海内外对这个话题的的关注度

import cufflinks as cf 
cf.go_offline()
cf.set_config_file(offline=False, world_readable=True)

fig = df.发布者地区.value_counts().iloc[:15].iplot(kind='barh', asFigure=True)
fig.update_layout(height=400, title='微博发布者所在地区 Count')

在这里插入图片描述

其实想一想也知道,海外华人,作为受影响最严重的群体,自然而然更加关注这个话题。对于国内这些城市,北上广也都是人口密集省份,自然而然,发的微博也就多一些。

但是,我这里有一个很大的疑问,北京的话题关注度要比其他地区都多一些。

  • 可能是各大媒体的官方发布地址是在北京吧。。。
  海外 北京 上海
大V比例 2% 19% 10%
官媒比例 1% 16% 6%
自媒比例 2% 4% 3%
普通用户比例 94% 61% 81%

统计一下,果然,北京还是这些媒体比例最高的地区,也难怪对这些个话题关注度要高一些。

再展开看看北京这些个微博都是什么时候发的:

# 关注度
df_temp = df[df['发布者地区'] == '北京']
delays = (df_temp['date'] - df_temp['date'].min()).dt.days.values

# color_pallet = sns.color_palette('RdBu_r',delays.max()+1)

fig=go.Figure(layout=dict(width=800, height=700, title='北京地区对‘五个一’ 话题关注度 - 颜色越浅,发布时间越久'))

trace = go.Scatter(
    x=df_temp['发布者粉丝数'],
    y=df_temp['nb_interactivity'],
    opacity=0.7,
    mode='markers',
    marker=dict(
        # size=(delays**2)/500,
        color=delays[::-1]
    ),
    text=df_temp['date'].dt.date
)

fig.add_trace(trace)
fig.update_layout(
    yaxis={'type':'log', 'title':'互动次数'}, 
    xaxis={'type':'log', 'title':'发布者粉丝数'})
fig.show()

在这里插入图片描述 从上图可以看出, 粉丝多互动多的微博都是很久之前发的,5月份的微博(紫色)也主要是个人用户发的。(果然,一被喷,官方微博就哑巴了。。。lol)

粗略看了一下数据,下面为了方便文字情感分析,我们还得稍微清理一下微博正文的奇怪符号。

from string import punctuation
import re

# Used in preprocessing_hanzi
def clean(text):
    """
    copy from : https://blog.csdn.net/blmoistawinde/article/details/103648044
    """
    text = re.sub(r"(回复)?(//)?\s*@\S*?\s*(:| |$)", " ", text)  # 去除正文中的@和回复/转发中的用户名
    text = re.sub(r"\[\S+\]", "", text)      # 去除表情符号
    # text = re.sub(r"#\S+#", "", text)      # 保留话题内容
    URL_REGEX = re.compile(
        r'(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))',
        re.IGNORECASE)
    text = re.sub(URL_REGEX, "", text)       # 去除网址
    text = text.replace("转发微博", "")       # 去除无意义的词语
    text = re.sub(r"\s+", " ", text) # 合并正文中过多的空格
    return text.strip()
    
# Clean Weibo Texts
def preprocessing_hanzi(text):
    """
    微博正文预处理
    """
   
    text = clean(text)

    # 去除发帖人
    text = ":".join(text.split(':')[1:])
    # 去除 \xa0之后的内容
    text = text.split('\xa0')[0]
    
    # 去除 中英符号 non stop 符号
    punctuation_hanzi = punctuation+"()【】‘’”“《》¥&……—"
    text = re.sub(r'[{}]+'.format(punctuation_hanzi), "", text)

    # 去除奇怪特殊符号
    text = re.sub(r'\u3000', "", text)
    
    # TODO 去除连续重复无意义符号 / 文字, 
    return text.strip()

df['preprocessed_text'] = df['微博正文'].apply(preprocessing_hanzi)

处理前:[‘易尬一爆炸HAN:从小到大 就被说 没心没肺 很多事情不容易当回事但真的这个五个一 限航** 想必 我能记很久 甚至一辈子吧 英国·格拉斯哥 \xa0显示地图\xa0’, 处理后: ‘从小到大 就被说 没心没肺 很多事情不容易当回事但真的这个五个一 限航** 想必 我能记很久 甚至一辈子吧 英国·格拉斯哥 显示地图’

看得出这个 博主的愤懑的心情,拿出小本本,开始记仇。。。

2.2 自然语言处理

X师傅平时基本没怎么接触过中文NLP,这次要对微博内容进行情感分析,本来打算用百度基于paddlepaddle的中文NLP模型(Senta)[https://github.com/baidu/Senta],这些个开源平台实在是对 Windows 平台不友好。搞了一整天,预训练模型也加载不出来。。。于是就去百度AI 申请了个账号,用一用人家的(文字情感分析API)[https://ai.baidu.com/tech/nlp/sentiment_classify]。 造好的轮子用起来也是真的方便(就是准确度仍有待商榷)

2.2.1 文字情感分析: 百度ai API

# encoding:utf-8
import pandas as pd
import numpy as np
import tqdm 
tqdm.tqdm_notebook().pandas()

import os
import urllib 
import json
import requests 
import time

def get_AT(AK,SK):
    # client_id 为官网获取的AK, client_secret 为官网获取的SK
    host = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={}&client_secret={}".format(AK,SK)

    response = requests.get(host)
    if response:
        print('Successfully get access_token')
        return response.json()['access_token']
    else:
        print('Unable get access_token, retry in 3s...')
        time.sleep(3)
        return get_AT(AK,SK)

def request_api(text, url):
    """
    UTF-8 encode, use given url
    """
    # send request
    data = json.dumps({'text':text}).encode('utf-8')
    request = urllib.request.Request(url=url,data=data)
    request.add_header('Content-Type', 'application/json')
    
    # get answer
    response = urllib.request.urlopen(request)
    if response:
        content = response.read().decode('utf-8')

        rdata = json.loads(content)
        return rdata
    else:
        print('Error, retry in 3s...')
        time.sleep(3)
        return request_api(text, api, AT)

# Config  
AK = " your acces key 去百度AI 申请"
SK = "your security key 同上"

AT = get_AT(AK,SK) # Access Token 一般来说 申请一次就好,有效期可以持续一个月
sentiment_classify_api = "https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify"

# prepare data and url
url = sentiment_classify_api+"?charset=UTF-8&access_token="+AT

res_list = []
items_dic = {
    'positive_prob':[],
    'negative_prob':[],
    'confidence':[],
    'sentiment':[]
    }

# POST baidu API
for s in tqdm.notebook.tqdm(texts):
    res_list.append(request_api(s, url))

# flatten results
for res in res_list:
    try:
        res_items = res['items'][0]
    except:
        res_items = {'positive_prob': 0, 'confidence': 0, 'negative_prob': 0, 'sentiment': 1}
    
    for key in res_items.keys():
        items_dic[key].append(res_items[key])   

# update result to df
for key in items_dic.keys():
    df[key] = items_dic[key]

df['error_sentiment'] = 0
df[df['confidence'] == 0]['error_sentiment'] = 1

df['sentiment'].value_counts()

调用完API, 总体来看,这些微博对 五个一 态度还是消极偏多一些 | 积极 | 消极| 中性 | |–|–|–| | 1297 | 2191 | 142 |

再看看API给的情感分析置信度: 在这里插入图片描述 普遍来说,置信度还是挺高的,看似情感分析完美无缺。但是,我们还是亲眼瞧瞧数据比较好。

积极 :

  • ‘二度神话:五个一**真的绝了。别说留学生了,那些只是来短暂看望留学生的父母也回不去…个中滋味,只有滞留在海外的人才能体会了。 \xa0’
  • ‘绘子Nance:6月出发 商务舱 芝加哥/洛杉矶 转机两次飞厦门行李直挂无需签证 5.3w #回国机票##美国机票# #留学生回国##国际客运航班五个一**#
  • ‘宿迁之声:【外交部:#目前142万留学生尚在国外##目前已有36名中国留学生确诊#】4月2日,国新办就疫情期间中国海外留学人员安全问题举行发布会。外交部副部长马朝旭透露,我国海外留学生总数160万,目前142万人尚在国外,分布在不同国家和地区。大多数留学人员按照权威建议,仍然选择留在当地,可以避免旅行交叉感染和中转国家管控措施中途受阻……

中性:

  • ‘果嘞坑坑坑:#国际客运航班五个一**#愚蠢,但是忠心爱国啊
  • ‘Veneno小嘉:一个中国人一旦出了国当了一个留学生就会一直买不到一张回国的机票,简称五个一**@中国民航网

看了一大圈,我挑了几个 有代表性的错误分析。

  • 有一些买机票的黄牛党发布的买票信息,因为全文都在陈述卖票,基本都被划分到积极情感里了。
  • 还有一些微博情态特征不明显,例如使用 ‘xx真是绝了’ 这一类关键词,不同的解读会导致情感划分失败。
  • 还有一些微博使用非常调侃的语气,了解前因后果的人可以根据当前讨论主题,自动构建‘新解读的五个一’与‘五个一**’的联系。从而读出 嘲讽,不满等语气。但API提供的模型无法自动联系上下文进行解读。

所以为了确保情感分析的质量,我们只选择 置信度>0.8 的微博。

import plotly.figure_factory as ff

df_temp = df[df['confidence'] > 0.8]
# 不同情感的微博每日数量
data = df_temp.groupby('date').agg({
    'positive':'sum',
    'negative':'sum',
    'neutre':'sum',
    })

# data = pd.DataFrame(data.values / data.sum(axis=1).values.reshape(-1,1), index=data.index, columns=data.columns)

data = pd.melt(data.reset_index(), id_vars='date', value_vars=['positive','negative','neutre'])

fig = px.bar(data_frame=data, x='date',y='value',color='variable', )
fig.update_layout(
    height=300,
    width=800,
    title='情感分析变化 数值')
fig.show()

# 不同情感的微博每日比例
data = df_temp.groupby('date').agg({
    'positive':'sum',
    'negative':'sum',
    'neutre':'sum',
    })

data = pd.DataFrame(data.values / data.sum(axis=1).values.reshape(-1,1), index=data.index, columns=data.columns)

data = pd.melt(data.reset_index(), id_vars='date', value_vars=['positive','negative','neutre'])

fig = px.bar(data_frame=data, x='date',y='value',color='variable', )
fig.update_layout(
    height=300,
    width=800,
    title='情感分析变化 相对比例')
fig.show()

# 置信度

fig = ff.create_distplot([df_temp.loc[df_temp['sentiment'] == 2, 'confidence'].values,
                          df_temp.loc[df_temp['sentiment'] == 0, 'confidence'].values], ['Positive', 'Negative'], bin_size=[0.05, .025])
fig.update_layout(width=800, height=500, title='Confidence Distribution')
fig.show()

在这里插入图片描述 这段时间以来,刚出来的时候,微博上的声音还是相对积极的。继续执行,消极的声音越来越高(中性的基本都是黄牛。。。) 事实又一次证明,没有对比就没有伤害,从时间线上来看,刚执行的时候,欧洲美国的疫情还没有到最高点,随着外国的疫情越来越严重(4月20日左右),国内国外一对比,诶呦,还是有**控制得好,不明真相的群众,纷纷鼓起了掌。。。。但是,5月19号,因为民航局宣布持续到10月底,曾经在国外航司预购的7 8 月份回国机票全部被取消,话题关注度一下飙升。。。毕竟事不关己,高高挂起。。。X师傅在机票被取消之前,也觉得5个1妙的厉害。。。

那国内国外的人都怎么看呢? 用甜甜圈图 看数据

fig = make_subplots(rows=1, cols=2, specs=[[{'type':'domain'}, {'type':'domain'}]])

fig.add_trace(go.Pie(labels=['负面', '中性','正面'], 
                     values=df[df['发布者地区']=='海外']['sentiment'].value_counts().sort_index(), 
                     hole=.6,
                     name = '海外微博情绪'), 1,1)
fig.add_trace(go.Pie(labels=['负面', '中性','正面'], 
                     values=df[df['发布者地区'] !='海外']['sentiment'].value_counts().sort_index(), 
                     hole=.6,
                     name = '非海外微博情绪'), 1,2)

fig.update_traces(hole=0.5, hoverinfo='label+percent+name')
fig.update_layout(
    title_text = '海外/国内 对待五个一 微博情感分析',
    annotations=[dict(text='海外', x=0.18, y=0.5, font_size=20, showarrow=False),
                 dict(text='国内', x=0.82, y=0.5, font_size=20, showarrow=False)]
)

fig.show()

在这里插入图片描述

fig = make_subplots(rows=1, cols=3, specs=[[{'type':'domain'}, {'type':'domain'},{'type':'domain'}]])

fig.add_trace(go.Pie(labels=['负面', '中性','正面'], 
                     values=df[df['if_官媒']==1]['sentiment'].value_counts().sort_index(), 
                     hole=.6,
                     name = '官媒微博情绪'), 1,1)
fig.add_trace(go.Pie(labels=['负面', '中性','正面'], 
                     values=df[df['if_自媒'] == 1]['sentiment'].value_counts().sort_index(), 
                     hole=.6,
                     name = '自媒微博情绪'), 1,2)
fig.add_trace(go.Pie(labels=['负面', '中性','正面'], 
                     values=df[df['if_个人用户'] == 1]['sentiment'].value_counts().sort_index(), 
                     hole=.6,
                     name = '个人用户微博情绪'), 1,3)

fig.update_traces(hole=0.5, hoverinfo='label+percent+name')
fig.update_layout(
    title_text = '各种类型媒体 对待五个一 微博情感分析',
    annotations=[dict(text='官媒', x=0.1, y=0.5, font_size=20, showarrow=False),
                 dict(text='自媒', x=0.5, y=0.5, font_size=20, showarrow=False),
                 dict(text='个人', x=0.9, y=0.5, font_size=20, showarrow=False)]
)

fig.show()

在这里插入图片描述

2.2.2 词云图

import jieba
from wordcloud import WordCloud

def get_stopwords(path):
    with open(path, 'r', encoding="utf8") as f:
        stopwords = [line.strip() for line in f.readlines()]    
    stopwords = list(set(stopwords))
    return stopwords

def plot_wordcloud(words, stopwords, path = '../output/词云.png'):
    wordcloud = WordCloud(stopwords=stopwords, background_color="white", max_words=1000).generate(words)
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.axis("off")
    plt.savefig(path ,dpi=400)

jieba.load_userdict('../user_dict/dict.txt')

texts = df['text_preprocessed'].values.tolist()
texts_splited = []
for s in texts:
    texts_splited.append(' '.join([i for i in jieba.cut(s,use_paddle=True)]))

df['texts_splited'] = texts_splited

stopwords = get_stopwords('../user_dict/dict.txt') +\
    ['客运','航班','可能','知道','就是','可以','不是',
    '不能','因为','显示','原图','我们','你们','自己',
    '还是','地图','真的','没有','这个','什么','他们',
    '现在','但是','如果']
plot_wordcloud(' '.join(texts_splited))

在这里插入图片描述

其实开始听说什么5个1政策的时候,我是举双手双脚赞成。虽然政策上基本一刀切除了回国途径,但是紧急状况就应该快速处理,身为中国人,X师傅还是十分理解咱国家的困难的。尤其当是海外疫情正处于爆发期,国内疫情也才刚刚控制住,理应让咱那些辛辛苦苦抗疫2个月的医护人员们,稍微休息一下了。除了保留一部分给必须回国的小孩子们,剩下的国际航班能减少就减少一些吧,咱们这种身强体壮,自给自足的年轻人也应该给央妈减点负担。当时的X师傅始终抱着这样的心态。理解

这两个月里,身边的朋友陆陆续续回国,曾经爆满的隔离酒店也都空闲了下来。现在的情况是待检测的人数还没有工作人员多(听上周回国的朋友说,下了飞机之后,被乌泱泱的一群工作人包围起来,用30cm长的面签戳鼻孔 lol。痛快 (-_-))。而且世界各国(川建国他们家除外)的疫情也都平安度过了第一段爆发期。然而,民航局再次宣布延长五个一到10月底。X师傅的心态瞬间崩了。。。前段时间刚吹了一波大使馆派发的健康包,怎么也想不到这个健康包竟然是最后的补给。再去微博上一看,老铁们的嘴可真碎。什么留学生回国就是千里投毒理应被驱逐,什么当初疫情时期选择离开活该现在回不来。除了心寒还是心寒。只能奉劝一句,嘴上不积德,生娃没PY。

回想起当初国内疫情初期,身边的华人朋友联系商会,拉拢校友,有钱的出钱有力的出力,从欧洲的各大药店购置口罩,防护服,空运回国。当初捐钱出力的留学生们毕业了,想回国,还得花比平时高7倍的价格 (平时往返机票6000块,如今单程机票3W+ 还不一定能买到)。失望。不只是因为票价高的离谱而失望,更是因为民航局的不作为而失望。

希望五个一可以早日变成五个二 五个三 ,给需要回国的留学生一个机会吧