3.1 从网络和硬盘访问文本
电子书
NLTK 语料库集合中有古腾堡项目的一小部分样例文本。然而,你可能对分析古腾堡项目的其它文本感兴趣。你可以在http://www.gutenberg.org/catalog/
上浏览 25,000 本免费在线书籍的目录,获得 ASCII 码文本文件的 URL。虽然 90%的古腾堡项目的文本是英语的,它还包括超过 50 种语言的材料,包括加泰罗尼亚语、中文、荷兰语、芬兰语、法语、德语、意大利语、葡萄牙语和西班牙语(每种语言都有超过 100 个文本)。
编号 2554 的文本是 《罪与罚》 的英文翻译,我们可以如下方式访问它。
>>> from urllib import request
>>> url = "http://www.gutenberg.org/files/2554/2554.txt"
>>> response = request.urlopen(url)
>>> raw = response.read().decode('utf8')
>>> type(raw)
<class 'str'>
>>> len(raw)
1176893
>>> raw[:75]
'The Project Gutenberg EBook of Crime and Punishment, by Fyodor Dostoevsky\r\n'
注意
read()
过程将需要几秒钟来下载这本大书。如果你使用的 Internet 代理 Python 不能正确检测出来,你可能需要在使用urlopen
之前用下面的方法手动指定代理:
>>> proxies = {'http': 'http://www.someproxy.com:3128'}
>>> request.ProxyHandler(proxies)
变量raw
包含一个有 1,176,893 个字符的字符串。(我们使用type(raw)
可以看到它是一个字符串。)这是这本书原始的内容,包括很多我们不感兴趣的细节,如空格、换行符和空行。请注意,文件中行尾的\r
和\n
,这是 Python 用来显示特殊的回车和换行字符的方式(这个文件一定是在 Windows 机器上创建的)。对于语言处理,我们要将字符串分解为词和标点符号,正如我们在1.中所看到的。这一步被称为分词,它产生我们所熟悉的结构,一个词汇和标点符号的列表。
>>> tokens = word_tokenize(raw)
>>> type(tokens)
<class 'list'>
>>> len(tokens)
254354
>>> tokens[:10]
['The', 'Project', 'Gutenberg', 'EBook', 'of', 'Crime', 'and', 'Punishment', ',', 'by']
请注意,分词需要 NLTK,但所有前面的打开一个 URL 以及读入一个字符串的任务都不需要。如果我们现在采取进一步的步骤从这个列表创建一个 NLTK 文本,我们可以进行我们在1.中看到的所有的其他语言的处理,也包括常规的列表操作例如切片:
>>> text = nltk.Text(tokens)
>>> type(text)
<class 'nltk.text.Text'>
>>> text[1024:1062]
['CHAPTER', 'I', 'On', 'an', 'exceptionally', 'hot', 'evening', 'early', 'in',
'July', 'a', 'young', 'man', 'came', 'out', 'of', 'the', 'garret', 'in',
'which', 'he', 'lodged', 'in', 'S.', 'Place', 'and', 'walked', 'slowly',
',', 'as', 'though', 'in', 'hesitation', ',', 'towards', 'K.', 'bridge', '.']
>>> text.collocations()
Katerina Ivanovna; Pyotr Petrovitch; Pulcheria Alexandrovna; Avdotya
Romanovna; Rodion Romanovitch; Marfa Petrovna; Sofya Semyonovna; old
woman; Project Gutenberg-tm; Porfiry Petrovitch; Amalia Ivanovna;
great deal; Nikodim Fomitch; young man; Ilya Petrovitch; n't know;
Project Gutenberg; Dmitri Prokofitch; Andrey Semyonovitch; Hay Market
请注意,Project Gutenberg 以一个搭配出现。这是因为从古腾堡项目下载的每个文本都包含一个首部,里面有文本的名称、作者、扫描和校对文本的人的名字、许可证等信息。有时这些信息出现在文件末尾页脚处。我们不能可靠地检测出文本内容的开始和结束,因此在从原始
文本中挑出正确内容且没有其它内容之前,我们需要手工检查文件以发现标记内容开始和结尾的独特的字符串:
>>> raw.find("PART I")
5338
>>> raw.rfind("End of Project Gutenberg's Crime")
1157743
>>> raw = raw[5338:1157743] ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
>>> raw.find("PART I")
0
方法find()
和rfind()
(反向的 find)帮助我们得到字符串切片需要用到的正确的索引值。我们用这个切片重新给raw
赋值,所以现在它以“PART I”开始一直到(但不包括)标记内容结尾的句子。
这是我们第一次接触到网络的实际内容:在网络上找到的文本可能含有不必要的内容,并没有一个自动的方法来去除它。但只需要少量的额外工作,我们就可以提取出我们需要的材料。
处理 HTML
网络上的文本大部分是 HTML 文件的形式。你可以使用网络浏览器将网页作为文本保存为本地文件,然后按照下面关于文件的小节描述的那样来访问它。不过,如果你要经常这样做,最简单的办法是直接让 Python 来做这份工作。第一步是像以前一样使用urlopen
。为了好玩,我们将挑选一个被称为 Blondes to die out in 200 years 的 BBC 新闻故事,一个都市传奇被 BBC 作为确立的科学事实流传下来:
>>> url = "http://news.bbc.co.uk/2/hi/health/2284783.stm"
>>> html = request.urlopen(url).read().decode('utf8')
>>> html[:60]
'<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN'
你可以输入print(html)
来查看 HTML 的全部内容,包括 meta 元标签、图像标签、map 标签、JavaScript、表单和表格。
要得到 HTML 的文本,我们将使用一个名为 BeautifulSoup 的 Python 库,可从 http://www.crummy.com/software/BeautifulSoup/
访问︰
>>> from bs4 import BeautifulSoup
>>> raw = BeautifulSoup(html).get_text()
>>> tokens = word_tokenize(raw)
>>> tokens
['BBC', 'NEWS', '|', 'Health', '|', 'Blondes', "'to", 'die', 'out', ...]
它仍然含有不需要的内容,包括网站导航及有关报道等。通过一些尝试和出错你可以找到内容索引的开始和结尾,并选择你感兴趣的词符,按照前面讲的那样初始化一个文本。
>>> tokens = tokens[110:390]
>>> text = nltk.Text(tokens)
>>> text.concordance('gene')
Displaying 5 of 5 matches:
hey say too few people now carry the gene for blondes to last beyond the next
blonde hair is caused by a recessive gene . In order for a child to have blond
have blonde hair , it must have the gene on both sides of the family in the g
ere is a disadvantage of having that gene or by chance . They do n't disappear
des would disappear is if having the gene was a disadvantage and I do not thin
处理搜索引擎的结果
网络可以被看作未经标注的巨大的语料库。网络搜索引擎提供了一个有效的手段,搜索大量文本作为有关的语言学的例子。搜索引擎的主要优势是规模:因为你正在寻找这样庞大的一个文件集,会更容易找到你感兴趣语言模式。而且,你可以使用非常具体的模式,仅仅在较小的范围匹配一两个例子,但在网络上可能匹配成千上万的例子。网络搜索引擎的第二个优势是非常容易使用。因此,它是一个非常方便的工具,可以快速检查一个理论是否合理。
表 3.1:
搭配的谷歌命中次数:absolutely 或 definitely 后面跟着 adore, love, like 或 prefer 的搭配的命中次数。(Liberman, in LanguageLog, 2005)。
>>> import feedparser
>>> llog = feedparser.parse("http://languagelog.ldc.upenn.edu/nll/?feed=atom")
>>> llog['feed']['title']
'Language Log'
>>> len(llog.entries)
15
>>> post = llog.entries[2]
>>> post.title
"He's My BF"
>>> content = post.content[0].value
>>> content[:70]
'<p>Today I was chatting with three of our visiting graduate students f'
>>> raw = BeautifulSoup(content).get_text()
>>> word_tokenize(raw)
['Today', 'I', 'was', 'chatting', 'with', 'three', 'of', 'our', 'visiting',
'graduate', 'students', 'from', 'the', 'PRC', '.', 'Thinking', 'that', 'I',
'was', 'being', 'au', 'courant', ',', 'I', 'mentioned', 'the', 'expression',
'DUI4XIANG4', '\u5c0d\u8c61', '("', 'boy', '/', 'girl', 'friend', '"', ...]
伴随着一些更深入的工作,我们可以编写程序创建一个博客帖子的小语料库,并以此作为我们 NLP 的工作基础。
读取本地文件
为了读取本地文件,我们需要使用 Python 内置的open()
函数,然后是read()
方法。假设你有一个文件document.txt
,你可以像这样加载它的内容:
>>> f = open('document.txt')
>>> raw = f.read()
注意
轮到你来: 使用文本编辑器创建一个名为document.txt
的文件,然后输入几行文字,保存为纯文本。如果你使用 IDLE,在 File 菜单中选择 New Window 命令,在新窗口中输入所需的文本,然后在 IDLE 提供的弹出式对话框中的文件夹内保存文件为document.txt
。然后在 Python 解释器中使用f = open('document.txt')
打开这个文件,并使用print(f.read())
检查其内容。
当你尝试这样做时可能会出各种各样的错误。如果解释器无法找到你的文件,你会看到类似这样的错误:
>>> f = open('document.txt')
Traceback (most recent call last):
File "<pyshell#7>", line 1, in -toplevel-
f = open('document.txt')
IOError: [Errno 2] No such file or directory: 'document.txt'
要检查你正试图打开的文件是否在正确的目录中,使用 IDLE File 菜单上的 Open 命令;另一种方法是在 Python 中检查当前目录:
>>> import os
>>> os.listdir('.')
另一个你在访问一个文本文件时可能遇到的问题是换行的约定,这个约定因操作系统不同而不同。内置的open()
函数的第二个参数用于控制如何打开文件:open('document.txt', 'rU')
—— 'r'
意味着以只读方式打开文件(默认),'U'
表示“通用”,它让我们忽略不同的换行约定。
假设你已经打开了该文件,有几种方法可以阅读此文件。read()
方法创建了一个包含整个文件内容的字符串:
>>> f.read()
'Time flies like an arrow.\nFruit flies like a banana.\n'
回想一'\n'
字符是换行符;这相当于按键盘上的 Enter 开始一个新行。
我们也可以使用一个for
循环一次读文件中的一行:
>>> f = open('document.txt', 'rU')
>>> for line in f:
... print(line.strip())
Time flies like an arrow.
Fruit flies like a banana.
在这里,我们使用strip()
方法删除输入行结尾的换行符。
NLTK 中的语料库文件也可以使用这些方法来访问。我们只需使用nltk.data.find()
来获取语料库项目的文件名。然后就可以使用我们刚才讲的方式打开和阅读它:
>>> path = nltk.data.find('corpora/gutenberg/melville-moby_dick.txt')
>>> raw = open(path, 'rU').read()
从 PDF、MS Word 及其他二进制格式中提取文本
ASCII 码文本和 HTML 文本是人可读的格式。文字常常以二进制格式出现,如 PDF 和 MSWord,只能使用专门的软件打开。第三方函数库如pypdf
和pywin32
提供了对这些格式的访问。从多列文档中提取文本是特别具有挑战性的。一次性转换几个文件,会比较简单些,用一个合适的应用程序打开文件,以文本格式保存到本地驱动器,然后以如下所述的方式访问它。如果该文档已经在网络上,你可以在 Google 的搜索框输入它的 URL。搜索结果通常包括这个文档的 HTML 版本的链接,你可以将它保存为文本。
捕获用户输入
有时我们想捕捉用户与我们的程序交互时输入的文本。调用 Python 函数input()
提示用户输入一行数据。保存用户输入到一个变量后,我们可以像其他字符串那样操纵它。
>>> s = input("Enter some text: ")
Enter some text: On an exceptionally hot evening early in July
>>> print("You typed", len(word_tokenize(s)), "words.")
You typed 8 words.
NLP 的流程
3.1总结了我们在本节涵盖的内容,包括我们在1..中所看到的建立一个词汇表的过程。(其中一个步骤,规范化,将在3.6讨论。)
图 3.1:处理流程:打开一个 URL,读里面 HTML 格式的内容,去除标记,并选择字符的切片;然后分词,是否转换为nltk.Text
对象是可选择的;我们也可以将所有词汇小写并提取词汇表。
在这条流程后面还有很多操作。要正确理解它,这样有助于明确其中提到的每个变量的类型。使用type(x)
我们可以找出任一 Python 对象x
的类型,如type(1)
是<int>
因为1
是一个整数。
当我们载入一个 URL 或文件的内容时,或者当我们去掉 HTML 标记时,我们正在处理字符串,也就是 Python 的<str>
数据类型。(在3.2节,我们将学习更多有关字符串的内容):
>>> raw = open('document.txt').read()
>>> type(raw)
<class 'str'>
当我们将一个字符串分词,会产生一个(词的)列表,这是 Python 的<list>
类型。规范化和排序列表产生其它列表:
>>> tokens = word_tokenize(raw)
>>> type(tokens)
<class 'list'>
>>> words = [w.lower() for w in tokens]
>>> type(words)
<class 'list'>
>>> vocab = sorted(set(words))
>>> type(vocab)
<class 'list'>
一个对象的类型决定了它可以执行哪些操作。比如我们可以追加一个链表,但不能追加一个字符串:
>>> vocab.append('blog')
>>> raw.append('blog')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'append'
同样的,我们可以连接字符串与字符串,列表与列表,但我们不能连接字符串与列表:
>>> query = 'Who knows?'
>>> beatles = ['john', 'paul', 'george', 'ringo']
>>> query + beatles
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'list' objects