-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser_registry.py
More file actions
212 lines (172 loc) · 7.32 KB
/
Copy pathparser_registry.py
File metadata and controls
212 lines (172 loc) · 7.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
"""
解析器注册表
文件扩展名 → 解析器实例 的路由分发中心。
新增文件类型只需 register() 即可,无需修改已有代码。
"""
from __future__ import annotations
import logging
from pathlib import Path
from core.parser_protocol import FileParser, ParseOutput
logger = logging.getLogger(__name__)
class ParserRegistry:
"""文件扩展名 → 解析器实例 的路由注册表
用法:
registry = ParserRegistry()
registry.register(OracleTabAdapter(table_parser))
registry.register(OraclePrcAdapter(proc_parser))
registry.register(WarehouseSQLParser(schema_resolver))
# 按文件类型自动路由
output = registry.parse_file(Path("some_file.sql"))
output = registry.parse_directory(Path("some_dir/"))
"""
def __init__(self):
self._parsers: dict[str, FileParser] = {}
self._extension_map: dict[str, str] = {} # ext → parser name (for logging)
def register(self, parser: FileParser) -> None:
"""注册解析器
Args:
parser: 实现 FileParser Protocol 的解析器实例
Raises:
ValueError: 如果某个扩展名已被其他解析器注册
"""
parser_name = type(parser).__name__
for ext in parser.supported_extensions():
ext_lower = ext.lower()
if ext_lower in self._parsers:
existing = self._extension_map[ext_lower]
raise ValueError(f"扩展名 {ext_lower} 已被 {existing} 注册,无法再注册 {parser_name}")
self._parsers[ext_lower] = parser
self._extension_map[ext_lower] = parser_name
logger.info(
"注册解析器 %s: %s",
parser_name,
", ".join(parser.supported_extensions()),
)
def get_parser(self, file_path: Path) -> FileParser | None:
"""根据文件扩展名获取对应的解析器
Args:
file_path: 文件路径
Returns:
对应的解析器实例,无匹配则返回 None
"""
return self._parsers.get(file_path.suffix.lower())
def parse_file(self, file_path: Path) -> ParseOutput:
"""解析单个文件(自动路由到对应解析器)
Args:
file_path: 文件路径
Returns:
ParseOutput 统一产出
"""
parser = self.get_parser(file_path)
if parser is None:
ext = file_path.suffix.lower()
supported = ", ".join(sorted(self._parsers.keys())) or "无"
logger.warning(
"无可用的解析器处理文件 %s (扩展名: %s),已注册: %s",
file_path.name,
ext,
supported,
)
return ParseOutput(errors=[f"不支持的文件类型: {ext}"])
try:
return parser.parse_file(file_path)
except Exception as e:
logger.error("解析文件失败: %s - %s", file_path, e, exc_info=True)
return ParseOutput(errors=[f"文件 {file_path.name}: {str(e)}"])
def parse_directory(self, dir_path: Path) -> ParseOutput:
"""递归解析目录下所有支持的文件(自动路由到对应解析器)
★ 优化:对 WarehouseSQLParser 等有特殊两阶段逻辑的解析器,
仍调用其 parse_directory;对简单解析器,直接逐文件调用 parse_file,
避免让解析器重新扫描目录(浪费 I/O)。
Args:
dir_path: 目录路径
Returns:
ParseOutput 合并后的统一产出
"""
if not dir_path.exists():
logger.warning("目录不存在: %s", dir_path)
return ParseOutput(errors=[f"目录不存在: {dir_path}"])
if not dir_path.is_dir():
logger.warning("路径不是目录: %s", dir_path)
return ParseOutput(errors=[f"路径不是目录: {dir_path}"])
# 按解析器分组扫描文件(只扫描一次)
parser_files: dict[FileParser, list[Path]] = {}
skipped_extensions: set[str] = set()
for file_path in dir_path.rglob("*"):
if not file_path.is_file():
continue
ext = file_path.suffix.lower()
parser = self._parsers.get(ext)
if parser is not None:
parser_files.setdefault(parser, []).append(file_path)
else:
skipped_extensions.add(ext)
if skipped_extensions:
logger.info(
"跳过不支持的文件类型: %s",
", ".join(sorted(skipped_extensions)),
)
if not parser_files:
logger.info("目录 %s 中无支持的文件", dir_path)
return ParseOutput()
total_output = ParseOutput()
total_files = sum(len(files) for files in parser_files.values())
logger.info(
"开始解析目录 %s: %d 个文件, %d 个解析器",
dir_path,
total_files,
len(parser_files),
)
for parser, files in parser_files.items():
parser_name = type(parser).__name__
exts = ", ".join(parser.supported_extensions())
logger.info(
"解析器 %s (%s): 处理 %d 个文件",
parser_name,
exts,
len(files),
)
# ★ 区分策略:有特殊两阶段逻辑的解析器走 parse_directory,
# 简单解析器直接逐文件调用 parse_file
needs_directory_parse = parser_name in (
"WarehouseSQLParser", # 两阶段 DDL→DML 依赖
)
if needs_directory_parse:
try:
output = parser.parse_directory(dir_path)
total_output.merge(output)
continue
except Exception as e:
logger.warning(
"解析器 %s.parse_directory 失败,回退逐文件: %s",
parser_name,
e,
)
# 默认:逐文件解析(避免重复扫描目录)
for file_path in files:
try:
output = parser.parse_file(file_path)
total_output.merge(output)
except Exception as fe:
logger.error("解析文件失败: %s - %s", file_path, fe)
total_output.errors.append(f"文件 {file_path.name}: {str(fe)}")
summary = total_output.summary()
logger.info(
"目录 %s 解析完成: %d 表, %d 过程, %d 血缘, %d 字段映射, %d 错误",
dir_path,
summary["tables"],
summary["procedures"],
summary["table_lineages"],
summary["field_mappings"],
summary["errors"],
)
return total_output
def supported_extensions(self) -> list[str]:
"""所有已注册的扩展名列表"""
return sorted(self._parsers.keys())
def is_registered(self, ext: str) -> bool:
"""检查扩展名是否已注册"""
return ext.lower() in self._parsers
def __repr__(self) -> str:
entries = [f"{ext} → {name}" for ext, name in sorted(self._extension_map.items())]
return f"ParserRegistry({', '.join(entries)})"