3.1 从网络和硬盘访问文本

电子书

NLTK 语料库集合中有古腾堡项目的一小部分样例文本。然而,你可能对分析古腾堡项目的其它文本感兴趣。你可以在http://www.gutenberg.org/catalog/上浏览 25,000 本免费在线书籍的目录,获得 ASCII 码文本文件的 URL。虽然 90%的古腾堡项目的文本是英语的,它还包括超过 50 种语言的材料,包括加泰罗尼亚语、中文、荷兰语、芬兰语、法语、德语、意大利语、葡萄牙语和西班牙语(每种语言都有超过 100 个文本)。

编号 2554 的文本是 《罪与罚》 的英文翻译,我们可以如下方式访问它。

  1. >>> from urllib import request
  2. >>> url = "http://www.gutenberg.org/files/2554/2554.txt"
  3. >>> response = request.urlopen(url)
  4. >>> raw = response.read().decode('utf8')
  5. >>> type(raw)
  6. <class 'str'>
  7. >>> len(raw)
  8. 1176893
  9. >>> raw[:75]
  10. 'The Project Gutenberg EBook of Crime and Punishment, by Fyodor Dostoevsky\r\n'

注意

read()过程将需要几秒钟来下载这本大书。如果你使用的 Internet 代理 Python 不能正确检测出来,你可能需要在使用urlopen之前用下面的方法手动指定代理:

  1. >>> proxies = {'http': 'http://www.someproxy.com:3128'}
  2. >>> request.ProxyHandler(proxies)

变量raw包含一个有 1,176,893 个字符的字符串。(我们使用type(raw)可以看到它是一个字符串。)这是这本书原始的内容,包括很多我们不感兴趣的细节,如空格、换行符和空行。请注意,文件中行尾的\r\n,这是 Python 用来显示特殊的回车和换行字符的方式(这个文件一定是在 Windows 机器上创建的)。对于语言处理,我们要将字符串分解为词和标点符号,正如我们在1.中所看到的。这一步被称为分词,它产生我们所熟悉的结构,一个词汇和标点符号的列表。

  1. >>> tokens = word_tokenize(raw)
  2. >>> type(tokens)
  3. <class 'list'>
  4. >>> len(tokens)
  5. 254354
  6. >>> tokens[:10]
  7. ['The', 'Project', 'Gutenberg', 'EBook', 'of', 'Crime', 'and', 'Punishment', ',', 'by']

请注意,分词需要 NLTK,但所有前面的打开一个 URL 以及读入一个字符串的任务都不需要。如果我们现在采取进一步的步骤从这个列表创建一个 NLTK 文本,我们可以进行我们在1.中看到的所有的其他语言的处理,也包括常规的列表操作例如切片:

  1. >>> text = nltk.Text(tokens)
  2. >>> type(text)
  3. <class 'nltk.text.Text'>
  4. >>> text[1024:1062]
  5. ['CHAPTER', 'I', 'On', 'an', 'exceptionally', 'hot', 'evening', 'early', 'in',
  6. 'July', 'a', 'young', 'man', 'came', 'out', 'of', 'the', 'garret', 'in',
  7. 'which', 'he', 'lodged', 'in', 'S.', 'Place', 'and', 'walked', 'slowly',
  8. ',', 'as', 'though', 'in', 'hesitation', ',', 'towards', 'K.', 'bridge', '.']
  9. >>> text.collocations()
  10. Katerina Ivanovna; Pyotr Petrovitch; Pulcheria Alexandrovna; Avdotya
  11. Romanovna; Rodion Romanovitch; Marfa Petrovna; Sofya Semyonovna; old
  12. woman; Project Gutenberg-tm; Porfiry Petrovitch; Amalia Ivanovna;
  13. great deal; Nikodim Fomitch; young man; Ilya Petrovitch; n't know;
  14. Project Gutenberg; Dmitri Prokofitch; Andrey Semyonovitch; Hay Market

请注意,Project Gutenberg 以一个搭配出现。这是因为从古腾堡项目下载的每个文本都包含一个首部,里面有文本的名称、作者、扫描和校对文本的人的名字、许可证等信息。有时这些信息出现在文件末尾页脚处。我们不能可靠地检测出文本内容的开始和结束,因此在从原始文本中挑出正确内容且没有其它内容之前,我们需要手工检查文件以发现标记内容开始和结尾的独特的字符串:

  1. >>> raw.find("PART I")
  2. 5338
  3. >>> raw.rfind("End of Project Gutenberg's Crime")
  4. 1157743
  5. >>> raw = raw[5338:1157743] ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
  6. >>> raw.find("PART I")
  7. 0

方法find()rfind()(反向的 find)帮助我们得到字符串切片需要用到的正确的索引值[1]。我们用这个切片重新给raw赋值,所以现在它以“PART I”开始一直到(但不包括)标记内容结尾的句子。

这是我们第一次接触到网络的实际内容:在网络上找到的文本可能含有不必要的内容,并没有一个自动的方法来去除它。但只需要少量的额外工作,我们就可以提取出我们需要的材料。

处理 HTML

网络上的文本大部分是 HTML 文件的形式。你可以使用网络浏览器将网页作为文本保存为本地文件,然后按照下面关于文件的小节描述的那样来访问它。不过,如果你要经常这样做,最简单的办法是直接让 Python 来做这份工作。第一步是像以前一样使用urlopen。为了好玩,我们将挑选一个被称为 Blondes to die out in 200 years 的 BBC 新闻故事,一个都市传奇被 BBC 作为确立的科学事实流传下来:

  1. >>> url = "http://news.bbc.co.uk/2/hi/health/2284783.stm"
  2. >>> html = request.urlopen(url).read().decode('utf8')
  3. >>> html[:60]
  4. '<!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/ 访问︰

  1. >>> from bs4 import BeautifulSoup
  2. >>> raw = BeautifulSoup(html).get_text()
  3. >>> tokens = word_tokenize(raw)
  4. >>> tokens
  5. ['BBC', 'NEWS', '|', 'Health', '|', 'Blondes', "'to", 'die', 'out', ...]

它仍然含有不需要的内容,包括网站导航及有关报道等。通过一些尝试和出错你可以找到内容索引的开始和结尾,并选择你感兴趣的词符,按照前面讲的那样初始化一个文本。

  1. >>> tokens = tokens[110:390]
  2. >>> text = nltk.Text(tokens)
  3. >>> text.concordance('gene')
  4. Displaying 5 of 5 matches:
  5. hey say too few people now carry the gene for blondes to last beyond the next
  6. blonde hair is caused by a recessive gene . In order for a child to have blond
  7. have blonde hair , it must have the gene on both sides of the family in the g
  8. ere is a disadvantage of having that gene or by chance . They do n't disappear
  9. 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)。

  1. >>> import feedparser
  2. >>> llog = feedparser.parse("http://languagelog.ldc.upenn.edu/nll/?feed=atom")
  3. >>> llog['feed']['title']
  4. 'Language Log'
  5. >>> len(llog.entries)
  6. 15
  7. >>> post = llog.entries[2]
  8. >>> post.title
  9. "He's My BF"
  10. >>> content = post.content[0].value
  11. >>> content[:70]
  12. '<p>Today I was chatting with three of our visiting graduate students f'
  13. >>> raw = BeautifulSoup(content).get_text()
  14. >>> word_tokenize(raw)
  15. ['Today', 'I', 'was', 'chatting', 'with', 'three', 'of', 'our', 'visiting',
  16. 'graduate', 'students', 'from', 'the', 'PRC', '.', 'Thinking', 'that', 'I',
  17. 'was', 'being', 'au', 'courant', ',', 'I', 'mentioned', 'the', 'expression',
  18. 'DUI4XIANG4', '\u5c0d\u8c61', '("', 'boy', '/', 'girl', 'friend', '"', ...]

伴随着一些更深入的工作,我们可以编写程序创建一个博客帖子的小语料库,并以此作为我们 NLP 的工作基础。

读取本地文件

为了读取本地文件,我们需要使用 Python 内置的open()函数,然后是read()方法。假设你有一个文件document.txt,你可以像这样加载它的内容:

  1. >>> f = open('document.txt')
  2. >>> raw = f.read()

注意

轮到你来: 使用文本编辑器创建一个名为document.txt的文件,然后输入几行文字,保存为纯文本。如果你使用 IDLE,在 File 菜单中选择 New Window 命令,在新窗口中输入所需的文本,然后在 IDLE 提供的弹出式对话框中的文件夹内保存文件为document.txt。然后在 Python 解释器中使用f = open('document.txt')打开这个文件,并使用print(f.read())检查其内容。

当你尝试这样做时可能会出各种各样的错误。如果解释器无法找到你的文件,你会看到类似这样的错误:

  1. >>> f = open('document.txt')
  2. Traceback (most recent call last):
  3. File "<pyshell#7>", line 1, in -toplevel-
  4. f = open('document.txt')
  5. IOError: [Errno 2] No such file or directory: 'document.txt'

要检查你正试图打开的文件是否在正确的目录中,使用 IDLE File 菜单上的 Open 命令;另一种方法是在 Python 中检查当前目录:

  1. >>> import os
  2. >>> os.listdir('.')

另一个你在访问一个文本文件时可能遇到的问题是换行的约定,这个约定因操作系统不同而不同。内置的open()函数的第二个参数用于控制如何打开文件:open('document.txt', 'rU') —— 'r'意味着以只读方式打开文件(默认),'U'表示“通用”,它让我们忽略不同的换行约定。

假设你已经打开了该文件,有几种方法可以阅读此文件。read()方法创建了一个包含整个文件内容的字符串:

  1. >>> f.read()
  2. 'Time flies like an arrow.\nFruit flies like a banana.\n'

回想一'\n'字符是换行符;这相当于按键盘上的 Enter 开始一个新行。

我们也可以使用一个for循环一次读文件中的一行:

  1. >>> f = open('document.txt', 'rU')
  2. >>> for line in f:
  3. ... print(line.strip())
  4. Time flies like an arrow.
  5. Fruit flies like a banana.

在这里,我们使用strip()方法删除输入行结尾的换行符。

NLTK 中的语料库文件也可以使用这些方法来访问。我们只需使用nltk.data.find()来获取语料库项目的文件名。然后就可以使用我们刚才讲的方式打开和阅读它:

  1. >>> path = nltk.data.find('corpora/gutenberg/melville-moby_dick.txt')
  2. >>> raw = open(path, 'rU').read()

从 PDF、MS Word 及其他二进制格式中提取文本

ASCII 码文本和 HTML 文本是人可读的格式。文字常常以二进制格式出现,如 PDF 和 MSWord,只能使用专门的软件打开。第三方函数库如pypdfpywin32提供了对这些格式的访问。从多列文档中提取文本是特别具有挑战性的。一次性转换几个文件,会比较简单些,用一个合适的应用程序打开文件,以文本格式保存到本地驱动器,然后以如下所述的方式访问它。如果该文档已经在网络上,你可以在 Google 的搜索框输入它的 URL。搜索结果通常包括这个文档的 HTML 版本的链接,你可以将它保存为文本。

捕获用户输入

有时我们想捕捉用户与我们的程序交互时输入的文本。调用 Python 函数input()提示用户输入一行数据。保存用户输入到一个变量后,我们可以像其他字符串那样操纵它。

  1. >>> s = input("Enter some text: ")
  2. Enter some text: On an exceptionally hot evening early in July
  3. >>> print("You typed", len(word_tokenize(s)), "words.")
  4. You typed 8 words.

NLP 的流程

3.1总结了我们在本节涵盖的内容,包括我们在1..中所看到的建立一个词汇表的过程。(其中一个步骤,规范化,将在3.6讨论。)

Images/pipeline1.png

图 3.1:处理流程:打开一个 URL,读里面 HTML 格式的内容,去除标记,并选择字符的切片;然后分词,是否转换为nltk.Text对象是可选择的;我们也可以将所有词汇小写并提取词汇表。

在这条流程后面还有很多操作。要正确理解它,这样有助于明确其中提到的每个变量的类型。使用type(x)我们可以找出任一 Python 对象x的类型,如type(1)&lt;int&gt;因为1是一个整数。

当我们载入一个 URL 或文件的内容时,或者当我们去掉 HTML 标记时,我们正在处理字符串,也就是 Python 的&lt;str&gt;数据类型。(在3.2节,我们将学习更多有关字符串的内容):

  1. >>> raw = open('document.txt').read()
  2. >>> type(raw)
  3. <class 'str'>

当我们将一个字符串分词,会产生一个(词的)列表,这是 Python 的&lt;list&gt;类型。规范化和排序列表产生其它列表:

  1. >>> tokens = word_tokenize(raw)
  2. >>> type(tokens)
  3. <class 'list'>
  4. >>> words = [w.lower() for w in tokens]
  5. >>> type(words)
  6. <class 'list'>
  7. >>> vocab = sorted(set(words))
  8. >>> type(vocab)
  9. <class 'list'>

一个对象的类型决定了它可以执行哪些操作。比如我们可以追加一个链表,但不能追加一个字符串:

  1. >>> vocab.append('blog')
  2. >>> raw.append('blog')
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. AttributeError: 'str' object has no attribute 'append'

同样的,我们可以连接字符串与字符串,列表与列表,但我们不能连接字符串与列表:

  1. >>> query = 'Who knows?'
  2. >>> beatles = ['john', 'paul', 'george', 'ringo']
  3. >>> query + beatles
  4. Traceback (most recent call last):
  5. File "<stdin>", line 1, in <module>
  6. TypeError: cannot concatenate 'str' and 'list' objects