为了充分挖掘本书的价值,需要熟悉Python,需要达到玩转Python的程度。当代码不能正常运行时,需要能够尝试各种方法,探寻一种能够让Python按照我们的设想运行的方式。

即使代码运行正常,各种尝试也有助于发现很酷的新方法或者隐藏在代码中的“怪物”。因为类似于英语这样的语言有很多不同的表达方式,所以隐藏的错误和边界情况在自然语言处理中非常常见。

为了获得乐趣,你只需像孩子一样,对Python代码进行各种尝试。如果是复制和粘贴代码,那么试着去修改。尝试一下破坏并修复代码,将代码拆分成尽可能多的独立表达式,通过函数或类为代码片段创建模块,然后将其还原成尽可能少的代码行。

使用自己创建的数据结构、模型或函数随意尝试。尝试运行你认为应该包含在模块或类中的命令。经常使用键盘上的Tab键。当按下Tab键时,编辑器或shell会尝试基于已经完成键入的变量、类、函数、方法、属性或者路径名称来补全你的输入。

使用Python和shell提供的所有help命令。就像Linux shell中的man一样,help()是内置在Python中的好朋友。尝试在Python控制台输入help或help(object)。当IPython上运行?和??命令失败时,它应该也能正常运行。如果读者以前从未这样做过,尝试一下在Jupyter控制台或Jupyter记事本中运行object?和object??命令。

这篇Python入门介绍的剩余部分列举了本书中用到的数据结构和函数,以便我们可以开始使用它们:

  • str和bytes;
  • ord和chr;
  • .format();
  • dict和OrderedDict;
  • list、np.array、pd.Series;
  • pd.DataFrame。

我们还介绍了在本书和nlpia包中用到的一些模式和内置Python函数:

  • 列表解析式——[x for x in range(10)];
  • 生成器——(x for x in range(1000000000));
  • 正则表达式——re.match(r'[A-Za-z ]+', 'Hello World');
  • 文件操作符——open('path/to/file.txt')。

B.1 处理字符串

自然语言处理完全就是处理字符串。Python 3中的字符串有很多可能会令人感到意外的怪异之处,特别当大家有丰富的Python 2经验时更是如此。因此,大家需要熟悉字符串以及处理它们的所有方式,以便能以轻松处理自然语言中的字符串。

B.1.1 字符串类型(str和bytes)

字符串(str)是Unicode字符序列。如果在str中使用非ASCII字符,则这些字符可能包含多个字节。如果从网上复制字符串并粘贴到Python控制台或者程序中,会经常包含非ASCII字符,其中有一些还很难发现,例如那些不对称的引号和省略号。

当使用Python的open命令打开文件时,默认情况下该文件会被作为str读入。如果打开二进制文件,如预训练Word2vec模型的“.txt”文件,却没有指定mode = 'b',那么文件将无法正确加载。尽管gensim.KeyedVectors模型的类型可能是文本而不是二进制文件,但必须以二进制模式打开,这样gensim在加载模型时Unicode字符不会出现乱码,这一点对于使用Python 2保存的CSV文件或其他任何文本文件都适用。

二进制文件(bytes)是8位的数组,通常用于保存ASCII字符或扩展ASCII字符(十进制整数值大于128的字符)。[1]二进制文件有时也用来存储RAW格式图像、WAV音频文件或其他二进制数据对象。

B.1.2 Python中的模板(.format())

Python附带了一个多功能字符串模板系统,允许使用变量值填充字符串。这可以让我们使用数据库的结果或者运行中的python程序(locals())的上下文创建动态的响应。

B.2 Python中的映射(dict和OrderedDict)

哈希表(或映射)数据结构内置在Python的dict对象中。但是dict不强制一致的键顺序,因此标准Python库中的collections模块包含一个OrderedDict,允许大家控制键值对以一致的顺序来存储(基于插入新键时的顺序)。

B.3 正则表达式

正则表达式是具备自己的编程语言的小型计算机程序。每个像r'[a-z] +'这样的正则表达式字符串都可以编译成一个小程序,用于在其他字符串上运行以查找匹配项。我们在后面提供了快速参考和一些示例,但是如果要认真对待NLP的话,那么可能希望深入研究一些在线教程。像前面一样,最好的学习方法是在命令行中进行各种尝试。nlpia包有很多自然语言文本文档和一些有用的正则表达式示例供大家尝试。

正则表达式定义了条件表达式序列(类似于Python中的if语句),每个条件表达式都作用于单个字符。条件序列形成一棵树,最终得出“输入字符串是否匹配”这个问题的答案。因为每个正则表达式只能匹配有限数量的字符串并且具有有限数量的条件分支,所以它定义了一个有限状态机(FSM)。[2]

re包是Python中默认的正则表达式编译器/解释器,但新的官方包是regex,可以使用pip install regex轻松安装。后者更强大,能更好地支持Unicode字符和模糊匹配(对于NLP来说非常棒)。下面的示例不需要这些额外功能,因此可以使用上述两个包中的任意一个。只需要学习一些正则表达式符号,就可以解决本书中的问题:

  • |——或(OR)符号;
  • ()——用括号分组,就像在Python表达式中一样;
  • []——字符类;
  • \s、\b、\d、\w——常见字符类的快捷方式;
  • *、?、+——一些限制字符类出现次数的快捷方式;
  • {7, 10}——当*、?、和+不够用时,可以使用花括号指定出现次数的范围。

B.3.1 “|”——或

“|”操作符用于分隔字符串,这些字符串可以选择性地匹配输入字符串从而得到正则表达式的整体匹配。因此,正则表达式Hobson|Cole|Hannes将匹配本书任何一个作者的名字(名)。和其他大多数编程语言一样,模式从左到右进行匹配,当匹配上之后停止匹配(“短路”)。所以在这种情况下,OR符号(|)之间模式的顺序不会影响匹配,因为所有模式(作者名字)对应的前两个字符都是唯一的字符序列。代码清单B-1展示了作者姓名的位置变换,便于大家自己查看结果。

代码清单B-1 正则表达式中的OR符号

>>> import re
>>> re.findall(r'Hannes|Hobson|Cole', 'Hobson Lane, Cole Howard,
➥ and Hannes Max Hapke')
['Hobson', 'Cole', 'Hannes']   ⇽--- .findall()函数会在输入字符串中查找所有非重叠的正则表达式匹配结果,因此其返回的是一个列表

要尝试Python的趣味性,看看是否可以让正则表达式在第一个模式上“短路”,而当由人来判断这3个模式时,可能会选择更好的匹配:

>>> re.findall(r'H|Hobson|Cole', 'Hobson Lane, Cole Howard,
➥ and Hannes Max Hapke')
['H', 'Cole', 'H', 'H', 'H']

B.3.2 “()”——分组

可以使用括号将多个符号模式分组到一个表达式中。每个分组表达式作为一个整体进行匹配。所以r'(kitt|dogg)ie'匹配“kitty”或“doggy”。如果没有括号,r'kitt|dogg将匹配“kitt”或“doggy”(注意没有“kitty”)。

分组有另一个目的,它们可用于捕获(提取)输入文本的一部分。每个分组都在groups()列表中分配了一个位置,可以根据它们的索引从左到右进行提取。.group()方法返回整个表达式的默认整体组。可以使用前一个组来捕获kitty/doggy正则表达式的“词干”(没有y的部分),如代码清单B-2所示。

代码清单B-2 正则表达式中的分组括号

>>> import re
>>> match = re.match(r'(kitt|dogg)y', "doggy")
>>> match.group()'doggy'
>>> match.group(0)
'dogg'
>>> match.groups()('dogg',)
>>> match = re.match(r'((kitt|dogg)(y))', "doggy")  ⇽--- 如果想捕获其分组中的每一部分
>>> match.groups()('doggy', 'dogg', 'y')
>>> match.group(2)
'y'

如果希望/需要为命名分组,以便将信息提取为结构化数据类型(dict),则需要在分组的开头使用符号P,如(P?<animal_stemm>dogg|kitt)y。[3]

B.3.3 “[]”——字符类

字符类等同于一组字符之间使用OR符号(|)连接,因此[abcd]相当于(a|b|c|d),[abc123]相当于(a|b|c|1|2|3)。

如果字符类中的某些字符是字符表(ASCII或Unicode)中的连续字符,则可以在字符之间使用连字符进行缩写。因此[a-d]相当于[abcd]或(a|b|c|d),[a-c1-3]是[abc123]和(a|b|c|1|2|3)的缩写。

字符类快捷方式:

  • \s——[\t\n\r],空白符;
  • \b——字母或数字边界;
  • \d——[0-9],一位数字;
  • \w——[a-zA-Z0-9_],一个词或者变量名。

B.4 代码风格

即使不打算与他人共享代码,也要尝试遵守PEP8。未来大家将会为能够高效地阅读和调试代码而心存感激。将代码风格检查工具或自动风格修正器添加到编辑器或IDE中是引入PEP8工具的最简单方法。

另一种有助于自然语言处理的风格约定是如何在两个都是引号的符号('和")之间做出选择。无论怎么选择,都要保持一致性。有一个方法可以帮助专业人士提高代码的可读性。在定义用于机器处理的字符串时,总是使用单引号('),例如正则表达式、标记和标签。然后,对于人使用的自然语言语料库,可以使用双引号('"')。

对于原始字符串(r''和r"")怎么选择呢?所有正则表达式都应该是单引号的原始字符串,如r'match [ ] this',即使它们不包含反斜杠。文档字符串应该是三引号的原始字符串,如r"""This function does NLP """。这样做之后,如果曾经为doctests或正则表达式添加反斜杠的话,那么它们会达到所期望的效果。[4]

B.5 技巧

在加入生产项目之前,大家可以找一个交互式编码挑战网站来磨炼自己的Python技能。在阅读本书时,大家可以每周做一到两次。

(1)CodingBat——在基于Web的交互式Python解释器中的有趣挑战。

(2)Donne Martin的编码挑战——一个基于Jupyter记事本和Anki闪卡的开源代码库,有助于学习算法和数据。

(3)DataCamp——DataCamp上的Pandas和Python教程。


[1] 没有所谓的官方扩展ASCII字符集,所以不要将它们用于NLP,除非想让机器在学习通用语言模型时感到困惑。

[2] 这只适用于严格的正则表达式语法,不包括前向环视和后向环视的情况。

[3] 命名正则表达式分组:“P”代表什么?

[4] 这个在stack overflow网站上提出的问题解释了原因。

本文摘自《自然语言处理实战》

有趣的Python和正则表达式_字符串

本书是处理和生成自然语言文本的实用指南。在本书中,我们为大家提供了构建后端NLP系统所需的所有工具和技术,以支持虚拟助手(聊天机器人)、垃圾邮件过滤器、论坛版主、情感分析器、知识库构建器、自然语言文本挖掘器或者其他任何可以想到的NLP应用程序。

本书面向中高级Python开发人员。对于已经能够设计和构建复杂系统的读者,本书的大部分内容依然会很有用,因为它提供了许多实践示例,并深入讲解了先进的NLP算法的功能。虽然面向对象的Python开发知识可以帮助大家构建更好的系统,但并不是使用本书中学到的知识所必需的。

对于一些特定的主题,我们提供了充足的背景资料,为想深入了解的读者提供了参考资料(包括文本和在线资料)。