| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- #!/usr/bin/env python3
- """Audit saved VJudge MHTML files without copying problem statements.
- The script reports the metadata needed by the course and verifies that each
- snapshot contains an embedded statement, rather than only the VJudge shell.
- It intentionally stores no statement text.
- """
- from __future__ import annotations
- import argparse
- import html
- import json
- import re
- import sys
- from email import policy
- from email.parser import BytesParser
- from html.parser import HTMLParser
- from pathlib import Path
- class TextExtractor(HTMLParser):
- def __init__(self) -> None:
- super().__init__()
- self.parts: list[str] = []
- def handle_data(self, data: str) -> None:
- if data.strip():
- self.parts.append(data.strip())
- def text(self) -> str:
- return " ".join(self.parts)
- def plain_text(fragment: str) -> str:
- parser = TextExtractor()
- parser.feed(fragment)
- return html.unescape(parser.text())
- def decode_part(part) -> str:
- payload = part.get_payload(decode=True) or b""
- charset = part.get_content_charset() or "utf-8"
- return payload.decode(charset, errors="replace")
- def parse_snapshot(path: Path) -> dict[str, object]:
- message = BytesParser(policy=policy.default).parsebytes(path.read_bytes())
- snapshot_url = str(message.get("Snapshot-Content-Location", ""))
- subject = str(message.get("Subject", path.stem))
- html_parts = [
- part
- for part in message.walk()
- if not part.is_multipart() and part.get_content_type() == "text/html"
- ]
- main_part = next(
- (
- part
- for part in html_parts
- if str(part.get("Content-Location", "")) == snapshot_url
- ),
- None,
- )
- if main_part is None:
- raise ValueError("missing VJudge main HTML part")
- main_html = decode_part(main_part)
- match = re.search(
- r'<textarea[^>]*name=["\']dataJson["\'][^>]*>(.*?)</textarea>',
- main_html,
- flags=re.DOTALL,
- )
- if match is None:
- raise ValueError("missing dataJson metadata")
- metadata = json.loads(html.unescape(match.group(1)))
- official_keys = {
- str(item["key"])
- for item in metadata.get("descBriefs", [])
- if item.get("mainOfficial") or (item.get("official") and item.get("type") == "main")
- }
- statement_candidates: list[tuple[bool, str]] = []
- for part in html_parts:
- location = str(part.get("Content-Location", ""))
- if "/problem/description/" not in location:
- continue
- body = decode_part(part)
- key_match = re.search(r"/problem/description/(\d+)", location)
- is_official = bool(key_match and key_match.group(1) in official_keys)
- statement_candidates.append((is_official, body))
- if not statement_candidates:
- raise ValueError("missing embedded statement HTML")
- statement_candidates.sort(key=lambda item: (item[0], len(item[1])), reverse=True)
- statement_html = statement_candidates[0][1]
- statement_text = plain_text(statement_html)
- properties = {
- str(item.get("title", "")): plain_text(str(item.get("content", "")))
- for item in metadata.get("properties", [])
- }
- title = re.sub(r"\s+-\s+Virtual Judge$", "", subject)
- title = re.sub(r"\s+-\s+[^-]+\s+[^-]+$", "", title)
- has_input = bool(re.search(r"\bInput\b|输入", statement_text, re.IGNORECASE))
- has_output = bool(re.search(r"\bOutput\b|输出", statement_text, re.IGNORECASE))
- sample_count = len(re.findall(r'class=["\']vjudge_sample["\']', statement_html))
- return {
- "file": path.as_posix(),
- "query": f"{metadata.get('oj', '?')}-{metadata.get('prob', '?')}",
- "title": title,
- "source": properties.get("source", ""),
- "difficulty": properties.get("difficulty", ""),
- "time_limit": properties.get("time_limit", ""),
- "memory_limit": properties.get("mem_limit", ""),
- "statement_chars": len(statement_text),
- "samples": sample_count,
- "has_input": has_input,
- "has_output": has_output,
- "official_statement": statement_candidates[0][0],
- "url": snapshot_url,
- }
- def main() -> int:
- parser = argparse.ArgumentParser()
- parser.add_argument("root", nargs="?", default="problems")
- parser.add_argument("--json", action="store_true", dest="as_json")
- args = parser.parse_args()
- paths = sorted(Path(args.root).rglob("*.mhtml"))
- if not paths:
- print(f"no .mhtml files found under {args.root}", file=sys.stderr)
- return 2
- rows: list[dict[str, object]] = []
- errors: list[str] = []
- for path in paths:
- try:
- rows.append(parse_snapshot(path))
- except Exception as exc: # keep auditing the remaining snapshots
- errors.append(f"{path}: {exc}")
- if args.as_json:
- print(json.dumps(rows, ensure_ascii=False, indent=2))
- else:
- print("query\ttitle\tdifficulty\tsource\tstatement\tsamples\tofficial")
- for row in rows:
- statement = "ok" if row["has_input"] and row["has_output"] else "incomplete"
- official = "yes" if row["official_statement"] else "fallback"
- print(
- f"{row['query']}\t{row['title']}\t{row['difficulty'] or '-'}\t"
- f"{row['source'] or '-'}\t{statement}\t{row['samples']}\t{official}"
- )
- for error in errors:
- print(error, file=sys.stderr)
- incomplete = [
- row for row in rows if not row["has_input"] or not row["has_output"] or row["statement_chars"] < 200
- ]
- return 1 if errors or incomplete else 0
- if __name__ == "__main__":
- raise SystemExit(main())
|