第一部分:什么是正则表达式?为什么它很重要?
1.1 什么是正则表达式?
想象一下,你打开了一个 100MB 的文本文件,里面全是乱七八糟的服务器日志。你的老板让你找出里面所有的 "以 138 开头的手机号码"。
-
普通做法: 用眼睛一行行看?(不可能)用 Word 的"查找"功能?(只能查固定的词,查不了"模式")
-
高手做法 : 写一个规则 ,告诉计算机:"帮我找一个
138开头,后面跟着8个数字的东西"。
这个"规则",就是正则表达式 (Regular Expression),简称 Regex 或 RE。
1.2 在安全领域有什么用?
在网络安全和 CTF 比赛中,正则表达式是核心武器:
-
数据防泄露 (DLP): 扫描文件,自动发现身份证号、银行卡号、密码。
-
日志分析: 从几百万行 Web 日志中,把黑客攻击的 IP 地址和 Payload 提取出来。
-
Web 防火墙 (WAF) : 定义什么样的请求是攻击(例如:如果 URL 里包含
select ... from,就拦截)。 -
CTF 竞赛: 快速从乱码中提取 Flag。
第二部分:零基础语法入门
不要被奇怪的符号吓倒,正则其实就是一套拼积木的游戏。我们先认识最基础的积木。
2.1 基础字符类(代词)
有些字符代表了"一类人"。
| 符号 | 含义 | 记忆口诀 | 例子 | 匹配结果 |
|---|---|---|---|---|
. |
任意字符 (除了换行) | "通配符" | a.c |
abc, a@c, a+c |
\d |
数字 (0-9) | digit | user\d |
user1, user9 |
\w |
单词字符 (字母/数字/下划线) | word | \w\w\w |
abc, 123, _a1 |
\s |
空白符 (空格/Tab/换行) | space | hello\sworld |
hello world |
练习 1 : 如果你想匹配
2025年,正则可以写成\d\d\d\d年。
2.2 量词(控制数量)
如果我们要匹配 11 位手机号,写 11 个 \d 太累了。这时候需要"量词"。
| 符号 | 含义 | 例子 | 说明 |
|---|---|---|---|
* |
重复 0 次或更多次 | ab* |
匹配 a (0个b), ab, abbb |
+ |
重复 1 次或更多次 | ab+ |
匹配 ab, abb (必须有b) |
? |
重复 0 次或 1 次 (有没有都行) | https? |
匹配 http, https |
{n} |
重复 n 次 | \d{11} |
匹配连续 11 个数字 |
{n,m} |
重复 n 到 m 次 | \d{4,6} |
匹配 4 到 6 位数字 |
练习 2 : 匹配手机号(11位数字):
\d{11}匹配QQ号(5到11位数字):\d{5,11}
2.3 字符集(挑食)
有时候 \d (所有数字) 太宽泛了,我只想匹配 1 开头,第二位是 3 到 9 的数字。
| 符号 | 含义 | 例子 | 说明 |
|---|---|---|---|
[...] |
其中任意一个 | [abc] |
匹配 a 或 b 或 c |
[x-y] |
范围 | [0-9] |
等同于 \d |
[^...] |
排除 (取反) | [^abc] |
匹配除了 a,b,c 之外的任意字符 |
实战应用 : 匹配手机号的前两位(1开头,3-9之间):
1[3-9]
2.4 边界(不仅要像,位置也要对)
如果你用 \d{3} 去匹配字符串 123456,它会匹配到 123。但有时候我们需要精确匹配。
| 符号 | 含义 | 说明 |
|---|---|---|
^ |
开头 | ^Admin 必须以 Admin 开头 |
$ |
结尾 | end$ 必须以 end 结尾 |
\b |
单词边界 | \bcat\b 匹配 cat,但不匹配 category |
第三部分:Python 中的正则模块 (re) 深度解析
在 Python 中使用正则非常简单,只需要 import re。但要成为高手,你需要掌握更多函数。
3.1 核心函数详解
1. re.findall(pattern, string, flags=0) <-- 最常用
-
功能 : 扫描整个字符串,找到所有 匹配的内容,返回一个列表。
-
场景: 提取所有邮箱、所有 IP、所有 Flag。
-
注意 : 如果正则里有分组
(),它只会返回分组内的内容(坑点!如果不想要这个特性,请用非捕获分组(?:...))。
2. re.search(pattern, string, flags=0)
-
功能 : 扫描整个字符串,找到第一个 匹配的位置,返回一个 Match Object(匹配对象)。
-
场景: 判断"有没有"攻击特征,或者只需要提取第一个出现的值。
-
获取内容 : 必须调用
.group()才能拿到字符串。
3. re.match(pattern, string, flags=0)
-
功能 : 必须从字符串的最开头开始匹配。
-
场景 : 校验输入格式(如:检查用户输入的手机号是否合法)。不要用来在一段长文本里找东西!
4. re.sub(pattern, repl, string, count=0, flags=0)
-
功能 : 替换。找到所有匹配的字符串,把它们变成别的东西。
-
场景 : 数据脱敏(把手机号中间四位变成
****),清洗数据(把 HTML 标签删掉)。
5. re.split(pattern, string, maxsplit=0, flags=0)
-
功能 : 分割 。比字符串自带的
.split()更强大,支持多种分隔符。 -
场景: 分割日志,分隔符可能是空格、逗号或者分号的混合。
6. re.compile(pattern, flags=0)
-
功能 : 预编译。把正则表达式字符串编译成一个正则对象。
-
场景: 如果同一个正则要在一个大循环里跑几百万次,先 compile 一下速度会快很多。
3.2 常用修饰符 (Flags)
在函数中通过 flags= 参数传入,或者在正则开头写 (?i)。
| 修饰符 | 简写 | 含义 |
|---|---|---|
re.IGNORECASE |
re.I |
忽略大小写 (匹配 A 和 a) |
re.DOTALL |
re.S |
点号匹配所有 (让 . 可以匹配换行符 \n) |
re.MULTILINE |
re.M |
多行模式 (影响 ^ 和 $) |
3.3 Match Object (匹配对象) 深度解析
当 re.search 或 re.match 成功时,返回的是一个 Match 对象。它包含了很多有用的信息。
假设我们有匹配:m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', 'Date: 2025-12')
| 方法/属性 | 说明 | 示例 | 结果 |
|---|---|---|---|
m.group() |
返回匹配到的完整字符串 | m.group() |
'2025-12' |
m.group(n) |
返回第 n 个分组的内容 | m.group(1) |
'2025' |
m.groups() |
返回所有分组组成的元组 | m.groups() |
('2025', '12') |
m.groupdict() |
返回所有命名分组组成的字典 | m.groupdict() |
{'year': '2025', 'month': '12'} |
m.start() |
匹配开始的索引位置 | m.start() |
6 |
m.end() |
匹配结束的索引位置 | m.end() |
13 |
m.span() |
返回 (start, end) 元组 |
m.span() |
(6, 13) |
3.4 命名分组 (Named Groups) ------ Python 独门绝技
在复杂的正则中,数括号算出是第几个分组很头大。Python 允许给分组起名字!
-
语法 :
(?P<name>...) -
优势 : 代码可读性极强,直接用名字拿数据,不用数
group(1),group(2)。
演示代码:
import re
text = "我的身份证是 11010119900307123X"
# 给分组起名为 id
pattern = r"我的身份证是 (?P<id>\d{17}[\dXx])"
match = re.search(pattern, text)
if match:
print(match.group("id")) # 直接用名字取!
# 输出: 11010119900307123X
3.5 演示代码模板
import re
text = "我的电话是 13800138000,他的电话是 18912345678"
# 1. 定义正则:r表示原始字符串,避免转义坑
pattern = r"1[3-9]\d{9}"
# 2. 执行匹配 (findall)
results = re.findall(pattern, text)
print(f"findall结果: {results}")
# 输出: ['13800138000', '18912345678']
# 3. 替换 (sub) - 手机号脱敏
# (\d{3}): 前3位 -> group(1)
# \d{4}: 中间4位 (会被替换掉)
# (\d{4}): 后4位 -> group(2)
# 替换为: \1****\2
sanitized = re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", text)
print(f"脱敏结果: {sanitized}")
# 输出: 我的电话是 138****8000,他的电话是 189****5678
第四部分:安全场景实战详解
接下来我们将构建真实的比赛场景,手把手教你写正则。
场景一:从泄露数据中提取敏感信息 (DLP)
任务 1: 提取所有手机号码
我们知道手机号的规则:
-
11位数字。
-
第 1 位固定是
1。 -
第 2 位通常是
3-9。 -
后面跟着 9 位任意数字。
正则构建过程:
-
第1位:
1 -
第2位:
[3-9](表示3到9任意一个) -
后9位:
\d{9}
最终正则 : 1[3-9]\d{9}
实战代码演示:
import re
def extract_phone_numbers(text):
print(">>> 正在提取手机号...")
# 构造正则:1开头,第二位3-9,后面9位数字
pattern = r"1[3-9]\d{9}"
matches = re.findall(pattern, text)
if matches:
print(f"找到 {len(matches)} 个号码: {matches}")
else:
print("未找到手机号")
# 测试数据
sample_data = "我的手机号是 13812345678,备用 19988887777,错误号码 12345678901"
extract_phone_numbers(sample_data)
# 预期输出: ['13812345678', '19988887777']
任务 2: 提取身份证号 (18位)
身份证规则(简化版):
-
前 17 位是数字。
-
最后 1 位是数字或者
X(可能大写也可能小写)。
正则构建过程:
-
前17位:
\d{17} -
最后1位:
[\dXx](数字 或 X 或 x)
最终正则 : \d{17}[\dXx]
进阶问题 : 如果文本是
No11010119900307123Xabc,上面的正则会把中间的身份证号提出来。但如果要求必须是独立的身份证号 ,两边不能有字母呢? 解决方案 : 加边界\b。 进阶正则 :\b\d{17}[\dXx]\b
实战代码演示:
import re
def extract_id_cards(text):
print("\n>>> 正在提取身份证号...")
# 构造正则:单词边界 + 17位数字 + 1位数字或X
pattern = r"\b\d{17}[\dXx]\b"
matches = re.findall(pattern, text)
print(f"提取结果: {matches}")
# 测试数据
data = "张三 11010119900307123X, 李四 440102800101123 (这是15位,不匹配)"
extract_id_cards(data)
# 预期输出: ['11010119900307123X']
场景二:Web 攻击日志分析
任务 3: 发现 SQL 注入攻击
黑客经常在 URL 里输入 UNION SELECT 或 OR '1'='1。我们需要从日志中把这些请求抓出来。
难点 : 黑客可能会混合大小写,比如 uNiOn SeLeCt。
正则技巧:
-
(?i): 放在正则最前面,表示忽略大小写。 -
|: 表示"或者"。
正则构建:
-
匹配 union select:
union\s+select(\s+表示中间至少有一个空格) -
匹配万能密码:
or\s+['"]1['"]\s*=\s*['"]1(单引号或双引号,等号两边可能有空格)
最终正则 : (?i)union\s+select|or\s+['"]1['"]\s*=\s*['"]1
实战代码演示:
import re
def detect_sqli(log_line):
print(f"\n>>> 分析日志: {log_line}")
# (?i) 忽略大小写
# union\s+select 匹配联合查询
# or\s+... 匹配万能密码
pattern = r"(?i)union\s+select|or\s+['\"]1['\"]\s*=\s*['\"]1"
if re.search(pattern, log_line):
print("[!] 警告: 发现 SQL 注入攻击!")
else:
print("[+] 正常请求")
# 测试数据
detect_sqli("id=1' OR '1'='1") # 攻击
detect_sqli("id=100 union select 1") # 攻击
detect_sqli("id=1") # 正常
任务 4: 提取日志中的 URL
假设日志格式如下: 192.168.1.1 - - [Time] "GET /admin/login.php?id=1 HTTP/1.1" 200
我们要提取出 GET 和 /admin/login.php?id=1。
正则构建:
-
匹配引号开头:
" -
提取方法 (GET或POST):
(GET|POST)-> 分组1 -
中间有空格:
\s+ -
提取 URL:
(.*?)-> 分组2 -
后面有空格和HTTP:
\s+HTTP
最终正则 : "(GET|POST)\s+(.*?)\s+HTTP
知识点: 贪婪 vs 非贪婪
.*: 贪婪模式。会尽可能多地吃字符。
.*?: 非贪婪模式。一旦满足后面的条件,就立刻停止匹配。口诀 : 提取两头已知、中间未知的内容时,中间一定要用
.*?。
实战代码演示:
import re
def parse_log(log_line):
print("\n>>> 解析日志字段...")
# 分组提取:Group 1 是方法,Group 2 是 URL
pattern = r'"(GET|POST)\s+(.*?)\s+HTTP'
match = re.search(pattern, log_line)
if match:
method = match.group(1)
url = match.group(2)
print(f"方法: {method}, URL: {url}")
else:
print("解析失败")
# 测试数据
log = '192.168.1.1 - - [20/Dec/2025:10:00:00 +0800] "GET /admin/login.php?id=1 HTTP/1.1" 200 1234'
parse_log(log)
# 预期输出: 方法: GET, URL: /admin/login.php?id=1
场景三:CTF 夺旗赛 (Flag 提取)
任务 5: 自动提取 Flag
比赛中,Flag 的格式通常是 flag{...}。文本中可能混杂大量乱码。
文本 : dh82h@*# flag{W3lc0me_T0_CTF} #(@&!(
正则构建:
-
开头:
flag\{(注意{是特殊字符,最好加\转义,虽然在 Python 中不转义有时也行,但规范写法要转义) -
中间内容:
.*?(非贪婪,遇到右大括号就停) -
结尾:
\}
最终正则 : flag\{.*?\}
实战代码演示:
import re
def find_flag(text):
print("\n>>> 正在寻找 Flag...")
# .*? 非贪婪匹配,防止匹配到后面多余的内容
# re.I 忽略大小写,防止 Flag 写成 FLAG
pattern = r"flag\{.*?\}"
matches = re.findall(pattern, text, re.I)
if matches:
print(f"恭喜! 找到 Flag: {matches}")
else:
print("未发现 Flag")
# 测试数据
ctf_data = "dh82h@*# flag{W3lc0me_T0_CTF} #(@&!("
find_flag(ctf_data)
任务 6: 提取 IP 地址 (IPv4)
IPv4 地址通常是 x.x.x.x,其中 x 是 0-255。
正则构建 (简单版) : \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} 这个正则会匹配 999.999.999.999,虽然不合法,但在做初步提取时通常够用了。
正则构建 (精确版) : 如果你非要精确匹配 0-255,正则会变得很长,比赛中不建议背诵。 建议先提取出来,再用 Python 代码 if 0 <= int(x) <= 255 判断。
实战代码演示:
import re
def extract_ips(text):
print("\n>>> 正在提取 IP 地址...")
pattern = r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
matches = re.findall(pattern, text)
valid_ips = []
# 二次校验
for ip in matches:
parts = ip.split('.')
if all(0 <= int(part) <= 255 for part in parts):
valid_ips.append(ip)
print(f"发现合法 IP: {valid_ips}")
# 测试数据
log_data = "Attack from 192.168.1.1 and 999.999.999.999 (invalid)"
extract_ips(log_data)
任务 7: 识别 Base64 编码字符串
Base64 编码的特征:
-
由
A-Z,a-z,0-9,+,/组成。 -
长度通常是 4 的倍数。
-
结尾可能有 1 个或 2 个
=(填充符)。
正则构建 : [a-zA-Z0-9+/]{10,}={0,2} (这里设定至少匹配 10 位,防止把普通的单词误判为 Base64)
实战代码演示:
import re
import base64
def find_base64(text):
print("\n>>> 正在寻找 Base64 字符串...")
# 匹配至少 20 位的 Base64 字符串
pattern = r"[a-zA-Z0-9+/]{20,}={0,2}"
matches = re.findall(pattern, text)
for m in matches:
print(f"发现疑似 Base64: {m}")
try:
decoded = base64.b64decode(m).decode(errors='ignore')
print(f" -> 尝试解码: {decoded}")
except:
print(" -> 解码失败")
# 测试数据
data = "User: admin, Token: dXNlck1hbTphZG1pbg== (hidden info)"
find_base64(data)
任务 8: 提取 IPv6 地址 (进阶)
IPv6 也是比赛常客,格式如 2001:0db8:85a3:0000:0000:8a2e:0370:7334。 它由 8 组 16 进制数组成,中间用冒号分隔。
正则构建 : ([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4} (简单版,不考虑 :: 缩写形式,比赛中通常够用)
实战代码演示:
import re
def extract_ipv6(text):
print("\n>>> 正在提取 IPv6...")
# 简单匹配:4位16进制 + 冒号 (重复7次) + 4位16进制
pattern = r"([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}"
matches = re.finditer(pattern, text) # finditer 返回迭代器
for m in matches:
print(f"发现 IPv6: {m.group()}")
text = "Node IP: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 active"
extract_ipv6(text)
任务 9: 高级日志解析 (使用命名分组)
Nginx 日志格式复杂,如果用 group(1)、group(2) 会数晕。使用 (?P<name>...) 命名分组,可以直接转成 JSON。
日志样本 : 127.0.0.1 - - [21/Dec/2025:10:00:00 +0800] "GET /index.html HTTP/1.1" 200 612
实战代码演示:
import re
import json
def parse_nginx_log(log_line):
print("\n>>> 高级日志解析...")
# 定义正则,给每个部分起名字
pattern = r'(?P<ip>\d+\.\d+\.\d+\.\d+) - - \[(?P<time>.*?)\] "(?P<method>\w+) (?P<url>.*?) HTTP/.*?" (?P<status>\d+) (?P<size>\d+)'
match = re.search(pattern, log_line)
if match:
# 直接获取字典
log_dict = match.groupdict()
print(json.dumps(log_dict, indent=2))
else:
print("解析失败")
log = '127.0.0.1 - - [21/Dec/2025:10:00:00 +0800] "GET /index.html HTTP/1.1" 200 612'
parse_nginx_log(log)
# 输出:
# {
# "ip": "127.0.0.1",
# "time": "21/Dec/2025:10:00:00 +0800",
# "method": "GET",
# "url": "/index.html",
# "status": "200",
# "size": "612"
# }
任务 10: 提取跨行或混淆的 Flag
有些 Flag 故意写成这样:
flag
{Hidden
_In_
Newlines}
普通的 . 匹配不到换行符 \n。
解决方案 : 使用 re.S (DOTALL) 模式。
实战代码演示:
import re
def find_multiline_flag(text):
print("\n>>> 寻找跨行 Flag...")
# re.S 让 . 可以匹配换行符
# re.X (VERBOSE) 允许在正则里写注释和空格,增加可读性
pattern = r"flag\s*\{.*?\}"
matches = re.findall(pattern, text, re.S | re.I)
for m in matches:
# 去掉换行和空格,还原 Flag
clean_flag = re.sub(r"\s+", "", m)
print(f"发现并还原: {clean_flag}")
data = """
这里有一个隐藏的 flag
{Hidden
_In_
Newlines}
结束
"""
find_multiline_flag(data)
第五部分:高阶技巧 ------ 像黑客一样思考
普通人写正则只能匹配简单的字符,高手懂得利用"位置"和"逻辑"。
5.1 零宽断言 (Lookaround)
名字很吓人,其实就是**"只匹配位置,不消耗字符"** 。 当你想要提取 <div>内容</div> 里的 内容,但不想要 两边的 div 标签时,这个技巧非常有用。
| 语法 | 名称 | 含义 | 例子 |
|---|---|---|---|
(?=...) |
正向先行断言 | 后面必须跟着什么 | Windows(?=95) 能匹配 Windows95 中的 Windows,但不匹配 WindowsXP |
(?<=...) |
正向后行断言 | 前面必须是什么 | (?<=CNY:)\d+ 能匹配 CNY:100 中的 100 |
(?!...) |
负向先行断言 | 后面不能跟着什么 | Windows(?!95) 能匹配 WindowsXP 中的 Windows |
(?<!...) |
负向后行断言 | 前面不能是什么 | (?<!CNY:)\d+ 匹配不带货币前缀的数字 |
实战应用:提取 URL 中的域名 URL: https://www.example.com/login 我们想要 www.example.com。
-
前面是
:// -
后面是
/ -
正则:
(?<=://).*?(?=/)
5.2 反向引用 (Backreference)
如何匹配 "连续重复的单词" ?比如 go go,bye bye。 因为我们不知道单词是啥,所以不能写死。我们需要引用前面匹配到的内容。
-
语法:
\1,\2(引用第1个、第2个分组的内容) -
例子:
\b(\w+)\s+\1\b-
(\w+): 匹配一个单词,并放入分组1。 -
\s+: 中间有空格。 -
\1: 这里的内容必须和分组1一模一样。
-
5.3 非捕获分组 (Non-capturing Group)
有时候我们用 () 只是为了逻辑上的"或" (A|B),或者是为了加量词 (abc)+,但不想 让 re.findall 单独把这个分组提取出来。
-
语法:
(?:...) -
例子:
industry(?:s|ies)匹配industrys或industries,但结果只返回完整单词,不返回后缀。
5.4 贪婪 vs 非贪婪 (Greedy vs Non-Greedy)
这是一个必须彻底理解的概念,否则你的正则会"吃掉"不该吃的东西。
-
贪婪模式 (
*,+,?,{n,m}): 默认模式。尽可能多地匹配字符。 -
非贪婪模式 (
*?,+?,??,{n,m}?): 在量词后面加?。尽可能少地匹配字符。
经典案例 : 提取 HTML 标签 <div>test</div>
-
贪婪 :
<.+>-
匹配:
<div>test</div>(从第一个<一直吃到最后一个>) -
结果: 整个字符串被匹配了。
-
-
非贪婪 :
<.+?>-
匹配:
<div>和</div> -
结果: 遇到第一个
>就停下来。
-
比赛心法 : 提取两头已知、中间未知的内容(如 Flag、引号内的内容),永远使用非贪婪模式
.*?。
第六部分:性能与安全 (ReDoS)
6.1 什么是 ReDoS 攻击?
正则表达式如果写得不好,在处理特定字符串时,会发生**"灾难性回溯" (Catastrophic Backtracking)** ,导致 CPU 飙升到 100%,服务器卡死。这被称为 正则表达式拒绝服务攻击 (ReDoS)。
危险代码示例 : (a+)+$ 如果你给它一个字符串 aaaaaaaaaaaaaaaaaaaaaaaaaaaaa! (后面加个感叹号),它会尝试无数种组合,导致计算量指数级爆炸。
避坑指南:
-
避免嵌套量词 : 不要写
(a+)+,(\d*)*这种结构。 -
减少模糊匹配 : 尽量用精确的字符集
[^"\r\n]*代替.*。
6.2 性能优化:re.compile
在 Python 中,如果你在一个循环里调用 re.findall,每次调用 Python 都要重新解析一遍正则字符串。 优化方案 : 使用 re.compile 预编译。
import re
import time
# 慢的方式
start = time.time()
for _ in range(100000):
re.findall(r"\d+", "user1234")
print(f"普通模式耗时: {time.time() - start:.4f}s")
# 快的方式
pattern = re.compile(r"\d+") # 先编译好
start = time.time()
for _ in range(100000):
pattern.findall("user1234") # 直接调用对象的方法
print(f"编译模式耗时: {time.time() - start:.4f}s")
第七部分:新手练习题
请尝试编写 Python 脚本完成以下任务(参考代码在下方):
-
练习 1 : 提取文本中所有的 HTTP 链接 (以
http://或https://开头)。 -
练习 2 : 检查密码强度,要求必须包含大写字母 、小写字母 和数字。
-
练习 3 : 提取文本中所有的 MAC 地址 (格式如
00:11:22:33:44:55,十六进制数字)。
参考答案
import re
# 练习 1: 提取链接
def practice_1():
text = "请访问 https://www.google.com 或 http://test.com/login"
# https? 表示 s 可有可无
# [a-zA-Z0-9./-]+ 匹配域名和路径常见字符
pattern = r"https?://[a-zA-Z0-9./-]+"
print(f"练习1 链接: {re.findall(pattern, text)}")
# 练习 2: 密码强度 (简单版:分步判断)
def practice_2(password):
# 同时满足三个条件
has_upper = re.search(r'[A-Z]', password)
has_lower = re.search(r'[a-z]', password)
has_digit = re.search(r'\d', password)
if has_upper and has_lower and has_digit:
print(f"练习2 密码 '{password}': 强")
else:
print(f"练习2 密码 '{password}': 弱")
# 练习 3: MAC 地址
def practice_3():
text = "MAC: 00:1A:2B:3C:4D:5E, Invalid: 00-ZZ-..."
# [0-9a-fA-F]{2} 匹配两个十六进制字符
# (?:...){5} 重复 5 次 "冒号+两个字符"
pattern = r"[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}"
print(f"练习3 MAC: {re.findall(pattern, text)}")
# 运行练习
practice_1()
practice_2("Pass1234")
practice_3()
附录:常用元字符速查表
| 类别 | 符号 | 描述 |
|---|---|---|
| 基础 | . |
任意字符 |
\ |
转义字符 (如 \. 匹配点号本身) |
|
| 字符类 | \d |
数字 [0-9] |
\w |
单词字符 [a-zA-Z0-9_] |
|
\s |
空白符 (空格、换行等) | |
| 数量 | * |
0次或多次 |
+ |
1次或多次 | |
? |
0次或1次 | |
{n} |
恰好 n 次 | |
| 边界 | ^ |
字符串开头 |
$ |
字符串结尾 | |
\b |
单词边界 | |
| 逻辑 | ` | ` |
() |
分组捕获 | |
(?:...) |
非捕获分组 | |
(?i) |
忽略大小写模式 | |
| 断言 | (?=...) |
正向先行 (右边是) |
(?<=...) |
正向后行 (左边是) | |
(?!...) |
负向先行 (右边不是) | |
(?<!...) |
负向后行 (左边不是) | |
| 引用 | \1 |
引用第一个分组内容 |
(?P<name>...) |
命名分组 (Python 特有) |
祝你在数据安全比赛中,用正则这把利剑,披荆斩棘!