2025-04-23 17:48:46 +09:00
|
|
|
import tkinter as tk
|
|
|
|
|
from tkinter import filedialog, messagebox
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
import difflib
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import os
|
|
|
|
|
from lxml import etree as ET
|
|
|
|
|
import re
|
|
|
|
|
from difflib import SequenceMatcher
|
|
|
|
|
import pandas as pd
|
|
|
|
|
import base64
|
2025-05-12 18:24:35 +09:00
|
|
|
import math
|
2025-04-23 17:48:46 +09:00
|
|
|
# from xpathSearch import XMLPathHandler
|
|
|
|
|
|
|
|
|
|
class XMLScorer:
|
|
|
|
|
# 채점 기준 경로 초기화
|
|
|
|
|
def __init__(self, scoring_criteria_path):
|
|
|
|
|
# 채점 기준 로드
|
|
|
|
|
self.scoring_criteria = self._load_scoring_criteria(scoring_criteria_path)
|
|
|
|
|
self.total_score = 0
|
|
|
|
|
self.partial_score = 0
|
|
|
|
|
self.typo_score = 0
|
|
|
|
|
|
|
|
|
|
def set_typo_score(self, score):
|
|
|
|
|
self.typo_score = score
|
|
|
|
|
|
|
|
|
|
def get_typo_score(self):
|
|
|
|
|
return self.typo_score
|
|
|
|
|
|
|
|
|
|
# 채점 기준파일 로드(JSON 파일)
|
|
|
|
|
def _load_scoring_criteria(self, file_path):
|
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
2025-05-12 18:24:35 +09:00
|
|
|
# mm to pt
|
|
|
|
|
def convert_mm_to_pt(self, mm):
|
|
|
|
|
one_mm_per_pt = 2.83465
|
|
|
|
|
hwp_internal_conversion_method = 100
|
|
|
|
|
pt = math.trunc(mm * one_mm_per_pt * hwp_internal_conversion_method)
|
|
|
|
|
return pt
|
|
|
|
|
|
2025-04-23 17:48:46 +09:00
|
|
|
# XML 파일에서 element의 값을 찾아 반환
|
|
|
|
|
def query_xml(self, root, *args):
|
|
|
|
|
first_xpath = args[0]
|
|
|
|
|
second_xpath = args[1]
|
|
|
|
|
points = args[2]
|
|
|
|
|
category = args[3]
|
|
|
|
|
|
|
|
|
|
if ("특수문자" in category) and (second_xpath is not None):
|
|
|
|
|
try:
|
|
|
|
|
result = root.xpath(first_xpath)
|
|
|
|
|
# 결과값이 리스트형인데 내부에 정보가 없는경우
|
|
|
|
|
# 결과값이 없음
|
|
|
|
|
if type(result) is list and len(result) == 0:
|
|
|
|
|
return None
|
|
|
|
|
elif result < points:
|
|
|
|
|
result = root.xpath(second_xpath)
|
|
|
|
|
return result
|
|
|
|
|
else:
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
except ET.XPathEvalError as e:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
elif second_xpath is not None:
|
|
|
|
|
try:
|
|
|
|
|
result1 = root.xpath(first_xpath)
|
|
|
|
|
result2 = root.xpath(second_xpath)
|
|
|
|
|
if (type(result1) is list and len(result1) == 0) and (type(result2) is list and len(result2) == 0):
|
|
|
|
|
return None
|
|
|
|
|
return result1 if result1 else result2
|
|
|
|
|
|
|
|
|
|
except ET.XPathEvalError as e:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
result = root.xpath(first_xpath)
|
|
|
|
|
if type(result) is list and len(result) == 0:
|
|
|
|
|
return None
|
|
|
|
|
return result
|
|
|
|
|
except ET.XPathEvalError as e:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def chart_query_xml(self, tree, xpath, namespaces):
|
|
|
|
|
result = tree.xpath(xpath, namespaces=namespaces)
|
|
|
|
|
if type(result) is list and len(result) == 0:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
# 유사한 텍스트 찾기
|
|
|
|
|
def find_similar_text(self, root, target_text, threshold=0.7):
|
|
|
|
|
"""
|
|
|
|
|
전체 문서에서 유사한 텍스트를 찾아 반환
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
root (_type_): xml root element 객체
|
|
|
|
|
target_text (_type_): 찾을 텍스트
|
|
|
|
|
threshold (float, optional): 유사도 설정 Defaults to 0.3.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: 유사도 기준을 만족하는 텍스트
|
|
|
|
|
"""
|
|
|
|
|
# 전체 텍스트 추출
|
|
|
|
|
# all_text = root.xpath(f"//CHAR/text()")
|
|
|
|
|
# all_text.append(root.xpath(f"//TEXTART/@text"))
|
|
|
|
|
|
|
|
|
|
namespaces = {
|
|
|
|
|
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
|
|
|
|
|
'c': 'http://schemas.openxmlformats.org/drawingml/2006/chart'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
all_text = root.xpath(f"//BODY//text() | //TEXTART/@Text | //c:chart//text()", namespaces=namespaces)
|
|
|
|
|
|
|
|
|
|
# 유사도 비교
|
|
|
|
|
max_score = 0
|
|
|
|
|
similar_text = ''
|
|
|
|
|
|
|
|
|
|
for text in all_text:
|
|
|
|
|
score = SequenceMatcher(None, target_text, text).ratio()
|
|
|
|
|
|
|
|
|
|
if score > max_score:
|
|
|
|
|
max_score = score
|
|
|
|
|
similar_text = text
|
|
|
|
|
|
|
|
|
|
if max_score >= threshold:
|
|
|
|
|
return similar_text
|
|
|
|
|
else:
|
|
|
|
|
return target_text
|
|
|
|
|
|
|
|
|
|
# 정답 비교 및 점수 계산
|
|
|
|
|
def evaluate_answer(self, scoring, user_answer, right_answer, points,
|
|
|
|
|
method="equal", tolerance=0):
|
|
|
|
|
|
|
|
|
|
scoring['user_answer'] = user_answer
|
|
|
|
|
|
|
|
|
|
is_correct = False
|
|
|
|
|
|
|
|
|
|
if method == "equal":
|
|
|
|
|
is_correct = (user_answer == right_answer)
|
|
|
|
|
elif method == "tolerance":
|
|
|
|
|
if isinstance(user_answer, dict) and isinstance(right_answer, dict):
|
|
|
|
|
is_correct = all(abs(user_answer[k] - right_answer[k]) <= tolerance for k in right_answer)
|
|
|
|
|
else:
|
|
|
|
|
is_correct = abs(user_answer - right_answer) <= tolerance
|
|
|
|
|
elif method == "in":
|
|
|
|
|
is_correct = user_answer in right_answer
|
2025-04-25 17:57:04 +09:00
|
|
|
elif method == "partial_score":
|
|
|
|
|
# 부분 점수 계산
|
|
|
|
|
is_correct = isinstance(user_answer, (int, float)) and user_answer <= right_answer
|
|
|
|
|
points = min(points, user_answer)
|
2025-04-23 17:48:46 +09:00
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unknown comparison method: {method}")
|
|
|
|
|
|
|
|
|
|
if is_correct:
|
|
|
|
|
scoring['points'] = points
|
|
|
|
|
self.total_score += points
|
|
|
|
|
self.partial_score += points
|
|
|
|
|
else:
|
|
|
|
|
scoring['points'] = 0
|
|
|
|
|
|
|
|
|
|
# 하나의 XML 파일 채점
|
|
|
|
|
def _score_xml_file(self, xml_file, chart_xml):
|
|
|
|
|
try:
|
|
|
|
|
tree = ET.parse(xml_file)
|
|
|
|
|
root = tree.getroot()
|
|
|
|
|
|
|
|
|
|
# 네임스페이스 정의
|
|
|
|
|
namespaces = {
|
|
|
|
|
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
|
|
|
|
|
'c': 'http://schemas.openxmlformats.org/drawingml/2006/chart'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 차트 XML 파일이 없는 경우 0점 채점을 위헤 빈 XML 생성
|
|
|
|
|
if chart_xml is None:
|
|
|
|
|
chart_tree = ET.fromstring('<xml></xml>')
|
|
|
|
|
else:
|
|
|
|
|
chart_tree = ET.fromstring(chart_xml)
|
|
|
|
|
|
|
|
|
|
# 결과값을 Dictionary로 저장
|
|
|
|
|
# 하나의 xml파일 = 수험생 한명의 답안지
|
|
|
|
|
onePersonResult = {
|
|
|
|
|
'filename': os.path.basename(xml_file),
|
|
|
|
|
'score_results': [],
|
|
|
|
|
'total_score': 0,
|
|
|
|
|
'partial_scores': []
|
|
|
|
|
}
|
|
|
|
|
print(f"File name: {onePersonResult['filename']}")
|
|
|
|
|
|
|
|
|
|
self.total_score = 0
|
|
|
|
|
for section_id, section in self.scoring_criteria.items():
|
|
|
|
|
self.partial_score = 0
|
|
|
|
|
for criterion_id, criterion in section.items():
|
|
|
|
|
id = criterion_id
|
|
|
|
|
xpath = criterion.get('path', None)
|
2025-04-25 17:57:04 +09:00
|
|
|
xpath2 = criterion.get('path2', None)
|
2025-05-12 18:24:35 +09:00
|
|
|
xpath3 = criterion.get('path3', None)
|
2025-04-23 17:48:46 +09:00
|
|
|
search_value = criterion.get('searchValue', None)
|
|
|
|
|
right_answer = criterion.get('value', None)
|
|
|
|
|
points = criterion.get('points', 0)
|
|
|
|
|
category = criterion.get('category', None)
|
|
|
|
|
item = criterion.get('item', None)
|
|
|
|
|
similar_text = None
|
|
|
|
|
|
|
|
|
|
# search_value가 있는 경우
|
|
|
|
|
if search_value is not None:
|
|
|
|
|
# search_value를 포함하는 텍스트 찾기
|
|
|
|
|
similar_text = self.find_similar_text(root, search_value)
|
|
|
|
|
xpath = xpath.replace('{searchValue}', similar_text)
|
2025-05-13 19:46:19 +09:00
|
|
|
if xpath2 is not None:
|
|
|
|
|
xpath2 = xpath2.replace('{searchValue}', similar_text)
|
2025-04-23 17:48:46 +09:00
|
|
|
|
|
|
|
|
# 문항 별 채점 결과 저장
|
|
|
|
|
scoring = {
|
|
|
|
|
'section': section_id,
|
|
|
|
|
'id': id,
|
|
|
|
|
'category': category, # 채점 분류
|
|
|
|
|
'item': item, # 채점 항목
|
|
|
|
|
'right_answer': right_answer, # 정답
|
|
|
|
|
'user_answer': None, # 실제 작성 답안
|
|
|
|
|
'points': 0, # 점수
|
|
|
|
|
'deductions': [] # 각 기준별 감점 내역
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if "PageSetting" in (category or ""):
|
|
|
|
|
items = root.xpath(xpath)
|
|
|
|
|
error_range = criterion.get('tolerance', 0)
|
|
|
|
|
|
|
|
|
|
for item in items:
|
|
|
|
|
user_answer = {
|
|
|
|
|
'Bottom' : int(item.get("Bottom", 0)),
|
|
|
|
|
'Footer' : int(item.get("Footer", 0)),
|
|
|
|
|
'Gutter' : int(item.get("Gutter", 0)),
|
|
|
|
|
'Header' : int(item.get("Header", 0)),
|
|
|
|
|
'Left' : int(item.get("Left", 0)),
|
|
|
|
|
'Right' : int(item.get("Right", 0)),
|
|
|
|
|
'Top' : int(item.get("Top", 0)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance", tolerance=error_range)
|
|
|
|
|
|
|
|
|
|
if scoring['points'] > 0:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
elif "BasicSetting" in (category or ""):
|
|
|
|
|
# 바탕글(기본설정) 요소
|
|
|
|
|
normal_style = root.xpath("//STYLE[@Name='바탕글']")
|
|
|
|
|
|
|
|
|
|
# CHARSHAPE, PARASHAPE, FONTID 속성값 추출
|
|
|
|
|
charshape_id = normal_style[0].get("CharShape")
|
|
|
|
|
parashape_id = normal_style[0].get("ParaShape")
|
|
|
|
|
font_id = root.xpath(f"//CHARSHAPE[@Id='{charshape_id}']/FONTID/@Hangul")
|
|
|
|
|
|
|
|
|
|
# 필요한 속성값을 이용하여 정답과 비교
|
|
|
|
|
font_name = root.xpath(f"//FONT[@Id='{font_id[0]}']/@Name")
|
|
|
|
|
font_size = root.xpath(f"//CHARSHAPE[@Id='{charshape_id}']/@Height")
|
|
|
|
|
alignment = root.xpath(f"//PARASHAPE[@Id='{parashape_id}']/@Align")
|
|
|
|
|
line_spacing = root.xpath(f"//PARASHAPE[@Id='{parashape_id}']/PARAMARGIN/@LineSpacing")
|
|
|
|
|
|
|
|
|
|
# 수험자 답안
|
|
|
|
|
user_answer = {
|
2025-05-13 19:46:19 +09:00
|
|
|
########################################################################
|
|
|
|
|
# 보통 정답은 글꼴이 [바탕]이어야 하지만 대부분 설정하지 않고
|
|
|
|
|
# 기본 [함초롱바탕]으로 두는 경우가 많아
|
|
|
|
|
# 폰트명을 정답과 비교하면 감점이 많이 발생해서
|
|
|
|
|
# 폰트명은 정답과 비교하지 않게 적용된 상태
|
|
|
|
|
########################################################################
|
|
|
|
|
#'FontName': font_name[0],
|
|
|
|
|
'FontName': "바탕",
|
2025-04-23 17:48:46 +09:00
|
|
|
'FontSize': font_size[0],
|
|
|
|
|
'Alignment': alignment[0],
|
|
|
|
|
'LineSpacing': line_spacing[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
|
|
|
|
|
|
|
|
|
|
# 오타 감점 부분은 미리 계산 하고, 이후 점수만 계산
|
|
|
|
|
elif "오타감점" in (category or ""):
|
|
|
|
|
points = self.get_typo_score()
|
|
|
|
|
self.total_score += points
|
|
|
|
|
self.partial_score += points
|
|
|
|
|
scoring['points'] = points
|
|
|
|
|
|
|
|
|
|
# 정답이 하나인 경우
|
|
|
|
|
elif "SingleAnswer" in (category or ""):
|
|
|
|
|
items = root.xpath(xpath)
|
|
|
|
|
|
|
|
|
|
for item in items:
|
|
|
|
|
user_answer = item
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points)
|
|
|
|
|
|
|
|
|
|
if scoring['points'] > 0:
|
|
|
|
|
break
|
2025-05-12 18:24:35 +09:00
|
|
|
|
2025-05-13 19:46:19 +09:00
|
|
|
elif "DoubleAnswer" in (category or ""):
|
|
|
|
|
items1 = root.xpath(xpath)
|
|
|
|
|
items2 = root.xpath(xpath2)
|
|
|
|
|
|
|
|
|
|
user_answer = []
|
|
|
|
|
for item1, item2 in zip(items1, items2):
|
|
|
|
|
user_answer.append(item1)
|
|
|
|
|
user_answer.append(item2)
|
|
|
|
|
# user_answer[0] = item1
|
|
|
|
|
# user_answer[1] = item2
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points)
|
|
|
|
|
if scoring['points'] > 0:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 사용자 입력값이 mm단위인 경우
|
2025-05-12 18:24:35 +09:00
|
|
|
elif "mmSize" in (category or ""):
|
|
|
|
|
items = root.xpath(xpath)
|
|
|
|
|
error_range = criterion.get('tolerance', 0)
|
|
|
|
|
|
|
|
|
|
for item in items:
|
2025-05-13 19:46:19 +09:00
|
|
|
user_answer = float(item)
|
|
|
|
|
right_answer = self.convert_mm_to_pt(float(right_answer))
|
2025-05-12 18:24:35 +09:00
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance")
|
|
|
|
|
|
|
|
|
|
if scoring['points'] > 0:
|
|
|
|
|
break
|
|
|
|
|
|
2025-04-25 17:57:04 +09:00
|
|
|
elif "ParaShape" in (category or ""):
|
|
|
|
|
items = root.xpath(xpath)
|
|
|
|
|
|
|
|
|
|
for item in items:
|
|
|
|
|
user_answer = {
|
|
|
|
|
'Left': float(item.get('Left', 0)) / 200,
|
|
|
|
|
'Indent': float(item.get('Indent', 0)) / -200,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 정답과 수험자 답안 비교
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
|
|
|
|
|
|
|
|
|
|
if scoring['points'] > 0:
|
|
|
|
|
break
|
|
|
|
|
|
2025-04-23 17:48:46 +09:00
|
|
|
# Boolean 타입 정답인 경우
|
|
|
|
|
elif "Boolean" in (category or ""):
|
|
|
|
|
items = root.xpath(xpath)
|
2025-05-14 18:01:36 +09:00
|
|
|
items2 = root.xpath(xpath2) if xpath2 else False
|
|
|
|
|
|
|
|
|
|
user_answer = bool( items or items2 )
|
2025-04-23 17:48:46 +09:00
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points)
|
|
|
|
|
|
|
|
|
|
# 채점기준표 파일에 작성된 rgb값을 그대로 읽어와 HML파일 요소의 int형 rgb값과 비교
|
|
|
|
|
elif "Color" in (category or ""):
|
|
|
|
|
items = root.xpath(xpath)
|
|
|
|
|
|
|
|
|
|
rgb_text = right_answer
|
|
|
|
|
r, g, b = map(int, rgb_text.split(','))
|
|
|
|
|
rgb_int = (b << 16) + (g << 8) + r
|
|
|
|
|
|
|
|
|
|
for item in items:
|
|
|
|
|
user_answer = int(item)
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, rgb_int, points, method="equal")
|
|
|
|
|
|
|
|
|
|
if scoring['points'] > 0:
|
|
|
|
|
break
|
|
|
|
|
|
2025-04-25 17:57:04 +09:00
|
|
|
# 문단 첫글자 장식 채점
|
2025-04-23 17:48:46 +09:00
|
|
|
elif "TwoLineSize" in (category or ""):
|
|
|
|
|
items = root.xpath(xpath)
|
|
|
|
|
error_range = criterion.get('tolerance', 0)
|
|
|
|
|
for item in items:
|
|
|
|
|
user_answer = {
|
|
|
|
|
"Height": int(item.get('Height', 0)),
|
|
|
|
|
"Width": int(item.get('Width', 0))
|
|
|
|
|
}
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance", tolerance=error_range)
|
|
|
|
|
|
|
|
|
|
if scoring['points'] > 0:
|
|
|
|
|
break
|
|
|
|
|
|
2025-04-25 17:57:04 +09:00
|
|
|
# 폰트명
|
2025-04-23 17:48:46 +09:00
|
|
|
elif "FontName" in (category or ""):
|
|
|
|
|
charshape_id = root.xpath(xpath)
|
|
|
|
|
if not charshape_id:
|
|
|
|
|
charshape_id = None
|
2025-04-24 18:02:32 +09:00
|
|
|
user_answer = None
|
2025-04-23 17:48:46 +09:00
|
|
|
else:
|
|
|
|
|
font_id = root.xpath(f"//CHARSHAPE[@Id='{charshape_id[0]}']/FONTID/@Hangul")
|
|
|
|
|
font_name = root.xpath(f"//FONTFACE[@Lang='Hangul']/FONT[@Id='{font_id[0]}']/@Name")
|
2025-04-24 18:02:32 +09:00
|
|
|
user_answer = font_name[0]
|
2025-05-14 18:01:36 +09:00
|
|
|
|
|
|
|
|
# 폰트 "견고딕"과 "중고딕"은
|
|
|
|
|
# 한글프로그램 내부적으로 "한양견고딕", "한양중고딕"으로 저장되므로
|
|
|
|
|
# 수험자 답변에서 "한양"을 제거
|
|
|
|
|
if right_answer in ["견고딕", "중고딕"]:
|
|
|
|
|
user_answer = user_answer.replace("한양", "")
|
2025-04-23 17:48:46 +09:00
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
|
|
|
|
|
|
2025-04-25 17:57:04 +09:00
|
|
|
# 폰트 속성
|
2025-04-24 18:02:32 +09:00
|
|
|
elif "FontAttribute" in (category or ""):
|
|
|
|
|
charshape = root.xpath(xpath)
|
|
|
|
|
if not charshape:
|
|
|
|
|
charshape = None
|
|
|
|
|
user_answer = None
|
|
|
|
|
else:
|
|
|
|
|
font_attribute = charshape[0].find(right_answer)
|
|
|
|
|
if font_attribute is not None:
|
|
|
|
|
user_answer = font_attribute.tag
|
2025-04-25 17:57:04 +09:00
|
|
|
else:
|
|
|
|
|
user_answer = None
|
2025-04-24 18:02:32 +09:00
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
|
2025-04-25 17:57:04 +09:00
|
|
|
|
|
|
|
|
# 특수문자 갯수 채점
|
|
|
|
|
elif "SpecialChar" in (category or ""):
|
|
|
|
|
ch1 = criterion.get('char1', None)
|
|
|
|
|
ch2 = criterion.get('char2', None)
|
2025-05-12 18:24:35 +09:00
|
|
|
ch3 = criterion.get('char3', None)
|
2025-04-25 17:57:04 +09:00
|
|
|
xpath = xpath.replace('{char1}', ch1)
|
|
|
|
|
xpath2 = xpath2.replace('{char2}', ch2)
|
2025-05-12 18:24:35 +09:00
|
|
|
xpath3 = xpath3.replace('{char3}', ch3)
|
2025-04-25 17:57:04 +09:00
|
|
|
char1_ele = root.xpath(xpath)
|
|
|
|
|
char2_ele = root.xpath(xpath2)
|
2025-05-12 18:24:35 +09:00
|
|
|
char3_ele = root.xpath(xpath3)
|
2025-04-25 17:57:04 +09:00
|
|
|
sum_char = 0
|
|
|
|
|
|
|
|
|
|
# char1 요소에서 특수문자 갯수 세기 (최대 2점)
|
2025-05-12 18:24:35 +09:00
|
|
|
for item in char1_ele or []:
|
|
|
|
|
count_char1 = item.text.count(ch1)
|
|
|
|
|
sum_char += count_char1
|
|
|
|
|
if sum_char >= 2:
|
|
|
|
|
sum_char = 2
|
|
|
|
|
break
|
2025-04-25 17:57:04 +09:00
|
|
|
|
|
|
|
|
# char2 요소에서 특수문자 갯수 세기 (최대 1점)
|
2025-05-13 19:46:19 +09:00
|
|
|
# char1과 char2가 다른 경우 (예: ▶ 행사안내 ◀)
|
2025-05-12 18:24:35 +09:00
|
|
|
if (ch1 != ch2) and char2_ele:
|
2025-04-25 17:57:04 +09:00
|
|
|
count_char2 = char2_ele[0].text.count(ch2)
|
|
|
|
|
if count_char2 > 1:
|
|
|
|
|
count_char2 = 1
|
|
|
|
|
sum_char += count_char2
|
2025-05-12 18:24:35 +09:00
|
|
|
|
|
|
|
|
# char2 요소에서 특수문자 갯수 세기 (최대 1점)
|
|
|
|
|
if char3_ele:
|
|
|
|
|
count_char3 = char3_ele[0].text.count(ch3)
|
|
|
|
|
if count_char3 > 1:
|
|
|
|
|
count_char3 = 1
|
|
|
|
|
sum_char += count_char3
|
2025-04-25 17:57:04 +09:00
|
|
|
|
|
|
|
|
user_answer = sum_char
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="partial_score")
|
|
|
|
|
|
2025-05-13 19:46:19 +09:00
|
|
|
# 쪽 테두리 (이중 실선, 머리말 포함) 설정
|
2025-04-30 17:54:55 +09:00
|
|
|
elif "PageBorder" in (category or ""):
|
2025-05-13 19:46:19 +09:00
|
|
|
user_answer = {
|
|
|
|
|
"header_inside": False,
|
|
|
|
|
"all_double_slim": False
|
|
|
|
|
}
|
2025-04-25 17:57:04 +09:00
|
|
|
|
2025-05-13 19:46:19 +09:00
|
|
|
# 머릿말 포함 객체가 하나라도 있으면 정답
|
|
|
|
|
header_inside_elements = root.xpath(xpath)
|
|
|
|
|
for header_inside in header_inside_elements:
|
|
|
|
|
# print("머릿말포함: ",header_inside)
|
|
|
|
|
if "true" in header_inside:
|
|
|
|
|
user_answer["header_inside"] = True
|
2025-04-25 17:57:04 +09:00
|
|
|
|
2025-05-13 19:46:19 +09:00
|
|
|
# BORDERFILL요소의 자녀
|
|
|
|
|
# LEFTBORDER, RIGHTBORDER, TOPBORDER, BOTTOMBORDER 요소의 Type속성이
|
|
|
|
|
# 모두 DoubleSlim이면 정답
|
|
|
|
|
border_fill_elements = root.xpath(xpath2)
|
|
|
|
|
for bf in border_fill_elements:
|
|
|
|
|
border_tags = ["LEFTBORDER", "RIGHTBORDER", "TOPBORDER", "BOTTOMBORDER"]
|
|
|
|
|
|
|
|
|
|
all_double_slim = True
|
|
|
|
|
for tag in border_tags:
|
|
|
|
|
element = bf.find(tag)
|
|
|
|
|
if element is None or element.get("Type") != "DoubleSlim":
|
|
|
|
|
all_double_slim = False
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
#모든 BORDER 태그의 Type 속성이 'DoubleSlim'
|
|
|
|
|
if all_double_slim:
|
|
|
|
|
user_answer["all_double_slim"] = True
|
|
|
|
|
|
|
|
|
|
# 하나 이상의 BORDER 태그가 'DoubleSlim'이 아님
|
|
|
|
|
else:
|
|
|
|
|
user_answer["all_double_slim"] = False
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
|
|
|
|
|
|
2025-05-14 18:01:36 +09:00
|
|
|
elif "Hanja" in (category or ""):
|
|
|
|
|
word_list = criterion.get('word', [])
|
|
|
|
|
|
|
|
|
|
# 점수 계산
|
|
|
|
|
score = 0
|
|
|
|
|
max_score = points
|
|
|
|
|
|
|
|
|
|
# 부분점수 (최대점수에서 한자 갯수만큼 나눈 몫)
|
|
|
|
|
score_per_pair = max_score // len(word_list)
|
|
|
|
|
|
|
|
|
|
# 한자가 5개 고정일 경우
|
|
|
|
|
# score_per_pair = 2
|
|
|
|
|
|
|
|
|
|
for kor, chn in word_list:
|
|
|
|
|
# XPath 구문 구성 및 실행
|
|
|
|
|
exec_xpath = xpath.replace('{kor}', kor).replace('{chn}', chn)
|
|
|
|
|
matched = root.xpath(exec_xpath)
|
|
|
|
|
|
|
|
|
|
if matched:
|
|
|
|
|
score += score_per_pair
|
|
|
|
|
|
|
|
|
|
# 최대 점수 초과 방지
|
|
|
|
|
user_answer = min(score, max_score)
|
|
|
|
|
|
|
|
|
|
self.evaluate_answer(scoring, user_answer, right_answer, points, method="partial_score")
|
2025-04-24 18:02:32 +09:00
|
|
|
|
2025-04-23 17:48:46 +09:00
|
|
|
onePersonResult['score_results'].append(scoring)
|
|
|
|
|
print(f'scoring: {scoring}')
|
|
|
|
|
onePersonResult['partial_scores'].append({
|
|
|
|
|
'section': section_id,
|
|
|
|
|
'score': self.partial_score
|
|
|
|
|
})
|
|
|
|
|
onePersonResult['total_score'] = self.total_score
|
|
|
|
|
return onePersonResult
|
|
|
|
|
|
|
|
|
|
except ET.ParseError as e:
|
|
|
|
|
return {
|
|
|
|
|
'filename': os.path.basename(xml_file),
|
|
|
|
|
'error': f"XML 파싱 오류: {str(e)}",
|
|
|
|
|
'total_score': 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def binary_to_chartxml(self, xml_path):
|
|
|
|
|
tree = ET.parse(xml_path)
|
|
|
|
|
root = tree.getroot()
|
|
|
|
|
|
|
|
|
|
binary_data = root.xpath('//BINDATA[@Id=//BINITEM[@Format="OLE"]/@BinData]/text()')
|
|
|
|
|
if not binary_data:
|
|
|
|
|
return None
|
|
|
|
|
binary_data = binary_data[0].encode('utf-8')
|
|
|
|
|
|
|
|
|
|
# <BINDATA ...> 태그와 그 내부 내용을 삭제합니다.
|
|
|
|
|
encoded_data = re.sub(b'<BINDATA.*?>', b'', binary_data)
|
|
|
|
|
encoded_data = encoded_data.replace(b'</BINDATA>', b'')
|
|
|
|
|
encoded_data = encoded_data.replace(b'\r\n', b'')
|
|
|
|
|
|
|
|
|
|
# base64 디코딩을 수행합니다.
|
|
|
|
|
decoded_data = base64.b64decode(encoded_data+b'==')
|
|
|
|
|
|
|
|
|
|
# 디코딩된 데이터 내용 중 xml 형식만 추출할 때 <c:chartSpace>, </c:chartSpace> 사이의 데이터만 추출.
|
|
|
|
|
start = decoded_data.find(b'<?xml')
|
|
|
|
|
print(start)
|
|
|
|
|
end = decoded_data.find(b'</c:chartSpace>')
|
|
|
|
|
print(end)
|
|
|
|
|
xml_data = decoded_data[start:end+len(b'</c:chartSpace>')]
|
|
|
|
|
|
|
|
|
|
# xml 데이터가 없는 경우 None을 반환합니다.
|
|
|
|
|
if -1 in [start, end]:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 디코딩된 데이터를 파일로 저장합니다.
|
|
|
|
|
base_filename = os.path.splitext(xml_path)[0]
|
|
|
|
|
new_filename = f'{base_filename}.xml'
|
|
|
|
|
with open(new_filename, 'wb') as file:
|
|
|
|
|
file.write(xml_data)
|
|
|
|
|
|
|
|
|
|
return xml_data
|
|
|
|
|
|
|
|
|
|
def typo_check(self, correct_answer_file, user_answer_file):
|
|
|
|
|
user_answer_tree = ET.parse(user_answer_file)
|
|
|
|
|
user_answer_root = user_answer_tree.getroot()
|
|
|
|
|
correct_answer_tree = ET.parse(correct_answer_file)
|
|
|
|
|
correct_answer_root = correct_answer_tree.getroot()
|
|
|
|
|
|
|
|
|
|
# xpath로 바이너리 부분추출
|
|
|
|
|
user_input_text = user_answer_root.xpath('//CHAR//text()[not(ancestor::HEADER) and not(ancestor::TABLE)]')
|
|
|
|
|
user_table_text = user_answer_root.xpath('//TABLE//CHAR//text()')
|
|
|
|
|
user_input_text += user_table_text
|
|
|
|
|
|
|
|
|
|
correct_input_text = correct_answer_root.xpath('//CHAR//text()[not(ancestor::HEADER) and not(ancestor::TABLE)]')
|
|
|
|
|
correct_table_text = correct_answer_root.xpath('//TABLE//CHAR//text()')
|
|
|
|
|
correct_input_text += correct_table_text
|
|
|
|
|
|
|
|
|
|
# 각 요소에서 공백 제거
|
|
|
|
|
user_input_text = [text.replace(' ', '') for text in user_input_text]
|
|
|
|
|
correct_input_text = [text.replace(' ', '') for text in correct_input_text]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 숫자와 특정 형식 제거 (예: 1., 2., 3., -)
|
|
|
|
|
user_input_text = [re.sub(r'\d+\.\s*|-', '', text) for text in user_input_text]
|
|
|
|
|
correct_input_text = [re.sub(r'\d+\.\s*|-', '', text) for text in correct_input_text]
|
|
|
|
|
|
|
|
|
|
try :
|
|
|
|
|
xpath = self.scoring_criteria["2-29"]['path'].split("'")[1]
|
|
|
|
|
ignore_word = xpath.split("'")[1]
|
|
|
|
|
# 특정 단어 제거
|
|
|
|
|
# 오타와 누락의 경우만 판단하면 정상작동하지만
|
|
|
|
|
# 추가 된 단어의 경우를 채점기준에 추가하면 정확하게 채점 되지 않을 수 있음
|
|
|
|
|
# [정답] Hybrid [실제작성]
|
|
|
|
|
user_input_text = [text.replace(ignore_word, '') for text in user_input_text]
|
|
|
|
|
correct_input_text = [text.replace(ignore_word, '') for text in correct_input_text]
|
|
|
|
|
except (KeyError, IndexError, AttributeError):
|
|
|
|
|
ignore_word = None
|
|
|
|
|
|
|
|
|
|
print(f"ignore_word: {ignore_word}")
|
|
|
|
|
|
|
|
|
|
# 리스트를 하나의 문자열로 변경
|
|
|
|
|
user_input_text_str = ''.join(user_input_text)
|
|
|
|
|
currect_input_text_str = ''.join(correct_input_text)
|
|
|
|
|
|
|
|
|
|
print("user_input_text as string:")
|
|
|
|
|
print(user_input_text_str)
|
|
|
|
|
print("\ncurrect_input_text_answer as string:")
|
|
|
|
|
print(currect_input_text_str)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 문자열의 차이를 비교
|
|
|
|
|
diff = difflib.ndiff(currect_input_text_str, user_input_text_str)
|
|
|
|
|
diff_list = list(diff)
|
|
|
|
|
|
|
|
|
|
# 차이점을 정리하여 result_diff에 저장
|
|
|
|
|
result_diff = []
|
|
|
|
|
|
|
|
|
|
# 누락 된 단어만 따로 리스트로 저장
|
|
|
|
|
missing_list = []
|
|
|
|
|
|
|
|
|
|
# 오타와 누락된 단어 리스트 저장
|
|
|
|
|
error_missing_list = []
|
|
|
|
|
|
|
|
|
|
skip_next = False
|
|
|
|
|
|
|
|
|
|
for i, line in enumerate(diff_list):
|
|
|
|
|
if skip_next:
|
|
|
|
|
skip_next = False
|
|
|
|
|
continue
|
|
|
|
|
# diff_list의 line 시작이 '-'이면서 다음 line이 '+'이면 두 line을 붙여서 맞춤법이 틀린 단어로 판단
|
|
|
|
|
if line.startswith('- '):
|
|
|
|
|
# 오타
|
|
|
|
|
if i + 1 < len(diff_list) and diff_list[i + 1].startswith('+ '):
|
|
|
|
|
line = line.replace('- ', '-')
|
|
|
|
|
next = diff_list[i + 1].replace('+ ', '')
|
|
|
|
|
result_diff.append(line+'=>'+next)
|
|
|
|
|
error_missing_list.append(line+'=>'+next)
|
|
|
|
|
skip_next = True
|
|
|
|
|
# 누락
|
|
|
|
|
else:
|
|
|
|
|
line = line.replace('- ', '-')
|
|
|
|
|
result_diff.append(line)
|
|
|
|
|
missing_list.append(line)
|
|
|
|
|
error_missing_list.append(line)
|
|
|
|
|
# 없어도 되는 글자가 있는 경우 (추가)
|
|
|
|
|
elif line.startswith('+ '):
|
|
|
|
|
line = line.replace('+ ', '+')
|
|
|
|
|
result_diff.append(line)
|
|
|
|
|
|
|
|
|
|
# result_diff 출력
|
|
|
|
|
# print("\nResult Differences:")
|
|
|
|
|
# for diff in result_diff:
|
|
|
|
|
# print(diff)
|
|
|
|
|
|
|
|
|
|
# result_diff 배열의 길이를 맨 앞에 저장
|
|
|
|
|
|
|
|
|
|
# 모든 차이를 계산해 점수 차감
|
|
|
|
|
# temp = 40 - min(len(result_diff)*2, 40)
|
|
|
|
|
|
|
|
|
|
# 누락된 텍스트만 계산해 점수 차감
|
|
|
|
|
# temp = 40 - min(len(missing_list)*2, 40)
|
|
|
|
|
|
|
|
|
|
# 2503회 기준 오타 1개당 [2점]->[1점] 차감
|
|
|
|
|
temp = 40 - min(len(error_missing_list)*1, 40)
|
|
|
|
|
|
|
|
|
|
self.set_typo_score(temp)
|
|
|
|
|
|
|
|
|
|
result_diff.insert(0, temp)
|
|
|
|
|
return result_diff
|
|
|
|
|
|
|
|
|
|
# XML 파일 채점
|
|
|
|
|
def score_directory(self, xml_directory, correct_answer_file):
|
|
|
|
|
# xml 파일 불러오기
|
|
|
|
|
xml_files = Path(xml_directory).glob('*.hml')
|
|
|
|
|
|
|
|
|
|
# 채점결과 저장할 리스트
|
|
|
|
|
score_results = []
|
|
|
|
|
|
|
|
|
|
for user_answer_file in xml_files:
|
|
|
|
|
score_result = {}
|
|
|
|
|
chart_xml = self.binary_to_chartxml(user_answer_file)
|
|
|
|
|
score_result['typo'] = self.typo_check(correct_answer_file, user_answer_file)
|
|
|
|
|
score_result['score'] = self._score_xml_file(user_answer_file, chart_xml)
|
|
|
|
|
# score_result['score']['score_results'][2]['points'] = score_result['typo'][0]
|
|
|
|
|
score_results.append(score_result)
|
|
|
|
|
return score_results
|
|
|
|
|
|
|
|
|
|
def parse_filename(self, filename):
|
|
|
|
|
if isinstance(filename, dict):
|
|
|
|
|
filename = filename.get('파일명', '')
|
|
|
|
|
match = re.match(r'.*-(\d+)-(.+)\.hml', filename)
|
|
|
|
|
if match:
|
|
|
|
|
number = match.group(1)
|
|
|
|
|
name = match.group(2)
|
|
|
|
|
return number, name
|
|
|
|
|
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
def export_to_excel(self, results, output_path=None):
|
|
|
|
|
if output_path is None:
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") #연월일_시분초
|
|
|
|
|
# timestamp = datetime.now().strftime("%Y%m%d") #연월일
|
|
|
|
|
output_path = f"scoring_results_{timestamp}.xlsx"
|
|
|
|
|
|
|
|
|
|
summary_data = []
|
|
|
|
|
detail_data = []
|
|
|
|
|
typo_data = []
|
|
|
|
|
|
|
|
|
|
for temp in results:
|
|
|
|
|
# 요약 정보
|
|
|
|
|
result = temp['score']
|
|
|
|
|
summary_row = {
|
|
|
|
|
'파일명': result['filename'],
|
|
|
|
|
'총점': result.get('total_score', 0)
|
|
|
|
|
}
|
|
|
|
|
if 'error' in result:
|
|
|
|
|
summary_row['오류'] = result['error']
|
|
|
|
|
|
|
|
|
|
summary_data.append(summary_row)
|
|
|
|
|
|
|
|
|
|
# 상세 정보
|
|
|
|
|
if 'score_results' in result:
|
|
|
|
|
filename = {'파일명': result['filename']}
|
|
|
|
|
number, name = self.parse_filename(filename)
|
|
|
|
|
if (number or name) is None:
|
|
|
|
|
detail_row = {'채점항목': result['filename'] }
|
|
|
|
|
else:
|
|
|
|
|
detail_row = {'채점항목':f"{number}-{name}"}
|
|
|
|
|
|
|
|
|
|
section_num = None
|
|
|
|
|
row_index = []
|
|
|
|
|
for i, score_result in enumerate(result['score_results']):
|
|
|
|
|
current_section = score_result['section']
|
|
|
|
|
|
|
|
|
|
if section_num is None:
|
|
|
|
|
section_num = current_section
|
|
|
|
|
|
|
|
|
|
# 다음 섹션(문제0 => 문제1)로 넘어갔을 경우 or 마지막 문제일 경우
|
|
|
|
|
if current_section != section_num:
|
|
|
|
|
# 이전 섹션의 부분합을 출력
|
|
|
|
|
detail_row[f'문제{section_num}'] = result['partial_scores'][int(section_num)]['score']
|
|
|
|
|
row_index.append(f'문제{section_num}')
|
|
|
|
|
section_num = current_section
|
|
|
|
|
|
|
|
|
|
detail_row[f'{i+1}'] = score_result['points']
|
|
|
|
|
row_index.append(score_result['id'])
|
|
|
|
|
|
|
|
|
|
# 마지막 섹션(문제2)부분합 점수를 출력
|
|
|
|
|
if i == len(result['score_results']) - 1:
|
|
|
|
|
detail_row[f'문제{current_section}'] = result['partial_scores'][int(current_section)]['score']
|
|
|
|
|
row_index.append(f'문제{current_section}')
|
|
|
|
|
|
|
|
|
|
detail_row['총점'] = result.get('total_score', 0)
|
|
|
|
|
row_index.append('총점')
|
|
|
|
|
detail_data.append(detail_row)
|
|
|
|
|
|
|
|
|
|
summary_df = pd.DataFrame(summary_data)
|
|
|
|
|
detail_df = pd.DataFrame(detail_data).transpose()
|
|
|
|
|
detail_df.columns = detail_df.iloc[0]
|
|
|
|
|
detail_df = detail_df[1:]
|
|
|
|
|
|
|
|
|
|
detail_df.index = row_index
|
|
|
|
|
# detail_df = pd.DataFrame(detail_data)
|
|
|
|
|
|
|
|
|
|
for temp in results:
|
|
|
|
|
result = temp['typo']
|
|
|
|
|
typo_data.append(result)
|
|
|
|
|
|
|
|
|
|
typo_df = pd.DataFrame(typo_data).transpose()
|
|
|
|
|
# detail_df = pd.DataFrame(detail_data)
|
|
|
|
|
|
|
|
|
|
# ExcelWriter 객체 생성
|
|
|
|
|
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
|
|
|
|
|
summary_df.to_excel(writer, sheet_name='채점결과요약', index=False)
|
|
|
|
|
detail_df.to_excel(writer, sheet_name='채점상세내역', index=True)
|
|
|
|
|
typo_df.to_excel(writer, sheet_name='오타내역', index=False)
|
|
|
|
|
|
|
|
|
|
# 열 너비 자동 조정
|
|
|
|
|
# for sheet_name in writer.sheets:
|
|
|
|
|
# worksheet = writer.sheets[sheet_name]
|
|
|
|
|
# for column_cells in worksheet.columns:
|
|
|
|
|
# max_length = 0
|
|
|
|
|
# column = column_cells[0].column_letter # 열의 문자
|
|
|
|
|
# for cell in column_cells:
|
|
|
|
|
# try:
|
|
|
|
|
# if cell.value:
|
|
|
|
|
# max_length = max(max_length, len(str(cell.value)))
|
|
|
|
|
# except:
|
|
|
|
|
# pass
|
|
|
|
|
# adjusted_width = (max_length + 2)
|
|
|
|
|
# worksheet.column_dimensions[column].width = adjusted_width
|
|
|
|
|
|
|
|
|
|
return output_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
|
|
|
|
# 시험회차 및 유형
|
2025-05-12 18:24:35 +09:00
|
|
|
exam_round = '2504'
|
2025-04-23 17:48:46 +09:00
|
|
|
exam_types = [
|
|
|
|
|
'A',
|
|
|
|
|
# 'B',
|
|
|
|
|
# 'C',
|
|
|
|
|
]
|
|
|
|
|
# test_mode = False
|
|
|
|
|
test_mode = True
|
|
|
|
|
|
|
|
|
|
output_excel_paths = []
|
|
|
|
|
for exam_type in exam_types:
|
2025-04-30 17:54:55 +09:00
|
|
|
# JSON 채점기준표 파일 (예시:DIW_2503A.json)
|
|
|
|
|
# scoring_criteria_path = f'./DIW_{exam_round}.json'
|
2025-05-12 18:24:35 +09:00
|
|
|
scoring_criteria_path = f'./DIW_{exam_round}{exam_type}_new.json'
|
2025-04-23 17:48:46 +09:00
|
|
|
|
2025-04-30 17:54:55 +09:00
|
|
|
# xml(hml)파일 디렉토리 경로 (예시:./output/A/DIW)
|
|
|
|
|
# xml_directory = f'./output/{exam_type}/{"TEST" if test_mode else "DIW"}'
|
2025-04-23 17:48:46 +09:00
|
|
|
|
2025-04-30 17:54:55 +09:00
|
|
|
# 회차가 여러개인 경우
|
|
|
|
|
xml_directory = f'./output/{exam_round}/{exam_type}/{"TEST" if test_mode else "DIW"}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 오탈자 체크를 위한 정답 파일 경로 (예시:./output/A/DIW/DIW_2503A.hml)
|
|
|
|
|
# correct_answer_file = f'./output/{exam_type}/DIW/DIW_{exam_round}{exam_type}.hml'
|
|
|
|
|
correct_answer_file = f'./output/{exam_round}/{exam_type}/DIW/DIW_{exam_round}{exam_type}.hml'
|
2025-04-23 17:48:46 +09:00
|
|
|
|
2025-04-30 17:54:55 +09:00
|
|
|
# 엑셀 파일명 (비어있으면 자동생성) (예시:241001_DIW_2503A_채점결과.xlsx)
|
2025-04-23 17:48:46 +09:00
|
|
|
timestamp = datetime.now().strftime("%y%m%d")
|
2025-04-30 17:54:55 +09:00
|
|
|
output_path = f'{timestamp}_DIW_{exam_round}회_{exam_type}형_{"TEST" if test_mode else "채점결과"}.xlsx'
|
2025-04-23 17:48:46 +09:00
|
|
|
|
|
|
|
|
# 채점 클래스 초기화
|
|
|
|
|
scorer = XMLScorer(scoring_criteria_path)
|
|
|
|
|
|
|
|
|
|
# 폴더 내 모든 xml 파일 채점
|
|
|
|
|
results = scorer.score_directory(xml_directory, correct_answer_file)
|
|
|
|
|
|
|
|
|
|
# 채점 결과 엑셀로 저장
|
|
|
|
|
output_excel_paths.append(scorer.export_to_excel(results, output_path))
|
|
|
|
|
|
|
|
|
|
print(f"채점 결과 엑셀 파일: {output_excel_paths}")
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|