PAYATHON 2026

如何使用 Python re.compile 和 REGEX 清理姓名的不同写法?

技术老齐

建议用多个 re.compile 模式分步处理:先删除昵称和旧名,再把姓名与逗号后的附加内容分开,保留姓名部分的首尾两个词,并用白名单识别后缀。

处理时采用两条规则:

  • 删除昵称和旧名后,把第一个词视为名字;如果还有其他词,把最后一个词视为姓氏。
  • 逗号后的完整内容只有符合后缀白名单才会保留,例如 Jr.、Sr.、III,其余内容全部丢弃。

这两条规则能处理题目中的示例。真实姓名中的复姓、多词名字等情况,还需要额外规则或结构化数据才能准确处理。

原模式为什么会把姓氏捕获为中间名?

原模式的中间名部分使用了贪婪重复:

(?P<middle>(\s[a-zA-Z.'"\-\(\)]+)*)?

它会尽可能多地匹配名字后面的词。由于后面的 last 分组是可选的,即使 middle 连姓氏也匹配进去,整个模式仍能匹配成功。正则引擎因此不必回溯,把最后一个词交给 last。

后缀部分也有类似问题:

(?P<suffix>([a-zA-Z'\-]+)*)?
.*

Duke 符合字母字符类,后面的 .* 又能匹配剩余的 of CatTown,所以 Duke 会被误判为后缀。

改成非贪婪匹配也不能解决所有问题。姓名后缀和贵族头衔都由字母组成,又出现在同一位置,需要明确的规则来区分。

分步清理的实现

后缀白名单可按数据范围调整,代码中的名单仅作示例。

import re

# 双引号中的昵称,例如 "Lady Gaga"
NICKNAME_RE = re.compile(r'"[^"]*"')

# 非嵌套括号中的旧名,要求右括号前是连字符,例如 (Baldwin-)
OLD_NAME_RE = re.compile(r'\([^()]*-\)')

# 姓名与后缀或头衔之间的分隔符
SUFFIX_SEPARATOR_RE = re.compile(r',\s+')

# 删除后缀中的标点和符号,保留字母、数字及空白
SUFFIX_PUNCT_RE = re.compile(r'[^\w\s]|_')

# 键用于匹配,值用于统一输出格式
SUFFIXES = {
    "jr": "Jr",
    "sr": "Sr",
    "ii": "II",
    "iii": "III",
    "iv": "IV",
    "v": "V",
}


def clean_name(name: str) -> str:
    # 用空格替换,避免删除内容后把相邻姓名词拼接起来
    text = NICKNAME_RE.sub(" ", name)
    text = OLD_NAME_RE.sub(" ", text)

    # 先移除带分隔符的附加内容,再提取名字和姓氏
    parts = SUFFIX_SEPARATOR_RE.split(text.strip(), maxsplit=1)
    words = parts[0].split()

    if not words:
        return ""

    result = [words[0]]

    if len(words) > 1:
        result.append(words[-1])

    if len(parts) == 2:
        key = SUFFIX_PUNCT_RE.sub("", parts[1]).strip().casefold()
        suffix = SUFFIXES.get(key)
        if suffix:
            result.append(suffix)

    return " ".join(result)

先删除昵称和旧名,再提取姓氏,可以处理这样的输入:

Shania (Irene-) Twain

删除 (Irene-) 后剩下 Shania Twain,保留首尾两个词即可。

再看带昵称和头衔的情况:

Charles "Chaz" Fluffy Wong, Duke of CatTown

删除昵称后,逗号前是 Charles Fluffy Wong,保留 Charles Wong。逗号后的完整内容规范化为 duke of cattown,不在白名单中,因此丢弃。

示例结果

调用方式:

names = [
    "Sarah Michelle Gellar",
    "Leslie Marie DeGuzman Santorini",
    "Beyonce",
    'Stefani "Lady Gaga" Germanotta',
    "Lebron James, Jr.",
    "Hailey Bieber (Baldwin-)",
    "Shania (Irene-) Twain",
    'Charles "Chaz" Fluffy Wong, Duke of CatTown',
]

for name in names:
    print(f"{name} -> {clean_name(name)}")

输出:

Sarah Michelle Gellar -> Sarah Gellar
Leslie Marie DeGuzman Santorini -> Leslie Santorini
Beyonce -> Beyonce
Stefani "Lady Gaga" Germanotta -> Stefani Germanotta
Lebron James, Jr. -> Lebron James Jr
Hailey Bieber (Baldwin-) -> Hailey Bieber
Shania (Irene-) Twain -> Shania Twain
Charles "Chaz" Fluffy Wong, Duke of CatTown -> Charles Wong

为什么使用后缀白名单?

题目中的后缀和贵族头衔使用相同的分隔形式,单靠 ,\s+ 无法区分。白名单要求去除标点并忽略大小写后,整个附加字段都能匹配一个已知后缀。

这样,Jr. 会转成 Jr,Duke of CatTown 会被删除。Jr. of CatTown 也不会仅因开头有 Jr. 就被接受。

白名单也会丢弃尚未收录的合法后缀。需要支持更多后缀时,应扩充 SUFFIXES;放宽为任意字母会再次把头衔识别成后缀。

使用时的边界

  • 保留第一个词和最后一个词,是这里采用的数据规则,无法据此判断真实姓名结构。例如,输入只有名字和中间名、没有姓氏时,最后一个词仍会被当作姓氏。
  • 多词姓氏会被截短。姓氏包含空格时,需要独立的姓氏字段或额外识别规则;用连字符连接的姓氏会作为一个词保留。
  • 昵称模式只处理成对的英文双引号,不处理弯引号、转义引号或嵌套结构。
  • 旧名模式只删除非嵌套且以 -) 结尾的括号内容,普通括号内容不会自动删除。
  • 分隔符严格按 ,\s+ 处理,即逗号后至少有一个空白字符。如果数据也允许 James,Jr.,可将分隔模式改为 r',\s*'。

后缀集合和输入格式固定时,也可以把这些规则合并进单个正则,但仍需保留后缀白名单和姓名拆分的假设。分步实现更便于调整规则和排查误删。

备注:内容仅供参考。