学员信息
- 学号:1901100244
- 操作系统:Windows 10 version 1903 (64-bit)
- 学习内容:字符串的基本处理,词频统计,数组操作,进制转换
- 学习用时:6 小时
收获总结
- 了解了字符串相关的替换、删除、大小写转换等工作的方法;
- 了解了由字符串分词生成列表的方法;
- 了解了列表的排序方法;
- 了解了字典的生成和排序方法;
- 明确了列表、元组、字典的联系和区别;
- 了解了切片的使用方法;
- 了解了数字类型和字符串类型的转换方法;
- 了解了由列表拼接字符串的方法;
- 了解了十进制数到二进制数、八进制数、十六进制数的转换方法。
遇到的难点与问题
1. 字符串样本的形式
任务手册中给出的字符串样本是著名的《Python 之禅》(The Zen of Python),其最早由蒂姆·彼得斯(Tim Peters)在 Python 邮件列表中发表,它包含了影响 Python 编程语言设计的 19 条程序编写原则。
《Python 之禅》被内置到了 Python 的 this 模块中,只需要运行 import this 就会被输出。
但手册中所呈现的字符串样本又与内置的内容有所区别,表现在:
- 开头多了一个空行;
- 标题和正文间多了一个空行;
- 有错别字(“ambiguity”写成了“ambxiguity”);
- 结尾多了一个空行。
另外,原始的文本由于种种原因,使用了“打字机风格”,具体表现在:
- 单引号(‘、’)和缩略号(’)使用了直引号(’)进行替代;
- 破折号(—)使用了两个连字符(–)进行替代;
- 强调使用了成对星号(*)进行表达。
原始文本还有令人纠结的一点,就是破折号左右的空格。破折号左右要不要留空格是个见仁见智的问题,但至少应当全文一致,可原始文本的三个破折号用法各异,这就难以解释了。
这些问题虽然从完成任务的角度来说无需关心,但从排版的角度来说,足够让人产生如鲠在喉的感觉,况且 Python 3.x 默认使用的编码已经是 Unicode,完全可以在文本上解决引号、破折号等问题。
本着这样的想法,我在原始文本的基础上,考虑了排版规则,结合维基百科的 The Zen of Python 页面的内容,生成了作为练习使用的字符串文本。
此外,我顺着《Python 之禅》找到了一系列的《Python 增强提案》(Python Enhancement Proposals,缩写为 PEPs),这其中的《Python 编码规范》(Style Guide for Python Code,编号为 PEP 8)应该是学习 Python 编程的必读文本,其他的提案也可以选择性地读一读。
另可参见:
维基百科的 Python 之禅页面
蛇宗三字经(The Zen of Python)
写在最前面:The Zen of Python
Python 的众多 PEP 之中,除了 PEP 8,还有哪一些是值得阅读的?
维基百科的英文破折号(—)页面
别再用「六个点」当省略号了,这些标点都有更规范的输入方式
中文排版需求
Mathematical Alphanumeric Symbols
VS Code 写 Python 时的代码错误提醒和自动格式化
Linting Python in Visual Studio Code
2. 英文分词
分词是个大问题。
简单的文本,可以通过空格分词,但稍微复杂一点的文本,处理起来就很耗神。
怎么叫“复杂”?包括但不限于:
- 小数(1.25之类);
- 复杂词形($10、10% 之类);
- 缩略语(U.S.A. 之类);
- 包含连字符的词汇(self-teaching 之类);
- 特殊符号(…、– 之类)
- 转义符(\n 之类)。
还好,本次练习要处理的文本,不算太复杂。
不过要完成的目标(去掉文本中包含“ea”的单词),还是对分词有些要求的。举例来说,“idea—let’s”这样的部分,就得把“idea”去掉,别的内容保留,之后还要处理破折号两侧可能出现的多余空格。
简单的分词用 split() 函数就行,但本次用作分词的分隔符不只一个,故而选择通过加载正则表达式模块(re),通过 re.split() 函数完成分词。这里有两个要注意的地方:
- 间隔符表达式如果带括号((…)),生成的列表里就包括分隔符,反之则不包括;
- 正则表达式字符串建议在前面加上标记
r,举例来说,我使用的表达式为r'(\s|,|\.|—)'。
更复杂的分词可以通过加载其他的分词工具完成,具体可以参见《Python 文本数据分析初学指南》的“4.2 英文分词及词性标注”部分。
3. 正则表达式
正则表达式(Regular Expression,常简写为 regex、regexp 或 RE)是计算机科学的一个概念,其使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。^Wiki_cs_RE
简单地说,如果需要查找或替换某个具体的字词,使用普通的查找或替换工具就可以了,但如果需要查找或替换的,是符合某些特定规则的字符串,就需要依靠正则表达式了。
快速地了解一下正则表达式,可以阅读陆超编写的《正则表达式30分钟入门教程》,深入学习,可以参考杰弗里·弗里德尔(Jeffrey E. F. Friedl)撰写的 500 多页的大部头《精通正则表达式》和余晟撰写的《正则指引》,还可以通过使用 Regester 和 RegexBuddy 等软件进行练习和测试。
无论如何,正则表达式的语法都很难读写,令人头疼,可为了它能实现的功能,只能认了。
上文已经说过,在 Python 里使用正则表达式需要加载正则表达式模块(re),不过在我用的时候遭遇了这么几个问题:
- 后向引用
我认为包含撇号(’)的单词(如 ’s、’re、don’t 等),应该单独特别处理,而非简单地和它周围的词混在一起。举例来说,“Let’s”应该断成“Let”和“’s”,“don’t”就应该是一个整体。
好在要处理的文本只有 3 个撇号,都是断开就行的,我就简化处理了——在撇号之前加空格把它和关联单词分开。
完成这个任务需要使用“后向引用”,其默认的表达方式应该是类似“\1”、“\2”这样的,但我调了数次都报错,又作了一番功课才明白在这里应该用“\\1”、“\\2”。
捎带要提一下的是,VS Code 里的查找/替换功能也可以用正则表达式,但它的后向引用要写成“$1”、“$2”这种形式。 - 转义序列
“转义(escape)”是指用多个字符的有序组合来表示原本需要的字符的手段,“转义序列(escape sequence)”则指在转义时使用的有序字符组合。^Wiki_cs_Escape_Sequence
转义序列通常用作表示那些“无法直接输入”的字符,比如回车符、换行符、制表符等等。
在 Python 里,转义序列是用“\”开头的一系列字符组合,常用的有:换行符“\n”,制表符“\t”。
但在正则表达式里,也会用到转义序列,正则表达式的转义序列还比 Python 多些,如:空白符“\s”,数字“\d”,单词边缘“\b”等。
转义序列很有用,可也会带来麻烦,比如要输入'C:\new\'这个路径的字符串,其中的\n就会被转义,要避免这一问题,有两种方式:一,使用“\\”来表示不需要转义的反斜线(\),也即写为'C:\\new\',二,使用“r”标记告诉 Python 此处不转义,也即写为r'C:\new\'。
当在 Python 里用正则表达式时,问题就更严重了,因为 Python 本身会处理一层转义,正则表达式又会处理一层转义,故而要向正则表达式里传递一个不转义的“\”,字符串要写成“'\\\\'”,这实在是很麻烦,在这种情况下,建议更多地使用“r”标记来处理正则表达式。
Python 官方发布的静态代码检查工具 Flake8 现在已经把所有 Python 不支持的转义序列标记成错误,提示“invalid escape sequence ‘x’ (W605)”,解决的办法也是在相应字符串加“r”标记。
除了“r”外,字符串可用的标记还有“u”、“f”等,具体使用可以参见《The Python Language Reference》的“2.4.1. String and Bytes literals”和“2.4.3. Formatted string literals”部分。 - 特殊字符
正则表达式里的“\s”困惑了我一阵子。我一开始用它来匹配空格了,发现不好用。搜索了一下才明确,“\s”可以匹配任意的空白符,包括制表符(\t,即'\u0009')、换行符(\n,即'\u000A')、回车符(\r,即'\u000D')、换页符(\f,即'\u000C')、竖向制表符(\v,即'\u000B')、半角空格(“ ”,即'\u0020'),如果是 Unicode 的正则表达式,\s还可以匹配全角空格(“ ”,即'\u3000')。
半角空格没有转义序列,要怎么匹配呢?查了好一会儿我才反应过来,直接输入“ ”就可以了……
顺便我还了解了一下各种不同编码的转义序列表达法,前面“\u”开头的那些字符串,就是 Unicode 编码的转义序列表达法,具体细节可以参见《JavaScript 转义字符》(JavaScript character escape sequences),《字符编码笔记:ASCII,Unicode 和 UTF-8》,《Unicode 与 JavaScript 详解》等文,具体字符的转换可以用站长之家提供的Unicode编码转换工具。
4. 字典的统计和排序
生成字典不难,有这么几个值得记录的点:
使用“
not in”而非“not...in”
更具体地说,if w not in text和if not w in text是等价的,但前一种写法更易读。
现在 Flake8 会把“not...in”标记出来(E713)并给出修改为“not in”的建议,同理,“is not”和“not...is”也有同样的问题(E714)。PEP 8 对此也有说明。另可参考《Python 中 not 的用法》一文。字典、元组、列表
字典不能直接排序,需要用item()函数把字典转成一个由元组构成的列表,然后再用sorted()函数排序。
这里可能出现的波折是混淆了sort()和sorted()。简单说,由字典转成的列表,不能用sort()排序,只能用sorted()排序。更多的信息可以参考《Python 中列表、元组及字典的排序》、《Python 中方法 sort() 和函数 sorted() 的区别》、《Python 中排序函数 sort() 和 sorted() 的区别》等文。lambda 表达式
使用sorted()排序会用到 lambda 表达式,这个稍稍有些不易理解。
lambda 表达式定义的是一个“匿名函数”,是为了简化代码而使用的。
举例来说,1
lambda x: x[1]
和
1
2def g(x):
return x[1]的功能是一样的。
更进一步说,1
sorted(text, key=lambda x: x[1])
和
1
2
3
4
5def g(x):
return x[1]
sorted(text, key=g)的功能也是一样的。
显然,在使用sorted()的时候,lambda 表达式可以实现更简练的代码。不过,lambda 表达式会在一定程度上降低程序的可读性,所以要慎用。
什么时候用 lambda 表达式,Goodpy 在《Python lambda 介绍》一文给了些很好的建议:lambda 并不会带来程序运行效率的提高,只会使代码更简洁。
如果可以使用
for...in...if来完成的,坚决不用 lambda。如果使用 lambda,lambda 内不要包含循环,如果有,我宁愿定义函数来完成,使代码获得可重用性和更好的可读性。
另可参考:
Python: 使用 lambda 应对各种复杂情况的排序,包括 list 嵌套 dict
Pythondict()函数
5. 切片的使用
切片是从字符串、列表、元组中抽取部分内容的操作。它很常用,不过有些地方会让人有些困惑,记录如下。
首先要明确每一个元素的位置编号,以列表 li = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 为例——
| 列表元素 | ‘A’ | ‘B’ | ‘C’ | ‘D’ | ‘E’ | ‘F’ | ‘G’ |
|---|---|---|---|---|---|---|---|
| 正序编号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 逆序编号 | -7 | -6 | -5 | -4 | -3 | -2 | -1 |
注意正序编号是从 0 开始的,逆序编号是从 -1 开始的。
切片的语法是 list[start:stop:step],即 名称[开始:结束:步伐]。这其中:
- 步伐可以省略,默认值为“1”;
- 开始和结束的值可以省略,默认值为列表的两端(不是第一个元素和最后一个元素);
- 开始和结束中间的冒号不可省略;
- 步伐既可以为正值,也可以为负值,正值表示正序取值,负值表示逆序取值;
- 步伐为正值时,开始位置应在结束位置左侧,步伐为负值时,开始位置应在结束位置右侧,否则不会取出任何元素;
- 开始和结束的值既可以用正序编号,也可以用逆序编号,两种编号可以混用;
- 开始位置的元素会取出,结束位置的元素不会取出;
- 所用位置编号可以超出列表的元素位置编号;
- 所有的冒号两侧都没有空格。
还以列表 li = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 为例——
| 行号 | 代码 | 结果 | 注释 |
|---|---|---|---|
| 1 | li[:] |
['A', 'B', 'C', 'D', 'E', 'F', 'G'] |
从列表左端取到列表右端,即取了整个列表 |
| 2 | li[::1] |
['A', 'B', 'C', 'D', 'E', 'F', 'G'] |
从列表左端取到列表右端,步伐为“1”,即取了整个列表 |
| 3 | li[::2] |
['A', 'C', 'E', 'G'] |
从列表左端取到列表右端,步伐为“2”,即每 2 个元素取 1 个元素,取出第 0、2、4、6 号元素 |
| 4 | li[::3] |
['A', 'D', 'G'] |
从列表左端取到列表右端,步伐为“3”,即每 3 个元素取 1 个元素,取出第 0、3、6 号元素 |
| 5 | li[::-1] |
['G', 'F', 'E', 'D', 'C', 'B', 'A'] |
从列表左端取到列表右端,步伐为“-1”,把整个列表逆序 |
| 6 | li[::-2] |
['G', 'E', 'C', 'A'] |
从列表右端取到列表左端,步伐为“-2”,取出第 -1、-3、-5、-7 号元素 |
| 7 | li[2:5] |
['C', 'D', 'E'] |
从编号为 2 的元素(含)取到编号为 5 的元素(不含),步伐为默认值“1”,取出第 2、3、4 号元素 |
| 8 | li[2:5:1] |
['C', 'D', 'E'] |
从编号为 2 的元素(含)取到编号为 5 的元素(不含),步伐为“1”,取出第 2、3、4 号元素 |
| 9 | li[2:5:-1] |
[] |
从编号为 2 的元素(含)取到编号为 5 的元素(不含),步伐为“-1”,没有取出元素 |
| 10 | li[5:2:-1] |
['F', 'E', 'D'] |
从编号为 5 的元素(含)取到编号为 2 的元素(不含),步伐为“-1”,取出第 5、4、3 号元素 |
| 11 | li[0:6] |
['A', 'B', 'C', 'D', 'E', 'F'] |
从编号为 0 的元素(含)取到编号为 6 的元素(不含),步伐为默认值“1”,取出第 0、1、2、3、4、5 号元素 |
| 12 | li[0:-1] |
['A', 'B', 'C', 'D', 'E', 'F'] |
从编号为 0 的元素(含)取到编号为 -1 的元素(不含),步伐为默认值“1”,取出第 0、1、2、3、4、5 号元素 |
| 13 | li[0:] |
['A', 'B', 'C', 'D', 'E', 'F', 'G'] |
从编号为 0 的元素(含)取到列表右端,步伐为默认值“1”,取了整个列表 |
| 14 | li[0:9] |
['A', 'B', 'C', 'D', 'E', 'F', 'G'] |
从编号为 0 的元素(含)取到编号为 9 的元素(不含),步伐为默认值“1”,由于最大元素编号为 6,取了整个列表 |
| 15 | li[9:0:-1] |
['G', 'F', 'E', 'D', 'C', 'B'] |
从编号为 9 的元素(含)取到编号为 0 的元素(不含),步伐为默认值“-1”,由于最大元素编号为 6,取出第 6、5、4、3、2、1 号元素 |
| 16 | li[9:-9:-1] |
['G', 'F', 'E', 'D', 'C', 'B', 'A'] |
从编号为 9 的元素(含)取到编号为 - 的元素(不含),步伐为默认值“-1”,由于最大元素编号为 6,最小元素编号为 -7,取出第 6、5、4、3、2、1、0 号元素 |
| 17 | li[0::-1] |
['A'] |
从编号为 0 的元素(含)取到列表左端,步伐为“-1”,取出第 0 号元素 |
| 18 | li[:0:-1] |
['G', 'F', 'E', 'D', 'C', 'B'] |
从列表右侧取到编号为 0 的元素(不含),步伐为“-1”,取出第 -1、-2、-3、-4、-5、-6 号元素 |
| 19 | li[1:-2:-1] |
[] |
从编号为 1 的元素(含)取到编号为 -2 的元素(不含),步伐为“-1”,没有取出元素 |
| 20 | li[-2:1:-1] |
['F', 'E', 'D', 'C'] |
从编号为 -2 的元素(含)取到编号为 1 的元素(不含),步伐为“-1”,取出第 -2、-3、-4、-5 号元素 |
| 21 | li[0:3:-1] |
[] |
从编号为 0 的元素(含)取到编号为 3 的元素(不含),步伐为“-1”,没有取出元素 |
| 22 | li[:3:-1] |
['G', 'F', 'E'] |
从列表右端取到编号为 3 的元素(不含),步伐为“-1”,取出第 -1、-2、-3 号元素 |
字符串和元组的操作与列表类似。
要翻转(逆序)一个名为 li 的列表有两种途径,一种是用 li.reverse(),另一种是用 li[::-1]。但要翻转字符串和元组就只能用切片了。这样来看,切片的方法比较通用。
字典不能切片,不过可以用把字典的关键字转成列表,对列表切片,再用查询重新生成字典的方法实现切片,具体的操作可以参考李阳良的《Python 字典切片》一文。
6. 不同类型的转换
由数字构成的列表不能直接拼接成字符串,得把列表的每个元素都转成字符串才行。
类型转换调用相应的函数就能完成,遍历列表的每个元素可以使用 for...in...if 语句。
Python 里可用的转换函数有这些:
| 函数 | 说明 |
|---|---|
int(x[, base]) |
将 x 转换为一个整数,强制类型转换 |
long(x[, base]) |
将 x 转换为一个长整数 |
float(x) |
将 x 转换到一个浮点数,强制类型转换 |
complex(real[, imag]) |
创建一个复数 |
str(x) |
将对象 x 转换为字符串,强制类型转换 |
repr(x) |
将对象 x 转换为表达式字符串 |
eval(str) |
用来计算在字符串中的有效 Python 表达式,并返回一个对象 |
list(s) |
将序列 s 转换为一个列表,强制类型转换 |
tuple(s) |
将序列 s 转换为一个元组,强制类型转换 |
set(s) |
将序列 s 转换为一个集合,强制类型转换 |
chr(x) |
将一个整数转换为一个字符,Python 3.x 可以用其处理 Unicode 字符了 |
unichr(x) |
将一个整数转换为 Unicode 字符,Python 3.x 将其合并进了 chr(x) |
ord(str) |
将一个字符转换为它的整数值 |
bin(x) |
将一个整数转换为一个二进制字符串 |
oct(x) |
将一个整数转换为一个八进制字符串 |
hex(x) |
将一个整数转换为一个十六进制字符串 |
这里的 eval(str) 在第 3 天做计算器的时候也提到过。str(x) 和 repr(x) 有联系也有区别,具体可参见叶俊贤的《Python 中 str() 与 repr() 函数的区别》一文。还有一些数据类型转换的细节,可以参考范桂飓的《Python基本语法——强制数据类型转换》一文和 Davidham 的《Python 中的强制类型转换》一文。
其他参考资料:
《Python进阶》(Intermediate Python),穆罕默德·耶苏布·乌拉·哈立德(Muhammad Yasoob Ullah Khalid)著,刘宇(@liuyu),老高(@spawnris),大牙码特(@suqi),明源(@muxueqz)等译
给非 Python 开发的 Python 快速自学资源整理
总结
- 巩固了一些英文标点的用法
- 字符串内容的查找、替换、删除
- 字符串的大小写转换
- 英文单词词频统计与输出
- 数组的基础操作
- 不同进制数字的转换