侧边栏壁纸
  • 累计撰写 105 篇文章
  • 累计创建 42 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

pom.xml 依赖解析工具

天明
2026-08-28 / 0 评论 / 0 点赞 / 6 阅读 / 0 字 / 正在检测是否收录...
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
parse_pom.py — Maven pom.xml 依赖解析工具

从 pom.xml 中解析出 <dependencyManagement> 与 <dependencies> 两个部分的依赖,
并按 groupId:artifactId 分别排序输出。

版本解析准确性保证:
  1. 直接书写版本号          -> 原样输出
  2. ${property} 属性引用     -> 从 <properties> 解析(含继承自 parent 的属性),
                                支持嵌套引用(${a} -> ${b} -> "1.0")与字符串拼接
  3. 内置属性                -> ${project.version} / ${project.groupId} /
                                ${project.parent.version} 等 Maven 内置属性
  4. dependencyManagement 继承 -> <dependencies> 中未显式声明 version 的依赖,
                                自动匹配当前 pom 及 parent 链 dependencyManagement
                                中 (groupId, artifactId) 对应的版本
  5. parent 继承             -> 通过 relativePath 递归加载父 pom,合并其
                                properties 与 dependencyManagement(子 pom 优先),
                                groupId/version 缺省时继承自 parent

仅使用 Python 标准库,无第三方依赖。

用法:
  python3 parse_pom.py [path]              # path 为目录时递归扫描其所有子目录
                                           # 下的 pom.xml 并分别处理;为文件时
                                           # 只处理该文件;默认 "."(当前目录)
  python3 parse_pom.py [path] -c           # 紧凑模式,输出 g:a:v:scope

目录扫描说明:
  - 递归遍历目录及其子目录,逐个解析输出每个 pom.xml(内部依赖分别排序)
  - 自动跳过构建产物/版本控制目录:target、.git、node_modules、build 等
"""

import argparse
import os
import re
import sys
import xml.etree.ElementTree as ET

DEFAULT_PATH = "."
DEFAULT_POM = "pom.xml"
PROP_PATTERN = re.compile(r"\$\{([^}]+)\}")
MAX_RESOLVE_ITER = 10
# 递归扫描目录时跳过的目录(构建产物、版本控制、依赖安装目录等)
EXCLUDE_DIRS = {"target", ".git", ".svn", ".hg", "node_modules",
                ".idea", "build", "dist", "out", "__pycache__"}


def _local_name(tag):
    """去掉 XML 命名空间前缀,返回元素本地名(兼容带/不带命名空间的 pom)。"""
    return tag.rsplit("}", 1)[-1] if "}" in tag else tag


def _children(elem, name):
    """返回指定名称的直接子元素列表,过滤注释节点。"""
    if elem is None:
        return []
    return [c for c in list(elem) if isinstance(c.tag, str) and _local_name(c.tag) == name]


def _all_children(elem):
    """返回所有直接子元素(过滤注释节点)。"""
    if elem is None:
        return []
    return [c for c in list(elem) if isinstance(c.tag, str)]


def _child(elem, name):
    for c in _children(elem, name):
        return c
    return None


def _text(elem):
    if elem is None or elem.text is None:
        return ""
    return elem.text.strip()


class Pom:
    """单个 pom.xml 的解析结果,含合并父 pom 后的属性与依赖管理。"""

    def __init__(self, path, _stack=None):
        self.path = os.path.abspath(path)
        self.root = ET.parse(self.path).getroot()
        self._stack = set(_stack) if _stack else set()

        self.group_id = _text(_child(self.root, "groupId"))
        self.artifact_id = _text(_child(self.root, "artifactId"))
        self.version = _text(_child(self.root, "version"))

        # parent 引用信息:(groupId, artifactId, version, relativePath)
        # relativePath 为 None 表示未声明(默认 ../pom.xml);"" 表示显式禁用本地解析
        self.parent_ref = None
        self.parent = None  # 解析出的父 Pom 对象,或 None

        self._parse_parent()
        self._parse_properties()
        self._parse_dependency_management()

    # ------------------------------------------------------------------ parent

    def _parse_parent(self):
        parent_el = _child(self.root, "parent")
        if parent_el is None:
            return
        rel_el = _child(parent_el, "relativePath")
        rel = None if rel_el is None else (_text(rel_el) or "")
        pg = _text(_child(parent_el, "groupId"))
        pa = _text(_child(parent_el, "artifactId"))
        pv = _text(_child(parent_el, "version"))
        self.parent_ref = (pg, pa, pv, rel)

        # Maven 支持 groupId / version 从 parent 继承
        if not self.group_id:
            self.group_id = pg
        if not self.version:
            self.version = pv

        if self.path in self._stack:
            print(f"[警告] 检测到 parent 循环引用: {self.path}", file=sys.stderr)
            return
        ppath = self._locate_parent_pom(rel, pg, pa, pv)
        if ppath:
            stack = set(self._stack)
            stack.add(self.path)
            try:
                self.parent = Pom(ppath, stack)
            except (OSError, ET.ParseError) as e:
                print(f"[警告] 无法解析父 pom {ppath}: {e}", file=sys.stderr)
                self.parent = None

    def _locate_parent_pom(self, relative_path, group_id, artifact_id, version):
        """优先按 relativePath 查找本地父 pom,其次尝试本地仓库 ~/.m2/repository。"""
        if relative_path != "":
            base = os.path.dirname(self.path)
            rel = relative_path if relative_path else "../pom.xml"
            cand = os.path.normpath(os.path.join(base, rel))
            if os.path.isfile(cand):
                return cand
        if group_id and artifact_id and version and "$" not in version:
            repo = os.path.expanduser("~/.m2/repository")
            cand = os.path.join(repo, group_id.replace(".", "/"),
                                artifact_id, version, "pom.xml")
            if os.path.isfile(cand):
                return cand
        return None

    # ------------------------------------------------------------- properties

    def _parse_properties(self):
        own = {}
        props_el = _child(self.root, "properties")
        if props_el is not None:
            for p in _all_children(props_el):
                own[_local_name(p.tag)] = _text(p)

        pg, pa, pv, rel = self.parent_ref or ("", "", "", None)
        builtin = {
            "project.groupId": self.group_id,
            "project.artifactId": self.artifact_id,
            "project.version": self.version,
            "pom.groupId": self.group_id,
            "pom.artifactId": self.artifact_id,
            "pom.version": self.version,
            "project.parent.groupId": pg,
            "project.parent.artifactId": pa,
            "project.parent.version": pv,
            "project.parent.relativePath": rel if rel else "",
        }

        # 合并顺序:父 pom 属性 -> 内置属性 -> 自身属性(后者覆盖前者)
        merged = {}
        if self.parent:
            merged.update(self.parent.properties)
        merged.update(builtin)
        merged.update(own)
        self.properties = merged

    # --------------------------------------------------- dependencyManagement

    def _parse_dependency_management(self):
        own = {}
        dm_el = _child(self.root, "dependencyManagement")
        if dm_el is not None:
            deps_el = _child(dm_el, "dependencies")
            if deps_el is not None:
                for d in _children(deps_el, "dependency"):
                    g, a, v, scope, type_, cl = _raw_dependency(d)
                    # key 用解析后的 groupId/artifactId,保证属性定义组坐标也能正确匹配
                    key = (self.resolve(g), self.resolve(a))
                    own[key] = (v, scope, type_, cl)  # version 保留 raw,统一后解析

        merged = {}
        if self.parent:
            merged.update(self.parent.dm)  # 父 pom 的依赖管理
        merged.update(own)                 # 自身覆盖父
        self.dm = merged

    # ------------------------------------------------------------ dependencies

    def get_dependencies(self):
        """解析 <dependencies> 下的依赖,返回解析后的真实版本列表。"""
        deps = []
        deps_el = _child(self.root, "dependencies")
        if deps_el is None:
            return deps
        for d in _children(deps_el, "dependency"):
            g_raw, a_raw, v_raw, scope, type_, cl = _raw_dependency(d)
            g = self.resolve(g_raw) or self.group_id  # groupId 缺省继承 parent
            a = self.resolve(a_raw)
            v, source = self._resolve_version(g, a, v_raw)
            deps.append({
                "groupId": g,
                "artifactId": a,
                "version": v,
                "scope": self.resolve(scope) or "compile",
                "type": self.resolve(type_) or "jar",
                "classifier": self.resolve(cl),
                "source": source,  # "声明" / "dependencyManagement"
            })
        return deps

    def _resolve_version(self, group_id, artifact_id, raw_version):
        """版本解析:显式声明优先,其次匹配 dependencyManagement。"""
        if raw_version and raw_version.strip():
            return self.resolve(raw_version), "声明"
        key = (group_id, artifact_id)
        if key in self.dm:
            return self.resolve(self.dm[key][0]), "dependencyManagement"
        return "未解析(未声明version且dependencyManagement中无此依赖)", "未解析"

    # -------------------------------------------------------------- resolver

    def resolve(self, value):
        """解析 ${key} 属性引用,支持嵌套与字符串拼接;未定义引用原样保留。"""
        if not value:
            return value
        cur = value
        for _ in range(MAX_RESOLVE_ITER):
            def repl(m):
                return self.properties.get(m.group(1), m.group(0))
            nxt = PROP_PATTERN.sub(repl, cur)
            if nxt == cur:
                return cur
            cur = nxt
        return cur  # 超出迭代上限(可能属性循环引用),返回当前结果


def _raw_dependency(d):
    """提取 <dependency> 原始字段(未做属性解析)。"""
    g = _text(_child(d, "groupId"))
    a = _text(_child(d, "artifactId"))
    v = _text(_child(d, "version"))
    scope = _text(_child(d, "scope"))
    type_ = _text(_child(d, "type"))
    cl = _text(_child(d, "classifier"))
    return g, a, v, scope, type_, cl


def find_poms(root):
    """递归收集目录及其子目录下所有 pom.xml(跳过构建产物等目录),按路径排序。"""
    result = []
    for dirpath, dirnames, filenames in os.walk(root):
        # 原地过滤跳过的目录,实现剪枝
        dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDE_DIRS)
        for f in filenames:
            if f.lower() == DEFAULT_POM:
                result.append(os.path.join(dirpath, f))
    return sorted(result)


def _print_table(title, rows):
    """以对齐表格输出,rows 为 (groupId, artifactId, version, scope, note) 列表。"""
    print(f"===== {title} ({len(rows)}) =====")
    if not rows:
        print("  (无)")
        return
    header = ("#", "groupId", "artifactId", "version", "scope", "备注")
    cols = [header] + [(str(i + 1),) + r for i, r in enumerate(rows)]
    widths = [max(len(row[i]) for row in cols) for i in range(len(header))]
    sep = "  ".join("-" * w for w in widths)
    for idx, row in enumerate(cols):
        line = "  ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))
        print(line)
        if idx == 0:
            print(sep)


def process_one(pom_path, compact):
    """解析并输出单个 pom.xml,返回是否成功。"""
    try:
        pom = Pom(pom_path)
    except ET.ParseError as e:
        print(f"[错误] {pom_path} 解析失败: {e}", file=sys.stderr)
        return False

    # 分别排序:按 groupId、artifactId(大小写不敏感)
    dm_items = sorted(pom.dm.items(),
                      key=lambda kv: (kv[0][0].lower(), kv[0][1].lower()))
    deps = sorted(pom.get_dependencies(),
                  key=lambda d: (d["groupId"].lower(), d["artifactId"].lower()))

    if compact:
        print(f"# dependencyManagement ({len(dm_items)})")
        for (g, a), (v, scope, _t, _c) in dm_items:
            print(f"{g}:{a}:{pom.resolve(v) or '':s}:{scope or 'compile'}")
        print(f"# dependencies ({len(deps)})")
        for d in deps:
            print(f"{d['groupId']}:{d['artifactId']}:{d['version']}:{d['scope']}")
    else:
        parent_desc = "无"
        if pom.parent_ref:
            pg, pa, pv, _ = pom.parent_ref
            parent_desc = (f"{pg}:{pa}:{pv} (已解析)" if pom.parent
                           else f"{pg}:{pa}:{pv} (本地未找到,未继承)")
        print(f"pom.xml: {pom.path}")
        print(f"parent : {parent_desc}")
        print()

        dm_rows = []
        for (g, a), (v, scope, type_, cl) in dm_items:
            note = []
            if type_ and type_ != "jar":
                note.append(f"type={type_}")
            if cl:
                note.append(f"classifier={cl}")
            dm_rows.append((g, a, pom.resolve(v) or "(空)", scope or "compile",
                            "; ".join(note) or "声明"))
        _print_table("dependencyManagement", dm_rows)

        print()
        dep_rows = []
        for d in deps:
            note = []
            if d["type"] != "jar":
                note.append(f"type={d['type']}")
            if d["classifier"]:
                note.append(f"classifier={d['classifier']}")
            note.append(d["source"])
            dep_rows.append((d["groupId"], d["artifactId"], d["version"],
                             d["scope"], "; ".join(note)))
        _print_table("dependencies", dep_rows)
    return True


def main():
    ap = argparse.ArgumentParser(
        description="解析 Maven pom.xml 的 dependencyManagement 与 dependencies 依赖,分别排序输出。"
                    + "传目录时递归扫描其所有子目录下的 pom.xml 并分别处理。")
    ap.add_argument("path", nargs="?", default=DEFAULT_PATH,
                    help=f"pom.xml 文件路径或目录(默认 .,扫描当前目录及其子目录)")
    ap.add_argument("-c", "--compact", action="store_true",
                    help="紧凑模式,每行输出 groupId:artifactId:version:scope")
    args = ap.parse_args()

    path = args.path
    if os.path.isdir(path):
        poms = find_poms(path)
        if not poms:
            print(f"[提示] 目录下未找到 {DEFAULT_POM}: {os.path.abspath(path)}")
            return
        print(f"共找到 {len(poms)} 个 {DEFAULT_POM}(已跳过 target/.git 等目录):")
        ok = 0
        for i, p in enumerate(poms, 1):
            print(f"\n==================== [{i}/{len(poms)}] {p} ====================")
            if process_one(p, args.compact):
                ok += 1
        print(f"\n完成:成功 {ok} / 总数 {len(poms)}")
    elif os.path.isfile(path):
        process_one(path, args.compact)
    else:
        print(f"[错误] 路径不存在: {path}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    # 兼容 Windows 控制台编码
    reconfigure = getattr(sys.stdout, "reconfigure", None)
    if reconfigure is not None:
        reconfigure(encoding="utf-8")
    main()

0

评论区