v2 - mm to pt, 특수문자 앞뒤 다른 경우 처리

This commit is contained in:
2025-05-12 18:24:35 +09:00
parent 59c5c7c927
commit ac655d58d3
20 changed files with 364 additions and 16 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
_old_excel_files/ _old_excel_files/
output/ output/
input/ input/
*.hwp
~$*.xlsx

Binary file not shown.

319
DIW_2504A_new.json Normal file
View File

@@ -0,0 +1,319 @@
{
"0": {
"0": {
"path": "",
"path2": "",
"points": 0,
"category": "파일저장",
"item": "파일명 (수검번호.hwp/hwpx)"
},
"1": {
"path": "//PAGEMARGIN",
"value": {
"Bottom": 5669,
"Footer": 2834,
"Gutter": 0,
"Header": 2834,
"Left": 5669,
"Right": 5669,
"Top": 5669
},
"tolerance": 1,
"points": 4,
"category": "PageSetting",
"item": "A4용지, 왼쪽/오른쪽/위쪽/아래쪽 (각20mm), 머리말/꼬리말 (10mm), 제본(0mm)"
},
"2": {
"path": "//STYLE[@Name='바탕글']",
"value": {
"FontName": "바탕",
"FontSize": "1000",
"Alignment": "Justify",
"LineSpacing": "160"
},
"points": 4,
"category": "BasicSetting",
"item": "글꼴 (바탕, 10pt), 양쪽정렬, 줄간격 (160%)"
},
"3": {
"path": "",
"searchValue": null,
"value": null,
"points": 40,
"category": "오타감점",
"item": "오타 1개 -1점 / 2503회부터 오타 1개 -1점으로 변경"
}
},
"1": {
"1": {
"path": "//TEXTART[@Text='{searchValue}']/TEXTARTSHAPE/@FontName",
"searchValue": "클라우드컴퓨팅컨퍼런스",
"value": "맑은 고딕",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (클라우드컴퓨팅컨퍼런스)/① 글씨체 (맑은 고딕)"
},
"2": {
"path": "//TEXTART[@Text='{searchValue}']/descendant::WINDOWBRUSH/@FaceColor",
"searchValue": "클라우드컴퓨팅컨퍼런스",
"value": "28,61,98",
"points": 2,
"category": "Color",
"item": "문구 (클라우드컴퓨팅컨퍼런스)/② 채우기 : 색상(RGB:100,170,92)"
},
"3": {
"path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Width",
"searchValue": "클라우드컴퓨팅컨퍼런스",
"value": "110",
"tolerance": 1,
"points": 2,
"category": "mmSize",
"item": "문구 (클라우드컴퓨팅컨퍼런스)/③ 크기-너비 (110mm)"
},
"4": {
"path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Height",
"searchValue": "클라우드컴퓨팅컨퍼런스",
"value": "20",
"tolerance": 1,
"points": 2,
"category": "mmSize",
"item": "문구 (클라우드컴퓨팅컨퍼런스)/④ 크기-높이 (20mm)"
},
"5": {
"path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/POSITION/@TreatAsChar",
"searchValue": "클라우드컴퓨팅컨퍼런스",
"value": "true",
"points": 2,
"category": "SingleAnswer",
"item": "문구 (클라우드컴퓨팅컨퍼런스)/⑤ 위치 (글자처럼 취급)"
},
"6": {
"path": "//PARASHAPE[@Id=//TEXTART[@Text='{searchValue}']/ancestor::P/@ParaShape]/@Align",
"searchValue": "클라우드컴퓨팅컨퍼런스",
"value": "Center",
"points": 2,
"category": "SingleAnswer",
"item": "문구 (클라우드컴퓨팅컨퍼런스)/⑥ 정렬 (가운데 정렬)"
},
"7": {
"path": "//TEXTART[@Text='{searchValue}']",
"path2": null,
"searchValue": "클라우드컴퓨팅컨퍼런스",
"value": true,
"points": 2,
"category": "Boolean",
"item": "문구 (클라우드컴퓨팅컨퍼런스)/⑦ 글맵시모양 (육안확인)"
},
"8": {
"path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/SHAPEOBJECT/SIZE",
"path2": null,
"searchValue": "전",
"value": {
"Height": 2800,
"Width": 2800
},
"tolerance": 200,
"points": 1,
"category": "TwoLineSize",
"item": "전/① 모양 (2줄)"
},
"9": {
"path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape",
"path2": null,
"searchValue": "전",
"value": "궁서체",
"points": 1,
"category": "FontName",
"item": "전/② 글씨체 (궁서체)"
},
"10": {
"path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//WINDOWBRUSH/@FaceColor",
"path2": null,
"searchValue": "전",
"value": "255,132,58",
"points": 2,
"category": "Color",
"item": "전/③ 면색 : 색상(RGB:255,132,58)"
},
"11": {
"path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//OUTSIDEMARGIN/@Right",
"path2": null,
"searchValue": "전",
"value": "850",
"tolerance": 1,
"points": 2,
"category": "mmSize",
"item": "전/④ 본문과의 간격 : 3.0mm"
},
"12": {
"path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]",
"path2": null,
"searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회",
"value": "BOLD",
"points": 2,
"category": "FontAttribute",
"item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/① 진하게"
},
"13": {
"path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]",
"searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회",
"value": "UNDERLINE",
"points": 2,
"category": "FontAttribute",
"item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/② 밑줄"
},
"14": {
"path": "//CHAR[contains(text(),'{char1}')]",
"path2": "//CHAR[contains(text(),'{char2}')]",
"path3": "//CHAR[contains(text(),'{char3}')]",
"char1": "●",
"char2": "●",
"char3": "※",
"value": 3,
"points": 3,
"category": "SpecialChar",
"item": "① ●, ② ●, ③ ※"
},
"15": {
"path": "//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape",
"searchValue": "■ 행사안내 ■",
"value": "돋움",
"points": 1,
"category": "FontName",
"item": "문구 (■ 행사안내 ■)/① 글씨체 (돋움)"
},
"16": {
"path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align",
"searchValue": "■ 행사안내 ■",
"value": "Center",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (■ 행사안내 ■)/② 정렬 (가운데 정렬)"
},
"17": {
"path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]",
"searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수",
"value": "ITALIC",
"points": 1,
"category": "FontAttribute",
"item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/① 기울임"
},
"18": {
"path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]",
"searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수",
"value": "UNDERLINE",
"points": 1,
"category": "FontAttribute",
"item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/② 밑줄"
},
"19": {
"path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN",
"searchValue": "기타사항",
"value": {
"Left": 15,
"Indent": 12
},
"points": 2,
"category": "ParaShape",
"item": "문구 (※ 기타… 이하 문단)/왼쪽여백 (15pt), 내어쓰기 (12pt)",
"desc": "내부적으로 내어쓰기는 음수값"
},
"20": {
"path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]/@Height",
"searchValue": "2025. 03. 22.",
"value": "1300",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (2025. 03. 22.)/① 크기 (13pt)"
},
"21": {
"path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align",
"searchValue": "2025. 03. 22.",
"value": "Center",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (2025. 03. 22.)/② 정렬 (가운데 정렬)"
},
"22": {
"path": "//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape",
"searchValue": "한국고건축협회",
"value": "궁서",
"points": 1,
"category": "FontName",
"item": "문구 (한국고건축협회)/① 글씨체 (궁서)"
},
"23": {
"path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]/@Height",
"searchValue": "한국고건축협회",
"value": "2400",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (한국고건축협회)/② 크기 (24pt)"
},
"24": {
"path": "//PARASHAPE[@Id=//CHAR[text()='{searchValue}']/ancestor::P/@ParaShape]/@Align",
"searchValue": "한국고건축협회",
"value": "Center",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (한국고건축협회)/③ 정렬 (가운데 정렬)"
},
"25": {
"path": "//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape",
"searchValue": "DIAT",
"value": "굴림",
"points": 1,
"category": "FontName",
"item": "문구 (DIAT)/① 글꼴 (굴림)"
},
"26": {
"path": "//CHARSHAPE[@Id=//SECTION[1]//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]/@Height",
"searchValue": "DIAT",
"value": "900",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (DIAT)/② 크기 (9pt)"
},
"27": {
"path": "//PARASHAPE[@Id=//SECTION[1]//CHAR[text()='{searchValue}']/parent::TEXT/parent::P/@ParaShape]/@Align",
"searchValue": "DIAT",
"value": "Right",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (DIAT)/③ 정렬 (오른쪽 정렬)"
},
"28": {
"path": "//PAGENUM/@FormatType",
"value": "HangulSyllable",
"points": 2,
"category": "SingleAnswer",
"item": "① 쪽 번호 매기기 (가,나,다 순으로)"
},
"29": {
"path": "//PAGENUM/@Pos",
"value": "BottomCenter",
"points": 2,
"category": "SingleAnswer",
"item": "② 가운데 아래"
},
"30": {
"path": "not(//PARASHAPE[@Id=//SECTION[1]/P/@ParaShape]/PARAMARGIN[@LineSpacing!='180'])",
"value": true,
"points": 2,
"category": "Boolean",
"item": "문제 1 줄간격 180% 설정",
"desc": "1페이지 문단의 줄간격이 180%가 아닌 문단이 있으면 False(감점)"
}
},
"2": {
"1": {
"path": "//SECTION[2]//PAGEBORDERFILL[@Type='Both' or @Type='Even']",
"path2": "boolean(//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@HeaderInside='true' and //BORDERFILL[@Id=//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@BorferFill]/*[contains(local-name(), 'BORDER')]/@Type='DoubleSlim')",
"value": true,
"points": 4,
"category": "PageBorder",
"item": "문제2 쪽테두리(이중 실선, 머리말 포함) 설정"
},
"61": {}
}
}

View File

@@ -42,6 +42,7 @@ class XMLScorer:
hyperlink_xpath = self.scoring_criteria["1"]["17"]["hyperlink_xpath"] hyperlink_xpath = self.scoring_criteria["1"]["17"]["hyperlink_xpath"]
right_text = self.scoring_criteria["1"]["17"]["searchValue"].replace(" ","") right_text = self.scoring_criteria["1"]["17"]["searchValue"].replace(" ","")
try: try:
# 하이퍼링크가 포함된 p태그 인지 확인
p_elements = root.xpath(is_hyperlink) p_elements = root.xpath(is_hyperlink)
for p in p_elements: for p in p_elements:

View File

@@ -10,6 +10,7 @@ import re
from difflib import SequenceMatcher from difflib import SequenceMatcher
import pandas as pd import pandas as pd
import base64 import base64
import math
# from xpathSearch import XMLPathHandler # from xpathSearch import XMLPathHandler
class XMLScorer: class XMLScorer:
@@ -32,6 +33,13 @@ class XMLScorer:
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
return json.load(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의 값을 찾아 반환 # XML 파일에서 element의 값을 찾아 반환
def query_xml(self, root, *args): def query_xml(self, root, *args):
first_xpath = args[0] first_xpath = args[0]
@@ -188,6 +196,7 @@ class XMLScorer:
id = criterion_id id = criterion_id
xpath = criterion.get('path', None) xpath = criterion.get('path', None)
xpath2 = criterion.get('path2', None) xpath2 = criterion.get('path2', None)
xpath3 = criterion.get('path3', None)
search_value = criterion.get('searchValue', None) search_value = criterion.get('searchValue', None)
right_answer = criterion.get('value', None) right_answer = criterion.get('value', None)
points = criterion.get('points', 0) points = criterion.get('points', 0)
@@ -277,7 +286,20 @@ class XMLScorer:
if scoring['points'] > 0: if scoring['points'] > 0:
break break
elif "mmSize" in (category or ""):
items = root.xpath(xpath)
error_range = criterion.get('tolerance', 0)
for item in items:
user_answer = int(item)
right_answer = self.convert_mm_to_pt(int(right_answer))
self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance")
if scoring['points'] > 0:
break
elif "ParaShape" in (category or ""): elif "ParaShape" in (category or ""):
items = root.xpath(xpath) items = root.xpath(xpath)
@@ -362,31 +384,36 @@ class XMLScorer:
elif "SpecialChar" in (category or ""): elif "SpecialChar" in (category or ""):
ch1 = criterion.get('char1', None) ch1 = criterion.get('char1', None)
ch2 = criterion.get('char2', None) ch2 = criterion.get('char2', None)
ch3 = criterion.get('char3', None)
xpath = xpath.replace('{char1}', ch1) xpath = xpath.replace('{char1}', ch1)
xpath2 = xpath2.replace('{char2}', ch2) xpath2 = xpath2.replace('{char2}', ch2)
xpath3 = xpath3.replace('{char3}', ch3)
char1_ele = root.xpath(xpath) char1_ele = root.xpath(xpath)
char2_ele = root.xpath(xpath2) char2_ele = root.xpath(xpath2)
char3_ele = root.xpath(xpath3)
sum_char = 0 sum_char = 0
# char1 요소에서 특수문자 갯수 세기 (최대 2점) # char1 요소에서 특수문자 갯수 세기 (최대 2점)
if not char1_ele: for item in char1_ele or []:
user_answer = 0 count_char1 = item.text.count(ch1)
else: sum_char += count_char1
for item in char1_ele: if sum_char >= 2:
count_char1 = item.text.count(ch1) sum_char = 2
sum_char += count_char1 break
if sum_char >= 2:
sum_char = 2
break
# char2 요소에서 특수문자 갯수 세기 (최대 1점) # char2 요소에서 특수문자 갯수 세기 (최대 1점)
if not char2_ele: if (ch1 != ch2) and char2_ele:
user_answer = 0
else:
count_char2 = char2_ele[0].text.count(ch2) count_char2 = char2_ele[0].text.count(ch2)
if count_char2 > 1: if count_char2 > 1:
count_char2 = 1 count_char2 = 1
sum_char += count_char2 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 user_answer = sum_char
@@ -691,7 +718,7 @@ class XMLScorer:
def main(): def main():
# 시험회차 및 유형 # 시험회차 및 유형
exam_round = '2504_2' exam_round = '2504'
exam_types = [ exam_types = [
'A', 'A',
# 'B', # 'B',
@@ -704,7 +731,7 @@ def main():
for exam_type in exam_types: for exam_type in exam_types:
# JSON 채점기준표 파일 (예시:DIW_2503A.json) # JSON 채점기준표 파일 (예시:DIW_2503A.json)
# scoring_criteria_path = f'./DIW_{exam_round}.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}_new.json'
# xml(hml)파일 디렉토리 경로 (예시:./output/A/DIW) # xml(hml)파일 디렉토리 경로 (예시:./output/A/DIW)
# xml_directory = f'./output/{exam_type}/{"TEST" if test_mode else "DIW"}' # xml_directory = f'./output/{exam_type}/{"TEST" if test_mode else "DIW"}'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.