
本文介绍使用闭包结合 set 实现跨行、单元格内双重去重的技巧,确保每个邮箱仅保留首次出现的实例,适用于清洗含空格分隔邮箱的文本列。
本文介绍使用闭包结合 `set` 实现跨行、单元格内双重去重的技巧,确保每个邮箱仅保留首次出现的实例,适用于清洗含空格分隔邮箱的文本列。
在数据清洗中,常遇到类似场景:某列(如 center)存储以空格分隔的邮箱字符串,且需同时满足两个去重要求——单个单元格内去重(避免重复邮箱)与跨多行全局去重(后续行中若出现此前已出现的邮箱,则彻底剔除)。Pandas 默认的 drop_duplicates() 仅支持整行或整列级去重,无法处理这种“按字符串拆分→逐词全局追踪→重构”的细粒度逻辑。此时,闭包(closure)是优雅而高效的解决方案。
核心思路是:构造一个带状态的函数工厂 make_email_filter(),其内部维护一个 set 记录所有已见过的邮箱;返回的嵌套函数 _work() 在每次调用时,对输入字符串按空格切分,跳过已在 set 中存在的邮箱,并将新邮箱加入 set,最后拼接为去重后的字符串。
以下是完整可运行示例:
import pandas as pd
# 构造示例数据
df = pd.DataFrame({
"left": ["One", "Two", "Three"],
"center": [
"email1emailcom email2emailcom email3emailcom",
"email1emailcom email15emailcom email12emailcom",
"email15emailcom email16emailcom"
]
})
# 定义带状态的邮箱过滤器(闭包)
def make_email_filter():
seen = set() # 全局唯一邮箱集合,生命周期贯穿整个 apply 过程
def _work(emails_str):
result = []
for email in emails_str.split():
if email not in seen:
seen.add(email)
result.append(email)
return " ".join(result)
return _work
# 应用到 center 列
df["center"] = df["center"].apply(make_email_filter())
print(df)输出结果为:
left center 0 One email1emailcom email2emailcom email3emailcom 1 Two email15emailcom email12emailcom 2 Three email16emailcom
✅ 关键特性说明:
-
seen集合在make_email_filter()调用时初始化一次,在后续所有_work()调用中持续累积,实现真正的跨行全局记忆; - 每次
_work()处理一个单元格,先split()再逐个判断,天然支持单元格内去重; -
apply()的执行顺序保证了行为确定性(按 DataFrame 行索引顺序),因此结果可复现。
⚠️ 注意事项:
- 此方法依赖
apply()的顺序执行,不可与axis=1或并行化操作混用; - 若原始数据含异常字符(如 HTML 标签、多余空格、换行符),建议前置清洗:
str.strip().str.replace(r'\s+', ' ', regex=True); - 如需扩展功能(如屏蔽特定域名),可在
_work()中增加条件判断,例如if email.endswith("@bad.com"): continue; - 该方案不修改原 DataFrame 索引或结构,仅更新目标列,安全可控。
综上,利用闭包封装状态是处理“流式去重”类问题的经典模式。它简洁、高效、无副作用,特别适合在 Pandas 中实现复杂但有状态的列变换逻辑。

















