chore: auto release commit (2026-06-14 11:12:03)
All checks were successful
技能自动化发布 / release (push) Successful in 8s
All checks were successful
技能自动化发布 / release (push) Successful in 8s
This commit is contained in:
194
tools/highlight_answers.py
Normal file
194
tools/highlight_answers.py
Normal file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Highlight correct answer options in Excel quiz file."""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.cell.rich_text import CellRichText, InlineFont, TextBlock
|
||||
|
||||
# Option letter to index mapping
|
||||
LETTER_TO_IDX = {"A": 0, "B": 1, "C": 2, "D": 3}
|
||||
IDX_TO_LETTER = {0: "A", 1: "B", 2: "C", 3: "D"}
|
||||
|
||||
# Regex to find option boundaries: A/B/C/D followed by separator
|
||||
OPTION_MARKER_RE = re.compile(
|
||||
r"(?=[A-D](?:[\..、::\))]|\s))"
|
||||
)
|
||||
|
||||
NORMAL_FONT = InlineFont(b=False, color="000000")
|
||||
RED_BOLD_FONT = InlineFont(b=True, color="FF0000")
|
||||
|
||||
|
||||
def parse_answer(answer_raw) -> set[int]:
|
||||
"""Parse answer column into set of option indices (0=A, 1=B, ...)."""
|
||||
if answer_raw is None:
|
||||
return set()
|
||||
s = str(answer_raw).strip()
|
||||
if not s:
|
||||
return set()
|
||||
|
||||
indices = set()
|
||||
|
||||
# Pure digits like "13" -> 1 and 3 (not 13)
|
||||
if re.fullmatch(r"[1-4]+", s):
|
||||
for ch in s:
|
||||
indices.add(int(ch) - 1)
|
||||
return indices
|
||||
|
||||
# Split by common separators
|
||||
parts = re.split(r"[,,;;\s|]+", s)
|
||||
for part in parts:
|
||||
part = part.strip().upper()
|
||||
if not part:
|
||||
continue
|
||||
if part in LETTER_TO_IDX:
|
||||
indices.add(LETTER_TO_IDX[part])
|
||||
elif re.fullmatch(r"[1-4]", part):
|
||||
indices.add(int(part) - 1)
|
||||
|
||||
return indices
|
||||
|
||||
|
||||
def find_option_spans(text: str) -> list[tuple[int, int, str]]:
|
||||
"""Find spans for each A/B/C/D option in the options text."""
|
||||
if not text or not isinstance(text, str):
|
||||
return []
|
||||
|
||||
markers = []
|
||||
for m in OPTION_MARKER_RE.finditer(text):
|
||||
letter = text[m.start()]
|
||||
markers.append((m.start(), letter))
|
||||
|
||||
if not markers:
|
||||
return []
|
||||
|
||||
spans = []
|
||||
for i, (start, letter) in enumerate(markers):
|
||||
end = markers[i + 1][0] if i + 1 < len(markers) else len(text)
|
||||
spans.append((start, end, letter))
|
||||
return spans
|
||||
|
||||
|
||||
def build_rich_text(options_text: str, answer_indices: set[int]) -> CellRichText | str:
|
||||
"""Build rich text with correct options in red bold."""
|
||||
spans = find_option_spans(options_text)
|
||||
if not spans:
|
||||
return options_text
|
||||
|
||||
parts = []
|
||||
pos = 0
|
||||
for start, end, letter in spans:
|
||||
if start > pos:
|
||||
parts.append(TextBlock(NORMAL_FONT, options_text[pos:start]))
|
||||
idx = LETTER_TO_IDX.get(letter)
|
||||
font = RED_BOLD_FONT if idx is not None and idx in answer_indices else NORMAL_FONT
|
||||
parts.append(TextBlock(font, options_text[start:end]))
|
||||
pos = end
|
||||
|
||||
if pos < len(options_text):
|
||||
parts.append(TextBlock(NORMAL_FONT, options_text[pos:]))
|
||||
|
||||
return CellRichText(*parts)
|
||||
|
||||
|
||||
def load_workbook_from_xls(xls_path: Path) -> openpyxl.Workbook:
|
||||
"""Load .xls file, converting via pandas/xlrd if needed."""
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
xls_data = pd.read_excel(xls_path, sheet_name=None, header=None, dtype=str)
|
||||
wb = openpyxl.Workbook()
|
||||
# Remove default sheet
|
||||
wb.remove(wb.active)
|
||||
|
||||
for sheet_name, df in xls_data.items():
|
||||
ws = wb.create_sheet(title=str(sheet_name))
|
||||
for r_idx, row in enumerate(df.itertuples(index=False), start=1):
|
||||
for c_idx, val in enumerate(row, start=1):
|
||||
if val is not None and str(val) != "nan":
|
||||
ws.cell(row=r_idx, column=c_idx, value=str(val))
|
||||
return wb
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to read .xls file: {e}") from e
|
||||
|
||||
|
||||
def find_header_columns(ws) -> tuple[int, int, int] | None:
|
||||
"""Find header row and column indices for 题目选项 and 题目答案."""
|
||||
for row_idx in range(1, min(ws.max_row + 1, 20)):
|
||||
options_col = None
|
||||
answer_col = None
|
||||
for col_idx in range(1, ws.max_column + 1):
|
||||
val = ws.cell(row=row_idx, column=col_idx).value
|
||||
if val is None:
|
||||
continue
|
||||
s = str(val).strip()
|
||||
if s == "题目选项":
|
||||
options_col = col_idx
|
||||
elif s == "题目答案":
|
||||
answer_col = col_idx
|
||||
if options_col is not None and answer_col is not None:
|
||||
return row_idx, options_col, answer_col
|
||||
return None
|
||||
|
||||
|
||||
def process_sheet(ws, stats: dict) -> bool:
|
||||
"""Process a single worksheet. Returns True if processed."""
|
||||
header_info = find_header_columns(ws)
|
||||
if header_info is None:
|
||||
return False
|
||||
|
||||
header_row, options_col, answer_col = header_info
|
||||
stats["sheets"] += 1
|
||||
sheet_rows = 0
|
||||
sheet_matches = 0
|
||||
|
||||
for row_idx in range(header_row + 1, ws.max_row + 1):
|
||||
options_cell = ws.cell(row=row_idx, column=options_col)
|
||||
answer_cell = ws.cell(row=row_idx, column=answer_col)
|
||||
|
||||
options_text = options_cell.value
|
||||
if options_text is None:
|
||||
continue
|
||||
options_text = str(options_text).strip()
|
||||
if not options_text:
|
||||
continue
|
||||
|
||||
answer_indices = parse_answer(answer_cell.value)
|
||||
if not answer_indices:
|
||||
continue
|
||||
|
||||
sheet_rows += 1
|
||||
sheet_matches += len(answer_indices)
|
||||
|
||||
rich = build_rich_text(options_text, answer_indices)
|
||||
options_cell.value = rich
|
||||
|
||||
stats["rows"] += sheet_rows
|
||||
stats["matches"] += sheet_matches
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
src = Path(r"D:\下载\安全知识竞赛题库(1).xls")
|
||||
dst = Path(r"D:\下载\安全知识竞赛题库_答案标红.xlsx")
|
||||
|
||||
if not src.exists():
|
||||
print(f"ERROR: Source file not found: {src}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Reading: {src}")
|
||||
wb = load_workbook_from_xls(src)
|
||||
|
||||
stats = {"sheets": 0, "rows": 0, "matches": 0}
|
||||
for ws in wb.worksheets:
|
||||
process_sheet(ws, stats)
|
||||
|
||||
wb.save(dst)
|
||||
print(f"Saved: {dst}")
|
||||
print(f"处理结果: {stats['sheets']} 个工作表, {stats['rows']} 行, {stats['matches']} 个匹配选项")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
57
tools/verify_highlight.py
Normal file
57
tools/verify_highlight.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify highlighted answer output file."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
|
||||
dst = Path(r"D:\下载\安全知识竞赛题库_答案标红.xlsx")
|
||||
src = Path(r"D:\下载\安全知识竞赛题库(1).xls")
|
||||
|
||||
print(f"Source exists: {src.exists()}")
|
||||
print(f"Output exists: {dst.exists()}")
|
||||
|
||||
wb = openpyxl.load_workbook(dst, rich_text=True)
|
||||
ws = wb.active
|
||||
print(f"Sheet: {ws.title}, rows={ws.max_row}, cols={ws.max_column}")
|
||||
|
||||
# Find header columns
|
||||
header_row = None
|
||||
options_col = None
|
||||
answer_col = None
|
||||
for row_idx in range(1, 20):
|
||||
for col_idx in range(1, ws.max_column + 1):
|
||||
val = ws.cell(row=row_idx, column=col_idx).value
|
||||
if val and str(val).strip() == "题目选项":
|
||||
options_col = col_idx
|
||||
if val and str(val).strip() == "题目答案":
|
||||
answer_col = col_idx
|
||||
if options_col and answer_col:
|
||||
header_row = row_idx
|
||||
break
|
||||
|
||||
print(f"Header row={header_row}, options_col={options_col}, answer_col={answer_col}")
|
||||
|
||||
# Sample verification for first 5 rows with answers 1,2,3
|
||||
samples = []
|
||||
for row_idx in range(header_row + 1, ws.max_row + 1):
|
||||
answer = ws.cell(row=row_idx, column=answer_col).value
|
||||
if answer in ("1", "2", "3", 1, 2, 3):
|
||||
cell = ws.cell(row=row_idx, column=options_col)
|
||||
val = cell.value
|
||||
runs = []
|
||||
if hasattr(val, "__iter__") and not isinstance(val, str):
|
||||
for block in val:
|
||||
font = block.font
|
||||
bold = font.b if font else None
|
||||
color = font.color if font else None
|
||||
runs.append((block.text, bold, color))
|
||||
samples.append((row_idx, answer, runs))
|
||||
if len(samples) >= 5:
|
||||
break
|
||||
|
||||
for row_idx, answer, runs in samples:
|
||||
print(f"\nRow {row_idx}, answer={answer}:")
|
||||
for text, bold, color in runs:
|
||||
marker = "RED_BOLD" if bold and color == "FF0000" else "normal"
|
||||
print(f" [{marker}] {repr(text[:50])}")
|
||||
Reference in New Issue
Block a user