Python实战之字符串和文本处理
Posted 山河已无恙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python实战之字符串和文本处理相关的知识,希望对你有一定的参考价值。
写在前面
-
博文为
《Python Cookbook》
读书后笔记整理 -
涉及内容包括:
- 使用多个界定符分割字符串
- 字符串开头或结尾匹配,用Shell通配符匹配字符串
- 字符串匹配和搜索和替换(忽略大小写),最短匹配模式
- 将Unicode文本标准化,在正则式中使用Unicode
- 合并拼接字符串,字符串中插入变量,删除字符串中不需要的字符
- 以指定列宽格式化字符串,在字符串中处理html和xml
- 字节字符串上的字符串操作
-
理解不足小伙伴帮忙指正
傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。--------王小波
字符串和文本处理
针对任意多的分隔符拆分字符串
你需要将一个字符串分割为多个字段,但是分隔符 (还有周围的空格) 并不是固定的
string 对象的 split() 方法
只适应于非常简单的字符串分割情形,它并不允许有多个分隔符或者是分隔符周围不确定的空格。当你需要更加灵活的切割字符串的时候,最好使用re.split()
方法:
>>> line = 'asdf fjdk; afed, fjek,asdf, foo'
>>> import re
>>> re.split(r'[;,\\s]\\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>>
函数 re.split()
允许你为分隔符指定多个正则模式。 分隔符可以是逗号,分号或者是空格,并且后面紧跟着任意个的空格
。只要这个模式被找到,那么匹配的分隔符两边的实体都会被当成是结果中的元素返回。返回结果为一个字段列表
>>> re.split(r'(;|,|\\s)\\s*', line)
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
>>>
使用re.split()
函数时候,需要特别注意的是正则表达式中是否包含一个括号捕获分组
。如果使用了捕获分组,那么被匹配的文本也将出现在结果列表中
。
>>> fields = re.split(r'(;|,|\\s)\\s*', line)
>>> values = fields[::2]
>>> delimiters = fields[1::2] + ['']
>>> values
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>> delimiters
[' ', ';', ',', ',', ',', '']
>>> ''.join(v+d for v,d in zip(values, delimiters))
'asdf fjdk;afed,fjek,asdf,foo'
>>>
获取分割字符在某些情况下也是有用的,可能想保留分割字符串,用来在后面重新构造一个新的输出字符串:
如果你不想保留分割字符串到结果列表中去,但仍然需要使用到括号来分组正则表达式的话,确保你的分组是非捕获分组,形如(?:...)
。
>>> re.split(r'(?:,|;|\\s)\\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>>
在字符串的开头或结尾处做文本匹配
你需要通过指定的文本模式去检查字符串的开头或者结尾,比如文件名后缀,URLScheme 等等。
检查字符串开头或结尾的一个简单方法是使用str.startswith()
或者是str.endswith()
方法。比如:
>>> filename = 'spam.txt'
>>> filename.endswith('.txt')
True
>>> filename.startswith('file:')
False
>>> url = 'http://www.python.org'
>>> url.startswith('http:')
True
>>>
如果你想检查多种匹配可能
,只需要将所有的匹配项放入到一个元组
中去,然后传给 startswith() 或者 endswith() 方法
:
>>> import os
>>> filenames = os.listdir('.')
>>> filenames
['.bash_logout', '.bash_profile', '.cshrc', '.tcshrc', 'anaconda-ks.cfg', 'scp_script.py', 'uagtodata', '.bash_history', 'one-client-install.sh', 'calico.yaml', 'docker', '.mysql_history', 'UagAAA', 'Uag.tar', 'liruilong.snap1', '.python_history', '.cache', 'translateDemo', 'soft', 'jenkins.docker.sh', 'o3J6.txt', 'bak_shell', 'liruilong', 'index.html', 'load_balancing', 'redis-2.10.3.tar.gz', 'redis-2.10.3', '.kube', 'kc1', 'pod-demo.yaml', 'web-liruilong.yaml', 'shell.sh', '.config', 'nohup.out', '.viminfo', '.pki', 'kubectl.1', 'temp', 'go', '.vim', '111.txt', 'uagtodata.tar', 'set.sh', '.Xauthority', 'calico_3_14.tar', '.ssh', '.bashrc', 'db', '.docker', 'UagAAA.tar', 'Uag.war', 'Uag', 'txt.sh', '.lesshst', 'gitlab.docker.sh', 'kubectl', 'rsync', 'percona-toolkit-3.0.13-1.el7.x86_64.rpm', 'redisclear.py', 'Fetch']
>>> [name for name in filenames if name.endswith(('.yaml', '.sh')) ]
['one-client-install.sh', 'calico.yaml', 'jenkins.docker.sh', 'pod-demo.yaml', 'web-liruilong.yaml', 'shell.sh', 'set.sh', 'txt.sh', 'gitlab.docker.sh']
>>> any(name.endswith('.py') for name in filenames)
True
>>>
必须要输入一个元组作为参数。如果你恰巧有一个 list 或者 set 类型的选择项,要确保传递参数前先调用 tuple() 将其转换为元组类型
类似的操作也可以使用切片来实现,但是代码看起来没有那么优雅
>>> filename = 'spam.txt'
>>> filename[-4:] == '.txt'
True
>>> url = 'http://www.python.org'
>>> url[:5] == 'http:' or url[:6] == 'https:' or url[:4] == 'ftp:'
True
>>>
还可以使用正则表达式去实现
>>> import re
>>> url = 'http://www.python.org'
>>> re.match('http:|https:|ftp:', url)
<_sre.SRE_Match object; span=(0, 5), match='http:'>
>>>
利用Shell通配符做字符串匹配
你想使用Unix Shell
中常用的通配符 (比如 *.py , Dat[0-9]*.csv
等) 去匹配文本字符串
可以使用 fnmatch()
函数
>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('foo.txt', '*.txt')
True
>>> fnmatch('foo.txt', '?oo.txt')
True
>>> fnmatch('Dat45.csv', 'Dat[0-9]*')
True
>>> names = ['Dat1.csv', 'Dat2.csv', 'config.ini', 'foo.py']
>>> [name for name in names if fnmatch(name, 'Dat*.csv')]
['Dat1.csv', 'Dat2.csv']
>>>
fnmatch()
函数使用底层操作系统的大小写敏感规则 (不同的系统是不一样的) 来匹配模式
#winsows10
>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('foo.txt', '*.TXT')
True
>>>
# Linux
>>> fnmatch('foo.txt', '*.TXT')
False
>>>
如果你对这个区别很在意,可以使用fnmatchcase()
来代替。它完全使用你的模式大小写匹配。
>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('foo.txt', '*.TXT')
True
>>> fnmatchcase('foo.txt', '*.TXT')
False
>>>
fnmatch()
函数匹配能力介于简单的字符串方法和强大的正则表达式之间.在处理非文件名的字符串时也可以使用
>>> from fnmatch import fnmatchcase
>>> addresses = [
... '5412 N CLARK ST',
... '1060 W ADDISON ST',
... '1039 W GRANVILLE AVE',
... '2122 N CLARK ST',
... '4802 N BROADWAY',
... ]
>>> [addr for addr in addresses if fnmatchcase(addr, '* ST')]
['5412 N CLARK ST', '1060 W ADDISON ST', '2122 N CLARK ST']
>>> [addr for addr in addresses if fnmatchcase(addr, '54[0-9][0-9] *CLARK*')]
['5412 N CLARK ST']
>>>
文本模式的匹配和查找
你想匹配或者搜索特定模式的文本
如果你想匹配的是字面字符串,那么你通常只需要调用基本字符串方法就行,比如str.find() , str.endswith() , str.startswith()
或者类似的方法:
>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> text.startswith('yeah')
True
>>> text.endswith('no')
False
>>> text.find('no')
10
>>>
对于复杂的匹配需要使用正则表达式和 re 模块
>>> text1 = '11/27/2012'
>>> import re
>>> if re.match(r'\\d+/\\d+/\\d+', text1):
... print('yes')
... else:
... print('no')
...
yes
如果你想使用同一个模式去做多次匹配,你应该先将模式字符串预编译为模式对象。
>>> datepat = re.compile(r'\\d+/\\d+/\\d+')
>>> if datepat.match(text1):
... print('yes')
... else:
... print('no')
...
yes
>>>
match()
总是从字符串开始去匹配,如果你想查找字符串任意部分的模式出现位置,使用findall()
方法去代替
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
['11/27/2012', '3/13/2013']
>>>
在定义正则式的时候,通常会利用括号去捕获分组分别将每个组的内容提取出来
>>> datepat = re.compile(r'(\\d+)/(\\d+)/(\\d+)')
>>> m = datepat.match('11/27/2012')
>>> m
<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'
>>> m.group(3)
'2012'
>>> m.groups()
('11', '27', '2012')
findall()
方法会搜索文本并以列表形式返回所有的匹配,想以迭代方式返回匹配,可以使用finditer()
方法来代替
>>> [m.groups() for m in datepat.finditer(text)]
[('11', '27', '2012'), ('3', '13', '2013')]
>>>
查找和替换文本
你想在字符串中搜索和匹配指定的文本模式
对于简单的字面模式,直接使用str.repalce()
方法即可
>>> 'yeah, but no, but yeah, but no, but yeah'.replace('yeah', 'yep')
'yep, but no, but yep, but no, but yep'
>>>
复杂的模式,请使用 re 模块中的sub()
函数。sub() 函数
中的第一个参数是被匹配的模式
,第二个参数是替换模式
。反斜杠数字比如 \\3 指向前面模式的捕获组号。
>>> import re
>>> re.sub(r'(\\d+)/(\\d+)/(\\d+)', r'\\3-\\1-\\2','Today is 11/27/2012. PyCon starts 3/13/2013.')
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>>
如果你打算用相同的模式做多次替换,考虑先编译它来提升性能
>>> import re
>>> datepat = re.compile(r'(\\d+)/(\\d+)/(\\d+)')
>>> datepat.sub(r'\\3-\\1-\\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>>
对于更加复杂的替换,可以传递一个替换回调函数来代替
>>> from calendar import month_abbr
>>> def change_date(m):
... mon_name = month_abbr[int(m.group(1))]
... return ' '.format(m.group(2), mon_name, m.group(3))
...
>>> datepat.sub(change_date, text)
'Today is 27 Nov 2012. PyCon starts 13 Mar 2013.'
>>>
想知道有多少替换发生了,可以使用 re.subn()
>>> newtext, n = datepat.subn(r'\\3-\\1-\\2', text)
>>> newtext
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>> n
2
>>>
字符串忽略大小写的搜索替换
你需要以忽略大小写的方式搜索与替换文本字符串
类似 Lixnu 中grep的 -i 参数,python
中需要在使用 re 模块
的时候给这些操作提供re.IGNORECASE 标志参数
。
>>> text = 'UPPER PYTHON, lower python, Mixed Python'
>>> re.findall('python', text, flags=re.IGNORECASE)
['PYTHON', 'python', 'Python']
也可以用于替换
>>> re.sub('python', 'snake', text, flags=re.IGNORECASE)
'UPPER snake, lower snake, Mixed snake'
>>>
替换字符串并不会自动跟被匹配字符串的大小写保持一致。为了修复这个,你可能需要一个辅助函数
def matchcase(word):
def replace(m):
text = m.group()
if text.isupper():
return word.upper()
elif text.islower():
return word.lower()
elif text[0].isupper():
return word.capitalize()
else:
return word
return replace
re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE)
========
'UPPER SNAKE, lower snake, Mixed Snake'
定义实现最短匹配的正则表达式
用正则表达式匹配某个文本模式,但是它找到的是模式的最长可能匹配。而你想修改它变成查找最短的可能匹配。
在需要匹配一对分隔符之间的文本的时候,模式 r'\\"(.*)\\"'
的意图是匹配被双引号包含的文本
>>> str_pat = re.compile(r'\\"(.*)\\"')
>>> text1 = 'Computer says "no."'
>>> str_pat.findall(text1)
['no.']
>>> text2 = 'Computer says "no." Phone says "yes."'
>>> str_pat.findall(text2)
['no." Phone says "yes.']
>>> str_pat = re.compile(r'\\"(.*?)\\"')
>>> str_pat.findall(text2)
['no.', 'yes.']
>>>
正则表达式中 * 操作符是贪婪的
,因此匹配操作会查找最长的可能匹配,可以在模式中的 * 操作符后面加上? 修饰符
,使得匹配变成非贪婪模式
点 (.)
匹配除了换行外的任何字符,如果你将点 (.)
号放在开始与结束符 (比如引号) 之间的时候,那么匹配操作会查找符合模式的最长可能匹配
,在 * 或者 +
这样的操作符后面添加一个?
可以强制匹配算 法
改成寻找最短的可能匹配
。
编写多行模式的正则表达式
使用正则表达式去匹配一大块的文本,而你需要跨越多行去匹配。
很典型的出现在当你用点 (.)
去匹配任意字符的时候,忘记了点(.)
不能匹配换行符的事实,匹配 C 语言分割的注释:
>>> comment = re.compile(r'/\\*(.*?)\\*/')
>>> text1 = '/* this is a comment */'
>>> text2 = '''/* this is a
... multiline comment */
... '''
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[]
>>>
可以修改模式字符串,增加对换行的支持
>>> comment = re.compile(r'/\\*((?:.|\\n)*?)\\*/')
>>> comment.findall(text2)
[' this is a\\nmultiline comment ']
>>>
在这个模式中, (?:.|\\n)
指定了一个非捕获组
(也就是它定义了一个仅仅用来做匹配,而不能通过单独捕获或者编号的组)。
re.compile() 函数
接受一个标志参数叫 re.DOTALL
,在这里非常有用。它可以让正则表达式中的点 (.) 匹配包括换行符在内的任意字符
>>> comment = re.compile(r'/\\*(.*?)\\*/', re.DOTALL)
>>> comment.findall(text2)
[' this is a\\nmultiline comment ']
>>>
将Unicode文本统一表示为规范形式
你正在处理 Unicode 字符串,需要确保所有字符串在底层有相同的表示。
嗯,这块先记录下,感觉有些鸡肋…
在 Unicode 中,某些字符能够用多个合法的编码表示
>>> s1 = 'Spicy Jalape\\u00f1o'
>>> s2 = 'Spicy Jalapen\\u0303o'
>>> s1
'Spicy Jalapeño'
>>> s2
'Spicy Jalapeño'
>>> s1 == s2
False
>>> len(s1)
14
>>> len(s2)
15
>>>
在需要比较
以上是关于Python实战之字符串和文本处理的主要内容,如果未能解决你的问题,请参考以下文章