第八章 文本数据

导入所需模块

import numpy as np
import pandas as pd

一、str对象

1.1 str对象的设计意图

str对象是定义在IndexSeries上的属性,专门用于逐元素处理文本内容,其内部定义了大量方法,因此对一个序列进行文本处理,首先需要获取其str对象。在Python标准库中也有str模块,为了使用上的便利,有许多函数的用法pandas照搬了它的设计,例如字母转为大写的操作:

在这里插入图片描述

根据文档API材料,在pandas的50个str对象方法中,有31个是和标准库中的str模块方法同名且功能一致,这为批量处理序列提供了有力的工具。

1.2 []索引器

对于str对象而言,可理解为其对字符串进行了序列化的操作,例如在一般的字符串中,通过[]可以取出某个位置的元素:

var[0]
#'a'
#同时也能通过切片得到子串:
var[-1: 0: -2] #从尾往头切片,-2是步长
#'db'

通过对str对象使用[]索引器,可以完成完全一致的功能,并且如果超出范围则返回缺失值:

s
'''
0    abcd
1     efg
2      hi
dtype: object
'''
s.str[0]
'''
0    a
1    e
2    h
dtype: object
'''
s.str[-1: 0: -2]
'''
0    db
1     g
2     i
dtype: object
'''
s.str[2]
'''
0      c
1      g
2    NaN
dtype: object
'''

1.3 string类型

在上一章提到,从pandas1.0.0版本开始,引入了string类型,其引入的动机在于:原来所有的字符串类型都会以object类型的Series进行存储,但object类型只应当存储混合类型,例如同时存储浮点、字符串、字典、列表、自定义类型等,因此字符串有必要同数值型或category一样,具有自己的数据存放类型,从而引入了string类型。

总体上说,绝大多数对于objectstring类型的序列使用str对象方法产生的结果是一致,但是在下面提到的两点上有较大差异:

首先,应当尽量保证每一个序列中的值都是字符串的情况下才使用str属性,但这并不是必须的,其必要条件是序列中至少有一个可迭代(Iterable)对象,包括但不限于字符串、字典、列表。对于一个可迭代对象,string类型的str对象和object类型的str对象返回结果可能是不同的。

s = pd.Series([{1: 'temp_1', 2: 'temp_2'}, ['a', 'b'], 0.5, 'my_string'])
s
'''
0    {1: 'temp_1', 2: 'temp_2'}
1                        [a, b]
2                           0.5
3                     my_string
dtype: object
'''
s.str[1]
'''
0    temp_1
1         b
2       NaN
3         y
dtype: object
'''
s.astype('string').str[1]
'''
0    1
1    '
2    .
3    y
dtype: string
'''

除了最后一个字符串元素,前三个元素返回的值都不同,其原因在于当序列类型为 object 时,是对于每一个元素进行 [] 索引,因此对于字典而言,返回temp_1字符串,对于列表则返回第二个值,而第三个为不可迭代对象,返回缺失值,第四个是对字符串进行 [] 索引。而 string 类型的 str 对象先把整个元素转为字面意义的字符串,例如对于列表而言,第一个元素即 “{“,而对于最后一个字符串元素而言,恰好转化前后的表示方法一致,因此结果和 object 类型一致。

除了对于某些对象的 str 序列化方法不同之外,两者另外的一个差别在于, string 类型是 Nullable 类型,但 object 不是。这意味着 string 类型的序列,如果调用的 str 方法返回值为整数 Series 和布尔 Series 时,其分别对应的 dtypeIntbooleanNullable 类型,而 object 类型则会分别返回 int/floatbool/object ,取决于缺失值的存在与否。同时,字符串的比较操作,也具有相似的特性, string 返回 Nullable 类型,但 object 不会。

在这里插入图片描述
在这里插入图片描述

最后需要注意的是,对于全体元素为数值类型的序列,即使其类型为object或者category也不允许直接使用str属性。如果需要把数字当成string类型处理,可以使用astype强制转换为string类型的Series

s = pd.Series([12, 345, 6789])
s.astype('string').str[1]
'''
0    2
1    4
2    7
dtype: string
'''

二、正则表达式基础

正则表达式可以对指定的字符串与模式之间执行模式匹配。模式可以是普通的字符串,也可以是含有特殊意义字符的字符串。通过正则表达式,我们可以进行查找,校验等用途。在Python中,我们可以使用re模块来实现正则表达式的模式匹配操作。现在,我们就从最简单的,模式为普通文本字符开始。

模式为普通文本字符,指正则表达式的模式中不含有特殊字符,完全是按照字符本身的内容进行匹配的,这种情况下,实际上就是判断带匹配的文本是否包含模式,与判断子串是否出现在某个主串的含义相同。

2.1 字符相关

字符说明
.默认模式下,匹配除换行符(\n)之外的所有单个字符。在S(DOTALL)模式下,匹配所有单个字符。
[]匹配[]内的任意一个字符。[]中可以是单个字符,如[x9k],也可以是一个字符区间,如[a-k],[3-5]。如果需要匹配“-”,可以使用“-”转义,或者将该字符置于[]的两端,如[-axk]或[axk-]。如果需要匹配“]”,可以使用“]”转义,或者将该字符置于[]的最前端,如[]axk]。
[^]匹配不在[]内的任意一个字符,[]的取反匹配。
\d如果是str类型,匹配Unicode十进制数字,这包括但不限于0 ~ 9,例如0,٧等字符,也能够匹配成功。如果是bytes类型,匹配[0-9]。
\D匹配非Unicode数字字符,\d的取反匹配。
\s如果是str类型,匹配Unicode空白符,这包括但不限于[空格\t\v\r\n\f]。如果是bytes类型,匹配[空格\t\v\r\n\f]。
\S匹配非Unicode空白字符,\s的取反匹配。
\w如果是str类型,匹配Unicode单词字符,这包括但不限于[a-zA-Z0-9__]。如果是bytes类型,则匹配[a-zA-Z0-9_]。
\W匹配非Unicode单词字符,\w的取反匹配。
\转义字符,对正则表达式的特殊字符进行转义,例如,如果要匹配普通的“.”字符,则可以使用“.”。

说明:

\ 在Python中是转义的开始,在正则表达式中也是转义的开始,因此,建议模式使用原始字符串,这样可以减少转义的繁琐性。

# 1  . 
"""
作用:
默认模式下,匹配除了\n以外的单个字符
如果修改了S---re.DOTALL  模式,匹配的是所有的单个字符
"""
r=re.search("a.b","dkjfada\nbjfdlakfj",re.DOTALL)
if r:
    print(r.group()) # 匹配对象下的group方法可以返回匹配的内容
else:
    print("不匹配")
'''
a
b
'''
# 2. [] 匹配中括号内 任意一个字符,[]可以是多个单个字符,也可以是区间(从哪到哪)
# r=re.search("a[0-9]b","dkjfada5bjfdlakfj")
# r=re.search("a[a-z]b","dkjfada5bjfdlakfj")
# -代表区间,\表示转义,也可以放在两端
# r=re.search("a[\-4]b","dkjfada-bjfdlakfj")
r=re.search("a[-45]b","dkjfada-bjfdlakfj")
if r:
    print(r.group()) # 匹配对象下的group方法可以返回匹配的内容
else:
    print("不匹配")
# a-b
#3.  [^]  表示[]内容取反,注意^必须放在第一个位置
r=re.search("a[^-45]b","dkjfada1bjfdlakfj")
if r:
    print(r.group()) # 匹配对象下的group方法可以返回匹配的内容
else:
    print("不匹配")
# a1b
#4. \d  代表的数字(不限于0-9,还有有特殊的字符)
r=re.search("a\db","dkjfadavbjfdlakfj")
if r:
    print(r.group())
else:
    print("不匹配")
# 不匹配
#5.\D 代表\d的取反
r=re.search("a\Db","dkjfada2bjfdlakfj")
if r:
    print(r.group())
else:
    print("不匹配")
# 不匹配
#6. \s  代表空白( \n\r\t)
r=re.search("a\sb","dkjfada\nbjfdlakfj")
if r:
    print(r.group())
else:
    print("不匹配")
'''
a
b
'''
#7. \S \s取反
r = re.search("a\Sb", "dkjfada bjfdlakfj")
if r:
    print(r.group())
else:
    print("不匹配")
# 不匹配
#8. \w unicode单词字符a-zA-Z0-9_
r = re.search("a\wb", "dkjfada_bjfdlakfj")
if r:
    print(r.group())
else:
    print("不匹配")
# a_b
#9.\W \w的取反
r = re.search("a\Wb", "dkjfada+bjfdlakfj")
if r:
    print(r.group())
else:
    print("不匹配")
# a+b

2.2 次数相关

字符说明
*匹配前面的字符0次或多次。
+匹配前面的字符1次或多次。
?匹配前面的字符0次或1次。
{m}匹配前面的字符m次。
{m,}匹配前面的字符至少m次。
{,n}匹配前面的字符至多n次。
{m,n}匹配前面的字符m到n次。
X?X表示以上的任意一种模式({m}除外),即在对应的模式字符串后面加上一个问号?,表示该模式的非贪婪模式(否则为贪婪模式)。贪婪模式与非贪婪模式的区别在于:贪婪模式会尽可能匹配最多的字符,而非贪婪模式会尽可能匹配最少的字符。
# 次数相关:注意必须前面有字符才有作用
# 跟次数相关的特殊字符除了{m}以外都是贪婪匹配,即在合理范围内往多了匹配
# 1.* 匹配0或者多个字符
# r = re.search("ba*", "baaaaaa")
# r = re.search("ba*", "b")
r = re.search("a*", "b")
if r:
    print(r.group())
else:
    print("不匹配")
# 这里打印出是一个空格。因为*表示匹配0个或者多个。所以匹配0个就是空格
#2. +匹配1或者多次
r = re.search("ba+", "baaaaa")
if r:
    print(r.group())
else:
    print("不匹配")
# baaaaa
#3. ?匹配0或者1次
r = re.search("ba?", "ba")
if r:
    print(r.group())
else:
    print("不匹配")
# ba
#4. {m}精确匹配m次(非贪婪匹配)
r = re.search("ba{4}", "baaaaaaa")
if r:
    print(r.group())
else:
    print("不匹配")
# baaaa
#5.{m,}至少匹配m次
r = re.search("ba{4,}", "baaaaaaa")
if r:
    print(r.group())
else:
    print("不匹配")
# baaaaaaa
#6.{,n}最多匹配n次
r = re.search("ba{,4}", "baaaaa")
if r:
    print(r.group())
else:
    print("不匹配")
# baaaa
# 7.{m,n} 匹配m到n次
r = re.search("ba{1,4}", "baa")
if r:
    print(r.group())
else:
    print("不匹配")
# baa
#8.X?:将以上的特殊字符(除{m}以外),变成非贪婪模式
# r = re.search("ba{1,4}?", "baa")
# r = re.search("ba*?", "baaaaaaaaa")
r = re.search("ba+?", "baaaaaaaaa")
if r:
    print(r.group())
else:
    print("不匹配")
# ba 这里加了个?变成非贪婪。所以+只匹配一次了

2.3 边界相关

字符说明
^匹配字符串的开头。在多行模式下,可以匹配每一行的开头。
$匹配字符串的结尾。在多行模式下,可以匹配每一行的末尾。
\A仅匹配字符串的开头。
\Z仅匹配字符串的末尾。
\b匹配单词的边界。单词可以含有Unicode字符、数字与下划线组成(\w+匹配的内容)。\b匹配的是空串,该空串可以出现在\w(\W)与\W(\w)之间、字符串开头与\w之间或\w与字符串结尾之间。
\B匹配单词的非边界。\B匹配的是空串,该空串必须出现在两个\w之间。\B是\b的取反匹配。
# 1.^  匹配字符串的开头
# ^默认单行匹配
# 如果需要进行多行匹配,需要加入 re.MULTILINE
# r = re.search("^张", "张三丰")
# r = re.search("^张\w{1,3}", "三丰\n张四丰",re.MULTILINE)
r = re.search("^张\w{1,3}", "三丰\n张四丰\n张五丰",re.MULTILINE)
if r:
    print(r.group())
else:
    print("不匹配")

# 2.$ 匹配结尾的字符串,支持多行模式
r = re.search("\w{1,3}丰$", "张四丰\n张三",re.MULTILINE)
if r:
    print(r.group())
else:
    print("不匹配")

#3. \A 开头  只支持单行模式  \Z 结尾  只支持单行模式
r = re.search(r"\Z四", "张四丰张三",re.MULTILINE)
if r:
    print(r.group())
else:
    print("不匹配")
#4.\b边界:判断unicode字符和非unicode字符之间
# 之间:字符和字符之间,字符和“墙”(墙是非unicode字符)
# r = re.search(r"\bcd", "cd")
r = re.search(r"\bcd", "88#cd")
if r:
    print(r.group())
else:
    print("不匹配")
print(re.split(r"\bcd", "88#cd"))
#5.\B代表非边界
r = re.search(r"\Bcd", "acd")
if r:
    print(r.group())
else:
    print("不匹配")
print(re.split(r"\Bcd", "acd3"))
'''
张四丰
张四丰
不匹配
cd
['88#', '']
cd
['a', '3']
'''

2.4 组相关

字符说明
()对()内的字符进行分组。分组后,该组匹配的内容可以单独提取,同时,也可以在模式字符串后面使用\number进行引用。
\numbernumber用来指定组序号,序号从1开始。用来匹配number对应的分组内容。
(?:表达式)匹配()内的字符,但是不会进行分组。()内匹配的内容也无法单独提取,或者在后面使用\number引用。
(?P表达式)对()内的字符进行分组,组名为name,多个组之间的名称不能重复。分组后,该组匹配的内容可以单独提取,同时,也可以在模式字符串后面使用(?P=name)或\number进行引用。对比之前()进行的序号分组,此种方式可以称为命名分组。不过,命名分组依然也可以使用序号(\number)进行引用。
(?P=name)用来匹配同名的分组内容【之前使用(?P)进行的分组】。
|用来连接两个并列的模式字符串,匹配其中的一个即可。
# 1.() 对()字符进行分组,分组之后,括号内内容可以单独提取
# ()的作用:(1)括号内的内容整体操作  (2) 对组内的内容单独提取
r = re.search(r"c(ab)+", "cabababab")
if r:
    print(r.group())
    print(r.group(1)) # 索引代表括号,从1开始
else:
    print("不匹配")

# 应用一
r = re.search(r"<b>(.*)</b>", "<b>一些加粗的内容</b>")
if r:
    print(r.group())
    print(r.group(1)) # 索引代表括号,从1开始
else:
    print("不匹配")

# 应用二
r = re.search(r"([0-9]{3,4})-([0-9]{7,8})", "010-12345678")
if r:
    print(r.group())
    print(r.group(1))
    print(r.group(2))
else:
    print("不匹配")

# 2.\number 用来指定分组的序号,匹配对应的分组内容
r = re.search(r"<([a-zA-Z]+)>(.*)</\1>", "<title>一些加粗的内容</title></b>")
if r:
    print(r.group())
    # print(r.group(1)) # 索引代表括号,从1开始
    print(r.group(2)) # 索引代表括号,从1开始
else:
    print("不匹配")

#3. (?:正则表达式):削弱()功能,使得()功能只有括号内的内容整体操作
# r = re.search(r"(?:[0-9]{3,4})-(?:[0-9]{7,8})", "010-12345678")
# if r:
#     # print(r.group())
#     print(r.group(1))
#     # print(r.group(2))
# else:
#     print("不匹配")

#4.(?P<name>正则表达式),相当于给\number起名字
r = re.search(r"<(?P<tag>[a-zA-Z]+)>(.*)</(?P=tag)>", "<title>一些加粗的内容</title></b>")
if r:
    print(r.group())
    # print(r.group(1)) # 索引代表括号,从1开始
    print(r.group(2)) # 索引代表括号,从1开始
else:
    print("不匹配")

#5. | 连接两个并列模式的字符串,匹配其中之一就可以
# |级别很低,大范围的划取
#r = re.search(r"abc|def", "abd")
r = re.search(r"ab(c|d)ef", "abdef") # 可以使用()进行优先划分处理
if r:
    print(r.group())
else:
    print("不匹配")
'''
abababab
ab
<b>一些加粗的内容</b>
一些加粗的内容
010-12345678
010
12345678
<title>一些加粗的内容</title>
一些加粗的内容
<title>一些加粗的内容</title>
一些加粗的内容
abdef
'''

2.5 控制标记

正则表达式的第三个参数flag的使用。(3.6后,使用RegexFlag对象)

I(IGNORECASE):忽略大小写

M(MULTILINE):多行匹配

S(DOTALL):在该模式下,’.'可以匹配所有字符

# re.DOTALL----支持所有字符
# re.MULTILINE----支持多行
# re.IGNORECASE---忽略大小写
r = re.search(r"abc", "ABC",re.IGNORECASE)
if r:
    print(r.group())
else:
    print("不匹配")
#ABC

2.6 re模块的其他函数

# 1. compile 产生正则表达式对象
# 第一个参数:正则表达式,第二参数:控制标记
# 返回值:正则表达式对象
reobj=re.compile("abc+")
m=reobj.search("abcccc")
if m:
    print(m.group())
# re下的方法跟正则表达式对象下的方法几乎一样,功能有重叠,
# 当有重复或者循环使用的时候,
# 需要使用正则表达式对象,正则表达式只需要写一次。

# 2.match: 跟search一样,但是只能从头部进行查找,只能返回一个匹配内容
m=re.match("abc","xabc")
print(m)

# 3.findall 返回所有匹配内容,返回到一个列表中
r=re.findall("[0-9]","a3n9j7jrkre5")
print(r)

c="""
<div>我是一个兵1</div>
<div>我是一个兵2</div>
<div>我是一个兵3</div>
<div>我是一个兵4</div>
"""
li1=re.findall("<div>(.*?)</div>",c)
print(li1)

#4.finditer()功能跟findall类似,返回一个迭代器
iter=re.finditer("[0-9]","a3n9j7jrkre5")
# print(iter)
for i in iter:
    print(i.group())

#5.split()
#分隔
s="a abc d   d a"
print(s.split(" "))
print(re.split(" +",s))

#6.sub() 替换,跟replace很像
"""
(1)替换的模式
(2)替换后的内容
(3)待搜索的字符串
(4)最大替换次数
(5)控制标记
"""
print(re.sub(" +","-",s))
'''
abcccc
None
['3', '9', '7', '5']
['我是一个兵1', '我是一个兵2', '我是一个兵3', '我是一个兵4']
3
9
7
5
['a', 'abc', 'd', '', '', 'd', 'a']
['a', 'abc', 'd', 'd', 'a']
a-abc-d-d-a
'''

2.7 综合例子

import re
re.findall('Apple', 'Apple! This Is an Apple!')
# ['Apple', 'Apple'] re.findall()返回一个列表,列表里全是匹配的字符
re.findall('.', 'abc')
# ['a', 'b', 'c']
re.findall('[ac]', 'abc')
# ['a', 'c']
re.findall('[^ac]', 'abc')
# ['b']
re.findall('[ab]{2}', 'aaaabbbb') # {n}指匹配n次
# ['aa', 'aa', 'bb', 'bb']
re.findall('aaa|bbb', 'aaaabbbb')
# ['aaa', 'bbb']

在python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string,不要转意backslash ‘’ 。 例如,\n 在raw string中,是两个字符,\和n, 而不会转意为换行符。由于正则表达式和 \ 会有冲突,因此,当一个字符串使用了正则表达式后,最好在前面加上’r’。后面的演示我都会加上r

# 第一个参数里面?和*有特殊的含义,要想匹配出来需要\转义
re.findall(r'a\?|a\*', 'aa?a*a') 
#['a?', 'a*']
# 这里\\相当于转义成了单个\,后面的?代表0次或者一次
re.findall(r'a\\?|a\*', 'aa?a*a') 
# ['a', 'a\\', 'a', 'a'] 这里第二个元素有两个\\也是转义为了一个\
# 这里就是匹配a\?这三个单纯的字符
re.findall(r'a\\\?|a\*', 'aa\?a*a') 
# ['a\\?', 'a*']
# 匹配0个或1个a且后面带任意一个字符
re.findall('a?.', 'abaacadaae')
# ['ab', 'aa', 'c', 'ad', 'aa', 'e']
# 匹配除了\n之外的所有字符后和s
re.findall('.s', 'Apple! This Is an Apple!')
# ['is', 'Is']
# 匹配Unicode单词字符,这包括但不限于[a-zA-Z0-9_]。{2}是两个
re.findall('\w{2}', '09 8? 7w c_ 9q p@')
# ['09', '7w', 'c_', '9q']
# /B匹配单词的非边界
re.findall('\w\W\B', '09 8? 7w c_ 9q p@')
# ['8?', 'p@']
re.findall('.\s.', 'Constant dropping wears the stone.')
#['t d', 'g w', 's t', 'e s']
re.findall('上海市(.{2,3}区)(.{2,3}路)(\d+号)', '上海市黄浦区方浜中路249号 上海市宝山区密山路5号')
# [('黄浦区', '方浜中路', '249号'), ('宝山区', '密山路', '5号')]

三、文本处理的五类操作

3.1 拆分

str.split能够把字符串的列进行拆分,其中第一个参数为正则表达式,可选参数包括从左到右的最大拆分次数n,是否展开为多个列expand

s = pd.Series(['上海市黄浦区方浜中路249号', '上海市宝山区密山路5号'])
s.str.split('[市区路]')
'''
0    [上海, 黄浦, 方浜中, 249号]
1       [上海, 宝山, 密山, 5号]
dtype: object
'''
s.str.split('[市区路]', n=2, expand=True) #最大拆分次数为2次且展开为多个列
'''
	0	1	2
0	上海	黄浦	方浜中路249号
1	上海	宝山	密山路5号
'''

re模块下也有split,其参数为(pattern, string, maxsplit=0, flags=0),和字符串的split方法对比:

s1="a abc d   d a"
print(s1.split(" "))
print(re.split(" +",s1))
# ['a', 'abc', 'd', '', '', 'd', 'a']
# ['a', 'abc', 'd', 'd', 'a']
s2='上海市黄浦区方浜中路249号'
re.split('[市区路]',s2)
# ['上海', '黄浦', '方浜中', '249号']

所以Series.str.split可以看成是re.split()在序列上的应用。

与其类似的函数是str.rsplit,其区别在于使用n参数的时候是从右到左限制最大拆分次数。但是当前版本下rsplit因为bug而无法使用正则表达式进行分割:

在这里插入图片描述

3.2 合并

关于合并一共有两个函数,分别是str.joinstr.catstr.join表示用某个连接符把Series中的字符串列表连接起来,如果列表中出现了非字符串元素则返回缺失值:

s = pd.Series([['a','b'], [1, 'a'], [['a', 'b'], 'c']])
s.str.join('-')
'''
0    a-b
1    NaN
2    NaN
dtype: object
'''

str.cat用于合并两个序列,主要参数为连接符sep、连接形式join以及缺失值替代符号na_rep,其中连接形式默认为以索引为键的左连接。

s1 = pd.Series(['a','b'])
s2 = pd.Series(['cat','dog'])
s1.str.cat(s2,sep='-')
'''
0    a-cat
1    b-dog
dtype: object
'''
s2.index = [1, 2]
s1.str.cat(s2, sep='-', na_rep='?', join='outer')
'''
0      a-?
1    b-cat
2    ?-dog
dtype: object
'''

3.3 匹配

str.contains返回了每个字符串是否包含正则模式的布尔序列:

s = pd.Series(['my cat', 'he is fat', 'railway station'])
s.str.contains('\s\wat')
'''
0     True
1     True
2    False
dtype: bool
'''

str.startswithstr.endswith返回了每个字符串以给定模式为开始和结束的布尔序列,它们都不支持正则表达式:

s.str.startswith('my')
'''
0     True
1    False
2    False
dtype: bool
'''
s.str.endswith('t')
'''
0     True
1     True
2    False
dtype: bool
'''

如果需要用正则表达式来检测开始或结束字符串的模式,可以使用str.match,其返回了每个字符串起始处是否符合给定正则模式的布尔序列:

s.str.match('m|h')
'''
0     True
1     True
2    False
dtype: bool
'''
s.str[::-1].str.match('ta[f|g]|n') # 反转后匹配
'''
0    False
1     True
2     True
dtype: bool
'''

当然,这些也能通过在str.contains的正则中使用^$来实现:

s.str.contains('^[m|h]')
'''
0     True
1     True
2    False
dtype: bool
'''
s.str.contains('[f|g]at|n$')
'''
0    False
1     True
2     True
dtype: bool
'''

除了上述返回值为布尔的匹配之外,还有一种返回索引的匹配函数,即str.findstr.rfind,其分别返回从左到右和从右到左第一次匹配的位置的索引,未找到则返回-1。需要注意的是这两个函数不支持正则匹配,只能用于字符子串的匹配:

s = pd.Series(['This is an apple. That is not an apple.'])
s.str.find('apple')
'''
0    11
dtype: int64
'''
s.str.rfind('apple')
'''
0    33
dtype: int64
'''

3.4 替换

str.replacereplace并不是一个函数,在使用字符串替换时应当使用前者。

s = pd.Series(['a_1_b','c_?'])
s.str.replace('\d|\?', 'new')
'''
0    a_new_b
1      c_new
dtype: object
'''

当需要对不同部分进行有差别的替换时,可以利用子组的方法,并且此时可以通过传入自定义的替换函数来分别进行处理,注意group(k)代表匹配到的第k个子组(圆括号之间的内容):

s = pd.Series(['上海市黄浦区方浜中路249号',
                '上海市宝山区密山路5号',
                '北京市昌平区北农路2号'])
pat = '(\w+市)(\w+区)(\w+路)(\d+号)'
city = {'上海市': 'Shanghai', '北京市': 'Beijing'}
district = {'昌平区': 'CP District',
            '黄浦区': 'HP District',
            '宝山区': 'BS District'}
road = {'方浜中路': 'Mid Fangbin Road',
        '密山路': 'Mishan Road',
        '北农路': 'Beinong Road'}
def my_func(m):
    str_city = city[m.group(1)]
    str_district = district[m.group(2)]
    str_road = road[m.group(3)]
    str_no = 'No. ' + m.group(4)[:-1]
    return ' '.join([str_city,
                     str_district,
                     str_road,
                     str_no])
s.str.replace(pat, my_func)

这个函数的pat用了上面2.4节中的组相关知识,并且这个my_func()接收的参数是一个一个的正则表达式对象。.group(num)方法可以返回第num个括号内匹配的内容,.group()方法返回所有匹配内容。

在这里插入图片描述

这里的数字标识并不直观,可以使用命名子组更加清晰地写出子组代表的含义:

pat = '(?P<市名>\w+市)(?P<区名>\w+区)(?P<路名>\w+路)(?P<编号>\d+号)'
def my_func(m):
    str_city = city[m.group('市名')]
    str_district = district[m.group('区名')]
    str_road = road[m.group('路名')]
    str_no = 'No. ' + m.group('编号')[:-1]
    return ' '.join([str_city,
                     str_district,
                     str_road,
                     str_no])
s.str.replace(pat, my_func)
'''
0    Shanghai HP District Mid Fangbin Road No. 249
1           Shanghai BS District Mishan Road No. 5
2           Beijing CP District Beinong Road No. 2
dtype: object
'''

这里也是上面2.4节的内容,举个例子吧

r = re.search(r"<(?P<tag>[a-zA-Z]+)>(.*)</(?P=tag)>", "<title>一些加粗的内容</title></b>")
if r:
    print(r.group())
    # print(r.group(1)) # 索引代表括号,从1开始
    print(r.group(2)) # 索引代表括号,从1开始
    print(r.group('tag')) # 传入命名的‘tag’,其实相当于对num命名了
else:
    print("不匹配")
'''
<title>一些加粗的内容</title>
一些加粗的内容
title
'''

这里虽然看起来有些繁杂,但是实际数据处理中对应的替换,一般都会通过代码来获取数据从而构造字典映射,在具体写法上会简洁的多。

3.5 提取

提取既可以认为是一种返回具体元素(而不是布尔值或元素对应的索引位置)的匹配操作,也可以认为是一种特殊的拆分操作。前面提到的str.split例子中会把分隔符去除,这并不是用户想要的效果,这时候就可以用str.extract进行提取:

在这里插入图片描述

通过子组的命名,可以直接对新生成DataFrame的列命名:

在这里插入图片描述

str.extractall不同于str.extract只匹配一次,它会把所有符合条件的模式全部匹配出来,如果存在多个结果,则以多级索引的方式存储:

s = pd.Series(['A135T15,A26S5','B674S2,B25T6'], index = ['my_A','my_B'])
s
'''
my_A    A135T15,A26S5
my_B     B674S2,B25T6
dtype: object
'''

在这里插入图片描述

从上面可以看到extract只会匹配一次。

在这里插入图片描述

str.findall的功能类似于str.extractall,区别在于前者把结果存入列表中,而后者处理为多级索引,每个行只对应一组匹配,而不是把所有匹配组合构成列表。

s.str.findall(pat)
'''
my_A    [(135, 15), (26, 5)]
my_B     [(674, 2), (25, 6)]
dtype: object
'''

四、常用字符串函数

除了上述介绍的五类字符串操作有关的函数之外,str对象上还定义了一些实用的其他方法,在此进行介绍:

4.1 字母型函数

upper, lower, title, capitalize, swapcase这五个函数主要用于字母的大小写转化,从下面的例子中就容易领会其功能:

在这里插入图片描述
在这里插入图片描述

4.2 数值型函数

这里着重需要介绍的是pd.to_numeric方法,它虽然不是str对象上的方法,但是能够对字符格式的数值进行快速转换和筛选。其主要参数包括errorsdowncast分别代表了非数值的处理模式和转换类型。其中,对于不能转换为数值的有三种errors选项,raise, coerce, ignore分别表示直接报错、设为缺失以及保持原来的字符串。

在这里插入图片描述

在数据清洗时,可以利用coerce的设定,快速查看非数值型的行:

s[pd.to_numeric(s, errors='coerce').isna()]
'''
2    2e
3    ??
dtype: object
'''

4.3 统计型函数

countlen的作用分别是返回出现正则模式的次数和字符串的长度:

在这里插入图片描述

4.4 格式型函数

格式型函数主要分为两类,第一种是除空型,第二种时填充型。其中,第一类函数一共有三种,它们分别是strip, rstrip, lstrip,分别代表去除两侧空格、右侧空格和左侧空格。这些函数在数据清洗时是有用的,特别是列名含有非法空格的时候。

在这里插入图片描述

对于填充型函数而言,pad是最灵活的,它可以选定字符串长度(填充后)、填充的方向和填充内容:

在这里插入图片描述

上述的三种情况可以分别用rjust, ljust, center来等效完成,需要注意ljust是指右侧填充而不是左侧填充(可以理解为原有字符串位置):

在这里插入图片描述

在读取excel文件时,经常会出现数字前补0的需求,例如证券代码读入的时候会把"000007"作为数值7来处理,pandas中除了可以使用上面的左侧填充函数进行操作之外,还可用zfill来实现。

在这里插入图片描述

五、练习

Ex1:房屋信息数据集

现有一份房屋信息数据集如下:

在这里插入图片描述

  1. year列改为整数年份存储。
  2. floor列替换为Level, Highest两列,其中的元素分别为string类型的层类别(高层、中层、低层)与整数类型的最高层数。
  3. 计算房屋每平米的均价avg_price,以***元/平米的格式存储到表中,其中***为整数。

这道题是有缺失值的

在这里插入图片描述

# 1.1自己的方法,转格式转的费劲(由于nan是float格式)
s=df['year'].str.extract(r'(?P<year>\d{4}).+')['year']
s[s.isna()]=0
s=s.astype('int')
s[s==0]=np.NaN
df.year=s.astype('Int64')
df
# 1.1参考答案的方法 使用to_numeric
df.year=pd.to_numeric(df.year.str[:-2]).astype('Int64')
# 1.2.1 使用str.split
df['Level']=df.floor.str.split('(共|层)').str[0].astype('string')
df['Highest']=pd.to_numeric(df.floor.str.split('(共|层)').str[1]).astype('Int64')
df=df.drop('floor',axis=1)
df
# 1.2.2 使用str.extract
df['Level']=df.floor.str.extract('(\w+)(共(\d+)层)')[0].astype('string')
df['Highest']=pd.to_numeric(df.floor.str.extract('(\w+)(共(\d+)层)')[1]).astype('Int64')
df=df.drop('floor',axis=1)
df
# 1.3
s=((pd.to_numeric(df['price'].str[:-1])/pd.to_numeric(df['area'].str[:-1]))*10000).astype('int')
df['avg_price']=s.astype('string')+'元/平米'
df

Ex2:《权力的游戏》剧本数据集

现有一份权力的游戏剧本数据集如下:

在这里插入图片描述

  1. 计算每一个Episode的台词条数。
  2. 以空格为单词的分割符号,请求出单句台词平均单词量最多的前五个人。
  3. 若某人的台词中含有问号,那么下一个说台词的人即为回答者。若上一人台词中含有𝑛个问号,则认为回答者回答了𝑛个问题,请求出回答最多问题的前五个人。

这道题是有缺失值的

在这里插入图片描述

# 2.1 列名中有空格,用strip函数去掉
df.columns=df.columns.str.strip()
df.groupby(['Season', 'Episode'])['Sentence'].count().head()
# 2.2 自己的方法,对Name列进行groupby会忽略缺失值
df['Sentence'].str.split(' ').str.len()
df['num']=df['Sentence'].str.split(' ').str.len()
df.groupby('Name')['num'].mean().sort_values(ascending = False).head()
# 2.2 参考答案 先将Name列设为索引,这里没有去掉nan
df.set_index('Name').Sentence.str.split().str.len(
).groupby('Name').mean().sort_values(ascending=False).head()
# 2.3 也是对name分组,shift是窗口函数。
df['ansnum']=df.Sentence.str.count(r'\?').shift(-1)
df.groupby('Name')['ansnum'].sum().sort_values(ascending=False).head()
# 2.3 参考答案 
s = pd.Series(df.Sentence.values, index=df.Name.shift(-1))
s.str.count('\?').groupby('Name').sum().sort_values(ascending=False).head()

六、总结

6.1 str对象

1.str 对象是定义在 IndexSeries 上的属性,专门用于逐元素处理文本内容

2.可以对str对象进行[]操作。超出范围则返回缺失值。

3.string 类型的 str 对象先把整个元素转为字面意义的字符串,所以[]操作与object会有些许不通。例子参见第一节

6.2 正则表达式

写模式的时候记得在字符串前面加上r!!!

6.3 五大操作

1.拆分

str.split():第一个参数为正则表达式,可选参数包括从左到右的最大拆分次数 n ,是否展开为多个列 expand

2.合并

str.join(sep=)只用传入用什么连接(列表中出现了非字符串元素则返回缺失值)

str.cat 用于合并两个序列,主要参数为连接符 sep 、连接形式 join 以及缺失值替代符号 na_rep ,其中连接形式默认为以索引为键的左连接

3.匹配

返回布尔值的匹配函数str.contains 返回了每个字符串是否包含正则模式的布尔序列。

str.startswithstr.endswith 返回了每个字符串以给定模式为开始和结束的布尔序列,它们都不支持正则表达式。

如果需要用正则表达式来检测开始或结束字符串的模式,可以使用 str.match ,其返回了每个字符串起始处是否符合给定正则模式的布尔序列(可以在 str.contains 的正则中使用 ^$ 来实现)

返回索引的匹配函数,即 str.findstr.rfind ,其分别返回从左到右和从右到左第一次匹配的位置的索引,未找到则返回-1。需要注意的是这两个函数不支持正则匹配,只能用于字符子串的匹配

4.替换

s.str.replace(pat, repl, n=-1, case=None, flags=0, regex=True)

repl可以传入函数!!!

5.提取

str.extract从左往右匹配一次

str.extractall它会把所有符合条件的模式全部匹配出来,如果存在多个结果,则以多级索引的方式存储

6.4 常用字符串函数

1.字母型函数

upper, lower, title, capitalize, swapcase这五个函数主要用于字母的大小写转化。

2.数值型函数

pd.to_numeric方法,它虽然不是str对象上的方法,但是能够对字符格式的数值进行快速转换和筛选。其主要参数包括errorsdowncast分别代表了非数值的处理模式和转换类型。其中,对于不能转换为数值的有三种errors选项,raise, coerce, ignore分别表示直接报错、设为缺失以及保持原来的字符串。

3.统计型函数

countlen的作用分别是返回出现正则模式的次数和字符串的长度

4.格式型函数

除空型有三种,它们分别是strip, rstrip, lstrip,分别代表去除两侧空格、右侧空格和左侧空格。

填充型:pad是最灵活的,它可以选定字符串长度(填充后)、填充的方向和填充内容

rjust, ljust, center分别保湿左填充,右填充,和两侧填充

.zfill填充0进去,只用传入填充后的字符长度

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值