하이퍼링크 수정, 차트 타입에 따른 x축 y축 채점요소 변경적용

This commit is contained in:
2025-05-23 17:18:55 +09:00
parent c8554adbb7
commit 828752e05a
35 changed files with 3813 additions and 1266 deletions

View File

@@ -94,15 +94,22 @@ class XMLScorer:
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
@@ -176,7 +183,8 @@ class XMLScorer:
xpath = xpath.replace('{option}', option) if xpath else ""
xpath2 = xpath2.replace('{option}', option) if xpath2 else ""
chart_xpath = chart_xpath.replace('{option}', option) if chart_xpath else ""
# 문항 별 채점 결과 저장
scoring = {
'section': section_id,
@@ -281,33 +289,72 @@ class XMLScorer:
self.partial_score += points
scoring['points'] = points
# 정답이 하나 또는 테이블의 모든 값이 정답인 경우
elif (category or "") in ["OneAnswer", "TableOneAnswer"]:
# 테이블의 경우 모든 셀에 요구사항이 적용되어야 정답처리
elif (category or "") == "TableAnswer":
items = root.xpath(xpath) if xpath else []
items2 = root.xpath(xpath2) if xpath2 else []
def is_all_match(item_list):
return item_list and all(item == right_answer for item in item_list)
## 위 코드와 동일한 기능(풀어서 설명)
# 리스트가 비어 있으면 False 반환
# if not item_list:
# return False
# # 리스트의 모든 항목이 right_answer와 같은지 검사
# for item in item_list:
# if item != right_answer:
# return False # 하나라도 다르면 False 반환
# return True # 전부 일치하면 True 반환
if is_all_match(items):
user_answer = right_answer
elif is_all_match(items2):
user_answer = right_answer
else:
user_answer = ""
points = 0
self.evaluate_answer(scoring, user_answer, right_answer, points)
# 정답이 하나인 경우
# elif (category or "") == "OneAnswer":
elif (category or "") in ["OneAnswer", "ChartOneAnswer"]:
items = root.xpath(xpath) if xpath else []
items2 = root.xpath(xpath2) if xpath2 else []
# 차트 XML에서 정답을 찾는 경우
# 차트 종류가
# 세로막대형이면 x축이 카테고리(catAx) y축이 값(valAx)
# 가로막대형이면 x축이 값(valAx) y축이 카테고리(catAx)
if category == "ChartOneAnswer":
# 하드코딩이라 [2-45문항] 변경시 수정 필요
# chart_type = self.scoring_criteria["2"]["45"]["chart_type"].replace(" ","")
# chart_type 변수의 경우 45번 문항을 먼저 채점하므로
# xy축의 변경이 필요한 53~58번 문항 채점시에 chart_type변수에 차트모양의 정보는 입력 되어있음
# 가로 차트일 경우에만 x축과 y축을 바꿔줌
# 세로, 꺾은선, 원형 차트의 경우 그대로 사용
if "가로" in chart_type:
chart_xpath = chart_xpath.replace("catAx", "valAx")
chart_xpath = chart_xpath.replace("valAx", "catAx")
chart_items = chart_tree.xpath(chart_xpath, namespaces=namespaces) if chart_xpath else []
require_all_match = (category == "TableOneAnswer")
any_match = False
all_match = True
for item in chain(items, items2, chart_items):
user_answer = item
if user_answer == right_answer:
any_match = True
else:
all_match = False
if require_all_match:
break # 하나라도 다르면 바로 탈출
if require_all_match:
score = points if all_match else 0
else:
score = points if any_match else 0
self.evaluate_answer(scoring, user_answer, right_answer, score)
self.evaluate_answer(scoring, user_answer, right_answer, points)
if scoring['points'] > 0:
break
# 정답이 두개인 경우
elif (category or "") == "DoubleAnswer":
items1 = root.xpath(xpath) if xpath else []
items2 = root.xpath(xpath2) if xpath else []
@@ -326,7 +373,7 @@ class XMLScorer:
items = root.xpath(xpath)
# 오차범위 설정
# 한글 프로그램 내부에서 드물게 mm단위는 0mm이지만 pt단위는 1pt로 저장되는 경우가 있음
# 한글 프로그램 내부에서 드물게 0mm이지만 1pt로 저장되는 경우가 있음
#
# XML파일의 요소 옵션값은 내부적으로 1=0.01pt
# 이 경우를 대비하여 tolerance를 10으로 설정 (1pt=약0.04mm 만큼의 오차 혀용)
@@ -419,6 +466,7 @@ class XMLScorer:
require_all_match = (category == "TableFontName")
any_match = False
all_match = True
matched_user_answer = None # 일치하는 user_answer를 기억
for charshape_id in charshape_list:
font_id = root.xpath(f"//CHARSHAPE[@Id='{charshape_id}']/FONTID/@Hangul")
@@ -439,6 +487,7 @@ class XMLScorer:
if user_answer == right_answer:
any_match = True
matched_user_answer = user_answer
else:
all_match = False
if require_all_match:
@@ -446,19 +495,19 @@ class XMLScorer:
if require_all_match:
score = points if all_match else 0
self.evaluate_answer(scoring, user_answer, right_answer, score)
else:
score = points if any_match else 0
self.evaluate_answer(scoring, user_answer, right_answer, score, method="equal")
self.evaluate_answer(scoring, matched_user_answer if any_match else "", right_answer, score)
# 폰트 속성
elif (category or "") == "FontAttribute":
# 하이퍼링크 처리
hyperlink_ptag = criterion.get('hyperlink_ptag', None)
has_ptag = root.xpath(hyperlink_ptag) if hyperlink_ptag else False
has_hyperlink_ptag = root.xpath(hyperlink_ptag) if hyperlink_ptag else False
# hyperlink가 아닌 경우(일반적인 텍스트 일 경우)
if not has_ptag:
if not has_hyperlink_ptag:
charshape = root.xpath(xpath)
if not charshape:
charshape = None
@@ -473,12 +522,20 @@ class XMLScorer:
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
# 하이퍼링크인 경우
elif has_ptag:
elif has_hyperlink_ptag:
hyperlink_text = search_value.replace(" ", "")
p_elements = has_ptag
p_elements = has_hyperlink_ptag
for p in p_elements:
# 수험자가 입력한 텍스트 중 하이퍼링크가 들어간 문단의 모든 텍스트를 가져와
# 채점하고자 하는 (정답) 하이퍼링크 텍스트와 시작 위치를 비교
# (예시)
# [수험자입력] 1. 사전등록 : 서울 국제 도서 박람회 흠페이지(http://www.ind.or.kr) 참조
# [정답] 서울 국제 도서 박람회 흠페이지(http://www.ind.or.kr) 참조
# 수험자 텍스트의 "1. 사전등록" 부분을 제외하고 난 뒤
# 남은 "서울 국제 도서 박람회 흠페이지(http://www.ind.or.kr) 참조"의 정답 부분과 유사도를 비교
text_list = p.xpath(".//CHAR/text()")
full_text = ''.join(text_list).replace(" ", "")
# print("full_text: ", full_text)
@@ -623,13 +680,12 @@ class XMLScorer:
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
# 한자
elif (category or "") == "Hanja":
word_list = criterion.get('word', [])
elif (category or "") == "Hanja":
# 점수 계산
score = 0
max_score = points
word_list = criterion.get('word', [])
# 부분점수 (최대점수에서 한자 갯수만큼 나눈 몫)
score_per_pair = max_score // len(word_list)
@@ -652,15 +708,13 @@ class XMLScorer:
elif (category or "") == "ChartType":
chart_type_list = {
'꺾은선형': "//c:lineChart[c:grouping[@val='standard']]",
'가로막대형': "//c:barChart[c:barDir[@val='bar'] and c:grouping[@val='clustered']]",
'세로막대형': "//c:barChart[c:barDir[@val='col'] and c:grouping[@val='clustered']]",
'묶은가로막대형': "//c:barChart[c:barDir[@val='bar'] and c:grouping[@val='clustered']]",
'묶은세로막대형': "//c:barChart[c:barDir[@val='col'] and c:grouping[@val='clustered']]",
'원형': "//c:pieChart",
'분산형': "//c:scatterChart"
}
chart_type = criterion.get('chart_type').replace(" ","")
if "묶은" in chart_type:
chart_type = chart_type.replace("묶은", "")
# 입력한 chart_type에 해당하는 xpath를 가져옴
chart_xpath = chart_type_list[chart_type]
@@ -723,10 +777,8 @@ class XMLScorer:
return xml_data
def typo_check(self, correct_answer_file, user_answer_file, chart_xml):
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()
user_answer_root = ET.parse(user_answer_file).getroot()
correct_answer_root = ET.parse(correct_answer_file).getroot()
# xpath로 바이너리 부분추출
user_input_text = user_answer_root.xpath('//CHAR//text()[not(ancestor::HEADER) and not(ancestor::TABLE)]')
@@ -762,8 +814,6 @@ class XMLScorer:
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]
ignore_word = self.scoring_criteria["2"]["29"]["ignoreWord"]
# 특정 단어 제거
# 오타와 누락의 경우만 판단하면 정상작동하지만
@@ -975,19 +1025,22 @@ def main():
# 시험회차 및 유형
exam_round = '2504'
# 채점하고자 하는 유형은 주석 해제
exam_types = [
# 'A',
# 'B',
'A',
'B',
'C',
]
test_mode = False
# test_mode = True
# test_mode = True #/TEST 폴더 채점시
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'
scoring_criteria_path = f'./DIW_{exam_round}{exam_type}.json'
# xml(hml)파일 디렉토리 경로 (예시:./output/A/DIW)
# xml_directory = f'./output/{exam_type}/{"TEST" if test_mode else "DIW"}'