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
import math
from itertools import chain
# 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)
# 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
# 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
elif method == "partial_score":
# 부분 점수 계산
is_correct = isinstance(user_answer, (int, float)) and user_answer <= right_answer
points = min(points, user_answer)
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('')
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)
xpath2 = criterion.get('path2', None)
xpath3 = criterion.get('path3', None)
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)
option = criterion.get('option', 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)
if xpath2 is not None:
xpath2 = xpath2.replace('{searchValue}', similar_text)
if option:
xpath = xpath.replace('{option}', option) if xpath else ""
xpath2 = xpath2.replace('{option}', option) if xpath2 else ""
# 문항 별 채점 결과 저장
scoring = {
'section': section_id,
'id': id,
'category': category, # 채점 분류
'item': item, # 채점 항목
'right_answer': right_answer, # 정답
'user_answer': None, # 실제 작성 답안
'points': 0, # 점수
'deductions': [] # 각 기준별 감점 내역
}
if (category or "") == "PageSetting":
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 (category or "") == "BasicSetting":
# 바탕글(기본설정) 요소
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 = {
########################################################################
# 보통 정답은 글꼴이 [바탕]이어야 하지만 대부분 설정하지 않고
# 기본 [함초롱바탕]으로 두는 경우가 많아
# 폰트명을 정답과 비교하면 감점이 많이 발생해서
# 폰트명은 정답과 비교하지 않게 적용된 상태
########################################################################
#'FontName': font_name[0],
'FontName': "바탕",
'FontSize': font_size[0],
'Alignment': alignment[0],
'LineSpacing': line_spacing[0]
}
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
# 오타 감점 부분은 미리 계산 하고, 이후 점수만 계산
elif (category or "") == "오타감점":
points = self.get_typo_score()
self.total_score += points
self.partial_score += points
scoring['points'] = points
# 정답이 하나인 경우
elif (category or "") == "OneAnswer":
items = root.xpath(xpath)
items2 = root.xpath(xpath2) if xpath2 else []
for item in chain(items, items2):
user_answer = item
self.evaluate_answer(scoring, user_answer, right_answer, points)
if scoring['points'] > 0:
break
elif (category or "") == "DoubleAnswer":
items1 = root.xpath(xpath)
items2 = root.xpath(xpath2) if xpath else []
user_answer = []
for item1, item2 in zip(items1, items2):
user_answer.append(item1)
user_answer.append(item2)
self.evaluate_answer(scoring, user_answer, right_answer, points)
if scoring['points'] > 0:
break
# 사용자 입력값이 mm단위인 경우
elif (category or "") == "mmSize":
items = root.xpath(xpath)
error_range = criterion.get('tolerance', 0)
for item in items:
user_answer = float(item)
right_answer = self.convert_mm_to_pt(float(right_answer))
self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance")
if scoring['points'] > 0:
break
elif (category or "") == "ParaShape":
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
# Boolean 타입 정답인 경우
elif (category or "") == "Boolean":
items = root.xpath(xpath)
items2 = root.xpath(xpath2) if xpath2 else False
user_answer = bool( items or items2 )
self.evaluate_answer(scoring, user_answer, right_answer, points)
# 채점기준표 파일에 작성된 rgb값을 그대로 읽어와 HML파일 요소의 int형 rgb값과 비교
elif (category or "") == "Color":
items = root.xpath(xpath) if xpath else []
items2 = root.xpath(xpath2) if xpath2 else []
rgb_text = right_answer
r, g, b = map(int, rgb_text.split(','))
rgb_int = (b << 16) + (g << 8) + r
# items, items2를 순차적으로 순회
for item in chain(items, items2):
user_answer = int(item)
self.evaluate_answer(scoring, user_answer, rgb_int, points, method="equal")
if scoring['points'] > 0:
break
# 문단 첫글자 장식 채점
elif (category or "") == "TwoLineSize":
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
# 폰트명
elif (category or "") == "FontName":
charshape_list = root.xpath(xpath)
if not charshape_id:
user_answer = ""
else:
for charshape_id in charshape_list:
font_id = root.xpath(f"//CHARSHAPE[@Id='{charshape_id}']/FONTID/@Hangul")
font_name = root.xpath(f"//FONTFACE[@Lang='Hangul']/FONT[@Id='{font_id[0]}']/@Name")
user_answer = font_name[0]
# 폰트 "견고딕"과 "중고딕"은
# 한글프로그램 내부적으로 "한양견고딕", "한양중고딕"으로 저장되므로
# 수험자 답변에서 "한양"을 제거
if right_answer in ["견고딕", "중고딕"]:
user_answer = user_answer.replace("한양", "")
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
if scoring['points'] > 0:
break
# 테이블 폰트명
# 테이블 내부 모든 셀의 폰트가 정답과 일치해야 함
elif (category or "") == "TableFontName":
charshape_list = root.xpath(xpath)
# 문자속성이 없는 경우
if not charshape_list:
user_answer = ""
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
else:
all_match = True # 모든 항목이 정답과 일치해야 함
for charshape_id in charshape_list:
font_id = root.xpath(f"//CHARSHAPE[@Id='{charshape_id}']/FONTID/@Hangul")
font_name = root.xpath(f"//FONTFACE[@Lang='Hangul']/FONT[@Id='{font_id[0]}']/@Name")
user_answer = font_name[0]
# 내부 저장된 접두어 제거
if right_answer in ["견고딕", "중고딕"]:
user_answer = user_answer.replace("한양", "")
if user_answer != right_answer:
all_match = False
break # 하나라도 다르면 바로 오답 처리
if all_match:
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
else:
self.evaluate_answer(scoring, user_answer, right_answer, 0, method="equal") # 오답 처리
# 폰트 속성
elif (category or "") == "FontAttribute":
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
else:
user_answer = None
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
# 특수문자 갯수 채점
elif "SpecialChar" in (category or ""):
ch1 = criterion.get('char1', None)
ch2 = criterion.get('char2', None)
ch3 = criterion.get('char3', None)
xpath = xpath.replace('{char1}', ch1)
xpath2 = xpath2.replace('{char2}', ch2)
xpath3 = xpath3.replace('{char3}', ch3)
char1_ele = root.xpath(xpath)
char2_ele = root.xpath(xpath2)
char3_ele = root.xpath(xpath3)
sum_char = 0
# char1 요소에서 특수문자 갯수 세기 (최대 2점)
for item in char1_ele or []:
count_char1 = item.text.count(ch1)
sum_char += count_char1
if sum_char >= 2:
sum_char = 2
break
# char2 요소에서 특수문자 갯수 세기 (최대 1점)
# char1과 char2가 다른 경우 (예: ▶ 행사안내 ◀)
if (ch1 != ch2) and char2_ele:
count_char2 = char2_ele[0].text.count(ch2)
if count_char2 > 1:
count_char2 = 1
sum_char += count_char2
# char2 요소에서 특수문자 갯수 세기 (최대 1점)
if char3_ele:
count_char3 = char3_ele[0].text.count(ch3)
if count_char3 > 1:
count_char3 = 1
sum_char += count_char3
user_answer = sum_char
self.evaluate_answer(scoring, user_answer, right_answer, points, method="partial_score")
# 쪽 테두리 (이중 실선, 머리말 포함) 설정
elif "PageBorder" in (category or ""):
user_answer = {
"header_inside": False,
"all_double_slim": False
}
# 머릿말 포함 객체가 하나라도 있으면 정답
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
# 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")
# 한자
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")
elif (category or "") == "chart_type":
chart_type_list = {
'꺾은선형': "//c:lineChart",
'가로막대형': "//c:barChart[c:barDir[@val='bar']]",
'세로막대형': "//c:barChart[c:barDir[@val='col']]",
'원형': "//c:pieChart",
'분산형': "//c:scatterChart"
}
chart_type = criterion.get('chart_type').replace(" ","")
if "묶은" in chart_type:
chart_type = chart_type.replace("묶은", "")
chart_xpath = chart_type_list[chart_type]
user_answer = bool(chart_tree.xpath(chart_xpath, namespaces=namespaces))
self.evaluate_answer(scoring, user_answer, right_answer, points)
# 문항 채점 결과를 리스트에 입력
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')
# 태그와 그 내부 내용을 삭제합니다.
encoded_data = re.sub(b'', b'', binary_data)
encoded_data = encoded_data.replace(b'', b'')
encoded_data = encoded_data.replace(b'\r\n', b'')
# base64 디코딩을 수행합니다.
decoded_data = base64.b64decode(encoded_data+b'==')
# 디코딩된 데이터 내용 중 xml 형식만 추출할 때 , 사이의 데이터만 추출.
start = decoded_data.find(b'')
print(end)
xml_data = decoded_data[start:end+len(b'')]
# 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():
# 시험회차 및 유형
exam_round = '2504'
exam_types = [
'A',
# 'B',
# 'C',
]
# test_mode = False
test_mode = True
output_excel_paths = []
for exam_type in exam_types:
# JSON 채점기준표 파일 (예시:DIW_2503A.json)
# scoring_criteria_path = f'./DIW_{exam_round}.json'
scoring_criteria_path = f'./DIW_{exam_round}{exam_type}_new.json'
# xml(hml)파일 디렉토리 경로 (예시:./output/A/DIW)
# xml_directory = f'./output/{exam_type}/{"TEST" if test_mode else "DIW"}'
# 회차가 여러개인 경우
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'
# 엑셀 파일명 (비어있으면 자동생성) (예시:241001_DIW_2503A_채점결과.xlsx)
timestamp = datetime.now().strftime("%y%m%d")
output_path = f'{timestamp}_DIW_{exam_round}회_{exam_type}형_{"TEST" if test_mode else "채점결과"}.xlsx'
# 채점 클래스 초기화
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()