《生活大爆炸》(英文:The Big Bang Theory 简称:TBBT)广受喜爱(据说还可以练听力练口语blabla),去年随着第12季的播出而完结,最近也算是在补。有一天闲聊的时候偶然冒出来一个点子,就是利用词云来将大爆炸台词中的高频词汇可视化一下,也是一个有趣的练习。
简单来说我们的任务分为两部分,第一部分是获取台词数据,第二部分是生成词云和其他NLP分析。
目录
获取数据
数据源
网页分析
编写爬虫
文本分析
直接生成词云并绘制
NLP分析
分词
去停词和滤词
词干提取和词形还原
词云生成
其他NLP分析
代码与台词文本
参考资料
首先我找到的台词数据来自于该网站,这是一位国外网友自己写了台词,并放到了自己用wordpress建的站上,目前更新到了第10季(看修改日期发现最后一个网页是在一年前更新的),有喜欢TBBT并欣赏他的工作的朋友可以去捐赠一下。
选择英文台词的原因是该剧本身台词就是英文为主,我看到的中文翻译是由YYeTs字幕组翻译的,信达雅,笑点也十分接地气,不过毕竟还是不如原文的“高频词”更有意义些。
在谷歌浏览器中按F12使用“检查”功能,可以观察我们要爬取的网站的各项元素和它们的信息。
首先我们发现,每一季每一集的台词分别保存在各自的网页中,因此我们需要爬取多个网页,而这些网页的url都可以直接从robots.txt里的sitemap.xml里取即可(我们写爬虫时最好遵循robots.txt中所写的爬取该网站的建议,不过目前这个网站似乎并没有什么限制);
接着分析每个包含独立内容的网页中,该网站不仅包含了台词,就像剧本一样,它还包括了用斜体表示的场景信息和动作信息等,这些信息通常在台词所在标签对应的标签或下,而这部分信息我们其实是不需要的,我们可以根据这一特点只提取台词信息。注意所有台词都在 标签的标签内,如第一季第一集 标签内,如第七季第二十三集 那么最后我们可以先将每集的台词独立保存,到时候只需要把对应部分的拼起来即可组成全部的台词。 第一步,先利用sitemap.xml获取所有需要爬取的网页的url。这里我们先尝试使用传统的xml库中的ElementTree库来解析xml内容,通常对于一个本地文件,可以用parse方法来解析,而对于一个字符串,则可以用fromstring方法来解析,之后通过循环索引的方式获取深藏在xml中的可爬取url。 第二步,分别爬取每一集的网页并获取信息。这一步我们使用lxml库,它和传统的xml中的ElementTree库同样都是用来解析xml文档的,但lxml使用xpath语法来进行文件格式解析,更加轻便简洁。下面我们就来尝试一下,根据我们先前对网页的分析,通过xpath路径来爬取台词内容。 由此我们就得到了每集的分集台词和总的台词语料库,可以进行文本分析了~ (由于分集台词网页的异质性,根据之前观察到的特点使用的该方法目前只提取了约四分之一的台词,后续会再逐渐尝试增加爬取更多有效内容) 在将所有台词保存到txt文件中后,我们就可以开始文本分析的工作了,这里使用的库主要是wordcloud和nltk,分别用来进行生成词云和分词。另外绘制词云需要使用matplotlib库。 事实上,由于wordcloud对英文支持较好,也包含了停用词的处理,不需要过多操作也可以绘制一个还不错的词云,如果仅仅是生成词云可以一步到位: 但如果不仅要生成词云,还想获得一定的自由,自主进行一些NLP的分析,那就需要手动进行一些步骤了。 NLP分析在获取语料之后,通常要进行文本预处理。英文的预处理包括:分词,去停词,词干提取和词形还原等步骤。 中文的分词相对于英文更复杂一些,也需要去停词。但没有词干提取和词形还原的需要。 在nltk中,分词即意味着“token化”: 代码第二行调用了word_tokenize()函数,此函数的作用是基于空格/标点等对文本进行分词,返回分词后的列表。如果要处理中文,需要三方的分词器先处理,之后才能使用nltk进行处理。 然而此处的分词仅仅是一个比较基本的分词,其中还保留了‘s这样的词,为了解决这些问题,我们在此之前还需要手动使用正则表达式将标点符号以及He’s之类的缩写进行替换: 但这时如果直接使用FreqDist类统计词频并绘制频率图,将得到许多虚词,因此需要进行停词。 所谓停用词就是那些非常常见,但没有多大信息含量的词,我们可以使用nltk自带的语料库(corpus)来去除这些停用词,在nltk.corpus下包含了一个stopword的停词库(这里需要提前下载好nltk中的stopwords): 注意这里的停用词都是小写的,所以在去除时我们需要调用lower()函数: 此外可能还需要停用一些数字和名字,这时我们就需要通过词的词性来进行过滤(这里需要提前下载好nltk中的averaged_perceptron_tagger),数字对应基数标签(CD),姓名对应着单数形式的专有名词(NNP)标签: 统计英文词频需要考虑到词形变化,例如move和moved、is和are、dog和dogs应该归为一个词而不应该分为两个词来统计,考虑到词干提取和词形还原有部分结果是交叉的,所以这里我们只对各种形式的单词进行词形还原(lemmatization)即可: 首先需要获取词性,在NLP中,使用Parts of speech(POS)技术实现。可以先定义一个函数转换为可供词形还原函数调用的形式: 在nltk中,可以使用nltk.pos_tag()获取单词在句子中的词性,之后使用WordNetLemmatizer()的lemmatize方法即可: nltk提供的FreqDist类可以用来将单词封装成字典,并计算给定单词列表中各个单词出现的次数,而词云的wordcloud类的generate_from_frequencies方法恰好可以通过字典生成词云: 可以看到由于包含了姓名过滤,最终的结果与直接生成词云有很大的不同。 从这一词云也能看出,大爆炸的台词很接地气,很适合用来学习英语~ Github项目 Big Bang Theory Transcripts xml.etree.ElementTree — The ElementTree XML API python爬虫之xpath的基本使用 利用python的jieba库、wordcloud库,实现中英文文本的快速分词(代码详解版) NLTK学习之一:简单文本分析 python数据分析(分析文本数据和社交媒体) 使用Python+NLTK实现英文单词词频统计 NLTK之词频
编写爬虫
import urllib.request
import xml.etree.ElementTree as ET
response = urllib.request.urlopen(sitemap)
xml = response.read()
root = ET.fromstring(xml) # 从parse中读出来的是tree,从fromstring中读出来的是root
urls = [child[0].text for child in root] # 遍历root的子树并提取第一个子节点的文本内容
urls = urls[0:-3] # 最后三个不是剧集的台词页面,而是网站的介绍页面,因此去掉
import urllib.request
from lxml import etree
filename = './txts/' + url[35:-1] + '.txt' # 提取季数和集数作为单独储存的文件名
response = urllib.request.urlopen(url)
xml = response.read()
html = etree.HTML(xml)
html_data = html.xpath('//p/span/text()') # 根据台词信息的特点获取所有台词:所有p标签下的span标签中的内容
if len(html_data) < 10: # 如果上一种方法提取失败,就用另一种方法提取:所有div标签下的p标签中的内容
html_data = html.xpath('//div/p/text()')
with open(filename,'w', encoding='UTF-8')as f: # 保存到分文档中,换行便于阅读
f.writelines([line+'\n' for line in html_data])
with open(txtname, 'a', encoding='UTF-8')as f: # 保存到总文档中
f.writelines([line+' ' for line in html_data])
文本分析
直接生成词云并绘制
with open(txtname,'r', encoding='UTF-8') as f:
mytext = f.read() # 直接读取全部内容
back_coloring = imread(imgname)
image_colors = ImageColorGenerator(back_coloring)
wc = WordCloud(background_color="white", # 背景颜色
max_words=2000, # 词云显示的最大词数
mask=back_coloring, # 设置背景图片
random_state=2,
margin=2 # 词语边缘像素距离
)
wc.generate(mytext)
plt.figure()
plt.imshow(wc.recolor(color_func=image_colors))
plt.axis("off")
plt.show()
wc.to_file(eptname)
NLP分析
分词
import nltk
fdist = nltk.FreqDist(nltk.word_tokenize(text))
fdist.plot(30, cumulative=True)
import re
# to find the punctuation
pat_letter = re.compile(r'[^a-zA-Z \']+')
# to find the 's following the pronouns. re.I is refers to ignore case
pat_is = re.compile("(it|he|she|that|this|there|here)(\'s)", re.I)
# to find the 's following the letters
pat_s = re.compile("(?<=[a-zA-Z])\'s")
# to find the ' following the words ending by s
pat_s2 = re.compile("(?<=s)\'s?")
# to find the abbreviation of not
pat_not = re.compile("(?<=[a-zA-Z])n\'t")
# to find the abbreviation of would
pat_would = re.compile("(?<=[a-zA-Z])\'d")
# to find the abbreviation of will
pat_will = re.compile("(?<=[a-zA-Z])\'ll")
# to find the abbreviation of am
pat_am = re.compile("(?<=[I|i])\'m")
# to find the abbreviation of are
pat_are = re.compile("(?<=[a-zA-Z])\'re")
# to find the abbreviation of have
pat_ve = re.compile("(?<=[a-zA-Z])\'ve")
new_text = pat_letter.sub(' ', new_text)
new_text = pat_is.sub(r"\1 is", new_text)
new_text = pat_s.sub("", new_text)
new_text = pat_s2.sub("", new_text)
new_text = pat_not.sub(" not", new_text)
new_text = pat_would.sub(" would", new_text)
new_text = pat_will.sub(" will", new_text)
new_text = pat_am.sub(" am", new_text)
new_text = pat_are.sub(" are", new_text)
new_text = pat_ve.sub(" have", new_text)
new_text = new_text.replace('\'', ' ')
去停词和滤词
from nltk.corpus import stopwords
stop_words = stopwords.words('english') # 提取停词库
text_stop = [word for word in text_token if len(word.lower())>1 and (word.lower()not in stop_words)] # 去除停词
taggled = nltk.pos_tag(text_stop) # 获得词性
text_filtered = [word[0] for word in taggled if word[1] != 'NNP' and word[1] != 'CD']
词干提取和词形还原
def get_wordnet_pos(tag):
from nltk.corpus import wordnet
if tag.startswith('J'):
return wordnet.ADJ
elif tag.startswith('V'):
return wordnet.VERB
elif tag.startswith('N'):
return wordnet.NOUN
elif tag.startswith('R'):
return wordnet.ADV
else:
return None
from nltk.stem import WordNetLemmatizer
wnl = WordNetLemmatizer()
text_tag = nltk.pos_tag(text_filtered) # 获取词性
text_lemmatized = []
for tag in text_tag:
wordnet_pos = get_wordnet_pos(tag[1]) or wordnet.NOUN # 转换词性格式
text_lemmatized.append(wnl.lemmatize(tag[0], pos=wordnet_pos)) # 根据词性进行词形还原
词云生成
fdist = nltk.FreqDist(text_lemmatized)
wc.generate_from_frequencies(fdist)
其他NLP分析
fdist = nltk.FreqDist(nltk.bigrams(text_lemmatized)) # 双词统计分析
fdist = nltk.FreqDist(nltk.trigrams(text_lemmatized)) # 三词统计分析
fdist.plot(20, cumulative=True) # 频率分布图
fdist.tabulate(20) # 频率分布表
fdist.tabulate(20, cumulative=True) # 频率累计表
代码与台词文本
参考资料