Python -- 正则表达式

文章目录

match函数的使用

python 复制代码
re.match(pattern, string, flags=0)
  • re.match()尝试从字符串的起始位置 匹配一个模式,如果不是起始位置匹配成功的话,match()就会返回None
  • re.search()则是全文搜索
python 复制代码
import re
# 定义正则表达式
pattern = "hello"
# 目标字符串
s = "hello world"
# result = re.match(pattern, s, re.I) 忽略大小写
result = re.match(pattern, s)
# print(result) # <re.Match object; span=(0, 5), match='hello'>
# print(result.group()) # hello
# print(result.span()) # (0, 5)
ss = "world, hello"
# result = re.match(pattern, ss)
# print(result) # None
result = re.search(pattern, ss)
# print(result) # <re.Match object; span=(7, 12), match='hello'>
print(result.group()) # hello
print(result.span()) # (7, 12)
  • re.match()是一个较为宽松的匹配机制,它只关心字符串的开头 是否符合模式。只要开头匹配上,哪怕字符串后面跟着一堆乱七八糟的字符,其也会返回一个Match对象;但是re.fullmatch()匹配则更加严格,它要求整个字符串 从第一个字符到最后一个字符,必须百分之百匹配正则表达式,多一个字符不行,少一个字符也不行。从本质上来讲,使用re.fullmatch()等同于在正则表达式强行加上了字符串结束锚点\Z(或$),即re.fullmatch(pattern, text) <=> re.match(pattern\Z, text)
python 复制代码
import re

pattern = r'\d{3}'
text = "123abc123"
# print(re.match(pattern, text)) # <re.Match object; span=(0, 3), match='123'>
# print(re.fullmatch(pattern, text)) # None
text = "123"
# print(re.match(pattern, text)) # <re.Match object; span=(0, 3), match='123'>
# print(re.fullmatch(pattern, text)) # <re.Match object; span=(0, 3), match='123'>

常用匹配符

符号 描述
. 匹配任意一个字符(除了\n)
[] 匹配列表中的字符
\w 匹配字母、数字、下划线,即a-z,A-Z,0-9,_
\W 匹配不是字母、数字、下划线
\s 匹配空白字符,即空格(\n,\t)
\S 匹配不是空白的字符
\d 匹配数字,即0-9
\D 匹配不是数字的字符
  • 一个正则表达式是由字母、数字和特殊字符(括号、星号、问号等)组成。正则表达式中有许多特殊的字符,这些特殊的字符是构成正则表达式的要素。
python 复制代码
import re

# . every
pattern = '.'
s = "hello world"
result = re.match(pattern, s)
# print(result.group()) # h
# print(result.span()) # (0, 1)
s = '\n'
result = re.match(pattern, s)
# print(result) # None

# \w a-z || A-Z || 0-9 || _
pattern = r'\w\w\w\w' # 在使用\w时会尝试将其转化为转义字符,但是因为没有该转义字符,因此不加r的话会出现警告
s = "7l_Lchenxing"
result = re.match(pattern, s)
# print(result) # <re.Match object; span=(0, 4), match='7l_L'>
# print(result.group()) # 7l_L
# print(result.span()) # (0, 4)

pattern = r'\W\W\W'
s = "s()..."
result = re.match(pattern, s)
# print(result) # None
s = "()[]"
result = re.match(pattern, s)
# print(result) # <re.Match object; span=(0, 3), match='()['>
# print(result.group()) # ()[
# print(result.span()) # (0, 3)

# [] 匹配列表中的字符
pattern = '[13579]'
print(re.match(pattern, s))

# 匹配电话号码
pattern = r'1[35789]\d\d\d\d\d\d\d\d\d'

限定符

  • 从上面的示例中可以看到如果要匹配手机号码,需要形如\d\d\d\d\d\d\d\d\d\d\d这样的正则表达式。其中表现了11次\d,表达式烦琐。正则表达式作为一门小型的语言,还提供了对表达式的一部分重复处理的功能。例如,*可以对正则表达式的某个部分重复匹配多次。这种匹配符号称为限定符。
符号 描述 符号 描述
* 匹配零次或多次 {m} 重复m次
+ 匹配一次或多次 {m,n} 重复m到n次,其中n可以省略,表示m到任意次
? 匹配一次或零次 {m,} 至少m次
python 复制代码
import re
# '*' 0 ~ ∞
pattern = r'\d*'
s = "123456abc"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 6), match='123456'>
s = "abc123456"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 0), match=''>
# 注意,此时返回的是"",而不是None

# '+' 1 ~ ∞
pattern = r'\d+'
s = "abc123456"
# print(re.match(pattern, s)) # None
# 注意此时返回的是None
s = "123456abc"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 6), match='123456'>

# '?' 0/1
pattern = r'\d?'
s = "a1"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 0), match=''>
s = "11a"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 1), match='1'>

# {m} m-times
pattern = r'\d{4}'
s = "12345bc"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 4), match='1234'>
s = "123abc4"
# print(re.match(pattern, s)) # None

# {m, } m - ∞
pattern = r'\d{4,}' # 注意逗号后面不要是空格,负责结果是None
s = "123456abc"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 6), match='123456'>
s = "123abc"
# print(re.match(pattern, s)) # None

# {m,n} m-n
pattern = r'\d{2,5}'
s = "1abc"
# print(re.match(pattern, s)) # None
s = "123abc"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 3), match='123'>
s = "123456"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 5), match='12345'>

限定符使用示例

  • 匹配出一个字符串首字母为大写字符,后面都是小写字符,这些小写字符可有可无
python 复制代码
import re

pattern = r'[A-Z][a-z]*'
s = "Hello world"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 5), match='Hello'>
s = "HEllo world."
# print(re.match(pattern, s)) # <re.Match object; span=(0, 1), match='H'>
  • 匹配出有效的变量名
python 复制代码
import re

pattern = r'[A-Za-z_][A-Za-z0-9_]*'
s = "a_"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='a_'>
s = "0a"
# print(re.match(pattern, s)) # None
s = "mm"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='mm'>
pattern = r'[A-Za-z_]\w*' # (=) [A-Za-z_][A-Za-z0-9_]*
  • 匹配出1 ~ 99的数字
python 复制代码
import re

pattern = r'[1-9][0-9]?' # pattern = r'[1-9]\d?'
# ?: 0/1 否则 1 ~ 9上午的数字没办法进行匹配
s = "100"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='10'>
s = "0"
# print(re.match(pattern, s)) # None
  • 匹配出一个随机密码8~20位以内(大写字母 小写字母 下划线 数字)
python 复制代码
import re

pattern = r'\w{8,20}'
s = "chenxing0000"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 12), match='chenxing0000'>
s = "123456"
# print(re.match(pattern, s)) # None

边界字符

字符 功能
^ 匹配字符串开头
$ 匹配字符串结尾
\b 匹配一个字符的边界
\B 匹配非单词的边界
  • 注意:^[^m]中的""的含义并不相同,后者""表示"除了..."的意思。
  • 边界字符本身不匹配任何实际的字符,它们只匹配位置。
    1. ^匹配字符串的开头。如果开启了多行模式(re.MULTILINE),它会匹配每一行的开头。
    2. $匹配字符串的结尾,在多行模式下,匹配每一行的结尾。
  • 例如:你需要从一堆文本中严格校验并提取符合flag{...}格式的字符串,且该字符串必须独占一行:
python 复制代码
import re

text = """
flag{this_is_a_fake_flag}
trailing_garbage
flag{correct_crypto_fake_format}
prefix flag{another_fake_flag}
"""

# 不加边界,会匹配左右包含flag{...}的子串
pattern = r'flag\{.*?\}'
# print(re.findall(pattern, text)) # ['flag{this_is_a_fake_flag}', 'flag{correct_crypto_fake_format}', 'flag{another_fake_flag}']

# 使用^和$配合re.MULTILINE,只匹配独占一行的标准格式
pattern = re.compile(r'^flag\{.*?\}$', re.MULTILINE)
# print(pattern.findall(text)) # ['flag{this_is_a_fake_flag}', 'flag{correct_crypto_fake_format}']
  • 单词边界:\b\B
    1. \b:匹配一个单词的边界。也就是指单词和非单词(如空格、标点符号或字符串开头/结尾)之间的那个隐形位置。在正则表达式中,"单词"通常指字母、数字和下划线(\w)
    2. \B:匹配非单词边界。即要求当前位置的前后要么都是单词字符,要么都是非单词字符。
  • 假如你正在审计一段代码或分析日志系统,需要找出所有的独立单词admin,但不能把administratorsuperadmin给误报出来:
python 复制代码
log_entry = "The admin user logged in. Superadmin failed. Need to contact the administrator."
# 不加边界会把所有包含admin的词都找出来
pattern = r'admin'
# print(re.findall(pattern, log_entry)) # ['admin', 'admin', 'admin']

# 使用\b确保admin是一个独立的单词
pattern = r'\badmin\b'
# print(re.findall(pattern, log_entry)) # ['admin']

# 使用\B找出所有包含admin但admin不是独立单词的情况
pattern = r'\b\w*\Badmin\b|\badmin\B\w*\b'
# print(re.findall(pattern, log_entry)) # ['Superadmin', 'administrator']
  • 详细解释一下这里的\b与·\B,其实就是当边界符出现的时候,他会去判断此时非非边界符的正则表达式匹配到的子串的两边,例如\badmin会去判断admin的左边是不是单词边界(空格、\t等),如果使得话,这次才算是完全的匹配。拿\b\w*\Badmin\b举个例子,当正则表达式匹配到Superadmin时,admin的左边是Super,其中\B会去判定,radmin之间是不是单词边界,对于\B来说答案是True,所以满足条件,之后\w*\Badmin就匹配成功了,而最外侧的两个\b则是要求Superadmin两侧都是单词边界,也就是类似于... Superadmin ...的清苦那个才算是我们此时的正则表达式的要求。
  • 绝对字符串起止边界:\A\Z
    \A:永远只匹配整个字符串的绝对开头
    \Z:永远只匹配整个字符串的绝对结尾
    区别:无论是否开启re.MULTILINE模式,\A\Z都不会受到换行符的影响,只看整个大文本的头和尾。

search函数的使用

  • search是在一个字符串中搜索满足文本模式的字符串,其格式如下:
    re.search(pattern, string, flags=0)
  • 函数参数与match类似:
参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符-可选标志
  • re.match()只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search()匹配整个字符串,直到找到一个匹配。
python 复制代码
import re

pattern = r'abc'
s = "abcdefabc"
result = re.search(pattern, s)
# print(result) # <re.Match object; span=(0, 3), match='abc'>
# print(result.group()) # abc
# print(result.span()) # (0, 3)
s = "defabc"
# print(re.search(pattern, s)) # <re.Match object; span=(3, 6), match='abc'>
s = "defdef"
# print(re.search(pattern, s)) # None
s = "deAbcf"
# print(re.search(pattern, s)) # None
# print(re.search(pattern, s, re.I)) # <re.Match object; span=(2, 5), match='Abc'>
s = "Abcdefabc"
# print(re.search(pattern, s, re.I)) # <re.Match object; span=(0, 3), match='Abc'>
  • 择一匹配(|)的使用
    search方法搜索一个字符串,要想搜索多个字符串,如搜索aabbcc,最简单的方法是在文本模式字符串中使用择一匹配符号(|)。择一匹配符合和逻辑或类似,只要满足任何一个就算匹配成功。
python 复制代码
pattern = r'aa|bb|cc'
s = "aa"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='aa'>
s = "bb"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='bb'>
s = "where is cc"
# print(re.search(pattern, s)) # <re.Match object; span=(9, 11), match='cc'>
  • 匹配0~100之间的所有数字
python 复制代码
pattern = r'\d{1,2}|100$'
s = "56"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='56'>
s = "0"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 1), match='0'>
s = "101"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='10'>
s = "100"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 2), match='10'>
pattern = r'\d{1,2}$|100'
# print(re.match(pattern, s)) # <re.Match object; span=(0, 3), match='100'>
s = "101"
# print(re.match(pattern, s)) # None
  • 如果待匹配的字符串中,某些字符可以有多个选择,就可以使用字符集[],也就是一对中括号括起来的字符串。例如,[xyz]表示x、y、z三个字符中取其中任何一个,相当于x|y|z,所以对单个字符使用或关系时,字符集和择一匹配的效果是一样的。
  • 两者并不完全等效,其中[]对于排列组合式的效果更好,而|对于特定或关系效果更好:
python 复制代码
pattern = r'[ab][cd]' # ac, ad, bc, cd
# print(re.match(pattern, "bc")) # <re.Match object; span=(0, 2), match='bc'>
pattern = r'ac|cd'
# print(re.match(pattern, "bc")) # None
pattern = r'apple|banana'
# print(re.match(pattern, "banana")) # <re.Match object; span=(0, 6), match='banana'>

分组

  • 如果一个模式字符串中有用一对圆括号括起来的部分,那么这部分就会作为一个分组,可以通过group方法的参数获取指定的组匹配的字符串。当然,如果模式字符串中没有任何用圆括号括起来的部分,那么就不会对待匹配的字符串进行分组。
字符 功能
(ab) 将括号中的字符作为一个分组
\num 引用分组num匹配到的字符串
(?P<name>) 分别起组名
(?P=name) 引用别名为name分组匹配到的字符串
  • 匹配座机号码
python 复制代码
import re

pattern = r'(\d+)-(\d{5,8}$)'
phonenumber = "010-66668888"
result = re.match(pattern, phonenumber)
# print(result) # <re.Match object; span=(0, 12), match='010-66668888'>
# print(result.group()) # 010-66668888
# print(result.group(1)) # 010
# print(result.group(2)) # 66668888
# print(result.groups()) # ('010', '66668888')
# print(result.groups()[0]) # 010
# print(result.groups()[1]) # 66668888
  • 分组的引用
python 复制代码
import re

pattern = r'<.+><.+>.+</.+></.+>'
s = "<html><head>分组的使用</head></html>"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 31), match='<html><head>分组的使用</head></html>'>
s = "<html><head>分组的使用</body></h1>"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 29), match='<html><head>分组的使用</body></h1>'>

# \num
pattern = r'<(.+)><(.+)>.+</\2></\1>'
s = "<html><head>分组的使用</head></html>"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 31), match='<html><head>分组的使用</head></html>'>
s = "<html><head>分组的使用</body></h1>"
# print(re.match(pattern, s)) # None

# (?P<别名>)
# useage (?P=别名)
pattern = r'<(?P<k1>.+)><(?P<k2>.+)>.+</(?P=k2)></(?P=k1)>'
s = "<html><head>分组的使用</head></html>"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 31), match='<html><head>分组的使用</head></html>'>
s = "<html><head>分组的使用</body></h1>"
# print(re.match(pattern, s)) # None
  • 使用分组的注意事项:
    • 只有圆括号括起来的部分才算一组,如果模式字符串中既有圆括号括起来的部分,也有没有被圆括号括起来的部分,那么只会将圆括号括起来的部分算作一组,其他部分忽略。
    • group()方法获取指定分组时,组是从1开始,也就是说,group(1)获取第一组的值,group(2)获取的是第二组的值。
    • groups()方法获取所有分组的值,以元组的形式返回。除了使用group(1)获取第1组的值外,还可以用groups()[0]获取第一组的值,以此类推。
  • 针对于sub(pattern, repl, string)函数的使用过程中,如果要在repl中调用pattern中的分组,对于命名别名的方式与上述表格一致,但是在调用时则需要使用r'\g<k1>'这种引用规则了,亦或者直接使用r'\1'这样的\num形式。

re模块中其他常用的函数

sub和subn搜索与替换
  • subsubn函数用于实现搜索和替换功能。这两个函数的功能几乎完全相同,都是将某个字符串中所有匹配正则表达式的部分替换成其他字符串。用来替换的部分可能是一个字符串,也可以是一个函数,该函数返回一个用来替换的字符串。sub函数返回替换后的结果,subn函数返回一个元组,元组的第1个元素是替换后的结果,第2个元素是替换的总数。语法如下:
    re.sub(pattern, repl, string, count=0, flags=0)
参数 描述
pattern 匹配的正则表达式
repl 替换的字符串,也可为一个函数
string 要查找替换的字符串
count 模式匹配后替换的最大次数,默认0表示替换所有匹配
python 复制代码
import re

# sub & subn
phone = "2004-959-559 # 这是一个国外电话号码"
# 删除其中的注释部分
pattern = r'\s#.+'
result = re.sub(pattern, "", phone)
# print(result) # 2004-959-559
result = re.subn(pattern, "", phone)
# print(result) # ('2004-959-559', 1)
compile函数
  • compile函数用于编译正则表达式,生成一个正则表达式(Pattern)对象,供match()search()这两个函数使用。语法结构如下:
    re.compile(pattern[, flags])
参数 描述
pattern 一个字符串形式的正则表达式
flags 可选,表示匹配模式,比如忽略大小写,多行模式等
python 复制代码
import re

s = 'one1 two2 three3'
pattern = r'\w+'
regex = re.compile(pattern)
# print(regex.match(s)) # <re.Match object; span=(0, 4), match='one1'>
# print(re.match(pattern, s)) # <re.Match object; span=(0, 4), match='one1'>
findall函数
  • 在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果没有找到匹配的,则返回空列表。语法如下:
    re.findall(pattern, string, flags=0)
参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
python 复制代码
import re

pattern = r'\w+' # 按照空格/制表符分割字符串
s = "one1 two2 three3 _-=+ @#"
result = re.findall(pattern, s)
# print(result) # ['one1', 'two2', 'three3', '_']
  • 注意:match()search()都只是匹配一次,而findall()是匹配所有。
finditer函数
  • findall()类似,在字符串中找到正则表达式所匹配的所有子串,并将其作为一个迭代器(内部match()返回结果类型)返回。
python 复制代码
import re

pattern = r'\w+' # 按照空格/制表符分割字符串
s = "one1 two2 three3 _-=+ @#"
result = re.findall(pattern, s)
# print(result) # ['one1', 'two2', 'three3', '_']

pattern = r'\w+' # A-Za-z0-9_
s = "one1 two2 three3"
result = re.finditer(pattern, s)
print(result) # # <callable_iterator object at 0x75c1d6f38b50>
for i in result:
    print(i) # 需要使用group()进行提取
"""
<re.Match object; span=(0, 4), match='one1'>
<re.Match object; span=(5, 9), match='two2'>
<re.Match object; span=(10, 16), match='three3'>
"""
split函数
  • split()函数用于根据正则表达式分隔字符串,也就是说,将字符串与模式匹配的子字符串都作为分隔符来分割这个字符串。split()函数返回一个list对象,每个元素都是被分隔之后的子字符串。语法如下:
    re.split(pattern, string[, maxsplit=0, flags=0])
参数 描述
pattern 匹配的正则表达式
string 要进行分隔的字符串
maxsplit 分隔次数,maxsplit=1,分隔一次,默认为0,表示全部满足要求的子字符串都会作为分隔符
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
python 复制代码
pattern = r'\d'
s = "one 1 two 2 three 3 "
result = re.split(pattern, s)
print(result) # ['one ', ' two ', ' three ', ' ']
result = re.split(pattern, s, maxsplit=2)
# print(result) # ['one ', ' two ', ' three 3 ']

贪婪模式与非贪婪模式

  • 贪婪模式是指Python里数量词默认是尝试匹配尽可能多的字符。非贪婪模式与贪婪模式相反,总是尝试匹配尽可能少的字符,可以用*?+{m,n}后面加上?,使得贪婪变成非贪婪模式。
python 复制代码
import re

s = "This is my telephonenumber:132-3126-6688"
pattern = r'(.+)(\d+-\d+-\d+)'
result = re.match(pattern, s)
# print(result.group(1)) # This is my telephonenumber:13
# (.+)会尽可能多的去读取A-Za-z0-9_,直到遇到不匹配的字符
# print(result.group(2)) # 2-3126-6688
pattern = r'(.+?)(\d+-\d+-\d+)'
result = re.match(pattern, s)
# print(result.groups()[0]) # This is my telephonenumber:
# (.+?)会尽可能少的去匹配字符串,在满足匹配模式的前提下尽可能少的去匹配
# print(result.groups()[1]) # 132-3126-6688
python 复制代码
pattern = r'abc\d+'
s = "abc123"
# print(re.match(pattern, s)) # <re.Match object; span=(0, 6), match='abc123'>
pattern = r'abc\d+?'
# print(re.match(pattern, s)) # <re.Match object; span=(0, 4), match='abc1'>

Task

2025年能源网络安全大赛 -- 数据脱敏
text 复制代码
为了抵抗黑客攻击导致数据拖库等问题,需要将敏感数据识别并脱敏存储成⼀个表。给定脱敏算法逻辑,要求选⼿⽣成脱敏后的数据表数据(所有数据均为随机⽣成,与现实世界⽆任何联系)。为了防⽌⼀些隐私数据泄漏,现需要对该数据表进⾏脱敏操作,请按照指定要求对各字段进⾏脱敏处理,并按照先行后列拼接字符串(不包含标题行),对此字符串进行md5计算,得到答案。
脱敏要求:
编号:⽆需脱敏。
姓名:⼆字姓名对最后⼀位字使⽤ * 进⾏替换,三字姓名对中间⼀位字使⽤ * 进⾏替换,四字姓名对中间两位字使⽤ * 进⾏替换。
手机号:请对中间五位信息使⽤ * 进⾏替换。
身份证号码:请对除了前6位信息使⽤ * 进⾏替换。
银⾏卡:请对前四位和后十位信息使⽤ * 进⾏替换。
Email:请对字符 @ 前除 . 外的字符使⽤ * 进⾏替换。
性别:替换成未知。
微信号:请对为字符的信息使用 * 进行替换。
  • 这道题目解出之后发现与网上的WP中的flag对不上,看了一下处理方式中,只有银行卡号的处理方式不同,因此上网查了一下,一般的银行卡脱敏方式都是[银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234>,因此这里还是相信一下WP中的题目描述银⾏卡:请对除前四位和后十位的信息使⽤ * 进⾏替换。
python 复制代码
import pandas as pd
import re
import hashlib
from tqdm import tqdm

# # 所有的数据
df = pd.read_excel('data.xlsx', engine='openpyxl', dtype=str)
# print(df)

# 读取指定名称的Sheet
# df = pd.read_excel('data.xlsx', sheet_name='Sheet2', engine='openpyxl')
# print(df)

# 打印前5行数据
# print(df.head())
# 编号 姓名 手机号 身份证号 银行卡号 Email 性别 微信号

# 获取特定列的数据
Number = df['编号'].tolist()
Name = df['姓名'].tolist()
Phone = df['手机号'].tolist()
Idnumber = df['身份证号'].tolist()
Banknumber = df['银行卡号'].tolist()
Email = df['Email'].tolist()
Gender = df['性别'].tolist()
Wechat = df['微信号'].tolist()

def solve_name(name:str):
    if len(name) == 2:
        return name[0] + '*'
    elif len(name) == 3:
        return name[0] + '*' + name[-1]
    else:
        return name[0] + '**' + name[-1]

def solve_phonenumber(phone:str):
    assert len(phone) == 11
    pattern = r'(?P<k1>\d{3})\d{5}(?P<k2>\d{3})'
    repl = r'\g<k1>*****\g<k2>'
    
    return re.sub(pattern, repl, phone)

def solve_id(id:str):
    assert len(id) == 18
    repl = r'\1************'
    pattern = r'(\d{6}).+'
    return re.sub(pattern, repl, id)

def solve_bank(bankid:str):
    # pattern = r'\d{4}(\d+)\d{10}'
    # repl = r'****\1**********'
    # return re.sub(pattern, repl, bankid)
    return bankid[:4] + '*' * (len(bankid) - 14) + bankid[-10:]

def solve_email(email:str):
    local, domain = email.split('@', 1)
    local = re.sub(r'[^.]', '*', local) # 此处的 ^ 表示否定,即对于不是 . 的字符都替换为 *
    return f"{local}@{domain}"

def solve_gender():
    return "未知"

def solve_wechat(wechatid:str):
    return '*' * len(wechatid)

# 姓名 手机号 身份证号 银行卡号 Email 性别 微信号
if __name__ == "__main__":
    ans = []
    length = len(Number)
    for i in tqdm(range(length)):
        temp = solve_name(Name[i]) + solve_phonenumber(Phone[i]) + solve_id(Idnumber[i]) + solve_bank(Banknumber[i]) + solve_email(Email[i]) + solve_gender() + solve_wechat(Wechat[i])
        ans.append(temp)
    flag = 'flag{' + hashlib.md5("".join(ans).encode()).hexdigest() + '}'
    print(flag)
# flag{5747b7c25c4569141fd83b45c042d365}
  • 这里的脱敏方式尽量采取了正则表达式,主要是为了巩固一下正则表达式的学习效果,因此看起来并不是很美观,也可能看起来会显得有些繁琐,甚至你会感觉还不如直接对字符串进行分隔,替换操作,但是解题并非现在写这道题目的目的,不然AI模型对于此类题目的解题是绝大概率能完成的。
2025年能源网络安全大赛 -- 结构化数据识别
text 复制代码
请参赛选手对给定的结构化文件《data.xlsx》进行处理,文件中包括手机号、身份证号码、银行卡号、邮箱等四类敏感数据以及干扰的脏数据(数据不满足特征)。具体数据检测规则可参考下文。参赛选手需要对数据进行清洗,编写规则,识别其中正确的敏感数据,并输出识别结果。仅当一行中所有的数据命中数据类型的规则时,才认为该行命中某一数据类型。 请参赛选手编写程序对数据进行识别,计算全部都命中的行数,计算答案的 MD5 值(MD5 值英文字符全小写,长度 32 个字符;计算答案的 MD5 值时,用 UTF-8 字符编码),并将答案的 MD5 值提交至平台。

1. 手机号是指11 位纯数字的民用手机号, 具体规则为:a) 1-3 位------ 138------运营商网络识别号,运营商包括中国移动、中国联通、中国电信、中国广电b) 4-7 位------8888------地区编码或其他(无规律)c) 8-11 位------8888------用户编码
2. 身份证号码是指18 位二代身份证号码, 具体规则为:a) 1-6 位------ 140521------为行政区代码(不定年份有修订)b) 7-14 位------ 19701231------为出生年月日,出生年月日不早于 1930 年1 月 1 日, 不晚于当前日期c) 15-17 位------543------顺序码,任意 3 位数字,第 17 位奇数为男性,偶数位女性d) 18 位------2------校验位,取值范围为 0~9 或 X。身份证号校验算法参照国标GB11643-1999《公民身份号码》。
3. 银行卡号是指19 位纯数字的银行卡号, 具体规则为:银行卡号规则是银行卡号由发卡银行标识、账户标识、校验位等部分组成。发卡银行标识为开头的6 位数字,不同的银行有不同的BIN。银行卡号的中间部分用于标识持卡人的账户信息,包括账户类型、账户分行等,通常包含12 位数字。银行卡号的最后一位通常是校验位,用于检查卡号是否合法,计算方法按照一定的算法生成。银行卡号校验位采用Luhn 算法。
4. 邮箱由用户名@域名组成,不限制长度,例如:[123456789@163.com](mailto:123456789@163.com) 具体规则为:用户名@域名,用户名可由字母、数字、下划线构成,不以下划线开始;域名:至少包含一个".",以最后一个"."分割为两个部分,前半部分由字母、数字或"."构成,"."不能在最前面;后半部分由字母或数字构成。

正确答案示例:2323行,md5后答案提交为:flag{149815eb972b3c370dee3b89d645ae14}
  • 这里有两个问题,第一个是我的正确的谷歌邮箱都没办法通过上述描述的邮箱校验准则,但是对于题目给出的数据而言,不论用户名部分是否出现了.最终邮箱校验的数目并不会改变,因此这里还是按照题目的说法进行脚本编写。
  • 另外一个就是,经过不断地审查纠错发现在赛事方给出的数据中,其中身份证号是存在可以校验成功的身份证号的,但是银行卡号均没有通过校验,但是我新建的数据表格中,如果均采用我真实的正确数据的话,是可以完成校验的,因此这里认为脚本的正则匹配没有问题。最终的结果ans = 0
python 复制代码
import re
import pandas as pd
import gb2260 # pip install gb2260
import hashlib
from tqdm import tqdm
from datetime import datetime

df = pd.read_excel('data.xlsx', engine='openpyxl', dtype=str, sheet_name='Sheet1')
# print(df.head())
IDnumber = df['col1'].tolist()
Phone = df['col2'].tolist()
Email = df['col3'].tolist()
Bank = df['col4'].tolist()

# id
# 地址码
def check_idhead(idhead:int):
    try:
        ans = gb2260.get(idhead)
        return True # 若能查询到则身份证号前6位有效
    except:
        return False
# print(check_idhead(110101))
# 出生日期限定
def check_birth(birth:str):
    # 1930年1月1日 ~ 2025年4月17日
    try:
        birth = datetime.strptime(birth, '%Y%m%d')
        start = datetime(1930, 1, 1)
        end = datetime(2025, 4, 17)
        return start <= birth <= end
    except:
        return False
    
# 检验位
W = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2, 1]
def check_last(idnumber:str):
    a = list(idnumber)
    if a[-1].upper() == 'X':
        ans = 10
    else:
        ans = int(a[-1])
    for i in range(17):
        ans = (ans + int(a[i]) * W[i]) % 11
    return ans == 1

def check_id(idnumber:str):
    if re.match(r'\d{17}[\dxX]$', idnumber):
        if check_idhead(int(idnumber[:6])):
            if check_birth(idnumber[6:14]):
                if check_last(idnumber):
                    return True
    return False

# phonenumber
phonehead = [
    '134', '135', '136', '137', '138', '139', '147', '148', '150', '151', '152', '157', '158', '159', '172', '178', '182', '183', '184', '187', '188', '198',
    '130', '131', '132', '145', '146', '155', '156', '166', '171', '175', '176', '185', '186', '196',
    '133', '149', '153', '173', '174', '177', '180', '181', '189', '190', '191', '193', '199',
    '192'
    ]

def check_phone(phone:str):
    if phone[:3] in phonehead:
        if re.fullmatch(r'\d{11}', phone):
            return True
    return False

# bank
# 正则表达式提取网页内容即可
bankhead = [
    '955880', '955881', '955882', '955888', '621225', '621226', '621227', # 工商银行
    '95599', '622848', '622849', '623052', # 农业银行
    '601382', '621661', '621660', '456351', # 中国银行
    '621700', '621081', '621466', '436742', # 建设银行
    '601428', '622260', '521899', # 交通银行
    '621483', '621485', '621486', '410062', # 招商银行
    '621098', '622150', '622151', '955100', # 邮政储蓄
    '621768', '621771', '442730', # 中信银行
    '621489', '621492', '303781', # 光大银行
    '621691', '621692', '472067', # 民生银行
    '621792', '621793', '622177', # 浦发银行
    '623058', '627066', '623058', # 平安银行
    '622909', '622908', '486861', # 兴业银行
    '621462', '621468', '406365' # 广发银行
]

def checkLuhn(purportedCC=''):
    sum_ = 0
    parity = len(purportedCC) % 2
    for i, digit in enumerate([int(x) for x in purportedCC]):
        if i % 2 == parity:
            digit *= 2
            if digit > 9:
                digit -= 9
        sum_ += digit
    return sum_ % 10 == 0

def check_bank(banknumber:str):
    if re.match(r'\d{19}$', banknumber):
        if banknumber[:6] in bankhead or banknumber[:5] in bankhead:
            return checkLuhn(banknumber)
    return False

# Email
def check_email(email:str):
    try:
        local, domain = email.split('@', 1)
        if re.fullmatch(r'[A-Za-z0-9]\w*', local): # 这种情况下 正常的谷歌邮箱无法通过校验,但是符合土木要求
            try:
                frond, back = domain.rsplit('.', 1)
                if re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9.]*', frond):
                    if re.fullmatch(r'[A-Za-z0-9]+', back):
                        return True
            except:
                return False
        else:
            return False
    except:
        return False

if __name__ == "__main__":
    length = min([len(Bank), len(IDnumber), len(Email), len(Phone)])
    # bankcount = 0
    # emailcount = 0
    # idcount = 0
    # phonecount = 0
    ans = 0
    for i in tqdm(range(length)):
        if check_bank(Bank[i]) and check_email(Email[i]) and check_id(IDnumber[i]) and check_phone(Phone[i]):
            ans  += 1
    print(ans) # 0
    print('flag{' + hashlib.md5(str(ans).encode()).hexdigest() + '}')
    # flag{cfcd208495d565ef66e7dff9f98764da}
    # print(bankcount, emailcount, idcount, phonecount) # (0, 8900, 29, 6317)
相关推荐
l1t4 小时前
对在aarch64 Linux环境编译安装的CinderX补充测试
linux·运维·服务器·python·jit
getapi4 小时前
Windows 11 安装 uv包括:更新、常用命令、Python 管理、环境切换等,(Astral 的 Python 包/环境工具)完整指南
windows·python·uv
智算菩萨4 小时前
【Pygame】第1章 Pygame入门与环境搭建
python·ai编程·pygame
Dxy12393102164 小时前
Python 使用 `raise` 报错抛出异常显示 Unicode 码如何解决
开发语言·python
源码之家4 小时前
计算机毕业设计:Python 共享单车数据分析可视化系统 Flask框架 可视化 大数据 机器学习 深度学习 数据挖掘(建议收藏)✅
大数据·python·数据挖掘·数据分析·汽车·课程设计·美食
SiYuanFeng4 小时前
uv初步介绍及简单的使用方法例子
开发语言·python·uv
zero15974 小时前
Python 8天极速入门笔记(大模型工程师专用):第八篇-Python 综合实战|完整大模型调用脚本,8 天成果落地
人工智能·python·ai编程·大模型开发
孤魂2334 小时前
机器学习基本概念
python·机器学习
赵钰老师4 小时前
ArcGIS在洪水灾害普查、风险评估及淹没制图中的实践技术应用(洪水风险区划、防治区划、淹没制图、洪水灾害数据管理)
arcgis·数据分析