1. 识别项目中的开源许可证声明文件

识别项目中的开源许可证声明文件 学习卡片

为什么许可证声明文件是合规扫描的第一入口

开源合规审查的第一步不是读代码,而是找声明。一个仓库可能把许可证放在根目录、子目录、包元数据或纯文本文件中,文件名也不统一。若只检查根目录的 LICENSE,很容易漏掉子模块、第三方目录或 vendored 代码携带的许可证,导致后续依赖清单和 SPDX 标识符解析建立在错误前提上。

本案例的任务边界很明确:扫描指定代码仓库根目录及子目录,定位 LICENSE、COPYING、NOTICE 等许可证声明文件,并输出文件路径与初步许可证类型判断。它不解析 SPDX 标识符,也不提取 package.json 依赖树,只解决“有哪些声明文件、它们在哪里、初步像什么许可证”这三个问题。

扫描对象与识别规则

许可证声明文件通常具有以下特征:文件名包含 LICENSE、LICENCE、COPYING、NOTICE、COPYRIGHT 等关键词,扩展名可能是 .txt、.md、无扩展名,甚至带 .license 后缀。扫描时应覆盖根目录和所有子目录,但需要排除 .git、node_modules、vendor 等可能产生大量噪声的目录——除非任务明确要求检查 vendored 代码。

初步许可证类型判断不依赖复杂解析,而是基于文件内容中的典型短语进行匹配。例如:MIT 许可证通常包含 Permission is hereby granted, free of charge;Apache-2.0 包含 Licensed under the Apache License, Version 2.0;GPL-3.0 包含 GNU GENERAL PUBLIC LICENSE 和 Version 3。这种判断是启发式的,用于快速分类,不能替代后续的 SPDX 标准化。

代码仓库根目录 /repo 递归遍历目录树 排除 .git / node_modules 文件名关键词匹配 LICENSE / COPYING / NOTICE 读取文件内容 提取前 2KB 文本用于匹配 跳过 继续遍历 许可证类型初步判断 MIT / Apache-2.0 / GPL-3.0 基于典型短语启发式匹配 输出结果 文件路径 + 初步许可证类型 仓库目录示例 📄 LICENSE 📄 NOTICE 📁 src/ 📄 main.py 📁 third_party/ 📄 COPYING 📄 lib.c 📁 docs/ 📄 LICENSE.md 📁 .git/ (排除) 📁 node_modules/ (排除) 图例 命中声明文件 排除目录

用 Python 实现扫描与初步判断

下面给出一个可直接运行的脚本。它使用标准库 os.walk 遍历目录,用文件名正则匹配声明文件,再读取文件前 2048 字节进行许可证类型启发式判断。脚本不依赖第三方包,适合作为合规扫描工具的最小原型。

import os
import re

# 文件名匹配模式:不区分大小写,覆盖常见许可证声明文件名
LICENSE_FILE_PATTERN = re.compile(
    r'^(LICEN[CS]E|COPYING|NOTICE|COPYRIGHT)(\.(txt|md|rst))?$',
    re.IGNORECASE
)

# 需要排除的目录,避免噪声
EXCLUDE_DIRS = {'.git', 'node_modules', 'vendor', '__pycache__', '.venv'}

# 许可证类型启发式规则:按优先级从高到低匹配
LICENSE_RULES = [
    ('Apache-2.0', [
        r'Licensed under the Apache License, Version 2\.0',
        r'Apache License\s+Version 2\.0',
    ]),
    ('GPL-3.0', [
        r'GNU GENERAL PUBLIC LICENSE',
        r'Version 3, 29 June 2007',
    ]),
    ('GPL-2.0', [
        r'GNU GENERAL PUBLIC LICENSE',
        r'Version 2, June 1991',
    ]),
    ('MIT', [
        r'Permission is hereby granted, free of charge',
        r'THE SOFTWARE IS PROVIDED "AS IS"',
    ]),
    ('BSD-3-Clause', [
        r'Redistribution and use in source and binary forms',
        r'Neither the name of',
    ]),
    ('ISC', [
        r'Permission to use, copy, modify, and/or distribute',
    ]),
    ('MPL-2.0', [
        r'Mozilla Public License Version 2\.0',
    ]),
]

def detect_license_type(text: str) -> str:
    """基于文件内容前若干字节进行许可证类型初步判断。"""
    for license_name, patterns in LICENSE_RULES:
        for pattern in patterns:
            if re.search(pattern, text, re.IGNORECASE):
                return license_name
    return 'UNKNOWN'

def scan_license_files(repo_root: str):
    """扫描仓库根目录及子目录,返回声明文件路径与初步类型。"""
    results = []
    for dirpath, dirnames, filenames in os.walk(repo_root):
        # 原地修改 dirnames,跳过排除目录
        dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]

        for filename in filenames:
            if LICENSE_FILE_PATTERN.match(filename):
                filepath = os.path.join(dirpath, filename)
                try:
                    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                        head = f.read(2048)
                except OSError:
                    head = ''
                license_type = detect_license_type(head)
                results.append((filepath, license_type))
    return results

if __name__ == '__main__':
    import sys
    repo = sys.argv[1] if len(sys.argv) > 1 else '.'
    for path, lic in scan_license_files(repo):
        print(f'{path}\t{lic}')

运行方式与输出解读

将上述代码保存为 scan_license.py,在终端执行:

python scan_license.py /path/to/repo

输出为制表符分隔的两列:文件路径和初步许可证类型。例如:

/repo/LICENSE	MIT
/repo/third_party/COPYING	GPL-3.0
/repo/docs/LICENSE.md	Apache-2.0
/repo/NOTICE	UNKNOWN

其中 UNKNOWN 表示文件内容未命中任何启发式规则,需要人工复核。注意:NOTICE 文件通常不是许可证正文,而是版权声明或第三方组件说明,因此返回 UNKNOWN 是合理现象,不应直接当作许可证缺失处理。

判断依据:初步类型判断只用于分类和排序,不能作为最终合规结论。后续需要结合 SPDX 标识符解析、依赖树提取和人工复核,才能形成完整的许可证清单。

常见误判与规避

  • 把 NOTICE 当成许可证:NOTICE 文件通常包含第三方组件版权声明,不是许可证正文。扫描时应单独标记,不要与 LICENSE 混为一类。
  • 忽略子目录中的 COPYING:很多项目在 third_party/ 或 vendor/ 下放置独立许可证。若排除这些目录,会漏掉关键声明。
  • 只匹配文件名不读内容:有些文件名为 LICENSE 但内容为空或仅为占位符。读取前 2KB 内容可以识别这种情况。
  • 编码问题导致读取失败:使用 errors='ignore' 避免因非 UTF-8 编码中断扫描,但可能丢失部分匹配信息,需在结果中标记。

验证扫描结果是否完整

完成扫描后,用以下检查项确认结果可信:

检查项通过条件不通过时的处理
根目录 LICENSE 是否被找到输出中包含根目录下的 LICENSE 或 LICENCE 文件检查文件名正则是否覆盖无扩展名和大写变体
子目录 COPYING 是否被找到third_party/ 或 vendor/ 下的 COPYING 出现在结果中确认排除目录列表未误伤目标目录
NOTICE 是否被单独标记NOTICE 文件类型为 UNKNOWN 或单独分类不要将 NOTICE 强制归入某个许可证类型
排除目录是否生效.git 和 node_modules 下无结果输出检查 EXCLUDE_DIRS 集合是否包含这些目录名
空文件或占位文件是否被识别内容为空的 LICENSE 返回 UNKNOWN人工复核该文件,确认是否为有效声明
边界说明:本案例只完成“定位声明文件 + 初步类型判断”。如果仓库中没有 LICENSE 文件,扫描结果为空,这本身就是一个合规风险信号,应记录为“未发现许可证声明”,而不是直接判定为无许可证。后续案例会处理 SPDX 标识符和依赖许可证提取。