3.2 字符串:最底层的文本处理

现在是时候研究一个之前我们一直故意避开的基本数据类型了。在前面的章节中,我们侧重于将文本作为一个词列表。我们并没有细致的探讨词汇以及它们是如何在编程语言中被处理的。通过使用 NLTK 中的语料库接口,我们可以忽略这些文本所在的文件。一个词的内容,一个文件的内容在编程语言中是由一个叫做字符串的基本数据类型来表示的。在本节中,我们将详细探讨字符串,并展示字符串与词汇、文本和文件之间的联系。

字符串的基本操作

可以使用单引号[1]或双引号[2]来指定字符串,如下面的例子代码所示。如果一个字符串中包含一个单引号,我们必须在单引号前加反斜杠[3]让 Python 知道这是字符串中的单引号,或者也可以将这个字符串放入双引号中[2]。否则,字符串内的单引号[4]将被解释为字符串结束标志,Python 解释器会报告一个语法错误:

  1. >>> monty = 'Monty Python' ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
  2. >>> monty
  3. 'Monty Python'
  4. >>> circus = "Monty Python's Flying Circus" ![[2]](/projects/nlp-py-2e-zh/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg)
  5. >>> circus
  6. "Monty Python's Flying Circus"
  7. >>> circus = 'Monty Python\'s Flying Circus' ![[3]](/projects/nlp-py-2e-zh/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg)
  8. >>> circus
  9. "Monty Python's Flying Circus"
  10. >>> circus = 'Monty Python's Flying Circus' ![[4]](/projects/nlp-py-2e-zh/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg)
  11. File "<stdin>", line 1
  12. circus = 'Monty Python's Flying Circus'
  13. ^
  14. SyntaxError: invalid syntax

有时字符串跨好几行。Python 提供了多种方式表示它们。在下面的例子中,一个包含两个字符串的序列被连接为一个字符串。我们需要使用反斜杠[1]或者括号[2],这样解释器就知道第一行的表达式不完整。

  1. >>> couplet = "Shall I compare thee to a Summer's day?"\
  2. ... "Thou are more lovely and more temperate:" ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
  3. >>> print(couplet)
  4. Shall I compare thee to a Summer's day?Thou are more lovely and more temperate:
  5. >>> couplet = ("Rough winds do shake the darling buds of May,"
  6. ... "And Summer's lease hath all too short a date:") ![[2]](/projects/nlp-py-2e-zh/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg)
  7. >>> print(couplet)
  8. Rough winds do shake the darling buds of May,And Summer's lease hath all too short a date:

不幸的是,这些方法并没有展现给我们十四行诗的两行之间的换行。为此,我们可以使用如下所示的三重引号的字符串:

  1. >>> couplet = """Shall I compare thee to a Summer's day?
  2. ... Thou are more lovely and more temperate:"""
  3. >>> print(couplet)
  4. Shall I compare thee to a Summer's day?
  5. Thou are more lovely and more temperate:
  6. >>> couplet = '''Rough winds do shake the darling buds of May,
  7. ... And Summer's lease hath all too short a date:'''
  8. >>> print(couplet)
  9. Rough winds do shake the darling buds of May,
  10. And Summer's lease hath all too short a date:

现在我们可以定义字符串,也可以在上面尝试一些简单的操作。首先,让我们来看看+操作,被称为连接 [1]。此操作产生一个新字符串,它是两个原始字符串首尾相连粘贴在一起而成。请注意,连接不会做一些比较聪明的事,例如在词汇之间插入空格。我们甚至可以对字符串用乘法[2]

  1. >>> 'very' + 'very' + 'very' ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
  2. 'veryveryvery'
  3. >>> 'very' * 3 ![[2]](/projects/nlp-py-2e-zh/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg)
  4. 'veryveryvery'

注意

轮到你来: 试运行下面的代码,然后尝试使用你对字符串+*操作的理解,弄清楚它是如何运作的。要小心区分字符串' ',这是一个空格符,和字符串'',这是一个空字符串。

  1. >>> a = [1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1]
  2. >>> b = [' ' * 2 * (7 - i) + 'very' * i for i in a]
  3. >>> for line in b:
  4. ... print(line)

我们已经看到加法和乘法运算不仅仅适用于数字也适用于字符串。但是,请注意,我们不能对字符串用减法或除法:

  1. >>> 'very' - 'y'
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. TypeError: unsupported operand type(s) for -: 'str' and 'str'
  5. >>> 'very' / 2
  6. Traceback (most recent call last):
  7. File "<stdin>", line 1, in <module>
  8. TypeError: unsupported operand type(s) for /: 'str' and 'int'

这些错误消息是 Python 的另一个例子,告诉我们的数据类型混乱。第一种情况告诉我们减法操作(即-) 不能适用于str(字符串)对象类型,而第二种情况告诉我们除法的两个操作数不能分别为strint

输出字符串

到目前为止,当我们想看看变量的内容或想看到计算的结果,我们就把变量的名称输入到解释器。我们还可以使用print语句来看一个变量的内容:

  1. >>> print(monty)
  2. Monty Python

请注意这次是没有引号的。当我们通过输入变量的名字到解释器中来检查它时,解释器输出 Python 中的变量的值。因为它是一个字符串,结果被引用。然而,当我们告诉解释器print这个变量时,我们没有看到引号字符,因为字符串的内容里面没有引号。

print语句可以多种方式将多个元素显示在一行,就像这样:

  1. >>> grail = 'Holy Grail'
  2. >>> print(monty + grail)
  3. Monty PythonHoly Grail
  4. >>> print(monty, grail)
  5. Monty Python Holy Grail
  6. >>> print(monty, "and the", grail)
  7. Monty Python and the Holy Grail

访问单个字符

正如我们在2看到的列表,字符串也是被索引的,从零开始。当我们索引一个字符串时,我们得到它的一个字符(或字母)。一个单独的字符并没有什么特别,它只是一个长度为1的字符串。

  1. >>> monty[0]
  2. 'M'
  3. >>> monty[3]
  4. 't'
  5. >>> monty[5]
  6. ' '

与列表一样,如果我们尝试访问一个超出字符串范围的索引时,会得到了一个错误:

  1. >>> monty[20]
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in ?
  4. IndexError: string index out of range

也与列表一样,我们可以使用字符串的负数索引,其中-1是最后一个字符的索引[1]。正数和负数的索引给我们两种方式指示一个字符串中的任何位置。在这种情况下,当一个字符串长度为 12 时,索引5-7都指示相同的字符(一个空格)。(请注意,5 = len(monty) - 7。)

  1. >>> monty[-1] ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
  2. 'n'
  3. >>> monty[5]
  4. ' '
  5. >>> monty[-7]
  6. ' '

我们可以写一个for循环,遍历字符串中的字符。print函数包含可选的end=' '参数,这是为了告诉 Python 不要在行尾输出换行符。

  1. >>> sent = 'colorless green ideas sleep furiously'
  2. >>> for char in sent:
  3. ... print(char, end=' ')
  4. ...
  5. c o l o r l e s s g r e e n i d e a s s l e e p f u r i o u s l y

我们也可以计数单个字符。通过将所有字符小写来忽略大小写的区分,并过滤掉非字母字符。

  1. >>> from nltk.corpus import gutenberg
  2. >>> raw = gutenberg.raw('melville-moby_dick.txt')
  3. >>> fdist = nltk.FreqDist(ch.lower() for ch in raw if ch.isalpha())
  4. >>> fdist.most_common(5)
  5. [('e', 117092), ('t', 87996), ('a', 77916), ('o', 69326), ('n', 65617)]
  6. >>> [char for (char, count) in fdist.most_common()]
  7. ['e', 't', 'a', 'o', 'n', 'i', 's', 'h', 'r', 'l', 'd', 'u', 'm', 'c', 'w',
  8. 'f', 'g', 'p', 'b', 'y', 'v', 'k', 'q', 'j', 'x', 'z']
  1. >>> monty[6:10]
  2. 'Pyth'

在这里,我们看到的字符是'P', 'y', 't''h',它们分别对应于monty[6]monty[9]而不包括monty[10]。这是因为切片开始于第一个索引,但结束于最后一个索引的前一个。

我们也可以使用负数索引切片——也是同样的规则,从第一个索引开始到最后一个索引的前一个结束;在这里是在空格字符前结束。

  1. >>> monty[-12:-7]
  2. 'Monty'

与列表切片一样,如果我们省略了第一个值,子字符串将从字符串的开头开始。如果我们省略了第二个值,则子字符串直到字符串的结尾结束:

  1. >>> monty[:5]
  2. 'Monty'
  3. >>> monty[6:]
  4. 'Python'

我们使用in操作符测试一个字符串是否包含一个特定的子字符串,如下所示:

  1. >>> phrase = 'And now for something completely different'
  2. >>> if 'thing' in phrase:
  3. ... print('found "thing"')
  4. found "thing"

我们也可以使用find()找到一个子字符串在字符串内的位置:

  1. >>> monty.find('Python')
  2. 6

注意

轮到你来: 造一句话,将它分配给一个变量, 例如,sent = 'my sentence...'。写切片表达式抽取个别词。(这显然不是一种方便的方式来处理文本中的词!)

更多的字符串操作

Python 对处理字符串的支持很全面。3.2.所示是一个总结,其中包括一些我们还没有看到的操作。关于字符串的更多信息,可在 Python 提示符下输入help(str)

表 3.2:

有用的字符串方法:4.2中字符串测试之外的字符串上的操作;所有的方法都产生一个新的字符串或列表

  1. >>> query = 'Who knows?'
  2. >>> beatles = ['John', 'Paul', 'George', 'Ringo']
  3. >>> query[2]
  4. 'o'
  5. >>> beatles[2]
  6. 'George'
  7. >>> query[:2]
  8. 'Wh'
  9. >>> beatles[:2]
  10. ['John', 'Paul']
  11. >>> query + " I don't"
  12. "Who knows? I don't"
  13. >>> beatles + 'Brian'
  14. Traceback (most recent call last):
  15. File "<stdin>", line 1, in <module>
  16. TypeError: can only concatenate list (not "str") to list
  17. >>> beatles + ['Brian']
  18. ['John', 'Paul', 'George', 'Ringo', 'Brian']

当我们在一个 Python 程序中打开并读入一个文件,我们得到一个对应整个文件内容的字符串。如果我们使用一个for循环来处理这个字符串元素,所有我们可以挑选出的只是单个的字符——我们不选择粒度。相比之下,列表中的元素可以很大也可以很小,只要我们喜欢:例如,它们可能是段落、句子、短语、单词、字符。所以,列表的优势是我们可以灵活的决定它包含的元素,相应的后续的处理也变得灵活。因此,我们在一段 NLP 代码中可能做的第一件事情就是将一个字符串分词放入一个字符串列表中(3.7)。相反,当我们要将结果写入到一个文件或终端,我们通常会将它们格式化为一个字符串(3.9)。

列表与字符串没有完全相同的功能。列表具有增强的能力使你可以改变其中的元素:

  1. >>> beatles[0] = "John Lennon"
  2. >>> del beatles[-1]
  3. >>> beatles
  4. ['John Lennon', 'Paul', 'George']

另一方面,如果我们尝试在一个 字符串 上这么做——将query的第 0 个字符修改为'F'——我们得到:

  1. >>> query[0] = 'F'
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in ?
  4. TypeError: object does not support item assignment

这是因为字符串是不可变的:一旦你创建了一个字符串,就不能改变它。然而,列表是可变的,其内容可以随时修改。作为一个结论,列表支持修改原始值的操作,而不是产生一个新的值。

注意

轮到你来: 通过尝试本章结尾的一些练习,巩固你的字符串知识。