SWE / Hiring Event · VO Round 2 Coding

Microsoft 邮件日志解析:分号仅在引号与注释外分隔

逐字符状态机处理巨大字符串中的邮件 record,忽略展示名/注释内的分号,抽取并去重邮箱;明确未给的邮箱语法边界。

题目重点:逐字符状态机处理巨大字符串中的邮件 record,忽略展示名/注释内的分号,抽取并去重邮箱;明确未给的邮箱语法边界。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版引号包裹展示名、圆括号包裹注释,二者内容不作为邮箱 token;允许注释嵌套和内部反斜杠转义。
  • 每条 record 最多一个 ASCII 常见邮箱,语法以代码正则为准;多个候选邮箱报错,空 record 跳过。
  • 返回首次出现顺序去重的邮箱列表,大小写敏感;题目只说 list+dedupe,未规定排序或域名规范化。
  • 不声称实现完整 RFC 5322、国际化邮箱、quoted local part 或任意嵌套引号规则。

解题路线

  1. 逐字符维护 quote:boolean 与 commentDepth:int;在引号/注释里,分号只是内容,遇转义时跳过被转义字符。
  2. 只在两状态均关闭时遇 ; 才提交当前 record;为避免注释里的假邮箱,外层扫描不把引号/注释字符写到候选缓冲。
  3. 对每条 record 的外层文本匹配一个训练语法邮箱;用 seen 集合去重,用 result 列表保留首见顺序。
  4. 若输入真的‘巨大’,当前实现仍会存全部 record;工程版在遇到有效分号时立即解析并清空缓冲,可把工作内存降到最长 record 加去重集合。
  5. 真实面试要先问括号是否可嵌套、引号中反斜杠规则、逗号/分号是否可出现在邮箱本身,再决定 lexer。

手动走一遍

'"A; B" <a@example.com>; (note; x) b@example.org; a@example.com' 只有引号/注释外两个分号起分隔作用,抽得 a@example.com 和 b@example.org,最后重复项不再输出。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

import re


EMAIL = re.compile(r'(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?![A-Za-z0-9.-])')


def unique_emails(log: str) -> list[str]:
    """Training grammar: one ASCII email per ; record; ignore quoted names/comments."""
    outside = []
    records = []
    quote = False
    comment_depth = 0
    i = 0
    while i < len(log):
        char = log[i]
        if (quote or comment_depth) and char == '\\' and i + 1 < len(log):
            i += 2
            continue
        if quote:
            if char == '"':
                quote = False
        elif comment_depth:
            if char == '(':
                comment_depth += 1
            elif char == ')':
                comment_depth -= 1
        elif char == '"':
            quote = True
        elif char == '(':
            comment_depth = 1
        elif char == ')':
            raise ValueError('unmatched comment close')
        elif char == ';':
            records.append(''.join(outside))
            outside.clear()
        else:
            outside.append(char)
        i += 1
    if quote or comment_depth:
        raise ValueError('unterminated quote/comment')
    records.append(''.join(outside))
    seen = set()
    result = []
    for record in records:
        matches = EMAIL.findall(record)
        if len(matches) > 1:
            raise ValueError('multiple emails in one record')
        if matches and matches[0] not in seen:
            seen.add(matches[0])
            result.append(matches[0])
    return result


if __name__ == '__main__':
    sample = '"A; B" <a@example.com>; (note; x) b@example.org; a@example.com'
    assert unique_emails(sample) == ['a@example.com', 'b@example.org']

复杂度

状态扫描 O(L),正则在本训练语法的短 record 上处理;所有 record 物化后占 O(L),去重集合占 O(U) 个邮箱,U 为不同邮箱数。若改增量提交,可把临时文本内存降为 O(最长 record)。

易错点与追问

容易失分的边界

  • 直接 split(';'),把展示名或注释中的分号误当 record 分隔。
  • 只追踪引号、不追踪括号,或者遇转义引号就提前结束。
  • 用全局 email 正则扫原串,会把注释里的假邮箱也抽出来。
  • 把简化正则宣传为完整邮件协议解析器。
  • 题目未明确排序规则,去重集合直接转 list 会产生不稳定顺序。

继续深挖

  • 若真正支持 RFC quoted local-part,分号/注释如何与地址语法协同?
  • 输入是分块流时,quote/comment 状态如何跨块保留?
  • 去重邮箱数超过内存时,如何外部去重并保留首见顺序?
✓已复制微信号!