extract_db_templates.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 从 Oracle PL/SQL 存储过程源码中提取 v_o_errmsg 赋值语句,
  5. 生成 I18n/Templates.xml 可用的正则模板条目。
  6. 用法:
  7. python extract_db_templates.py <input.sql> [<input2.sql> ...] -o output.xml
  8. 生成的 XML 形如:
  9. <Template zh="序列号:(.*?) 已被工单(.*?)绑定"
  10. en="SN:{0} has already been bound to WO {1}"
  11. vi="SN:{0} đã được gán vào WO {1}"
  12. id="SN:{0} sudah terikat ke WO {1}"/>
  13. """
  14. import argparse
  15. import re
  16. import os
  17. import sys
  18. from xml.sax.saxutils import escape
  19. def tokenize_assignment(expr: str):
  20. """
  21. 把形如 'xxx'||var||'yyy'||var||'zzz' 的表达式拆成:
  22. [('txt', 'xxx'), ('var', 'var'), ('txt', 'yyy'), ...]
  23. """
  24. tokens = []
  25. i = 0
  26. expr = expr.strip()
  27. while i < len(expr):
  28. # 跳过空白和 ||
  29. if expr[i] in " \t\r\n":
  30. i += 1
  31. continue
  32. if expr.startswith("||", i):
  33. i += 2
  34. continue
  35. # 字符串字面量
  36. if expr[i] == "'":
  37. j = i + 1
  38. literal = []
  39. while j < len(expr):
  40. if expr[j] == "'" and j + 1 < len(expr) and expr[j + 1] == "'":
  41. literal.append("'")
  42. j += 2
  43. elif expr[j] == "'":
  44. j += 1
  45. break
  46. else:
  47. literal.append(expr[j])
  48. j += 1
  49. tokens.append(("txt", "".join(literal)))
  50. i = j
  51. continue
  52. # 变量 / 函数调用 / 表达式
  53. # 取到下一个 || 或字符串字面量或分号为止
  54. j = i
  55. while j < len(expr):
  56. if expr[j] in " \t\r\n":
  57. pass
  58. elif expr.startswith("||", j):
  59. break
  60. elif expr[j] == "'":
  61. break
  62. elif expr[j] == ";":
  63. break
  64. j += 1
  65. var_expr = expr[i:j].strip()
  66. if var_expr:
  67. tokens.append(("var", var_expr))
  68. i = j
  69. return tokens
  70. def build_template(tokens):
  71. """
  72. 根据 token 序列生成中文正则模式和英文/越文/印尼文占位符模板。
  73. """
  74. fixed_parts = []
  75. var_count = 0
  76. has_variable = False
  77. for kind, value in tokens:
  78. if kind == "txt":
  79. fixed_parts.append(re.escape(value))
  80. else:
  81. has_variable = True
  82. fixed_parts.append(r"(.*?)")
  83. var_count += 1
  84. if not has_variable:
  85. return None, None # 纯静态提示,不需要模板
  86. zh_pattern = "".join(fixed_parts)
  87. placeholders = "{" + "} {".join(str(i) for i in range(var_count)) + "}"
  88. # 默认英文模板:保留固定中文占位,后续需人工翻译
  89. fixed_texts = [v for k, v in tokens if k == "txt"]
  90. en_parts = []
  91. var_idx = 0
  92. for kind, value in tokens:
  93. if kind == "txt":
  94. # 这里先用原中文占位,实际使用时应替换为英文
  95. en_parts.append(value)
  96. else:
  97. en_parts.append("{" + str(var_idx) + "}")
  98. var_idx += 1
  99. en_template = "".join(en_parts)
  100. return zh_pattern, en_template
  101. def extract_assignments(sql_text: str):
  102. """提取所有 v_o_errmsg := ... ; 语句。"""
  103. # 去掉 PL/SQL 行注释
  104. sql_text = re.sub(r"--.*?\n", "\n", sql_text)
  105. # 去掉 /* */ 块注释
  106. sql_text = re.sub(r"/\*.*?\*/", "", sql_text, flags=re.DOTALL)
  107. # 匹配 v_o_errmsg:=...;(允许跨行)
  108. pattern = re.compile(
  109. r"v_o_errmsg\s*:=\s*(.*?);",
  110. re.IGNORECASE | re.DOTALL,
  111. )
  112. return [m.group(1).strip() for m in pattern.finditer(sql_text)]
  113. def escape_attr(value: str) -> str:
  114. """XML 属性转义。"""
  115. return escape(value, {'"': "&quot;"})
  116. def main():
  117. parser = argparse.ArgumentParser(description="从 PL/SQL 提取动态错误模板")
  118. parser.add_argument("sql_files", nargs="+", help="输入的 .sql 文件路径")
  119. parser.add_argument("-o", "--output", default="Templates.xml", help="输出 XML 文件")
  120. args = parser.parse_args()
  121. all_templates = []
  122. seen = set()
  123. for path in args.sql_files:
  124. if not os.path.exists(path):
  125. print(f"[WARN] File not found: {path}", file=sys.stderr)
  126. continue
  127. with open(path, "r", encoding="utf-8") as f:
  128. text = f.read()
  129. for expr in extract_assignments(text):
  130. # 跳过纯函数调用,如 SQLERRM、dbms_utility.format_error_backtrace
  131. if re.match(r"^[A-Za-z0-9_\(\)\.\s\|\|]+$", expr) and "'" not in expr:
  132. continue
  133. tokens = tokenize_assignment(expr)
  134. zh_pattern, en_template = build_template(tokens)
  135. if zh_pattern and zh_pattern not in seen:
  136. seen.add(zh_pattern)
  137. all_templates.append((zh_pattern, en_template))
  138. lines = [
  139. '<?xml version="1.0" encoding="utf-8"?>',
  140. '<!-- 由 scripts/extract_db_templates.py 从存储过程自动生成 -->',
  141. '<!-- 请把 en/vi/id 属性中的中文占位文本替换为真实翻译 -->',
  142. '<Templates>',
  143. ]
  144. for zh, en in all_templates:
  145. lines.append(
  146. f' <Template zh="{escape_attr(zh)}"'
  147. f' en="{escape_attr(en)}"'
  148. f' vi="{escape_attr(en)}"'
  149. f' id="{escape_attr(en)}"/>'
  150. )
  151. lines.append("</Templates>")
  152. with open(args.output, "w", encoding="utf-8") as f:
  153. f.write("\n".join(lines))
  154. print(f"Generated {len(all_templates)} templates -> {args.output}")
  155. if __name__ == "__main__":
  156. main()