Python实战之字符串和文本处理
时间:2023-10-21 14:07:01
写在前面
-
博文为
《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
>>>
在需要比较字符串的程序中使用字符的多种表示会产生问题。为了修正这个问题,你可以使用 unicodedata 模块先将文本标准化:
>>> import unicodedata >>> t1 = unicodedata.normalize('NFC', s1) >>> t2 = unicodedata.normalize('NFC', s2) >>> t1 == t2 True >>> print(ascii(t1)) 'Spicy Jalape\xf1o' >>> t3 = unicodedata.normalize元器件数据手册
、IC替代型号,打造电子元器件IC百科大全!