Python数据处理
¶

06. 文本编码和正则表达式
¶

主讲人:丁平尖

结构化数据¶

  • 存储:存储介质上的位(例如,硬盘)
    • 编码:位如何对应符号?
    • 解释/含义:例如,字符组合成单词
    • 分隔文件:单词组合成句子,文档
    • 结构化内容:元数据,标签等
    • 集合:数据库,目录,归档(.zip,.gz,.tar等)

文本数据无处不在¶

例子:

  • 生物统计学(DNA/RNA/蛋白质序列
    • 5'- AUGCGUAUGCUACGCUAGCUUGCUAGCU -3'
  • 数据库(例如,人口普查数据,产品库存)
  • 日志文件(程序名称,IP地址,用户ID等)
  • 医疗记录(病例历史,医生的笔记,药物清单)
  • 社交媒体(Facebook,Twitter等)

文本数据是如何存储的?¶

  • 基本上,计算机上的每个文件只是一串位
    • 0 1 1 0 0 0 1 1 0 1 1 0 0 0 0 1 0 1 1 1 0 1 0 0
  • 这些位被分成(例如)字节
    • 0 1 1 0 0 0 1 1 0 1 1 0 0 0 0 1 0 1 1 1 0 1 0 0
    • 这些字节对应于(在文本的情况下)字符。
    • 0 1 1 0 0 0 1 1 0 1 1 0 0 0 0 1 0 1 1 1 0 1 0 0
    • c                         a                       t

文本数据是如何存储的?¶

  • 一些编码(例如,UTF-8和UTF-16)使用“可变长度”编码,不同的字符可能使用不同数量的字节。
  • 我们今天会专注于ASCII,它使用固定长度编码。
    • 0 1 1 0 0 0 1 1 0 1 1 0 0 0 0 1 0 1 1 1 0 1 0 0
    • c                         a                       t

ASCII(美国信息交换标准代码)¶

  • 8位*固定长度编码,文件存储为字节流
  • 每个字节编码一个字符
    • 字母,数字,符号或“特殊”字符(例如,制表符,换行符,NULL)
  • 分隔符:一个或多个字符,用于指定边界
    • 例如:空格(‘ ’,ASCII 32),制表符(‘\t’,ASCII 9),换行符(‘\n’,ASCII 10)
    • https://en.wikipedia.org/wiki/ASCII\
  • 技术上,每个ASCII字符是7位,第8位保留用于错误检查

ASCII 表格¶

image.png

注意!¶

  • 不同的操作系统在保存文本文件时遵循略微不同的约定!
  • 最常见的问题:
    • UNIX/Linux/MacOS:换行符存储为\n
    • DOS/Windows:存储为\r\n(回车,然后换行)

Unicode¶

  • 几乎所有世界书写系统的通用编码
  • 每个符号被分配一个唯一的代码点,一个四位十六进制数字
    • 给定字符U+XXXX分配给唯一数字
    • ‘U+’表示unicode,XXXX是代码点(十六进制)
    • 例如:∰=U+2230;http://www.unicode.org/ 了解更多
  • 可变长度编码
    • UTF-8:前128个代码点使用1个字节,更高代码点使用2个或更多字节
    • 结果:ASCII是UTF-8的子集
  • Python 3+版本默认编码脚本为unicode

匹配文本:正则表达式(“regexes”)¶

  • 假设我想在一个大文本文件中找到所有地址。怎么做?
  • 正则表达式允许在文本中匹配模式的简洁规范

Python中的正则表达式:re包¶

  • 三个基本功能:
    • re.match():尝试在字符串开头应用正则表达式。
    • re.search():尝试匹配字符串的任何部分的正则表达式。
    • re.findall():在字符串中找到所有匹配模式的实例。
  • 查看 https://docs.python.org/3/library/re.html 了解更多信息和更多功能(例如,分割和替换)。
  • 简要介绍:https://docs.python.org/3/howto/regex.html#regex-howto
In [2]:
import re
help(re.match)
Help on function match in module re:

match(pattern, string, flags=0)
    Try to apply the pattern at the start of the string, returning
    a Match object, or None if no match was found.

In [3]:
# 模式匹配字符串1的开头,并返回匹配对象。
pat = 'cat'
string1 = 'cat on mat'
string2 = 'raning cats and dogs'
re.match(pat, string1)
Out[3]:
<re.Match object; span=(0, 3), match='cat'>
In [4]:
# 模式匹配字符串2,但不在开头,因此匹配失败并返回None。
re.match(pat, string2) is None
Out[4]:
True
In [5]:
help(re.search)
Help on function search in module re:

search(pattern, string, flags=0)
    Scan through string looking for a match to the pattern, returning
    a Match object, or None if no match was found.

In [6]:
# 模式匹配字符串1的开头,并返回匹配对象。
pat = 'cat'
string1 = 'cat on mat'
string2 = 'raining cats and dogs'
string3 = 'abracadabra'
re.search(pat, string1)
Out[6]:
<re.Match object; span=(0, 3), match='cat'>
In [7]:
# 模式匹配字符串2(不在开头!)并返回匹配对象。
re.search(pat, string2)
Out[7]:
<re.Match object; span=(8, 11), match='cat'>
In [8]:
# 模式在字符串3中不匹配任何内容,返回None。
re.search(pat, string3) is None
Out[8]:
True
In [12]:
help(re.findall)
Help on function findall in module re:

findall(pattern, string, flags=0)
    Return a list of all non-overlapping matches in the string.
    
    If one or more capturing groups are present in the pattern, return
    a list of groups; this will be a list of tuples if the pattern
    has more than one group.
    
    Empty matches are included in the result.

In [13]:
# 模式在字符串1中匹配一次,返回该匹配。
pat = 'cat'
string1 = 'cat on mat'
string2 = 'one cat, two cats, three cats'
string3 = 'abracadabra'
re.findall(pat, string1)
Out[13]:
['cat']
In [14]:
# 模式在字符串2中匹配三次;返回三个实例的列表。
re.findall(pat, string2)
Out[14]:
['cat', 'cat', 'cat']
In [15]:
# 模式在字符串3中不匹配任何内容,返回空列表。
re.findall(pat, string3)
Out[15]:
[]

那么更复杂的匹配呢?¶

  • 如果正则表达式只能搜索像cat这样的字符串,那它就不会非常有用
  • 正则表达式的威力在于指定复杂的模式。例子:
    • 空白字符:\t,\n,\r
    • 匹配字符类别(例如,数字,空白,字母数字)
    • 特殊字符:. ^ $ * + ? { } [ ] \ | ( )
      • 我们很快就会讨论特殊字符的含义
  • 特殊字符必须用反斜杠\转义
    • 例如:匹配包含反斜杠后跟美元符号的字符串:
In [16]:
# 在 Python 中,反斜杠 (\) 在字符串字面量中用于转义特殊字符。例如,\n 表示换行符,\t 表示制表符等。
# 但是,在正则表达式中,反斜杠 (\) 也用于转义特殊字符。例如,\d 表示数字,\w 表示单词字符等。
# 问题在于,当你在 Python 字符串字面量中写正则表达式时,反斜杠 (\) 的含义会冲突。
# 例如,如果你想匹配一个字面上的反斜杠,你需要写 \\\\ 作为模式字符串,因为:
# 在正则表达式中,反斜杠 (\) 表示转义特殊字符,所以需要写 \\ 来表示一个字面上的反斜杠。
# 在 Python 字符串字面量中,反斜杠 (\) 也表示转义特殊字符,所以需要写 \\ 来表示一个字面上的反斜杠。
# 因此,为了表示一个字面上的反斜杠,你需要写 \\\\,这是因为反斜杠 (\) 的含义在 Python 字符串字面量和正则表达式中都需要被转义。
re.match('\\\\\$', '\$') 
Out[16]:
<re.Match object; span=(0, 2), match='\\$'>

天哪,那真是很多反斜杠...¶

  • 正则表达式通常写成r‘text’
  • 在正则表达式前加上r会让事情变得更简单一些
    • r表示原始文本
      • 在这种字符串中,反斜杠不会被解释为转义字符,而是被视为普通字符。因此,r'\n' 实际上代表的是字符串 \n,而不是换行符。
    • 防止Python解析字符串
    • 避免转义每个反斜杠
    • 例如:
      • '\n'是一个单字符字符串,一个换行,而
      • r'\n'是一个双字符字符串,相当于 '\\n'。
In [17]:
re.match(r'\\\$', '\$')
Out[17]:
<re.Match object; span=(0, 2), match='\\$'>

回想一下¶

  • '\n'是一个单字符字符串,一个新行,而
  • r'\n'是一个双字符字符串,相当于'\\n'。
In [56]:
beatles = "hello\ngoodbye"
re.findall(r'\n', beatles)
Out[56]:
['\n']
In [55]:
# “这很复杂,很难理解,所以强烈建议你们使用原始字符串来表达所有但最简单的表达式。”
re.findall('\\n', beatles)
Out[55]:
['\n']
In [20]:
re.findall('\\\n', beatles)
Out[20]:
['\n']

特殊字符:基础¶

  • 有些字符具有特殊含义
  • 这些是:. ^ $ * + ? { } [ ] \ | ( )
  • 我们今天会讨论其中的一些,其他的,请参考文档
  • 重要的是:要匹配字面意义,必须转义特殊字符!
In [21]:
re.findall(r'$2', "2$2")
Out[21]:
[]
In [22]:
re.findall(r'\$2', "2$2")
Out[22]:
['$2']

特殊字符:集合和范围¶

  • 可以使用方括号匹配“集合”中的字符:
    • '[aeiou]'匹配字符'a','e','i','o','u'中的任何一个
    • '[^aeiou]'匹配集合中没有的任何单个字符。
  • 也可以匹配“范围”:
    • 示例:'[a-z]'匹配小写字母
      • 根据ASCII编号计算范围
    • 示例:'[0-9A-Fa-f]'将匹配任何十六进制数字
    • 转义'-'(例如'[a\\-z]')将匹配字面'-'
      • 替代方案:将'-'放在集合的第一个或最后一个以匹配字面
  • 方括号内的特殊字符失去特殊含义:
    • 示例:'[(+\*)]'将匹配'(','+','*'或')'中的任何一个
    • 要按字面意义匹配'^',请确保它不是第一个:'[(+*)^]'

特殊字符:单字符匹配¶

  • '^':匹配行的开头
  • '\$':匹配行的结尾(即,匹配新行前的“空字符”)
  • '.':匹配除新行外的任何字符
  • '\s':匹配空白(空格,制表符,新行)
  • '\d':匹配一个数字(0,1,2,3,4,5,6,7,8,9),等同于r'[0-9]'
  • '\w':匹配一个“单词”字符(数字,字母或下划线'_')
  • '\b':匹配单词的边界
In [23]:
# 示例:行的开头和结尾,通配符
# ‘.’匹配‘a’,并且开始和结束行正确匹配。
pat = r'^b.d$' 
re.findall(pat, 'bad')
Out[23]:
['bad']
In [24]:
# ‘.’匹配‘i’,并且开始和结束行正确匹配。
re.findall(pat, 'bid')
Out[24]:
['bid']
In [25]:
# 匹配失败是因为字符串末尾的‘s’,这意味着‘d’后面不是行尾。
re.findall(pat, 'bids')
Out[25]:
[]
In [26]:
# 匹配失败是因为字符串开头的‘a’,这意味着‘b’不是字符串的开头。
re.findall(pat, 'abad')
Out[26]:
[]

示例:空白和边界¶

In [65]:
string1 = 'c\ta t\ns\n'
print(string1)
c	a t
s

In [28]:
# ‘\s’匹配任何空白。包括空格,制表符和新行。
re.findall(r'\s', string1)
Out[28]:
['\t', ' ', '\n', '\n']
In [4]:
# 因为它后面没有空白-单词边界。
print(re.findall(r'hello\b', 'helloworld!'))
print(re.findall(r'hello\b', "hello world!"))
[]
['hello']

字符类别:补集¶

  • '\s','\d','\w','\b'都可以通过大写来补集:
In [59]:
# ‘\S’:匹配任何非空白
re.findall(r'\S', string1)
Out[59]:
['c', 'a', 't', 's']
In [31]:
# ‘\D’:匹配任何非数字字符
re.findall(r'\D', string1)
Out[31]:
['c', '\t', 'a', ' ', 't', '\n', 's', '\n']
In [32]:
# ‘\W’:匹配任何非单词字符
re.findall(r'\W', "abc123 \t\n_$*.")
Out[32]:
[' ', '\t', '\n', '$', '*', '.']
In [71]:
# ‘\B’:匹配不在单词边界的
re.findall(r'\B\d\B', "1 2X a3 747")
Out[71]:
['4']
In [13]:
print(re.findall(r'hello\B', 'helloworld!'))
print(re.findall(r'hello\B', "hello world!"))
['hello']
[]

匹配和重复¶

In [35]:
# ‘*’:前一个项目的零个或多个
re.findall(r'ca*t', "ct cat caat caaat")
Out[35]:
['ct', 'cat', 'caat', 'caaat']
In [36]:
# ‘+’:前一个项目的一或多个
re.findall(r'ca+t', "ct cat caat caaat")
Out[36]:
['cat', 'caat', 'caaat']
In [37]:
# ‘?’:前一个项目的零个或一个
re.findall(r'ca?t', "ct cat caat caaat")
Out[37]:
['ct', 'cat']
In [38]:
# ‘{2}’:确切四个前一个项目
re.findall(r'ca{2}t', "ct cat caat caaat")
Out[38]:
['caat']
In [39]:
# ‘{1,2}’:前一个项目的两个到五个(包括)
re.findall(r'ca{1,2}t', "ct cat caat caaat")
Out[39]:
['cat', 'caat']

测试你的理解¶

以下哪个将匹配r'^\d{2,4}\s'? '7 a1' '747 Boeing' 'C7777 C7778' '12345 ' '1234\tqq' 'Boeing 747'

In [72]:
re.findall(r'^\d{2,4}\s', '7 a1'), re.findall(r'^\d{2,4}\s', '747 Boeing'), re.findall(r'^\d{2,4}\s', 'C7777 C7778')
Out[72]:
([], ['747 '], [])
In [41]:
re.findall(r'^\d{2,4}\s', '12345 '), re.findall(r'^\d{2,4}\s', '1234\tqq'), re.findall(r'^\d{2,4}\s', 'Boeing 747')
Out[41]:
([], ['1234\t'], [])

或条款:|¶

  • |(“管道”)是一个特殊字符,允许指定“或”条款
  • 示例:我想匹配“cat”这个词或“dog”这个词
  • 解决方案:'cat|dog'
In [1]:
import re
print(re.findall(r'cat|dog', "cat"))
print(re.findall(r'cat|dog', "dog"))
print(re.findall(r'cat|dog', "cat\ndog"))
['cat']
['dog']
['cat', 'dog']

或条款:| 是懒惰的!¶

  • 当使用管道的表达式可以以多种方式匹配时会发生什么?
  • 这里发生了什么?!
  • 带有|的匹配是懒惰的
    • 尝试按顺序从左到右匹配由‘|’分隔的每个正则表达式。
    • 一旦它匹配了某物,它就返回该匹配…
      • …然后开始尝试另一个匹配。
In [43]:
re.findall(r'a|aa|aaa', "aaaa")
Out[43]:
['a', 'a', 'a', 'a']

匹配和贪婪¶

  • 管道运算符|是懒惰的。但令人困惑的是,Python re模块通常是贪婪的:
In [44]:
# ‘a+’吞噬了整个字符串,因为Python正则表达式是贪婪的。
re.findall(r'a+', 'aaaaaa')
Out[44]:
['aaaaaa']
In [74]:
# ‘?’修改操作符如‘+’和‘*’不贪婪,我们得到懒惰匹配,就像使用‘|’一样。
re.findall(r'a+?', 'aaaaaa')
Out[74]:
['a', 'a', 'a', 'a', 'a', 'a']

提取组¶

  • Python re让我们可以提取我们匹配的东西,并在以后使用
In [75]:
# 示例:匹配电子邮件地址中的用户和域名
string1 = "My USC email is dpj@usc.edu.cn"
m = re.search(r'([\w.-]+)@([\w.-]+)', string1)
# ‘re.search’返回一个匹配对象。组属性是被匹配的整个字符串。
m.group()
Out[75]:
'dpj@usc.edu.cn'
In [76]:
# 可以按顺序访问组(正则表达式中的括号部分)的数字顺序。每组括号获得一个组,从左到右。
# re.findall具有类似的功能!
m.group(1)
Out[76]:
'dpj'
In [77]:
m.group(2)
Out[77]:
'usc.edu.cn'

后引用¶

  • 可以在同一个正则表达式内引用早期的匹配!
    • \N,其中N是一个数字,引用第N组
  • 示例:查找形式为'X X'的字符串,其中X是任何非空白字符串。
In [80]:
m = re.search(r'(\S+) \1', 'cat cat')
m.group()
Out[80]:
'cat cat'
In [50]:
m = re.search(r'(\S+) \1', 'cat dog')
m is None
Out[50]:
True

后引用¶

  • 后引用允许非常复杂的模式匹配!

  • 测试你的理解:

    • 描述字符串'(\d+)([A-Z]+):\1+\2'匹配什么?'([a-zA-Z]+).*\1'呢?
  • 第一个正则表达式: (\d+)([A-Z]+):\1+\2

    • 一串数字 (\d+): 匹配一个或多个数字(0-9)。
    • 一串大写字母 ([A-Z]+): 匹配一个或多个大写字母(A-Z)。
    • 冒号 (:): 匹配一个冒号。
    • 引用前面捕获的内容 (\1+\2): \1引用第一个捕获组(数字),\2引用第二个捕获组(大写字母)。+ 表示一个或多个。
    • 总的来说,这个正则表达式匹配一个格式为 "数字+大写字母:数字+大写字母" 的字符串。
    • 示例匹配:
      • "123ABC:123ABC"
      • "456DEF:456DEF"

后引用¶

  • 第二个正则表达式: ([a-zA-Z]+).*\1
    • 一串字母 ([a-zA-Z]+): 匹配一个或多个字母(a-z 或 A-Z)。
    • 任意字符 .*: 匹配任意字符(包括空字符串)。
    • 引用前面捕获的内容 \1: \1 引用第一个捕获组(字母)。
    • 总的来说,这个正则表达式匹配一个格式为 "字母+任意字符+字母" 的字符串,其中最后的字母与前面的字母相同。

Python re模块提供的选项¶

  • 可选标志修改re.findall,re.search等的行为。
    • 例如:re.search(r'dog',‘DOG’,re.IGNORECASE)匹配。
  • re.IGNORECASE:形成匹配时忽略大小写。
  • re.MULTILINE:^,$匹配任何行的开始/结束,不仅仅是开始/结束字符串
  • re.DOTALL:.匹配任何字符,包括新行。
  • 查看 https://docs.python.org/2/library/re.html#contents-of-module-re 更多。
In [82]:
re.search(r'dog', 'DOG', re.IGNORECASE)
Out[82]:
<re.Match object; span=(0, 3), match='DOG'>

调试¶

  • 有疑问时,测试你的正则表达式!
    • 搜索一下可以获得许多工具来做到这一点
  • 编译然后使用re.DEBUG标志也可以有所帮助
    • 编译也很好,用于多次使用正则表达式,例如
In [85]:
# regex = re.compile(r'cat|dog|bird', re.DEBUG)
regex = re.compile(r'cat|dog|bird')
regex.findall("It's raining cats and dogs")
Out[85]:
['cat', 'dog']
In [52]:
regex.match("cat bird dog")
Out[52]:
<re.Match object; span=(0, 3), match='cat'>
In [53]:
regex.search("nothing to see here.") is None
Out[53]:
True

捕获替换¶

  • 将以下年月日格式替换为月日年格式输出
  • 年月日数据如下:
    • "2024-05-01\n2024/05/02\n2024.05.03\n2024/05/04\n2024.05.05"
In [92]:
import re
# 原始日期字符串
date_string = "2024-05-01\n2024/05/02\n2024.05.03\n2024/05/04\n2024.05.05"
# 正则表达式匹配 YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD 格式
pattern = r'(\d{4})[-/\.](\d{2})[-/\.](\d{2})'
# 替换为 MM/DD/YYYY 格式
formatted_dates = re.sub(pattern, r'\2-\3-\1', date_string)
print(formatted_dates)
05-01-2024
05-02-2024
05-03-2024
05-04-2024
05-05-2024
In [ ]:
* 原始文本
    * "联系我:123-456-7890 或者 987.654.3210."
* 替换格式:将电话号码格式化为 (XXX) XXX-XXXX
In [93]:
import re
# 原始文本
text = "联系我:123-456-7890 或者 987.654.3210."
# 正则表达式匹配电话号码
pattern = r'(\d{3})[-.](\d{3})[-.](\d{4})'
# 替换格式:将电话号码格式化为 (XXX) XXX-XXXX
formatted_text = re.sub(pattern, r'(\1) \2-\3', text)
print(formatted_text)
联系我:(123) 456-7890 或者 (987) 654-3210.