#!/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()
版权归属:
天明
许可协议:
本文使用《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》协议授权
评论区