simplify validate_json

This commit is contained in:
jilinchen
2026-01-07 12:44:16 +08:00
parent 779c447670
commit e9dd17b291
2 changed files with 131 additions and 281 deletions
+60 -135
View File
@@ -1,17 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
JSON Field Validation Script
Validates whether JSON files completely cover all fields defined in fields.yaml
"""
import json
import yaml
import sys
from collections import defaultdict
from pathlib import Path
from typing import Dict, Set, Tuple
# Category mapping (English keys only)
import yaml
CATEGORY_MAPPING = {
"basic_info": ["basic_info", "Basic Info"],
"technical_features": ["technical_features", "technical_characteristics", "Technical Features"],
@@ -23,108 +19,55 @@ CATEGORY_MAPPING = {
"market_positioning": ["market_positioning", "market", "Market Positioning"],
}
_SKIP_KEYS = {"_source_file", "uncertain"}
def load_fields_yaml(fields_path: Path) -> Tuple[Set[str], Set[str], Dict[str, str]]:
"""
Load fields.yaml and return:
- all_fields: set of all field names
- required_fields: set of field names where required=true
- field_categories: mapping from field name to category
"""
with open(fields_path, 'r', encoding='utf-8') as f:
def load_fields_yaml(fields_path):
with fields_path.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
all_fields = set()
required_fields = set()
field_categories = {}
for category_info in data.get("field_categories", []):
category_name = category_info["category"]
for field in category_info.get("fields", []):
field_name = field["name"]
all_fields.add(field_name)
field_categories[field_name] = category_name
if field.get("required", False):
required_fields.add(field_name)
items = [
(field["name"], category["category"], field.get("required", False))
for category in data.get("field_categories", [])
for field in category.get("fields", [])
]
all_fields = {name for name, _, _ in items}
required_fields = {name for name, _, required in items if required}
field_categories = {name: category for name, category, _ in items}
return all_fields, required_fields, field_categories
def extract_json_fields(data: Dict, category_mapping: Dict = None) -> Set[str]:
"""
Extract all field names from JSON (supports both flat and nested structures)
Only extracts field names at category level, not nested dict/list values
"""
if category_mapping is None:
category_mapping = CATEGORY_MAPPING
# Get all possible nested keys (category containers)
nested_keys = set()
for keys in category_mapping.values():
nested_keys.update(keys)
def extract_json_fields(data, category_mapping=None):
category_mapping = CATEGORY_MAPPING if category_mapping is None else category_mapping
nested_keys = {k for keys in category_mapping.values() for k in keys}
fields = set()
def collect_fields(d, is_category_level: bool = True):
"""
Collect fields from dict or list structures
is_category_level: True if we're at top level or inside a category container
"""
if isinstance(d, dict):
for k, v in d.items():
# Skip internal fields
if k in {"_source_file", "uncertain"}:
stack = [(data, True)]
while stack:
obj, is_category_level = stack.pop()
if isinstance(obj, dict):
for k, v in obj.items():
if k in _SKIP_KEYS:
continue
# If it's a category container key, recurse into it
if is_category_level and k in nested_keys:
if isinstance(v, dict):
collect_fields(v, is_category_level=True)
else:
# This is a field name, add it
stack.append((v, True))
continue
fields.add(k)
# Don't recurse into field values (avoid counting nested keys as fields)
elif isinstance(d, list):
# Handle list-of-dict structures at category level
for item in d:
if isinstance(item, dict):
collect_fields(item, is_category_level=is_category_level)
collect_fields(data)
elif isinstance(obj, list):
stack.extend((item, is_category_level) for item in obj if isinstance(item, dict))
return fields
def validate_json(json_path: Path, all_fields: Set[str], required_fields: Set[str],
field_categories: Dict[str, str]) -> Dict:
"""
Validate a single JSON file
Returns validation result dictionary
"""
with open(json_path, 'r', encoding='utf-8') as f:
def validate_json(json_path, all_fields, required_fields, field_categories):
with json_path.open(encoding="utf-8") as f:
data = json.load(f)
json_fields = extract_json_fields(data)
# Calculate coverage
covered = all_fields & json_fields
missing = all_fields - json_fields
extra = json_fields - all_fields
# Categorize missing fields
missing_required = missing & required_fields
missing_optional = missing - required_fields
# Group missing fields by category
missing_by_category = {}
missing_by_category = defaultdict(list)
for field in missing:
cat = field_categories.get(field, "Unknown")
if cat not in missing_by_category:
missing_by_category[cat] = []
missing_by_category[cat].append(field)
# Sort lists within categories for deterministic output
for cat in missing_by_category:
missing_by_category[cat].sort()
missing_by_category[field_categories.get(field, "Unknown")].append(field)
return {
"file": json_path.name,
"total_defined": len(all_fields),
@@ -133,43 +76,39 @@ def validate_json(json_path: Path, all_fields: Set[str], required_fields: Set[st
"extra": len(extra),
"coverage_rate": len(covered) / len(all_fields) * 100 if all_fields else 100,
"missing_required": sorted(missing_required),
"missing_optional": sorted(missing_optional),
"missing_by_category": missing_by_category,
"missing_optional": sorted(missing - required_fields),
"missing_by_category": {k: sorted(v) for k, v in missing_by_category.items()},
"extra_fields": sorted(extra),
"valid": len(missing_required) == 0, # Valid if all required fields are covered
"valid": len(missing_required) == 0,
}
def print_result(result: Dict, verbose: bool = True):
"""Print validation result"""
def print_result(result, verbose=True):
status = "PASS" if result["valid"] else "FAIL"
print(f"\n{'='*60}")
line = "=" * 60
print(f"\n{line}")
print(f"[{status}] {result['file']}")
print(f"{'='*60}")
print(line)
print(f"Coverage: {result['coverage_rate']:.1f}% ({result['covered']}/{result['total_defined']})")
if result["missing_required"]:
print(f"\n[ERROR] Missing required fields ({len(result['missing_required'])}):")
for field in result["missing_required"]:
print(f" - {field}")
print("\n".join(f" - {f}" for f in result["missing_required"]))
if verbose and result["missing_optional"]:
missing_required = set(result["missing_required"])
print(f"\n[WARN] Missing optional fields ({len(result['missing_optional'])}):")
for cat in sorted(result["missing_by_category"].keys()):
fields = result["missing_by_category"][cat]
optional_fields = [f for f in fields if f not in result["missing_required"]]
if optional_fields:
print(f" [{cat}]: {', '.join(optional_fields)}")
for cat in sorted(result["missing_by_category"]):
optional = [f for f in result["missing_by_category"][cat] if f not in missing_required]
if optional:
print(f" [{cat}]: {', '.join(optional)}")
if verbose and result["extra_fields"]:
print(f"\n[INFO] Extra fields ({len(result['extra_fields'])}):")
print(f" {', '.join(result['extra_fields'][:10])}")
if len(result["extra_fields"]) > 10:
print(f" ... and {len(result['extra_fields']) - 10} more")
extra = result["extra_fields"]
print(f"\n[INFO] Extra fields ({len(extra)}):")
print(f" {', '.join(extra[:10])}")
if len(extra) > 10:
print(f" ... and {len(extra) - 10} more")
def main():
"""Main function"""
import argparse
parser = argparse.ArgumentParser(description="Validate whether JSON files cover all fields defined in fields.yaml")
parser.add_argument("--fields", "-f", type=str, help="Path to fields.yaml", default="fields.yaml")
@@ -177,38 +116,26 @@ def main():
parser.add_argument("--dir", "-d", type=str, help="Directory containing JSON files", default="results")
parser.add_argument("--quiet", "-q", action="store_true", help="Show summary only")
args = parser.parse_args()
# Locate fields.yaml
fields_path = Path(args.fields)
if not fields_path.exists():
# Try to find in current and parent directory
for p in [Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"]:
for p in (Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"):
if p.exists():
fields_path = p
break
if not fields_path.exists():
print(f"[ERROR] fields.yaml not found: {fields_path}")
sys.exit(1)
print(f"Field definition file: {fields_path}")
all_fields, required_fields, field_categories = load_fields_yaml(fields_path)
print(f"Total fields: {len(all_fields)} (required: {len(required_fields)}, optional: {len(all_fields) - len(required_fields)})")
# Collect JSON files
json_files = []
if args.json:
json_files = [Path(p) for p in args.json]
else:
json_dir = Path(args.dir)
if json_dir.exists():
json_files = sorted(json_dir.glob("*.json"))
json_files = (
[Path(p) for p in args.json]
if args.json
else sorted(Path(args.dir).glob("*.json")) if Path(args.dir).exists() else []
)
if not json_files:
print(f"[WARN] No JSON files found")
print("[WARN] No JSON files found")
sys.exit(0)
# Validate each file
results = []
for json_path in json_files:
if not json_path.exists():
@@ -217,16 +144,14 @@ def main():
result = validate_json(json_path, all_fields, required_fields, field_categories)
results.append(result)
print_result(result, verbose=not args.quiet)
# Summary
print(f"\n{'='*60}")
line = "=" * 60
print(f"\n{line}")
print("Summary")
print(f"{'='*60}")
print(line)
passed = sum(1 for r in results if r["valid"])
avg_coverage = sum(r["coverage_rate"] for r in results) / len(results) if results else 0
print(f"Validation passed: {passed}/{len(results)}")
print(f"Average coverage: {avg_coverage:.1f}%")
if passed < len(results):
sys.exit(1)
+69 -144
View File
@@ -1,17 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
JSON字段验证脚本
验证JSON文件是否完整覆盖fields.yaml中定义的所有字段
"""
import json
import yaml
import sys
from collections import defaultdict
from pathlib import Path
from typing import Dict, Set, Tuple
# Category中英文映射
import yaml
CATEGORY_MAPPING = {
"基本信息": ["basic_info", "基本信息"],
"技术特性": ["technical_features", "technical_characteristics", "技术特性"],
@@ -23,108 +19,55 @@ CATEGORY_MAPPING = {
"市场定位": ["market_positioning", "market", "市场定位"],
}
_SKIP_KEYS = {"_source_file", "uncertain"}
def load_fields_yaml(fields_path: Path) -> Tuple[Set[str], Set[str], Dict[str, str]]:
"""
加载fields.yaml,返回:
- all_fields: 所有字段名集合
- required_fields: required=true的字段名集合
- field_categories: 字段名到类别的映射
"""
with open(fields_path, 'r', encoding='utf-8') as f:
def load_fields_yaml(fields_path):
with fields_path.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
all_fields = set()
required_fields = set()
field_categories = {}
for category_info in data.get("field_categories", []):
category_name = category_info["category"]
for field in category_info.get("fields", []):
field_name = field["name"]
all_fields.add(field_name)
field_categories[field_name] = category_name
if field.get("required", False):
required_fields.add(field_name)
items = [
(field["name"], category["category"], field.get("required", False))
for category in data.get("field_categories", [])
for field in category.get("fields", [])
]
all_fields = {name for name, _, _ in items}
required_fields = {name for name, _, required in items if required}
field_categories = {name: category for name, category, _ in items}
return all_fields, required_fields, field_categories
def extract_json_fields(data: Dict, category_mapping: Dict = None) -> Set[str]:
"""
从JSON中提取所有字段名(支持扁平和嵌套结构)
只提取category级别的字段名,不递归到字段值的dict/list中
"""
if category_mapping is None:
category_mapping = CATEGORY_MAPPING
# 获取所有可能的嵌套key(category容器)
nested_keys = set()
for keys in category_mapping.values():
nested_keys.update(keys)
def extract_json_fields(data, category_mapping=None):
category_mapping = CATEGORY_MAPPING if category_mapping is None else category_mapping
nested_keys = {k for keys in category_mapping.values() for k in keys}
fields = set()
def collect_fields(d, is_category_level: bool = True):
"""
从dict或list结构中收集字段
is_category_level: True表示在顶层或category容器内部
"""
if isinstance(d, dict):
for k, v in d.items():
# 跳过内部字段
if k in {"_source_file", "uncertain"}:
stack = [(data, True)]
while stack:
obj, is_category_level = stack.pop()
if isinstance(obj, dict):
for k, v in obj.items():
if k in _SKIP_KEYS:
continue
# 如果是category容器key,递归进入
if is_category_level and k in nested_keys:
if isinstance(v, dict):
collect_fields(v, is_category_level=True)
else:
# 这是一个字段名,添加它
stack.append((v, True))
continue
fields.add(k)
# 不递归到字段值中(避免将嵌套key误计为字段)
elif isinstance(d, list):
# 处理category级别的list-of-dict结构
for item in d:
if isinstance(item, dict):
collect_fields(item, is_category_level=is_category_level)
collect_fields(data)
elif isinstance(obj, list):
stack.extend((item, is_category_level) for item in obj if isinstance(item, dict))
return fields
def validate_json(json_path: Path, all_fields: Set[str], required_fields: Set[str],
field_categories: Dict[str, str]) -> Dict:
"""
验证单个JSON文件
返回验证结果字典
"""
with open(json_path, 'r', encoding='utf-8') as f:
def validate_json(json_path, all_fields, required_fields, field_categories):
with json_path.open(encoding="utf-8") as f:
data = json.load(f)
json_fields = extract_json_fields(data)
# 计算覆盖情况
covered = all_fields & json_fields
missing = all_fields - json_fields
extra = json_fields - all_fields
# 分类缺失字段
missing_required = missing & required_fields
missing_optional = missing - required_fields
# 按类别分组缺失字段
missing_by_category = {}
missing_by_category = defaultdict(list)
for field in missing:
cat = field_categories.get(field, "未知")
if cat not in missing_by_category:
missing_by_category[cat] = []
missing_by_category[cat].append(field)
# 对category内的列表排序,确保输出确定性
for cat in missing_by_category:
missing_by_category[cat].sort()
missing_by_category[field_categories.get(field, "未知")].append(field)
return {
"file": json_path.name,
"total_defined": len(all_fields),
@@ -133,100 +76,82 @@ def validate_json(json_path: Path, all_fields: Set[str], required_fields: Set[st
"extra": len(extra),
"coverage_rate": len(covered) / len(all_fields) * 100 if all_fields else 100,
"missing_required": sorted(missing_required),
"missing_optional": sorted(missing_optional),
"missing_by_category": missing_by_category,
"missing_optional": sorted(missing - required_fields),
"missing_by_category": {k: sorted(v) for k, v in missing_by_category.items()},
"extra_fields": sorted(extra),
"valid": len(missing_required) == 0, # required字段全覆盖则valid
"valid": len(missing_required) == 0,
}
def print_result(result: Dict, verbose: bool = True):
"""打印验证结果"""
status = "PASS" if result["valid"] else "FAIL"
print(f"\n{'='*60}")
def print_result(result, verbose=True):
status = "通过" if result["valid"] else "失败"
line = "=" * 60
print(f"\n{line}")
print(f"[{status}] {result['file']}")
print(f"{'='*60}")
print(line)
print(f"覆盖率: {result['coverage_rate']:.1f}% ({result['covered']}/{result['total_defined']})")
if result["missing_required"]:
print(f"\n[ERROR] 缺失必需字段 ({len(result['missing_required'])}):")
for field in result["missing_required"]:
print(f" - {field}")
print(f"\n[错误] 缺少必填字段 ({len(result['missing_required'])}):")
print("\n".join(f" - {f}" for f in result["missing_required"]))
if verbose and result["missing_optional"]:
print(f"\n[WARN] 缺失可选字段 ({len(result['missing_optional'])}):")
for cat in sorted(result["missing_by_category"].keys()):
fields = result["missing_by_category"][cat]
optional_fields = [f for f in fields if f not in result["missing_required"]]
if optional_fields:
print(f" [{cat}]: {', '.join(optional_fields)}")
missing_required = set(result["missing_required"])
print(f"\n[警告] 缺少可选字段 ({len(result['missing_optional'])}):")
for cat in sorted(result["missing_by_category"]):
optional = [f for f in result["missing_by_category"][cat] if f not in missing_required]
if optional:
print(f" [{cat}]: {', '.join(optional)}")
if verbose and result["extra_fields"]:
print(f"\n[INFO] 额外字段 ({len(result['extra_fields'])}):")
print(f" {', '.join(result['extra_fields'][:10])}")
if len(result["extra_fields"]) > 10:
print(f" ... 及 {len(result['extra_fields']) - 10} 个其他字段")
extra = result["extra_fields"]
print(f"\n[信息] 额外字段 ({len(extra)}):")
print(f" {', '.join(extra[:10])}")
if len(extra) > 10:
print(f" ... 还有 {len(extra) - 10}")
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description="验证JSON文件是否覆盖fields.yaml定义的所有字段")
parser = argparse.ArgumentParser(description="验证JSON文件是否覆盖fields.yaml定义的所有字段")
parser.add_argument("--fields", "-f", type=str, help="fields.yaml路径", default="fields.yaml")
parser.add_argument("--json", "-j", type=str, nargs="*", help="要验证的JSON文件路径")
parser.add_argument("--dir", "-d", type=str, help="JSON文件目录", default="results")
parser.add_argument("--quiet", "-q", action="store_true", help="显示摘要")
parser.add_argument("--dir", "-d", type=str, help="包含JSON文件目录", default="results")
parser.add_argument("--quiet", "-q", action="store_true", help="显示摘要")
args = parser.parse_args()
# 定位fields.yaml
fields_path = Path(args.fields)
if not fields_path.exists():
# 尝试在当前目录和父目录查找
for p in [Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"]:
for p in (Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"):
if p.exists():
fields_path = p
break
if not fields_path.exists():
print(f"[ERROR] fields.yaml不存在: {fields_path}")
print(f"[错误] 找不到fields.yaml: {fields_path}")
sys.exit(1)
print(f"字段定义文件: {fields_path}")
all_fields, required_fields, field_categories = load_fields_yaml(fields_path)
print(f"总字段数: {len(all_fields)} (必: {len(required_fields)}, 可选: {len(all_fields) - len(required_fields)})")
# 收集JSON文件
json_files = []
if args.json:
json_files = [Path(p) for p in args.json]
else:
json_dir = Path(args.dir)
if json_dir.exists():
json_files = sorted(json_dir.glob("*.json"))
print(f"总字段数: {len(all_fields)} (必: {len(required_fields)}, 可选: {len(all_fields) - len(required_fields)})")
json_files = (
[Path(p) for p in args.json]
if args.json
else sorted(Path(args.dir).glob("*.json")) if Path(args.dir).exists() else []
)
if not json_files:
print(f"[WARN] 未找到JSON文件")
print("[警告] 未找到JSON文件")
sys.exit(0)
# 验证每个文件
results = []
for json_path in json_files:
if not json_path.exists():
print(f"[WARN] 文件不存在: {json_path}")
print(f"[警告] 文件不存在: {json_path}")
continue
result = validate_json(json_path, all_fields, required_fields, field_categories)
results.append(result)
print_result(result, verbose=not args.quiet)
# 汇总
print(f"\n{'='*60}")
line = "=" * 60
print(f"\n{line}")
print("汇总")
print(f"{'='*60}")
print(line)
passed = sum(1 for r in results if r["valid"])
avg_coverage = sum(r["coverage_rate"] for r in results) / len(results) if results else 0
print(f"验证通过: {passed}/{len(results)}")
print(f"平均覆盖率: {avg_coverage:.1f}%")
if passed < len(results):
sys.exit(1)