3.7 用正则表达式为文本分词

分词是将字符串切割成可识别的构成一块语言数据的语言单元。虽然这是一项基础任务,我们能够一直拖延到现在为止才讲,是因为许多语料库已经分过词了,也因为 NLTK 中包括一些分词器。现在你已经熟悉了正则表达式,你可以学习如何使用它们来为文本分词,并对此过程中有更多的掌控权。

分词的简单方法

文本分词的一种非常简单的方法是在空格符处分割文本。考虑以下摘自 《爱丽丝梦游仙境》 中的文本:

  1. >>> raw = """'When I'M a Duchess,' she said to herself, (not in a very hopeful tone
  2. ... though), 'I won't have any pepper in my kitchen AT ALL. Soup does very
  3. ... well without--Maybe it's always pepper that makes people hot-tempered,'..."""

我们可以使用raw.split()在空格符处分割原始文本。使用正则表达式能做同样的事情,匹配字符串中的所有空格符[1]是不够的,因为这将导致分词结果包含\n换行符;我们需要匹配任何数量的空格符、制表符或换行符[2]

  1. >>> re.split(r' ', raw) ![[1]](/projects/nlp-py-2e-zh/Images/7e6ea96aad77f3e523494b3972b5a989.jpg)
  2. ["'When", "I'M", 'a', "Duchess,'", 'she', 'said', 'to', 'herself,', '(not', 'in',
  3. 'a', 'very', 'hopeful', 'tone\nthough),', "'I", "won't", 'have', 'any', 'pepper',
  4. 'in', 'my', 'kitchen', 'AT', 'ALL.', 'Soup', 'does', 'very\nwell', 'without--Maybe',
  5. "it's", 'always', 'pepper', 'that', 'makes', 'people', "hot-tempered,'..."]
  6. >>> re.split(r'[ \t\n]+', raw) ![[2]](/projects/nlp-py-2e-zh/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg)
  7. ["'When", "I'M", 'a', "Duchess,'", 'she', 'said', 'to', 'herself,', '(not', 'in',
  8. 'a', 'very', 'hopeful', 'tone', 'though),', "'I", "won't", 'have', 'any', 'pepper',
  9. 'in', 'my', 'kitchen', 'AT', 'ALL.', 'Soup', 'does', 'very', 'well', 'without--Maybe',
  10. "it's", 'always', 'pepper', 'that', 'makes', 'people', "hot-tempered,'..."]

正则表达式«[ \t\n]+»匹配一个或多个空格、制表符(\t)或换行符(\n)。其他空白字符,如回车和换页符,实际上应该也包含。于是,我们将使用一个re库内置的缩写\s,它表示匹配所有空白字符。前面的例子中第二条语句可以改写为re.split(r'\s+', raw)

注意

要点: 记住在正则表达式前加字母r(表示”原始的”),它告诉 Python 解释器按照字面表示对待字符串,而不去处理正则表达式中包含的反斜杠字符。

在空格符处分割文本给我们如'(not''herself,'这样的词符。另一种方法是使用 Python 提供给我们的字符类\w匹配词中的字符,相当于[a-zA-Z0-9_]。它还定义了这个类的补集\W,即所有字母、数字和下划线以外的字符。我们可以在一个简单的正则表达式中用\W来分割所有单词字符 以外 的输入:

  1. >>> re.split(r'\W+', raw)
  2. ['', 'When', 'I', 'M', 'a', 'Duchess', 'she', 'said', 'to', 'herself', 'not', 'in',
  3. 'a', 'very', 'hopeful', 'tone', 'though', 'I', 'won', 't', 'have', 'any', 'pepper',
  4. 'in', 'my', 'kitchen', 'AT', 'ALL', 'Soup', 'does', 'very', 'well', 'without',
  5. 'Maybe', 'it', 's', 'always', 'pepper', 'that', 'makes', 'people', 'hot', 'tempered',
  6. '']

可以看到,在开始和结尾都给了我们一个空字符串(要了解原因请尝试'xx'.split('x'))。通过re.findall(r'\w+', raw)使用模式匹配词汇而不是空白符号,我们得到相同的标识符,但没有空字符串。现在,我们正在匹配词汇,我们处在扩展正则表达式覆盖更广泛的情况的位置。正则表达式«\w+|\S\w*»将首先尝试匹配词中字符的所有序列。如果没有找到匹配的,它会尝试匹配后面跟着词中字符的任何 空白字符(\S\s的补)。这意味着标点会与跟在后面的字母(如’s)在一起,但两个或两个以上的标点字符序列会被分割。

  1. >>> re.findall(r'\w+|\S\w*', raw)
  2. ["'When", 'I', "'M", 'a', 'Duchess', ',', "'", 'she', 'said', 'to', 'herself', ',',
  3. '(not', 'in', 'a', 'very', 'hopeful', 'tone', 'though', ')', ',', "'I", 'won', "'t",
  4. 'have', 'any', 'pepper', 'in', 'my', 'kitchen', 'AT', 'ALL', '.', 'Soup', 'does',
  5. 'very', 'well', 'without', '-', '-Maybe', 'it', "'s", 'always', 'pepper', 'that',
  6. 'makes', 'people', 'hot', '-tempered', ',', "'", '.', '.', '.']

让我们扩展前面表达式中的\w+,允许连字符和撇号:«\w+([-']\w+)*»。这个表达式表示\w+后面跟零个或更多[-']\w+的实例;它会匹配 hot-tempered 和 it’s。(我们需要在这个表达式中包含?:,原因前面已经讨论过。)我们还将添加一个模式来匹配引号字符,让它们与它们包括的文字分开。

  1. >>> print(re.findall(r"\w+(?:[-']\w+)*|'|[-.(]+|\S\w*", raw))
  2. ["'", 'When', "I'M", 'a', 'Duchess', ',', "'", 'she', 'said', 'to', 'herself', ',',
  3. '(', 'not', 'in', 'a', 'very', 'hopeful', 'tone', 'though', ')', ',', "'", 'I',
  4. "won't", 'have', 'any', 'pepper', 'in', 'my', 'kitchen', 'AT', 'ALL', '.', 'Soup',
  5. 'does', 'very', 'well', 'without', '--', 'Maybe', "it's", 'always', 'pepper',
  6. 'that', 'makes', 'people', 'hot-tempered', ',', "'", '...']

上面的表达式也包括«[-.(]+»,这会使双连字符、省略号和左括号被单独分词。

3.4列出了我们已经在本节中看到的正则表达式字符类符号,以及一些其他有用的符号。

表 3.4:

正则表达式符号

  1. >>> text = 'That U.S.A. poster-print costs $12.40...'
  2. >>> pattern = r'''(?x) # set flag to allow verbose regexps
  3. ... ([A-Z]\.)+ # abbreviations, e.g. U.S.A.
  4. ... | \w+(-\w+)* # words with optional internal hyphens
  5. ... | \$?\d+(\.\d+)?%? # currency and percentages, e.g. $12.40, 82%
  6. ... | \.\.\. # ellipsis
  7. ... | [][.,;"'?():-_`] # these are separate tokens; includes ], [
  8. ... '''
  9. >>> nltk.regexp_tokenize(text, pattern)
  10. ['That', 'U.S.A.', 'poster-print', 'costs', '$12.40', '...']

使用 verbose 标志时,不可以再使用' '来匹配一个空格字符;使用\s代替。regexp_tokenize()函数有一个可选的gaps参数。设置为True时,正则表达式指定标识符间的距离,就像使用re.split()一样。

注意

我们可以使用set(tokens).difference(wordlist)通过比较分词结果与一个词表,然后报告任何没有在词表出现的标识符,来评估一个分词器。你可能想先将所有标记变成小写。

分词的进一步问题

分词是一个比你可能预期的要更为艰巨的任务。没有单一的解决方案能在所有领域都行之有效,我们必须根据应用领域的需要决定那些是词符。

在开发分词器时,访问已经手工分词的原始文本是有益的,这可以让你的分词器的输出结果与高品质(或称“黄金标准”)的词符进行比较。NLTK 语料库集合包括宾州树库的数据样本,包括《华尔街日报》原始文本(nltk.corpus.treebank_raw.raw())和分好词的版本(nltk.corpus.treebank.words())。

分词的最后一个问题是缩写的存在,如 didn’t。如果我们想分析一个句子的意思,将这种形式规范化为两个独立的形式:did 和 n’t(或 not)可能更加有用。我们可以通过查表来做这项工作。