python
很多内建方法很适合处理string
。而且对于更复杂的模式,可以配合使用正则表达式。而pandas
则混合了两种方式。
大部分string
处理,使用内建的一些方法就足够了。比如,可以用split
来分割用逗号区分的字符串:
val = 'a,b, guido'
val.split(',')
['a', 'b', ' guido']
split
经常和strip
一起搭配使用来去除空格(包括换行符):
pieces = [x.strip() for x in val.split(',')]
pieces
['a', 'b', 'guido']
可以使用+号把::和字符串连起来:
first, second, third = pieces
first + '::' + second + '::' + third
'a::b::guido'
但这种方法并不python
,更快的方法是直接用join
方法:
'::'.join(pieces)
'a::b::guido'
其他一些方法适合锁定子字符串位置相关的。用in
关键字是检测substring
最好的方法,当然,index
和find
也能完成任务:
'guido' in val
True
val.index(',')
1
val.find(':')
-1
注意index
和find
的区别。如果要找的string
不存在的话,index
会报错。而find
会返回-1:
val.index(':')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
in ()
----> 1 val.index(':')
ValueError: substring not found
count
会返回一个substring
出现的次数:
val.count(',')
2
replace
会取代一种出现方式(pattern
)。也通常用于删除pattern
,传入一个空字符串即可:
val.replace(',', '::')
'a::b:: guido'
val.replace(',', '')
'ab guido'
正则表达式能让我们寻找更复杂的pattern
。通常称一个表达式为regex
,由正则表达语言来代表一个字符串模式。可以使用python
内建的re
模块来使用。
关于正则表达式,有很多教学资源,可以自己找几篇来学一些,这里不会介绍太多。
re
模块有以下三个类别:patther matching
(模式匹配), substitution
(替换), splitting
(分割)。通常这三种都是相关的,一个regex
用来描述一种pattern
,这样会有很多种用法。这里举个例子,假设我们想要根据空格(tabs
,spaces
,newlines
)来分割一个字符串。用于描述一个或多个空格的regex
是\s+
:
import re
text = "foo bar\t baz \tqux"
re.split('\s+', text)
['foo', 'bar', 'baz', 'qux']
当调用re.split('\s+', text)
的时候,正则表达式第一次被compile
编译,并且split
方法会被调用搜索text
。我们可以自己编译regex
,用re.compile
,可以生成一个可以多次使用的regex object
:
regex = re.compile('\s+')
regex.split(text)
['foo', 'bar', 'baz', 'qux']
如果想要得到符合regex
的所有结果,以一个list
结果返回,可以使用findall
方法:
regex.findall(text)
[' ', '\t ', ' \t']
为了防止\在正则表达式中的逃逸,推荐使用raw string literal,比如
r'C:\x'
,而不是使用'C:\\x
使用re.compile
创建一个regex object
是被强烈推荐的,如果你打算把一个表达式用于很多string
上的话,这样可以节省CPU
的资源。
match
和search
,与findall
关系紧密。不过findall
会返回所有匹配的结果,而search
只会返回第一次匹配的结果。更严格地说,match
只匹配string
开始的部分。这里举个例子说明,我们想要找到所有的邮件地址:
text = """Dave [email protected]
Steve [email protected]
Rob [email protected]
Ryan [email protected] """
pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
# re.IGNORECASE makes the regex case-insensitive
regex = re.compile(pattern, flags=re.IGNORECASE)
使用findall
找到一组邮件地址:
regex.findall(text)
['[email protected]', '[email protected]', '[email protected]', '[email protected]']
search
返回text
中的第一个匹配结果。match object
能告诉我们找到的结果在text
中开始和结束的位置:
m = regex.search(text)
m
<_sre.SRE_Match object; span=(5, 20), match='[email protected]'>
text[m.start():m.end()]
'[email protected]'
regex.match
返回None
,因为它只会在pattern
存在于strng
开头的情况下才会返回匹配结果:
print(regex.match(text))
None
而sub
返回一个新的string
,把pattern
出现的地方替换为我们指定的string
:
print(regex.sub('REDACTED', text))
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED
假设你想要找到邮件地址,同时,想要把邮件地址分为三个部分,username, domain name, and domain suffix
.(用户名,域名,域名后缀)。需要给每一个pattern
加一个括号:
pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
regex = re.compile(pattern, flags=re.IGNORECASE)
match object
会返回一个tuple
,包含多个pattern
组份,通过groups
方法:
m = regex.match('[email protected]')
m.groups()
('wesm', 'bright', 'net')
findall
会返回a list of tuples
:
regex.findall(text)
[('dave', 'google', 'com'),
('steve', 'gmail', 'com'),
('rob', 'gmail', 'com'),
('ryan', 'yahoo', 'com')]
sub
也能访问groups
的结果,不过要使用特殊符号 \1, \2。\1表示第一个匹配的group
,\2表示第二个匹配的group
,以此类推:
print(regex.sub(r'Username: \1, Domain: \2, Suffix: \3', text))
Dave Username: dave, Domain: google, Suffix: com
Steve Username: steve, Domain: gmail, Suffix: com
Rob Username: rob, Domain: gmail, Suffix: com
Ryan Username: ryan, Domain: yahoo, Suffix: com
一些复杂的数据清理中,string
会有缺失值:
import numpy as np
import pandas as pd
data = {'Dave': '[email protected]', 'Steve': '[email protected]',
'Rob': '[email protected]', 'Wes': np.nan}
data = pd.Series(data)
data
Dave [email protected]
Rob [email protected]
Steve [email protected]
Wes NaN
dtype: object
data.isnull()
Dave False
Rob False
Steve False
Wes True
dtype: bool
可以把一些字符串方法和正则表达式(用lambda
或其他函数)用于每一个value
上,通过data.map
,但是这样会得到NA(null)
值。为了解决这个问题,series
有一些数组导向的方法可以用于字符串操作,来跳过NA
值。这些方法可以通过series
的str
属性;比如,我们想检查每个电子邮箱地址是否有'gmail' with str.contains
:
data.str
data.str.contains('gmail')
Dave False
Rob True
Steve True
Wes NaN
dtype: object
正则表达式也可以用,配合任意的re
选项,比如IGNORECASE
:
pattern
'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
data.str.findall(pattern, flags=re.IGNORECASE)
Dave [(dave, google, com)]
Rob [(rob, gmail, com)]
Steve [(steve, gmail, com)]
Wes NaN
dtype: object
有很多方法用于向量化。比如str.get
或index
索引到str
属性:
matches = data.str.match(pattern, flags=re.IGNORECASE)
matches
/Users/xu/anaconda/envs/py35/lib/python3.5/site-packages/ipykernel/__main__.py:1: FutureWarning: In future versions of pandas, match will change to always return a bool indexer.
if __name__ == '__main__':
Dave (dave, google, com)
Rob (rob, gmail, com)
Steve (steve, gmail, com)
Wes NaN
dtype: object
为了访问嵌套list
里的元素,我们可以传入一个index
给函数:
matches.str.get(1)
Dave google
Rob gmail
Steve gmail
Wes NaN
dtype: object
matches.str.get(0)
Dave dave
Rob rob
Steve steve
Wes NaN
dtype: object
也可以使用这个语法进行切片:
data.str[:5]
Dave dave@
Rob rob@g
Steve steve
Wes NaN
dtype: object