使用NLTK统计真题词频以及搜索真题句子
前言
作为一个教研和英语编辑,统计真题词频和搜题是刚需中的刚需。
虽然这个需求可以通过使用企业内部的数据库,或是自行去类似组卷网这样的网站解决,但是有的时候还是自己做会比较好,不仅因为自己操作有很高的自由度(如绘制词云、提取关键词、一键撰稿、筛选高频词等),更是因为数据都从自己手中过能从宏观角度加深对题目的认识。
自己统计词频所需要的东西很简单:
- 真题(为方便读取最好是txt格式的)
- 考纲词表
下面是代码讲解和示例:
统计词频
首先导入需要的包。NLTK可以说是民用NLP界的最强武器了。
import nltk
from nltk.probability import FreqDist
from nltk.corpus import wordnet
import pandas as pd
第一步,分句子
不要着急直接把所有内容变成token,splitter.tokenize()能够把文章分成句子,这对之后标记词性以及搜索整句真题句子很有帮助。
def get_sentences(text):
splitter = nltk.data.load('tokenizers/punkt/english.pickle')
sentences = splitter.tokenize(text)
return sentences
>>>['We had awakened early that winter morning, puzzled at first by the added light that filled the room.']
第二步,分词
此处使用Treebank的分词器分词
tokenizer = nltk.tokenize.TreebankWordTokenizer()
def get_token(sentences):
return [tokenizer.tokenize(sent) for sent in sentences]
>>>[['We', 'had', 'awakened', 'early', 'that', 'winter', 'morning', ',', 'puzzled', 'at', 'first', 'by', 'the', 'added', 'light', 'that', 'filled', 'the', 'room', '.']
第三步,标记词性
这里是为了更好的lemmatization而做准备。
统计词频肯定是需要尽量把所有变形的单词都统计进去,例如goose和geese,play、playing、played肯定不应该分开计数。对于更精确的统计可能需要用到各式各样的神经网络模型,像我们这种民用级的小玩具只需要查询字典+还原成原型就可以了。
lemmatization需要根据词性判断才能准确将单词还原成原型,例如meeting在'We're meeting in a room."中就应该还原成meet,而在"I am attending a meeting"中就应该还原成meeting。
NLTK自带的pos_tag()就可以很好的根据token所在句子的语境标记上正确的词性。
def pos_tag(tokens):
return [nltk.pos_tag(token) for token in tokens]
>>>[[('We', 'PRP'), ('had', 'VBD'), ('awakened', 'VBN'), ('early', 'RB'), ('that', 'IN'), ('winter', 'NN'), ('morning', 'NN'), (',', ','), ('puzzled', 'VBD'), ('at', 'IN'), ('first', 'JJ'), ('by', 'IN'), ('the', 'DT'), ('added', 'JJ'), ('light', 'NN'), ('that', 'WDT'), ('filled', 'VBD'), ('the', 'DT'), ('room', 'NN'), ('.', '.')]
这里的词性使用的是Penn Treebank格式,但是NLTK的lemmatize()所接受的词性参数只能是'a', 'v', 'n'这样的Wornet格式,所以我们还需要自己再写一个函数,把Treebank的词性改一下格式:
def get_wordnet_pos_tag(nltk_pos_tag):
if nltk_pos_tag.startswith('J'):
return wordnet.ADJ
elif nltk_pos_tag.startswith('V'):
return wordnet.VERB
elif nltk_pos_tag.startswith('N'):
return wordnet.NOUN
elif nltk_pos_tag.startswith('R'):
return wordnet.ADV
else:
return wordnet.NOUN
第四步,lemmatize
为了方便后续获取单词、词性和lemma,索性就直接打印在一个list里输出了
def lemmatize(pos_tokens):
pos_tokens = [[(word, lemmatizer.lemmatize(word, get_wordnet_pos_tag(pos_tag)), [pos_tag])
for (word, pos_tag) in pos] for pos in pos_tokens]
return pos_tokens
def get_lemma(pos_tokens):
return [lemma[1] for i in range(len(pos_tokens)) for lemma in pos_tokens[i]]
>>>[[('We', 'We', ['PRP']), ('had', 'have', ['VBD']), ('awakened', 'awaken', ['VBN']), ('early', 'early', ['RB']), ('that', 'that', ['IN']), ('winter', 'winter', ['NN']), ('morning', 'morning', ['NN']), (',', ',', [',']), ('puzzled', 'puzzle', ['VBD']), ('at', 'at', ['IN']), ('first', 'first', ['JJ']), ('by', 'by', ['IN']), ('the', 'the', ['DT']), ('added', 'added', ['JJ']), ('light', 'light', ['NN']), ('that', 'that', ['WDT']), ('filled', 'fill', ['VBD']), ('the', 'the', ['DT']), ('room', 'room', ['NN']), ('.', '.', ['.'])]
>>>['We', 'have', 'awaken', 'early', 'that', 'winter', 'morning', ',', 'puzzle', 'at', 'first', 'by', 'the', 'added', 'light', 'that', 'fill', 'the', 'room', '.']
第五步,统计词频
接下来就很简单了,直接用freqdist就可以拿到统计数字了,输出就是一个字典,然后你可以用你准备好的词表word_list去查询就可以获得想要单词的频率了
def get_freq_dict(lemma):
return FreqDict(lemma)
def get_freq(freq_dict, word_list):
return [freq_dict[word.strip('\n')] for word in word_list]
第六步,做成csv分享给你的同事
df = pd.DataFrame(columns=["单词", "频率"])
df['单词'] = word_list
df["频率"] = freq_list
df.to_csv("output.csv")
搜题
有了统计词频和lemmatize的代码之后,我们去搜真题句子就方便很多了。理想情况是我们把所有真题的txt文件都装在一个文件夹里,然后它们的标题就是题目年份,例如'2016年高考英语全国卷1B.txt', '2021英语高考全国甲卷D篇.txt'
我们可以用os遍历整个文件夹,每次循环查看一个文件,分词并lemmatize里面的内容,然后用词表匹配lemma,如果匹配到了考纲词汇,就可以直接输出上述第一步中的一整句话,最后再用document做成Word文档分享给同事即可。
下面是代码示例:
for word in word_list:
found = [] # 每一个单词专用一个存真题句子的list,每次循环重置
# 每个词都遍历一遍所有真题
for dirpath, dirnames, filenames in os.walk(paper_path):
for file in filenames:
# 过滤掉烦人的DS.store
if file.endswith('.txt'):
# 第一步~第三步
text = t.get_text(f"{dirpath}/{file}")
sentences = splitter.tokenize(text)
tokens = [tokenizer.tokenize(sent) for sent in sentences]
lemma_pos_token = lemmatize(tokens)
# 第四步
lemmatized_sent = []
for i in range(len(lemma_pos_token)):
lemmatized_words = []
for lemmas in lemma_pos_token[i]:
lemmatized_words.append(lemmas[1])
lemmatized_sent.append(lemmatized_words)
# 开始匹配
for token_sent in lemmatized_sent:
if word in token_sent:
target_sent = " ".join(token_sent)
found.append(file.strip('.txt') + " " + target_sent)
# 添加到document中,导出成Word文档
document.add_paragraph(word)
for f in found:
document.add_paragraph(f)
document.add_paragraph('')
document.save('~.docx')