用并查集归并日志中的多标识账号

举报
yd_246123679 发表于 2026/10/29 14:03:18 2026/10/29
【摘要】 日志中同一用户常出现邮箱、手机号和设备 ID 等多个标识。本文用并查集按记录执行 union,路径压缩后按根节点分组输出账号合并结果,并处理单标识、无标识记录与标识归一化。

问题场景

日志里同一用户可能留下多个标识:注册邮箱、手机号、设备 ID。单看一条记录,往往只有其中两个标识;但多条记录通过共同标识串联后,可能属于同一账号。例如一条记录同时出现邮箱和手机号,另一条记录同时出现手机号和设备 ID,三者应合并。若逐条比较,容易漏掉传递关系。

本文把每个标识视为图节点,把一条记录中出现的两个标识视为一条边,用并查集求连通分量。每条记录执行一次 union;若一条记录包含两个以上标识,则用第一个标识与其余标识依次 union,效果等同于把记录内标识连成一组。处理完所有记录后,做路径压缩,再按根节点分组输出。

最小可运行实现

下面的脚本包含标识归一化、并查集、分组输出和示例数据。示例数据中的空字符串表示该字段缺失。

from collections import defaultdict
import json


class UnionFind:
    def __init__(self):
        self.parent = {}
        self.rank = {}

    def add(self, x):
        if x not in self.parent:
            self.parent[x] = x
            self.rank[x] = 0

    def find(self, x):
        root = x
        while self.parent[root] != root:
            root = self.parent[root]
        while self.parent[x] != x:
            p = self.parent[x]
            self.parent[x] = root
            x = p
        return root

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.rank[ra] < self.rank[rb]:
            ra, rb = rb, ra
        self.parent[rb] = ra
        if self.rank[ra] == self.rank[rb]:
            self.rank[ra] += 1
        return True


def normalize(record):
    email = (record.get('email') or '').strip().lower()
    phone = (record.get('phone') or '').strip().replace(' ', '').replace('-', '')
    device = (record.get('device_id') or '').strip()
    result = []
    if email:
        result.append('email:' + email)
    if phone:
        result.append('phone:' + phone)
    if device:
        result.append('device:' + device)
    return result


def merge_accounts(records):
    uf = UnionFind()
    parsed = []
    no_identifier = []

    for rec in records:
        rid = rec.get('record_id')
        ids = normalize(rec)
        for item in ids:
            uf.add(item)
        if len(ids) >= 2:
            base = ids[0]
            for other in ids[1:]:
                uf.union(base, other)
        if not ids:
            no_identifier.append(rid)
        parsed.append((rid, ids))

    groups = defaultdict(list)
    for item in uf.parent:
        groups[uf.find(item)].append(item)

    record_map = defaultdict(list)
    for rid, ids in parsed:
        if ids:
            record_map[uf.find(ids[0])].append(rid)

    accounts = []
    for root, members in groups.items():
        accounts.append({
            'root': root,
            'identifiers': sorted(members),
            'record_ids': record_map.get(root, [])
        })
    accounts.sort(key=lambda x: x['root'])
    return accounts, no_identifier


if __name__ == '__main__':
    records = [
        {'record_id': 'r1', 'email': 'A@Example.com', 'phone': '138-0000-0001', 'device_id': ''},
        {'record_id': 'r2', 'email': '', 'phone': '13800000001', 'device_id': 'dev-1'},
        {'record_id': 'r3', 'email': '', 'phone': '', 'device_id': 'dev-2'},
        {'record_id': 'r4', 'email': 'b@example.com', 'phone': '', 'device_id': ''},
        {'record_id': 'r5', 'email': '', 'phone': '', 'device_id': ''},
    ]
    accounts, no_identifier = merge_accounts(records)
    print(json.dumps({'accounts': accounts, 'no_identifier': no_identifier}, ensure_ascii=False, indent=2))

运行方法

将代码保存为 union_log_accounts.py,在 Python 3 环境执行:

python3 union_log_accounts.py

示例中 r1 的邮箱统一为小写,手机号去掉分隔符后与 r2 的手机号相同,因此 r1、r2 合并;r3 只有设备 ID,r4 只有邮箱,二者分别形成独立账号;r5 没有任何标识,进入 no_identifier。实际使用时,把 records 替换为日志解析后的字典列表即可,字段名按业务调整。

边界与限制

  • 归一化规则要先确认。邮箱通常转小写、去首尾空白;手机号可去空格和短横线;设备 ID 去空白。不要随意删除邮箱本地部分的点号,也不要把不同国家的手机号规则混用,否则会把不同用户误合并。
  • 标识要加类型前缀,如 email:、phone:、device:,避免不同字段恰好同值造成碰撞。
  • 无标识记录不进入并查集,单独返回,便于补采或人工处理;单标识记录会形成只含一个标识的账号,后续批次出现新边时才能合并。本文是单批静态处理,跨批次合并需要持久化并查集结构或重新计算。
  • 并查集只保证等价关系的传递闭包,不能判断共享设备、公共邮箱、手机号换绑等业务语义,也不能自动识别脏数据。若同一设备被多人使用,合并结果可能偏大,需要业务规则或人工复核。
  • 根节点取决于 union 顺序,不保证稳定。对外输出账号 ID 时,建议使用组内最小标识、业务主键或另行生成的稳定 ID,不要直接把根节点当账号 ID。
  • 代码用迭代方式实现 find 并配合按秩合并,避免递归过深;标识数量很大时,内存占用与标识总数同阶,需关注日志规模。

小结

并查集适合处理「多条记录把多个标识连起来」的账号归并问题。实现重点不在 union 本身,而在标识归一化、单标识与无标识记录的去向,以及根节点不稳定带来的输出约定。先把这些边界定义清楚,再接入日志解析流程,结果更容易核对。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。