2. 解析 SPDX 许可证标识符并标准化

解析 SPDX 许可证标识符并标准化 学习卡片

SPDX 许可证标识符的构成与标准化目标

源码文件头或包元数据中常见的声明形式是 SPDX-License-Identifier: MIT、SPDX-License-Identifier: Apache-2.0 OR MIT、SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0。标准化要做三件事:把大小写、别名、旧式写法映射到 SPDX 许可证列表中的规范 ID;保留并规范化表达式中的 AND、OR、WITH 与括号结构;对无法匹配的标识符或非法表达式打上 UNKNOWN 标记并保留原始文本,便于人工复核。

SPDX-License-Identifier 解析与标准化流程 原始声明(示例) SPDX-License-Identifier: MIT Apache-2.0 OR MIT GPL-2.0-only WITH Classpath-exception-2.0 提取字段值 正则匹配冒号后内容 去除首尾空白 保留原始字符串 分词与规范化 按空格拆分 token 识别 AND / OR / WITH 括号配对检查 大小写统一为规范形式 SPDX 列表匹配 精确匹配规范 ID 别名映射表 旧式写法转换 异常项标记 UNKNOWN 标准化结果对照(示例) 原始声明 标准化结果 状态 MIT MIT ✓ 已识别 Apache-2.0 OR MIT Apache-2.0 OR MIT ✓ 已识别 GPL-2.0-only WITH Classpath-exception-2.0 GPL-2.0-only WITH Classpath-exception-2.0 ✓ 已识别(含例外) MyCustomLicense-1.0 UNKNOWN (MyCustomLicense-1.0) ✗ 无法识别 MIT OR (Apache-2.0 AND UNKNOWN (括号不匹配) ✗ 表达式非法 注:示例数据仅用于演示标准化逻辑,实际结果以 SPDX 许可证列表版本为准

标准化映射规则与实现要点

SPDX 许可证列表中的规范标识符区分大小写,但实际声明中常出现大小写混用、别名和旧式写法。标准化时按以下优先级处理:

  • 精确匹配:与 SPDX 列表中的 licenseId 完全一致时直接采用。
  • 别名映射:如 GPL-2.0 映射为 GPL-2.0-only,Apache 2.0 映射为 Apache-2.0,BSD 映射为 BSD-3-Clause(需结合上下文确认)。
  • 大小写归一:将 token 转为规范形式后再查表,例如 mit → MIT,apache-2.0 → Apache-2.0。
  • 表达式结构校验:检查 AND/OR/WITH 两侧是否有合法操作数,括号是否配对,WITH 右侧是否为已知例外标识符。
  • 无法识别处理:保留原始文本,输出 UNKNOWN 标记,并记录原因(未知 ID、括号不匹配、非法运算符组合等)。

可复用的标准化函数(Python 示例)

以下代码实现从文本中提取 SPDX-License-Identifier 字段并完成标准化,依赖一份简化的 SPDX 许可证 ID 集合与别名映射表。运行前需准备 spdx_ids.txt(每行一个规范 ID)和 aliases.json(别名到规范 ID 的映射)。

import re
import json

# 加载 SPDX 规范 ID 集合与别名映射
with open("spdx_ids.txt", encoding="utf-8") as f:
    SPDX_IDS = {line.strip() for line in f if line.strip()}

with open("aliases.json", encoding="utf-8") as f:
    ALIASES = json.load(f)  # 例如 {"GPL-2.0": "GPL-2.0-only", "Apache 2.0": "Apache-2.0"}

# 运算符与例外标识符(简化示例)
OPERATORS = {"AND", "OR", "WITH"}
KNOWN_EXCEPTIONS = {"Classpath-exception-2.0", "LLVM-exception"}

def extract_spdx_field(text: str):
    """从文本中提取 SPDX-License-Identifier 字段值,返回列表(可能多行声明)。"""
    pattern = re.compile(r"SPDX-License-Identifier:\s*(.+)", re.IGNORECASE)
    return [m.group(1).strip() for m in pattern.finditer(text)]

def normalize_token(token: str):
    """将单个 token 标准化为规范 ID 或返回 None。"""
    # 精确匹配
    if token in SPDX_IDS:
        return token
    # 别名映射
    if token in ALIASES:
        return ALIASES[token]
    # 大小写归一后匹配
    for sid in SPDX_IDS:
        if sid.lower() == token.lower():
            return sid
    return None

def normalize_expression(expr: str):
    """标准化 SPDX 表达式,返回 (标准化结果, 状态, 原因)。"""
    # 括号配对检查
    if expr.count("(") != expr.count(")"):
        return f"UNKNOWN ({expr})", "非法", "括号不匹配"

    # 按空格分词,保留括号
    tokens = re.findall(r"\(|\)|[^\s()]+", expr)
    result_tokens = []
    for tok in tokens:
        if tok in ("(", ")"):
            result_tokens.append(tok)
        elif tok.upper() in OPERATORS:
            result_tokens.append(tok.upper())
        else:
            normalized = normalize_token(tok)
            if normalized is None:
                return f"UNKNOWN ({expr})", "无法识别", f"未知标识符: {tok}"
            result_tokens.append(normalized)

    # 简单结构校验:WITH 右侧必须是已知例外
    for i, tok in enumerate(result_tokens):
        if tok == "WITH":
            if i + 1 >= len(result_tokens) or result_tokens[i + 1] not in KNOWN_EXCEPTIONS:
                return f"UNKNOWN ({expr})", "非法", "WITH 右侧例外标识符无效"

    return " ".join(result_tokens), "已识别", ""

# 示例调用
sample_text = """
// SPDX-License-Identifier: mit
// SPDX-License-Identifier: Apache-2.0 OR MIT
// SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
// SPDX-License-Identifier: MyCustomLicense-1.0
// SPDX-License-Identifier: MIT OR (Apache-2.0 AND
"""

for raw in extract_spdx_field(sample_text):
    normalized, status, reason = normalize_expression(raw)
    print(f"原始: {raw}")
    print(f"  标准化: {normalized}")
    print(f"  状态: {status}  {reason}")
验证方法:用上述示例文本运行后,预期输出中 mit 应变为 MIT,Apache-2.0 OR MIT 保持不变,GPL-2.0-only WITH Classpath-exception-2.0 保持不变,MyCustomLicense-1.0 标记为 UNKNOWN,括号不匹配的表达式标记为非法。若实际输出与预期不符,优先检查别名映射表是否覆盖了测试用例中的写法。
注意:SPDX 许可证列表会随版本更新,标准化时应固定使用某一版本的列表文件,并在输出中记录版本号,避免因列表变化导致同一份代码在不同时间得到不同结果。对于 UNKNOWN 项,不要自动猜测许可证类型,应保留原始文本并交由人工复核。