锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

Python实战之字符串和文本处理

时间:2023-10-21 14:07:01 j6w连接器

写在前面

  • 博文为《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百科大全!
          

相关文章