| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- # -*- coding: utf-8 -*-
- """规则校验器(RULE VALIDATOR)。
- 按 ARCHITECTURE.md Phase 7 要求,规则生成后必须运行三项校验:
- 1. **Schema 校验** — YAML 文件结构符合 ``schema/pattern_schema.json``
- 2. **黄金样例** — 正例必须匹配,负例必须不匹配
- 3. **正则性能** — 编译时间 < 100ms,单次匹配时间 < 1ms
- 使用方式::
- python -m BiddingKG.dl.rules.validator
- # 或
- from BiddingKG.dl.rules.validator import RuleValidator
- RuleValidator.run_all()
- 依赖方向:rules 只依赖 common,不反向依赖业务层。
- """
- from __future__ import absolute_import
- import os
- import re
- import time
- import json
- from typing import List, Dict, Tuple
- import yaml
- from BiddingKG.dl.rules.loader import load_rule_yaml
- # ----------------------------------------------------------------------------
- # 路径常量
- # ----------------------------------------------------------------------------
- _RULES_DIR = os.path.dirname(os.path.abspath(__file__))
- _PATTERNS_DIR = os.path.join(_RULES_DIR, "patterns")
- _SCHEMA_DIR = os.path.join(_RULES_DIR, "schema")
- # 性能阈值
- _COMPILE_TIME_THRESHOLD_MS = 100 # 正则编译时间阈值
- _MATCH_TIME_THRESHOLD_MS = 1 # 单次匹配时间阈值
- _PERF_MATCH_ITERATIONS = 1000 # 性能测试迭代次数
- # JSON Schema 可选依赖
- try:
- import jsonschema
- _HAS_JSONSCHEMA = True
- except ImportError:
- _HAS_JSONSCHEMA = False
- class RuleValidator:
- """规则校验器。"""
- def __init__(self):
- self.schema = self._load_schema()
- self.results = {
- "schema": [], # [(file, passed, message)]
- "golden": [], # [(pattern_id, passed, message)]
- "performance": [], # [(pattern_id, passed, compile_ms, match_ms, message)]
- }
- # ------------------------------------------------------------------------
- # Schema 加载
- # ------------------------------------------------------------------------
- def _load_schema(self) -> dict:
- path = os.path.join(_SCHEMA_DIR, "pattern_schema.json")
- with open(path, encoding="utf-8") as f:
- return json.load(f)
- # ------------------------------------------------------------------------
- # 1. Schema 校验
- # ------------------------------------------------------------------------
- def validate_schema(self) -> bool:
- """校验所有 YAML 文件是否符合 JSON Schema。"""
- print("=" * 70)
- print("1. Schema 校验")
- print("=" * 70)
- all_passed = True
- for fname in sorted(os.listdir(_PATTERNS_DIR)):
- if not fname.endswith((".yaml", ".yml")):
- continue
- fpath = os.path.join(_PATTERNS_DIR, fname)
- with open(fpath, encoding="utf-8") as f:
- data = load_rule_yaml(f)
- passed, msg = self._validate_one_schema(data, fname)
- status = "OK " if passed else "FAIL"
- print(" %s %s: %s" % (status, fname, msg if not passed else ""))
- self.results["schema"].append((fname, passed, msg))
- if not passed:
- all_passed = False
- n_pass = sum(1 for _, p, _ in self.results["schema"] if p)
- n_total = len(self.results["schema"])
- print(" ---")
- print(" Schema: %d/%d passed" % (n_pass, n_total))
- return all_passed
- def _validate_one_schema(self, data: dict, fname: str) -> Tuple[bool, str]:
- """校验单个 YAML 文件。"""
- if _HAS_JSONSCHEMA:
- try:
- jsonschema.validate(data, self.schema)
- return True, ""
- except jsonschema.ValidationError as e:
- return False, str(e.message)
- except jsonschema.SchemaError as e:
- return False, "Schema error: %s" % e
- else:
- return self._validate_basic(data, fname)
- def _validate_basic(self, data: dict, fname: str) -> Tuple[bool, str]:
- """无 jsonschema 时的基础结构校验。"""
- if not isinstance(data, dict):
- return False, "顶层应为 dict"
- for field in ("version", "category"):
- if field not in data:
- return False, "缺少必填字段: %s" % field
- if "patterns" not in data and "keyword_rules" not in data:
- return False, "缺少 patterns 或 keyword_rules"
- if "patterns" in data:
- for p in data["patterns"]:
- if "id" not in p or "pattern" not in p:
- return False, "pattern 缺少 id 或 pattern 字段"
- if "keyword_rules" in data:
- for r in data["keyword_rules"]:
- if "id" not in r or "label" not in r or "keywords" not in r:
- return False, "keyword_rule 缺少 id/label/keywords 字段"
- return True, ""
- # ------------------------------------------------------------------------
- # 2. 黄金样例
- # ------------------------------------------------------------------------
- def validate_golden(self) -> bool:
- """运行所有黄金样例。"""
- print()
- print("=" * 70)
- print("2. 黄金样例校验")
- print("=" * 70)
- all_passed = True
- for fname in sorted(os.listdir(_PATTERNS_DIR)):
- if not fname.endswith((".yaml", ".yml")):
- continue
- fpath = os.path.join(_PATTERNS_DIR, fname)
- with open(fpath, encoding="utf-8") as f:
- data = load_rule_yaml(f)
- patterns = data.get("patterns") or []
- for p in patterns:
- pid = p["id"]
- examples = p.get("golden_examples") or []
- if not examples:
- continue
- passed, msg = self._run_golden_for_pattern(p)
- status = "OK " if passed else "FAIL"
- print(" %s %s (%d examples): %s" % (
- status, pid, len(examples), msg if not passed else ""))
- self.results["golden"].append((pid, passed, msg))
- if not passed:
- all_passed = False
- n_pass = sum(1 for _, p, _ in self.results["golden"] if p)
- n_total = len(self.results["golden"])
- print(" ---")
- print(" Golden: %d/%d patterns passed" % (n_pass, n_total))
- return all_passed
- def _run_golden_for_pattern(self, p: dict) -> Tuple[bool, str]:
- """对单个 pattern 运行黄金样例。"""
- pattern_str = p["pattern"]
- method = p.get("method", "search")
- flags = 0
- for f in p.get("flags", []):
- flags |= getattr(re, f, 0)
- try:
- compiled = re.compile(pattern_str, flags)
- except re.error as e:
- return False, "正则编译失败: %s" % e
- examples = p.get("golden_examples") or []
- for ex in examples:
- text = ex["input"]
- should_match = ex["should_match"]
- if method == "match":
- m = compiled.match(text)
- elif method == "fullmatch":
- m = compiled.fullmatch(text)
- else:
- m = compiled.search(text)
- matched = m is not None
- if matched != should_match:
- return False, "输入 %r 期望 match=%s 实际 match=%s" % (
- text, should_match, matched)
- # 检查期望的匹配组内容
- if "expected_group" in ex and m:
- actual = m.group(0)
- if actual != ex["expected_group"]:
- return False, "输入 %r 期望 group(0)=%r 实际=%r" % (
- text, ex["expected_group"], actual)
- return True, ""
- # ------------------------------------------------------------------------
- # 3. 正则性能
- # ------------------------------------------------------------------------
- def validate_performance(self) -> bool:
- """正则性能测试。"""
- print()
- print("=" * 70)
- print("3. 正则性能测试")
- print("=" * 70)
- all_passed = True
- for fname in sorted(os.listdir(_PATTERNS_DIR)):
- if not fname.endswith((".yaml", ".yml")):
- continue
- fpath = os.path.join(_PATTERNS_DIR, fname)
- with open(fpath, encoding="utf-8") as f:
- data = load_rule_yaml(f)
- patterns = data.get("patterns") or []
- for p in patterns:
- pid = p["id"]
- examples = p.get("golden_examples") or []
- passed, compile_ms, match_ms, msg = self._perf_one_pattern(p, examples)
- status = "OK " if passed else "WARN"
- detail = "" if passed else " " + msg
- print(" %s %s: compile=%.2fms, match=%.3fms%s" % (
- status, pid, compile_ms, match_ms, detail))
- self.results["performance"].append(
- (pid, passed, compile_ms, match_ms, msg))
- if not passed:
- all_passed = False
- n_pass = sum(1 for _, p, _, _, _ in self.results["performance"] if p)
- n_total = len(self.results["performance"])
- print(" ---")
- print(" Performance: %d/%d passed (thresholds: compile<%dms, match<%dms)" % (
- n_pass, n_total, _COMPILE_TIME_THRESHOLD_MS, _MATCH_TIME_THRESHOLD_MS))
- return all_passed
- def _perf_one_pattern(self, p: dict, examples: list) -> Tuple[bool, float, float, str]:
- """测试单个 pattern 的性能。"""
- pattern_str = p["pattern"]
- flags = 0
- for f in p.get("flags", []):
- flags |= getattr(re, f, 0)
- # 编译时间
- t0 = time.perf_counter()
- try:
- compiled = re.compile(pattern_str, flags)
- except re.error as e:
- return False, 0, 0, "编译失败: %s" % e
- t1 = time.perf_counter()
- compile_ms = (t1 - t0) * 1000
- # 匹配时间(取第一个正例做迭代)
- match_ms = 0
- test_text = ""
- for ex in examples:
- if ex.get("should_match", True):
- test_text = ex["input"]
- break
- if test_text:
- method = p.get("method", "search")
- matcher = getattr(compiled, method)
- t0 = time.perf_counter()
- for _ in range(_PERF_MATCH_ITERATIONS):
- matcher(test_text)
- t1 = time.perf_counter()
- match_ms = (t1 - t0) / _PERF_MATCH_ITERATIONS * 1000
- msg = ""
- passed = True
- if compile_ms > _COMPILE_TIME_THRESHOLD_MS:
- msg += "编译时间 %.2fms > %dms; " % (compile_ms, _COMPILE_TIME_THRESHOLD_MS)
- passed = False
- if match_ms > _MATCH_TIME_THRESHOLD_MS:
- msg += "匹配时间 %.3fms > %dms; " % (match_ms, _MATCH_TIME_THRESHOLD_MS)
- passed = False
- return passed, compile_ms, match_ms, msg.strip("; ")
- # ------------------------------------------------------------------------
- # 汇总
- # ------------------------------------------------------------------------
- def run_all(self) -> bool:
- """运行全部三项校验,返回是否全部通过。"""
- print("BiddingKG 规则校验")
- print("patterns 目录: %s" % _PATTERNS_DIR)
- print("schema: %s" % ("jsonschema" if _HAS_JSONSCHEMA else "basic 结构校验"))
- print()
- s = self.validate_schema()
- g = self.validate_golden()
- p = self.validate_performance()
- print()
- print("=" * 70)
- print("SUMMARY")
- print("=" * 70)
- n_schema = sum(1 for _, p, _ in self.results["schema"] if p)
- n_golden = sum(1 for _, p, _ in self.results["golden"] if p)
- n_perf = sum(1 for _, p, _, _, _ in self.results["performance"] if p)
- print(" Schema: %d/%d" % (n_schema, len(self.results["schema"])))
- print(" Golden: %d/%d" % (n_golden, len(self.results["golden"])))
- print(" Performance: %d/%d" % (n_perf, len(self.results["performance"])))
- print(" ---")
- all_passed = s and g and p
- if all_passed:
- print(" ALL CHECKS PASSED")
- else:
- print(" SOME CHECKS FAILED")
- return all_passed
- # ----------------------------------------------------------------------------
- # 命令行入口
- # ----------------------------------------------------------------------------
- if __name__ == "__main__":
- validator = RuleValidator()
- passed = validator.run_all()
- exit(0 if passed else 1)
|