返回

基于新浪微博的男女性择偶观数据分析(上)

或许是因为我喜欢的姑娘从来都不喜欢我,而感情上的挫折一度让我陷入无尽的自卑。朋友在朋友圈里发布一条关于皮影戏的动态,我开玩笑说这个皮影戏结局应该是个悲剧,因为我注意到在剧中,无论一个人如何卖力地表演甚至双腿跪倒在地,有的人从故事开场到结束一直对此无动于衷。朋友回复我说,这不就是你现在的状态吗?我沉默半天终于熄灭手机屏幕。我听到过各种各样让我放弃她的话,即使这种念头在我脑海里萌生已久,是幻想让我硬生生地拖了这么久。当你努力想要融入对方的生活,而等待你的是一道冰冷的墙。这种感觉像什么呢?大概就是一个又一个“好友”安安静静地躺在联系人的置顶名单里,不敢发消息让对方知道,更不愿残忍地把对方删除。我安慰自己说,对我而言,我失去的是一个不喜欢你的人;而对对方而言,失去的是一个喜欢她的人。你当然可以说我没有那么喜欢她,如果一定要喜欢到卑微如尘土的地步,我宁愿一个人单身到天荒地老。

当我意识到人与人间,即使亲近如父母尚且无法完全理解彼此的时候,我忽然发现一个有趣的现象,我们喜欢一个人的时候,首先注意到的会是外表,我们将其称之为眼缘,所以人与人间的感情纽带最初会是吸引,而后是了解彼此的优缺点,最终是相互理解和扶持。可我们知道,外表是可以伪装出来的,所以我们习惯通过外表和言语来评价一个人,这就像是数学归纳法,我们总认为推倒第一块多诺米骨牌,就意味着所有多诺米骨牌都会倒下。可现实世界矛盾的地方就在于,我们认为理所当然正确的事情,或许正是我们无法证明其正确性的,这在数学上称为哥德尔不完备定理。所以,一件残酷的事情是,当你无法吸引一个人的时候,通往内心世界的路就被堵死了。朋友圈里精彩纷呈的社交互动,并不代表有人愿意真正了解你的生活,何况是你吸引不到的人呢?我很想知道,我们在选择伴侣的时候到底看中什么,所以我一直在关注@西安月老牵线上发布的征婚交友类微博,本文的故事从这里正式展开。

身高 175 的悲伤

或许你以为我会无聊到试图从微博上找到女朋友,可事实上作为一个程序员的我,即使整天投入精力在编程上,依然无法避免对象空引用的异常出现。如果说找到女朋友是个小概率事件,那么在我看来,找到一个真正懂我、喜欢我的女朋友,基本上是不可能事件。你不要觉得我对没有调整好心态、对生活过分悲观,如果你了解贝叶斯公式就会真正地理解我说的话。这个微博开始引起我的注意,是我发现身高在 155 到 165 左右的女生,对男生的要求基本上无疑例外地是 175+到 180+,我想知道到底有多少女生是有这样的想法,这是我想要抓取新浪微博的数据进行分析的初衷。更重要的是,身高不到 175 的我在面对这种要求的时候是悲伤的,因为我想起了《巴黎圣母院》中的卡西莫多,一个外表丑陋而拥有高尚人格的“丑八怪”。现代人整天都特别忙碌,以至于没有人会有耐心,园艺在忍受着你丑陋的外表的同时,同你讲一只小兔子亲了它喜欢的长颈鹿一下这种故事。

我听到这样一句话,“好看的皮囊千篇一律,有趣的灵魂万里挑一”,可谁会觉得像卡西莫夫这样的人,会拥有或者配拥有高尚的人格呢?我们这副皮囊不管好看与否,它们都是父母给予我们的最好的礼物。难道一个所谓情商高的人,会在收到别人的时候因为礼物不好看而生气吗? 我想起《画心》里懊悔受狐妖小唯皮相蛊惑而自毁双目的霍心,美丑都是父母赐予我们的,不该被我们拿来一番大肆炫耀,可我还是想知道,我们评价一个人的标准到底是什么?因为我渐渐明白,有些人不喜欢我们,并不是我们不好,而仅仅是某一点和对方不匹配。喜欢一个人的时候,像拔下身上的一根根刺,因为你越是得不到回应,就越像变成对方期待的样子,这个过程会让你觉得自己一无是处。直到今天看到一句话,一句足以热泪盈眶的话,如果不曾喜欢你,我本来非常可爱的。有时候,人做一件事情,或许就是在和自己过不去,比如说这件事情。

花点时间爬爬微博

好了,现在我们来考虑从新浪微博上抓取@西安月老牵线上发布的微博,因为这是我们进行数据分析的前提。事实上,在写这篇文章前我曾花了大量时间来调试爬虫,然后用了一天的时间对数据进行清洗,最终利用晚上下班的时间生成词云。由此我们可以理出整体的思路:

流程图
流程图

通过流程图我们可以注意到,在这里我选择了 Python 来实现整个功能。转眼间我已经 25 岁了,这是种什么样的概念呢?两年前我 23 岁的时候,听别人讨论结婚这个问题,我觉得它离我还很遥远。如今看着周围人都结婚了,我竟有种“高处不胜寒”的感觉。所以呢,人生苦短,当你不能阻止时间一天天消逝的时候,你只能趁着现在去做你想做的事情,为了节省时间去做技术以外的尝试,我选择拥有全世界最丰富的库的 Python。

这段时间学习数据分析,我渐渐意识到我们所熟悉的这个世界,如果以一种理性的角度,完全通过数据来解构的话,我们在这个数字时代里留下的每一条讯息,都冷冰冰地暴露着我们的喜怒哀乐,每一张照片里细微的表情变化,每一段文字里隐匿着的真实意图,都能被人脸识别和自然语言处理等等,这类人工智能为代表的技术所解读,我们努力想在朋友圈里隐藏些什么,当朋友圈的访问范围从半年逐渐缩小到三天,我们究竟能隐藏下什么呢?

微博爬虫分析

首先,我们需要从微博上抓取数据下来,我没有去做抓包分析这样的重复性工作,因为我注意到这个问题,在网络上有很多朋友在讨论,我主要参考了以下内容:

通过以上内容,我了解到在抓取新浪微博数据的问题上,我们基本会有以下思路:

  • 保存 cookie 信息,利用 requests 库发起请求并带上 cookie
  • 利用 requests 库模拟登录新浪微博并在请求过程中保持 cookie
  • 利用 selenium 库模拟登录新浪微博然后取得页面内容
  • 利用 PhantomJS 库模拟登录新浪微博然后取得页面内容

可以看出差异主要集中在 cookie 的获取以及是否支持 headless 模式,并且我们得到一个共识,抓取新浪微博移动版要比PC 版要容易,因为移动版优先为小尺寸屏幕设备提供服务,因而页面结构相对整洁便于我们提取数据。起初博主认为第一种方式太简单粗暴,坚持要采用第二种方式去实现,最终证明还是太年轻了啊,新浪微博的登录给人的感觉就是蛋疼,这里就简单介绍下思路哈。

首先我们会向服务器发出一次 GET 请求,它返回的结果是一段 JavaScript 代码,然后我们需要用正则匹配出其中的 JSON 字符,这样我们就获得了第二次请求需要用到的参数;接下来,第二次请求是一个 POST 请求,我们需要将用户名采用 Base64 加密,密码则采用 RSA 加密,需要用到第一次请求返回的参数。实际上,新浪微博官方给我们提供 API 获取微博数据,可这个 API 可以获取的微博数据非常有限,更让人难以接受的是新浪微博的应用授权方式,如果我们采用调用 API 的方式,在这里会有第三次 POST 请求,有朋友分析了完整的模拟登录过程,可我对此表示毫无兴趣啊。最早我采用了模拟这种方式,抓取第一页的时候还是登录的状态,可等到抓取第二页的时候变成了注销的状态,整个过程使用的是同一个 session 对象,所以我最后果断放弃了这种方式。

好了,现在我们只需要在 Chrome 里 F12 找到 Network 选项卡,抓一次包取得 cookie,然后在请求的时候带上 cookie 即可。我们不用过分担心 cookie 过期的问题,在博主测试的时候,一个 cookie 可以持续工作 3 至 5 天,而且在后面我们会讲到,这个爬虫抓取的数据量其实并不大,在一两个小时内就可以完成抓取,没有必要将爬虫考虑得太严谨。在下图中我们标记出了博主计算机上存储的 cookie,我们通过 cookie 就可以免登录抓取信息啦。

提取Cookie
提取Cookie

解决登录的问题以后,回到这个问题本身,我们需要抓取@西安月老牵线发布的所有微博,移动版对微博做了分页处理,所以我们只需要知道总共有多少页,然后循环去提取每一页里的信息即可,因为我们注意到每一页的地址都符合https://weibo.cn/u/3232168977?page=2这样的形式。首先页数,我们可以通过 name 为 mp 的隐藏控件来获得,其 value 属性表示总页数。其次,每条微博存放在 class 为 c,id 以 M_开头的 div 标签里,在这里我们只需要文本信息,顺藤摸瓜我们发现信息被存放在 class 为 ctt 的 span 标签里,这里博主遇到一个奇怪的问题,BeautifulSoup 默认的解析器 html.parser,不知道因为原因无法解析出标签,而 lxml 当时因为 pip 的问题无法安装,所以不能使用 XPath 来解析 DOM 解构,在这里我认为 XPath 更适合这个场景,如果有时间可以考虑对代码进行重构。

在抓取微博的过程中,博主发现官方的反爬虫策略非常给力,连续工作超过 5 分钟 IP 就会被封锁,进而无法访问微博的服务器, 大概经过 20 至 30 分钟后会自动解封。或许主流的方案是花钱买动态代理,可我这个就是临时起意的一个想法,所以我采取了最简单粗暴的方法,让线程睡一会儿,在这样条件下,我花了大概 1 个半小时到 2 个小时左右的时间,从微博上抓取了 5600 条数据,并将其存储在了 SQLite 数据库中。什么?你问我为什么不考虑多线程,因为我这个人懒啊,这个问题最难的地方在数据分析,数据抓取方面我不太关注效率,因为我有足够的时间去等这些数据,所以关于性能方面的问题,有时间我们再做进一步讨论吧!

数据处理过程

数据处理这块,我本来打算尝试下 MongoDB 这个数据库的,而实际上这是我今年计划要去学习的内容之一,后来因为种种原因一直搁置到现在,可当我注意到 Windows 下安装 MongoDB 的繁琐后,我果断放弃了这种念头回归简单的 SQLite,我基本上是交叉使用 Windows 和 Linux,而我知道 Linux 下安装 MongoDB 是非常简单的。我反复强调我喜欢小而美的东西,就是因为我想保留对方案的选择权。在这里我们的数据处理,主要是数据清洗和中文分词。首先,我们来一起看看数据库表的设计:

数据表结构
数据表结构

我这里一切从简,所以将这 5600 多条数据都存储在一张表里,表中有四个字段 ID、Post、Wish 和 Tags。显然,ID 是自增的主键,为每条微博提供一个唯一的标识;Post 存储我们从微博上抓取的原始信息,这里不含 HTML 标签,可是会含有微博表情字符啊摔;Wish 存储每条微博中对伴侣的要求具体有哪些,这里我们主要通过关键字来截取可谓简单粗暴,具体原因稍后会讲到:);Tags 存储 Wish 字段经过分词以后的结果,这里我们使用结巴分词和 SnowNLP,该字段中存储的是序列化后的 JSON 字符串,下面我们具体来讲这些字段的处理。

首先,分析@西安月老牵线发布的微博我们可以发现,所有征婚相关的微博都是以**#征婚交友#或者#月老爱牵线#这样的话题开始,并且每条微博都是先介绍个人情况,然后再描述对理想伴侣的期望,所以我们只需要找出每条微博里对理想伴侣的期望相关的描述,然后再根据这条微博是由男嘉宾还是女嘉宾**发布的,即可汇总出男、女性对各自伴侣的期望到底是什么,我们将这部分信息更新在 Wish 字段里,我们一起来看具体的代码:

# Filter Data
sql = "SELECT ID, Post FROM table_weibo WHERE POST LIKE '%%%%%s%%%%' OR POST LIKE '%%%%%s%%%%'"
sql = sql % (u'#征婚交友#',U'#月老爱牵线#')
self.cursor.execute(sql)
rows = self.cursor.fetchall()

# Adjust Data
patterns = ['想找','希望找','要求','希望另一半','择偶标准','希望对方',
'希望','找一位','找一个','一半','找','想','喜欢','择偶条件','寻','期待',
'女孩','男孩','女生','男生','女士','男士','理想型']
sql = "UPDATE table_weibo SET Wish = ? WHERE ID = ?"
for row in rows:
    id = row[0]
    post = row[1]
    match = -1
    for pattern in patterns:
        if(pattern in post):
            match = post.find(pattern) + len(pattern)
            break
        if(match != -1):
            wish = str(post[match:])
            wish = wish.replace('#西安月老牵线#','')
            wish = wish.replace('[心]@月老蜀黍' ,'')
            wish = wish.replace('#月老爱牵线#'  ,'')
            self.cursor.execute(sql,(wish,id))
        else:
            self.cursor.execute(sql,('',id))
self.connect.commit()

可以注意到,我们首先按照话题对微博进行了筛选,然后通过关键字列表 patterns 来截取我们所需要的 Wish 字段,实际上这里是需要反复去调整 patterns 的,直到所有满足我们期望的数据都被提取出来,所以这是一个渐进式的数据处理过程。或许我们能想到通过 NLP 相关的技术来分析这段文本,我尝试通过 SnowNLP 去分析这样一段长度为 100 到 500 的文本,因为 SnowNLP 具备分析一段话的摘要及关键字的能力。可我发现这样实践下来效果并不太好,这是因为 SnowNLP 本身是以电商网站的评论数据为基础的,所以遇到我们微博这样相对灵活的文本信息时,它提取出的关键字并不能完全地符合我们的期望。固然,我们可以通过训练 SnowNLP 来达到我们的目的,可训练需要准备大量的文本信息作为支撑。作为一个懒惰的人,我最终选择了通过关键字来提取关键信息,准确度基本可以保证 90%以上,因为暴力截取难免会拆分出不符合期望的信息。

接下来,我们有了针对男、女择偶要求期望的 Wish 字段,可这些信息对我们而言,依然显得繁重而冗余,所以接下来我们考虑对 Wish 字段进行分词,最初的设想是通过词性和语法来分析,可当我分完词以后我就不得不佩服中文的博大精深,这里我选择了两个中文处理相关的库,即结巴分词SnowNLP,它们都是开源项目并且有大量的文档作为参考,这里想说的是,SnowNLP 中支持中文文本的情感分析,这是我最初想要使用这个库的一个重要原因,因为我想从这些微博中找出评价一个人的形容词或者名词,而这些词的情感分析,可以作为我们是否将其作为一个评价指标的重要依据。

可我们有句话叫做“认真你就输了”,尤其在女性的思维模式中,充满太多太多不能直接去理解的信息。这种我不用举例子啦,现在铺天盖地的直男癌/女权癌席卷而来,其实有太多问题无关对错,你输就输在没有照顾好对方的情绪,我们现在常常把情商挂在嘴上,可情商概念中的自我意识控制情绪自我激励认知他人情绪处理相互关系,有 60%说的是自我管理,而其余的 40%,恰恰就是我们日常理解中关于人际关系方面的,所以我们说人工智能不能完全代替人类,因为只有绝对理性的世界是恐怖的,可只有情绪化的感情而不讲道理的世界则是空虚的。我们可以去追逐人类内心中的灵性,即真、善、美,这是任何冰冷的计算机所不具备的东西。可我们能不能真诚一点呢,明明知道这一切都是套路可你还满心期待,我们并非不懂得什么是爱,爱不是我用一个又一个套路去套路你,而是我明知这是套路还愿意陪你表演下去,我没有在讽刺浙江卫视某节目,愿温柔的你被这个世界温柔地对待。

关于结巴分词和 SnowNLP 地对比评测,大家可以参考:Python︱六款中文分词模块尝试,这里博主发现 SnowNLP 适合做大颗粒分词拆分,而结巴分词适合做小颗粒分词拆分。其实,从分词效果上来讲,结巴分词是要比 SnowNLP 效果更好一些的,可我这样说不是会显得情商比较高吗?这样你们会喜欢吗?最终我们采取的方案是两者混用,故而我们有了这样的代码:

def generateTags(self,text):
    snow = SnowNLP(text)
    sentences = snow.tags
    tags = []
    for s in sentences:
        words = pseg.cut(s[0])
        for w in words:
            tags.append({'word':w.word,'flag':w.flag})
    return json.dumps(tags)

可以注意到,这里我们使用结巴分词获得了每个词的词性,不过到我写这篇文章的时候,对于词性的处理我依然没有什么好的想法,这里仅将其作为结果以 JSON 的形式存储到数据库中,现在我们基本上完成了所有数据处理的流程,在这个过程中会有些特殊的中文字符,我们采取暴力替换的方式进行去除即可,对此不在这里展开说啦。下图展示了数据库中部分数据:

处理后的数据
处理后的数据

处理结果呈现

说起这篇文章,可以说这是我第一次接触数据分析,我们这个时代积累了大量的数据,所以我们有基于大数据的推荐和预测等等相关场景,知乎和微博的首页 Feed 流经过无数次算法调整,可人们依然在抱怨算法向人们推荐了无关的内容,这是否说明,我们所期待的智能,仅仅是让我们觉得智能而已,这一系列基于统计的数据分析理论,是否一定是符合某种冥冥之中的规则,我想起《模仿游戏》中和卷福扮演的图灵形成鲜明对比的,正是以休.压力山大为代表的统计方法派,电影中他们试图通过分析字母出现的频率来破解恩尼格玛。对于数据分析而言,如果说可视化是面向人类的分析手段,那么数据挖掘就是面向机器的分析手段。作为一个刚刚入门的萌新,我描述的是我对数据分析的一种感觉。回到本文主题,这里我选择以词云作为最终处理结果的呈现载体。

词云,即 WordCloud,是一种展现关键字出现频率的表达方式,如果你对博客写作比较熟悉的话,就会知道诸如 WordPress、Ghost、Hexo 等都提供了标签云功能,我们每篇文章中都会给文章添加若干标签,而标签基本可以让读者了解这个博客都有哪些内容,在标签云中出现频率越高的标签其字体通常会越大,这样我们可以非常直观地了解到,每个因素在整体上占到的比重。本文之所以采用这种方案,正是希望通过词云来呈现男女在择偶观上更看重什么。生成词云的方式有很多,具体可以参考这篇文章:除了 Tagxedo 外,还有什么好的软件制作可以词云?,而博主最终选择了wordcloud,你可以看到 Python 基本上是万能的语言,有这么多优秀的第三方库可以用,我就问你怕不怕,关于这个库的用法,请参考:https://amueller.github.io/word_cloud/,如果通过 pip 无法直接安装该库,可以通过这里下载.whl 文件进行安装,注意升级 pip 到最新版本即可。

参照官方的示例,我们从数据库中根据 Post 来过滤性别,根据 Tags 来获取关键字,然后将所有 Tags 串联成一个字符串,传递给 WordCloud 模块即可。下面给出代码片段:

# Filter Data
sql = "SELECT Post, Tags FROM table_weibo WHERE Tags <> ''"
self.cursor.execute(sql)
rows = self.cursor.fetchall()

# Filter Tags
male_tags = ''
female_tags = ''
for row in rows:
    post = row[0]
    tags = json.loads(row[1])
    if u'男嘉宾[向右]' in post:
        female_tags += ','.join(map(lambda x:x['word'],tags))
    elif u'女嘉宾[向右]' in row[0]:
        male_tags += ','.join(map(lambda x:x['word'],tags))
                
# WordCloud self.generateWordCloud(female_tags,'female.png','output_female.png') self.generateWordCloud(male_tags,'male.png','output_male.png')

在这里我们主要将男嘉宾/女嘉宾分别筛选出来,然后将分词结果用逗号串联起来,这样即可得到 male_tags 和 female_tags,我们会将其传递给 WordCloud 模块,可以注意到我们为男性/女性词云分别设置了不同的背景图片,最终会生成两张不同的图片,这里主要参考了Image-colored这个示例,代码片段展示如下:

def generateWordCloud(self,text,background,output):
    back_coloring = np.array(Image.open(background))
    stopwords = set(STOPWORDS)
    stopwords.add(u'西安')
    stopwords.add(u'生活')
    wordcloud = WordCloud(
        font_path='simfang.ttf',  # 设置字体
        background_color="white",  # 背景颜色
        max_words=5000,  # 词云显示的最大词数
        mask=back_coloring,  # 设置背景图片
        stopwords=stopwords, #停用词设置
        max_font_size=75,  # 字体最大值
        random_state=42,
        width=1000, height=860, margin=15,# 设置图片默认的大小,但是如果使用背景图片的话,那么保存的图片大小将会按照其大小保存,margin为词语边缘距离
    )

    wordcloud.generate(text)
    plt.imshow(wordcloud)
    plt.axis("off")
    plt.show()
    wordcloud.to_file(output)

这里我们注意到添加了两个停止词,这是因为我们发现,西安生活这两个关键词,在整体中所占权重虽然较高,可是因为我们这里抓取的是西安本地的微博,所以这两个关键词对我们而言是没有意义的。再对这两个关键字进行剔除以后,我们最终生成的词云如图:

男性心目中的伴侣
男性心目中的伴侣

女性心目中的伴侣
女性心目中的伴侣

这是个看脸的世界

对这样一个显然成立的结论,我是表示失望的,这种感觉像什么呢?就像你期待着对方说喜欢你,结果到最后她还是会说我们不合适。可花费如此大的篇幅来讲这样一个悲伤的故事,我们就象征性地分析下结论吧!首先,我们注意到男性心目中的伴侣,排名靠前关键字是性格孝顺善良懂事结婚身高眼缘,而女性心目中的伴侣,排名靠前的关键字是身高稳重责任心上进心工作成熟。所以,我现在完全可以理解,为什么女生会对 180 的身高如此迷恋,因为她们想被男朋友举高高呀,我的一位朋友如是说。

与此同时,我们发现很多指标譬如孝顺善良稳重责任心上进心等,其实都是需要两个人在相处久了以后慢慢去验证的,可这些最终会被眼缘身高这种因素阻挡在外面。或许你会觉得这样浅显的道理,居然值得我花费时间和精力去思考。一个你吸引不到的人,终究是难以拉近两个人心间的距离的。喜欢是两个人的事情,我不是要卑微地乞求你来见我,而是你想要来见我我就主动迎上去。有天在 QQ 空间看到有人在看《怦然心动》,就忍不住下载下来一个人看。有时候我们喜欢的那个人,或许并没有那么好,你会渐渐发现,那种因为喜欢而附加在对方身上的光环,会随着时光而慢慢变淡。而有那么一瞬间,我只想比她变得更优秀,而不再幻想她会转身回来看我,或许这就是成长吧!

本文小结

大概没有谁会像我这样,在写一篇技术文章的过程中,掺杂如此多的个人情感。可有时候,一个人做一件事情的动机,的确就是如此简单。我羡慕 175 以上的身高,可是不是我具备了这样的身高,你就会喜欢我呢?我想应该不会吧,因为你总能找到新的理由来拒绝我。所以,我写这篇文章,通过 Python 抓取新浪微博数据并对其进行分析,并不是想告诉你,你因为不具备哪些因素而不被人喜欢,而是想告诉你,我们每一个人都是这个世界上独一无二的存在,我们的优点同我们的缺点组合起来,这才是完整的我们。别人喜欢不喜欢我们到底有什么意义呢?就像我们喜欢许嵩、喜欢林依晨,难道就要让人家喜欢我们吗?我可以非常喜欢你,但我一定要骄傲地喜欢你,因为我骄傲时的样子最帅,谁让这是一个看脸的世界呢?

Built with Hugo v0.110.0
Theme Stack designed by Jimmy
已创作 266 篇文章,共计 1005454 字