结构化数据¶
- 存储:存储介质上的位(例如,硬盘)
- 编码:位如何对应符号?
- 解释/含义:例如,字符组合成单词
- 分隔文件:单词组合成句子,文档
- 结构化内容:元数据,标签等
- 集合:数据库,目录,归档(.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 表格¶
注意!¶
- 不同的操作系统在保存文本文件时遵循略微不同的约定!
- 最常见的问题:
- UNIX/Linux/MacOS:换行符存储为
\n
- DOS/Windows:存储为
\r\n
(回车,然后换行)
- UNIX/Linux/MacOS:换行符存储为
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.