或许是因为我喜欢的姑娘从来都不喜欢我,而感情上的挫折一度让我陷入无尽的自卑。朋友在朋友圈里发布一条关于皮影戏的动态,我开玩笑说这个皮影戏结局应该是个悲剧,因为我注意到在剧中,无论一个人如何卖力地表演甚至双腿跪倒在地,有的人从故事开场到结束一直对此无动于衷。朋友回复我说,这不就是你现在的状态吗?我沉默半天终于熄灭手机屏幕。我听到过各种各样让我放弃她的话,即使这种念头在我脑海里萌生已久,是幻想让我硬生生地拖了这么久。当你努力想要融入对方的生活,而等待你的是一道冰冷的墙。这种感觉像什么呢?大概就是一个又一个“好友”安安静静地躺在联系人的置顶名单里,不敢发消息让对方知道,更不愿残忍地把对方删除。我安慰自己说,对我而言,我失去的是一个不喜欢你的人;而对对方而言,失去的是一个喜欢她的人。你当然可以说我没有那么喜欢她,如果一定要喜欢到卑微如尘土的地步,我宁愿一个人单身到天荒地老。
当我意识到人与人间,即使亲近如父母尚且无法完全理解彼此的时候,我忽然发现一个有趣的现象,我们喜欢一个人的时候,首先注意到的会是外表,我们将其称之为眼缘,所以人与人间的感情纽带最初会是吸引,而后是了解彼此的优缺点,最终是相互理解和扶持。可我们知道,外表是可以伪装出来的,所以我们习惯通过外表和言语来评价一个人,这就像是数学归纳法,我们总认为推倒第一块多诺米骨牌,就意味着所有多诺米骨牌都会倒下。可现实世界矛盾的地方就在于,我们认为理所当然正确的事情,或许正是我们无法证明其正确性的,这在数学上称为哥德尔不完备定理。所以,一件残酷的事情是,当你无法吸引一个人的时候,通往内心世界的路就被堵死了。朋友圈里精彩纷呈的社交互动,并不代表有人愿意真正了解你的生活,何况是你吸引不到的人呢?我很想知道,我们在选择伴侣的时候到底看中什么,所以我一直在关注@西安月老牵线上发布的征婚交友类微博,本文的故事从这里正式展开。
身高 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 就可以免登录抓取信息啦。
解决登录的问题以后,回到这个问题本身,我们需要抓取@西安月老牵线发布的所有微博,移动版对微博做了分页处理,所以我们只需要知道总共有多少页,然后循环去提取每一页里的信息即可,因为我们注意到每一页的地址都符合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 抓取新浪微博数据并对其进行分析,并不是想告诉你,你因为不具备哪些因素而不被人喜欢,而是想告诉你,我们每一个人都是这个世界上独一无二的存在,我们的优点同我们的缺点组合起来,这才是完整的我们。别人喜欢不喜欢我们到底有什么意义呢?就像我们喜欢许嵩、喜欢林依晨,难道就要让人家喜欢我们吗?我可以非常喜欢你,但我一定要骄傲地喜欢你,因为我骄傲时的样子最帅,谁让这是一个看脸的世界呢?