数据安全基础:正则表达式 (Regex) 从入门到实战

第一部分:什么是正则表达式?为什么它很重要?

1.1 什么是正则表达式?

想象一下,你打开了一个 100MB 的文本文件,里面全是乱七八糟的服务器日志。你的老板让你找出里面所有的 "以 138 开头的手机号码"

  • 普通做法: 用眼睛一行行看?(不可能)用 Word 的"查找"功能?(只能查固定的词,查不了"模式")

  • 高手做法 : 写一个规则 ,告诉计算机:"帮我找一个 138 开头,后面跟着 8 个数字的东西"。

这个"规则",就是正则表达式 (Regular Expression),简称 Regex 或 RE。

1.2 在安全领域有什么用?

在网络安全和 CTF 比赛中,正则表达式是核心武器

  1. 数据防泄露 (DLP): 扫描文件,自动发现身份证号、银行卡号、密码。

  2. 日志分析: 从几百万行 Web 日志中,把黑客攻击的 IP 地址和 Payload 提取出来。

  3. Web 防火墙 (WAF) : 定义什么样的请求是攻击(例如:如果 URL 里包含 select ... from,就拦截)。

  4. 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 开头,第二位是 39 的数字。

符号 含义 例子 说明
[...] 其中任意一个 [abc] 匹配 abc
[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 忽略大小写 (匹配 Aa)
re.DOTALL re.S 点号匹配所有 (让 . 可以匹配换行符 \n)
re.MULTILINE re.M 多行模式 (影响 ^$)

3.3 Match Object (匹配对象) 深度解析

re.searchre.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: 提取所有手机号码

我们知道手机号的规则:

  1. 11位数字。

  2. 第 1 位固定是 1

  3. 第 2 位通常是 3-9

  4. 后面跟着 9 位任意数字。

正则构建过程:

  1. 第1位: 1

  2. 第2位: [3-9] (表示3到9任意一个)

  3. 后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位)

身份证规则(简化版):

  1. 前 17 位是数字。

  2. 最后 1 位是数字或者 X (可能大写也可能小写)。

正则构建过程:

  1. 前17位: \d{17}

  2. 最后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 SELECTOR '1'='1。我们需要从日志中把这些请求抓出来。

难点 : 黑客可能会混合大小写,比如 uNiOn SeLeCt

正则技巧:

  • (?i) : 放在正则最前面,表示忽略大小写

  • | : 表示"或者"。

正则构建:

  1. 匹配 union select: union\s+select (\s+表示中间至少有一个空格)

  2. 匹配万能密码: 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

正则构建:

  1. 匹配引号开头: "

  2. 提取方法 (GET或POST): (GET|POST) -> 分组1

  3. 中间有空格: \s+

  4. 提取 URL: (.*?) -> 分组2

  5. 后面有空格和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} #(@&!(

正则构建:

  1. 开头: flag\{ (注意 { 是特殊字符,最好加 \ 转义,虽然在 Python 中不转义有时也行,但规范写法要转义)

  2. 中间内容: .*? (非贪婪,遇到右大括号就停)

  3. 结尾: \}

最终正则 : 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 编码的特征:

  1. A-Z, a-z, 0-9, +, / 组成。

  2. 长度通常是 4 的倍数。

  3. 结尾可能有 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 gobye 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) 匹配 industrysindustries,但结果只返回完整单词,不返回后缀。

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! (后面加个感叹号),它会尝试无数种组合,导致计算量指数级爆炸。

避坑指南:

  1. 避免嵌套量词 : 不要写 (a+)+, (\d*)* 这种结构。

  2. 减少模糊匹配 : 尽量用精确的字符集 [^"\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. 练习 1 : 提取文本中所有的 HTTP 链接 (以 http://https:// 开头)。

  2. 练习 2 : 检查密码强度,要求必须包含大写字母小写字母数字

  3. 练习 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 特有)

祝你在数据安全比赛中,用正则这把利剑,披荆斩棘!

相关推荐
飞Link1 小时前
洞察数据的“分寸感”:深度解析对比学习(Contrastive Learning)
开发语言·python·学习·数据挖掘
无名-CODING1 小时前
java基础面试知识点
java·python·面试
带娃的IT创业者7 小时前
Python 异步编程完全指南:从入门到精通
服务器·开发语言·python·最佳实践·asyncio·异步编程
朱包林10 小时前
Python基础
linux·开发语言·ide·python·visualstudio·github·visual studio
Eward-an10 小时前
【算法竞赛/大厂面试】盛最多水容器的最大面积解析
python·算法·leetcode·面试·职场和发展
no_work11 小时前
基于python预测含MLP决策树LGBM随机森林XGBoost等
python·决策树·随机森林·cnn
进击的雷神11 小时前
地址语义解析、多语言国家匹配、动态重试机制、混合内容提取——德国FAKUMA展爬虫四大技术难关攻克纪实
爬虫·python
FreakStudio11 小时前
一行命令搞定驱动安装!MicroPython 开发有了自己的 “PyPI”包管理平台!
python·stm32·单片机·嵌入式·arm·电子diy
小浪花a11 小时前
计算机二级python-jieba库
开发语言·python