글상자 채점 방식 수정 [2-10] 까지 진행중
This commit is contained in:
BIN
250909_DIW_2508C_TEST.xlsx
Normal file
BIN
250909_DIW_2508C_TEST.xlsx
Normal file
Binary file not shown.
@@ -351,89 +351,79 @@
|
||||
"item": "② 다단 2단"
|
||||
},
|
||||
"4": {
|
||||
"path": "//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::RECTANGLE/SHAPEOBJECT/SIZE/@Width",
|
||||
"searchValue": "구강건강관리",
|
||||
"path": "//RECTANGLE/SHAPEOBJECT/SIZE/@Width",
|
||||
"value": "60",
|
||||
"points": 2,
|
||||
"category": "mmSize",
|
||||
"category": "Rectangle.mmSize",
|
||||
"item": "문구 (구강건강관리)/① 크기-너비 (60 mm)"
|
||||
},
|
||||
"5": {
|
||||
"path": "//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::RECTANGLE/SHAPEOBJECT/SIZE/@Height",
|
||||
"searchValue": "구강건강관리",
|
||||
"path": "//RECTANGLE/SHAPEOBJECT/SIZE/@Height",
|
||||
"value": "12",
|
||||
"points": 2,
|
||||
"category": "mmSize",
|
||||
"category": "Rectangle.mmSize",
|
||||
"item": "문구 (구강건강관리)/② 크기-높이 (12 mm)"
|
||||
},
|
||||
"6": {
|
||||
"path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//LINESHAPE",
|
||||
"searchValue": "구강건강관리",
|
||||
"path": "//RECTANGLE//LINESHAPE",
|
||||
"value": {
|
||||
"Style": "DoubleSlim",
|
||||
"Width": "283"
|
||||
},
|
||||
"points": 2,
|
||||
"category": "LineShape",
|
||||
"category": "Rectangle.LineShape",
|
||||
"item": "문구 (구강건강관리)/③ 테두리 : 이중 실선(1.00mm)",
|
||||
"desc": "1mm = 283pt value['Width']에 pt값 입력"
|
||||
},
|
||||
"7": {
|
||||
"path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/@Ratio",
|
||||
"searchValue": "구강건강관리",
|
||||
"path": "//RECTANGLE/@Ratio",
|
||||
"value": "20",
|
||||
"points": 2,
|
||||
"category": "OneAnswer",
|
||||
"category": "Rectangle.OneAnswer",
|
||||
"item": "문구 (구강건강관리)/④ 글상자 모서리 (반원)",
|
||||
"desc": "모서리 비율 반원:50 / 둥근모양:20"
|
||||
},
|
||||
"8": {
|
||||
"path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//WINDOWBRUSH/@FaceColor",
|
||||
"searchValue": "구강건강관리",
|
||||
"path": "//RECTANGLE//WINDOWBRUSH/@FaceColor",
|
||||
"value": "187,140,209",
|
||||
"points": 2,
|
||||
"category": "Color",
|
||||
"category": "Rectangle.Color",
|
||||
"item": "문구 (구강건강관리)/⑤ 채우기 : 색상(RGB:187,140,209)"
|
||||
},
|
||||
"9": {
|
||||
"path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/SHAPEOBJECT/POSITION/@TreatAsChar",
|
||||
"searchValue": "구강건강관리",
|
||||
"path": "//RECTANGLE/SHAPEOBJECT/POSITION/@TreatAsChar",
|
||||
"value": "true",
|
||||
"points": 1,
|
||||
"category": "OneAnswer",
|
||||
"category": "Rectangle.OneAnswer",
|
||||
"item": "문구 (구강건강관리)/⑥ 글상자 위치 (글자처럼 취급)"
|
||||
},
|
||||
"10": {
|
||||
"path": "//PARASHAPE[@Id=//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::P[last()]/@ParaShape]/@Align",
|
||||
"searchValue": "구강건강관리",
|
||||
"path": "//PARASHAPE[@Id='{rectangle_parashape_id}']/@Align",
|
||||
"value": "Center",
|
||||
"points": 1,
|
||||
"category": "OneAnswer",
|
||||
"category": "Rectangle.TextBoxAlign",
|
||||
"item": "문구 (구강건강관리)/⑦ 글상자 정렬 (가운데 정렬)"
|
||||
},
|
||||
"11": {
|
||||
"path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape",
|
||||
"searchValue": "구강건강관리",
|
||||
"value": "맑은 고딕",
|
||||
"points": 1,
|
||||
"category": "FontName",
|
||||
"category": "Rectangle.FontName",
|
||||
"item": "문구 (구강건강관리)/⑧ 글씨체 (맑은 고딕)"
|
||||
},
|
||||
"12": {
|
||||
"path": "//CHARSHAPE[@Id=//RECTANGLE//TEXT[./CHAR[text()='{searchValue}']]/@CharShape]/@Height",
|
||||
"searchValue": "구강건강관리",
|
||||
"value": "2300",
|
||||
"points": 1,
|
||||
"category": "OneAnswer",
|
||||
"category": "Rectangle.OneAnswer",
|
||||
"item": "문구 (구강건강관리)/⑨ 글씨크기 (2300)",
|
||||
"desc": "1pt당 100"
|
||||
},
|
||||
"13": {
|
||||
"path": "//PARASHAPE[@Id=//RECTANGLE//P[.//CHAR[text()='{searchValue}']]/@ParaShape]/@Align",
|
||||
"searchValue": "구강건강관리",
|
||||
"value": "Center",
|
||||
"points": 1,
|
||||
"category": "OneAnswer",
|
||||
"category": "Rectangle.OneAnswer",
|
||||
"item": "문구 (구강건강관리)/⑩ 정렬 (가운데 정렬)"
|
||||
},
|
||||
"14": {
|
||||
|
||||
148
diwScoring2.py
148
diwScoring2.py
@@ -3,7 +3,7 @@ import difflib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import os
|
||||
from lxml import etree as ET
|
||||
from lxml import etree
|
||||
import re
|
||||
from difflib import SequenceMatcher
|
||||
import pandas as pd
|
||||
@@ -133,32 +133,6 @@ class XMLScorer:
|
||||
|
||||
# 하나의 XML 파일 채점
|
||||
def _score_xml_file(self, xml_file, chart_xml):
|
||||
# def parse_pages_by_bookmark(root):
|
||||
# """
|
||||
# P/TEXT/BOOKMARK 구조를 가진 XML에서 페이지 구간별 <P> 요소를 파싱하여 반환
|
||||
# """
|
||||
# pages = {}
|
||||
# all_p_tags = root.xpath('//P')
|
||||
|
||||
# current_page = None
|
||||
# page_start_index = None
|
||||
|
||||
# for i, p in enumerate(all_p_tags):
|
||||
# # BOOKMARK가 존재하는지 확인 (어디에 있든 탐색)
|
||||
# bookmarks = p.xpath('.//BOOKMARK')
|
||||
# for bm in bookmarks:
|
||||
# name = bm.get('Name')
|
||||
# if name and name.endswith('_start'):
|
||||
# current_page = name.replace('_start', '')
|
||||
# page_start_index = i
|
||||
# elif name and name.endswith('_end') and current_page is not None:
|
||||
# page_end_index = i
|
||||
# page_content = all_p_tags[page_start_index:page_end_index + 1]
|
||||
# pages[current_page] = page_content
|
||||
# current_page = None
|
||||
# page_start_index = None
|
||||
|
||||
# return pages
|
||||
def parse_pages_by_bookmark(root):
|
||||
"""
|
||||
BOOKMARK(Name="Page_X_start" ~ "Page_X_end") 사이의 <P> 요소들을
|
||||
@@ -201,7 +175,7 @@ class XMLScorer:
|
||||
return full_text
|
||||
|
||||
try:
|
||||
tree = ET.parse(xml_file)
|
||||
tree = etree.parse(xml_file)
|
||||
root = tree.getroot()
|
||||
|
||||
# XML문서 페이지 파싱 전처리
|
||||
@@ -216,9 +190,9 @@ class XMLScorer:
|
||||
|
||||
# 차트 XML 파일이 없는 경우 0점 채점을 위헤 빈 XML 생성
|
||||
if chart_xml is None:
|
||||
chart_tree = ET.fromstring('<xml></xml>')
|
||||
chart_tree = etree.fromstring('<xml></xml>')
|
||||
else:
|
||||
chart_tree = ET.fromstring(chart_xml)
|
||||
chart_tree = etree.fromstring(chart_xml)
|
||||
|
||||
# 결과값을 Dictionary로 저장
|
||||
# 하나의 xml파일 = 수험생 한명의 답안지
|
||||
@@ -275,6 +249,9 @@ class XMLScorer:
|
||||
}
|
||||
|
||||
try:
|
||||
# 머릿말과 관련된 문항에서 1페이지에 머릿말이 없는 경우의 처리
|
||||
# [1-25, 26, 27] 문항 'DIAT' 머릿말 채점시 1페이지에 머릿말이 없으면
|
||||
# 채점하지 않고 0점 처리
|
||||
if "Header" in (category or ""):
|
||||
def has_elements(ptags, xpath):
|
||||
for p in ptags:
|
||||
@@ -285,9 +262,27 @@ class XMLScorer:
|
||||
|
||||
page1_ptags = pages.get('Page_1', [])
|
||||
header_xpath = ".//HEADER//P"
|
||||
has_page1_element = has_elements(page1_ptags, header_xpath)
|
||||
has_page1_header = has_elements(page1_ptags, header_xpath)
|
||||
|
||||
if not has_page1_element:
|
||||
if not has_page1_header:
|
||||
user_answer = None
|
||||
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
|
||||
continue
|
||||
|
||||
has_page2_rectangle = False
|
||||
if "Rectangle" in (category or ""):
|
||||
def has_elements(ptags, xpath):
|
||||
for p in ptags:
|
||||
element_list = p.xpath(xpath) if xpath else []
|
||||
if element_list:
|
||||
return True
|
||||
return False
|
||||
|
||||
page2_ptags = pages.get('Page_2', [])
|
||||
rectangle_xpath = ".//RECTANGLE"
|
||||
has_page2_rectangle = has_elements(page2_ptags, rectangle_xpath)
|
||||
|
||||
if not has_page2_rectangle:
|
||||
user_answer = None
|
||||
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
|
||||
continue
|
||||
@@ -459,10 +454,45 @@ class XMLScorer:
|
||||
if scoring['points'] > 0:
|
||||
break
|
||||
|
||||
# 글상자 정렬 [2-10] 문항
|
||||
# 2페이지의 글상자의 ParaShape ID를 동적으로 찾아서 채점
|
||||
elif "TextBoxAlign" in (category or ""):
|
||||
if has_page2_rectangle:
|
||||
# 2페이지 내에서만 검색
|
||||
search_root = etree.Element("Page_2")
|
||||
for p in page2_ptags:
|
||||
search_root.append(p)
|
||||
rectangle_parashape_id = search_root.xpath(".//RECTANGLE/ancestor::P[last()]/@ParaShape")
|
||||
|
||||
else:
|
||||
# 전체 root에서 검색
|
||||
rectangle_parashape_id = root.xpath(".//RECTANGLE/ancestor::P[last()]/@ParaShape")
|
||||
|
||||
# ParaShape ID가 있는 경우에만 xpath 치환 & 실행
|
||||
if rectangle_parashape_id:
|
||||
xpath = xpath.replace('{rectangle_parashape_id}', rectangle_parashape_id[0])
|
||||
items = root.xpath(xpath)
|
||||
else:
|
||||
# RECTANGLE이 없으면 items는 빈 리스트
|
||||
items = [None]
|
||||
|
||||
for item in items:
|
||||
user_answer = item
|
||||
self.evaluate_answer(scoring, user_answer, right_answer, points)
|
||||
if scoring['points'] > 0:
|
||||
break
|
||||
|
||||
# 정답이 하나인 경우
|
||||
# elif (category or "") in ["OneAnswer", "ChartOneAnswer"]:
|
||||
elif "OneAnswer" in (category or ""):
|
||||
if has_page2_rectangle:
|
||||
search_root = etree.Element("Page_2")
|
||||
for p in page2_ptags:
|
||||
search_root.append(p)
|
||||
items = search_root.xpath(xpath) if xpath else []
|
||||
items2 = search_root.xpath(xpath2) if xpath2 else []
|
||||
|
||||
else:
|
||||
items = root.xpath(xpath) if xpath else []
|
||||
items2 = root.xpath(xpath2) if xpath2 else []
|
||||
|
||||
@@ -496,7 +526,14 @@ class XMLScorer:
|
||||
break
|
||||
|
||||
# [2-6] 테두리 이중실선 1.00mm
|
||||
elif (category or "") == "LineShape":
|
||||
elif "LineShape" in (category or ""):
|
||||
if has_page2_rectangle:
|
||||
search_root = etree.Element("Page_2")
|
||||
for p in page2_ptags:
|
||||
search_root.append(p)
|
||||
line_shapes = search_root.xpath(xpath) if xpath else []
|
||||
|
||||
else:
|
||||
line_shapes = root.xpath(xpath) if xpath else []
|
||||
|
||||
user_answer = {
|
||||
@@ -516,7 +553,15 @@ class XMLScorer:
|
||||
break
|
||||
|
||||
# 사용자 입력값이 mm단위인 경우
|
||||
elif (category or "") == "mmSize":
|
||||
# elif (category or "") == "mmSize":
|
||||
elif "mmSize" in (category or ""):
|
||||
if has_page2_rectangle:
|
||||
search_root = etree.Element("Page_2")
|
||||
for p in page2_ptags:
|
||||
search_root.append(p)
|
||||
items = search_root.xpath(xpath)
|
||||
|
||||
else:
|
||||
items = root.xpath(xpath)
|
||||
# 오차범위 설정
|
||||
# 한글 프로그램 내부에서 드물게 0mm이지만 1pt로 저장되는 경우가 있음
|
||||
@@ -567,7 +612,15 @@ class XMLScorer:
|
||||
self.evaluate_answer(scoring, user_answer, right_answer, points)
|
||||
|
||||
# 채점기준표 파일에 작성된 rgb값을 그대로 읽어와 HML파일 요소의 int형 rgb값과 비교
|
||||
elif (category or "") == "Color":
|
||||
elif "Color" in (category or ""):
|
||||
if has_page2_rectangle:
|
||||
search_root = etree.Element("Page_2")
|
||||
for p in page2_ptags:
|
||||
search_root.append(p)
|
||||
items = search_root.xpath(xpath) if xpath else []
|
||||
items2 = search_root.xpath(xpath2) if xpath2 else []
|
||||
|
||||
else:
|
||||
items = root.xpath(xpath) if xpath else []
|
||||
items2 = root.xpath(xpath2) if xpath2 else []
|
||||
|
||||
@@ -605,6 +658,13 @@ class XMLScorer:
|
||||
|
||||
# 폰트명
|
||||
elif "FontName" in (category or ""):
|
||||
if has_page2_rectangle:
|
||||
search_root = etree.Element("Page_2")
|
||||
for p in page2_ptags:
|
||||
search_root.append(p)
|
||||
charshape_list = search_root.xpath(xpath)
|
||||
|
||||
else:
|
||||
charshape_list = root.xpath(xpath)
|
||||
|
||||
# 문자속성이 없는 경우
|
||||
@@ -978,7 +1038,7 @@ class XMLScorer:
|
||||
onePersonResult['total_score'] = self.total_score
|
||||
return onePersonResult
|
||||
|
||||
except ET.ParseError as e:
|
||||
except etree.ParseError as e:
|
||||
return {
|
||||
'filename': os.path.basename(xml_file),
|
||||
'error': f"XML 파싱 오류: {str(e)}",
|
||||
@@ -986,7 +1046,7 @@ class XMLScorer:
|
||||
}
|
||||
|
||||
def binary_to_chartxml(self, xml_path):
|
||||
tree = ET.parse(xml_path)
|
||||
tree = etree.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
|
||||
binary_data = root.xpath('//BINDATA[@Id=//BINITEM[@Format="OLE"]/@BinData]/text()')
|
||||
@@ -1038,8 +1098,8 @@ class XMLScorer:
|
||||
# 2. 공백제거, 특정 형식 제거
|
||||
# 3. 리스트를 문자열로 변환
|
||||
|
||||
user_answer_root = ET.parse(user_answer_file).getroot()
|
||||
correct_answer_root = ET.parse(correct_answer_file).getroot()
|
||||
user_answer_root = etree.parse(user_answer_file).getroot()
|
||||
correct_answer_root = etree.parse(correct_answer_file).getroot()
|
||||
|
||||
# xpath로 바이너리 부분추출
|
||||
user_input_text = user_answer_root.xpath('//CHAR//text()[not(ancestor::HEADER) and not(ancestor::TABLE)]')
|
||||
@@ -1054,7 +1114,7 @@ class XMLScorer:
|
||||
|
||||
# 차트 XML에서 차트제목 추출
|
||||
if chart_xml is not None:
|
||||
chart_xml_tree = ET.fromstring(chart_xml)
|
||||
chart_xml_tree = etree.fromstring(chart_xml)
|
||||
ns = {'c': 'http://schemas.openxmlformats.org/drawingml/2006/chart',
|
||||
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
|
||||
xpath_expr = '/c:chartSpace/c:chart/c:title/c:tx/c:rich/a:p/a:r/a:t'
|
||||
@@ -1325,12 +1385,12 @@ def main():
|
||||
exam_types = [
|
||||
# 'A',
|
||||
# 'B',
|
||||
# 'C',
|
||||
'D',
|
||||
'C',
|
||||
# 'D',
|
||||
]
|
||||
|
||||
test_mode = False
|
||||
# test_mode = True #/TEST 폴더 채점시
|
||||
# test_mode = False
|
||||
test_mode = True #/TEST 폴더 채점시
|
||||
|
||||
output_excel_paths = []
|
||||
for exam_type in exam_types:
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"kind":2,"language":"xpath","value":"//a:t[text()='클라우드 보안투자']/ancestor::a:r//a:ea/@typeface"},{"kind":2,"language":"xpath","value":"boolean(//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE/FONTID/@Hangul]/@Name='바탕' and //CHARSHAPE/@Height='1000' and //PARASHAPE/PARAMARGIN/@LineSpacing='160' and //PARASHAPE/@Align='Justify')"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE/FONTID/@Hangul]/@Name='바탕'"},{"kind":2,"language":"xpath","value":"//RECTANGLE//CHAR[text()='구강건강관거리']/ancestor::RECTANGLE/SHAPEOBJECT/SIZE/@Width"},{"kind":2,"language":"xpath","value":"//CHAR[contains(text(),'예방')][contains(text(),'豫防')]"},{"kind":2,"language":"xpath","value":"//TEXT[CHAR[text()='DIAT']]"},{"kind":2,"language":"xpath","value":"//HEADER//P"},{"kind":2,"language":"xpath","value":"//P[.//FIELDBEGIN[@Type='Hyperlink'] and .//CHAR[contains(., 'http')]]"},{"kind":2,"language":"xpath","value":"//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG']/@BinData]]/SHAPEOBJECT/POSITION[not(@TreatAsChar='true')]/@HorzOffset"},{"kind":2,"language":"xpath","value":"//CHAR[contains(string(.), '※')]/descendant-or-self::text()"},{"kind":2,"language":"xpath","value":"//P[@ParaShape=\"17\"]/TEXT[@CharShape='7']//CHAR[string(.)]"},{"kind":2,"language":"xpath","value":"//CHAR[contains(string(.), '기타')]/text()"}]
|
||||
[{"kind":2,"language":"xpath","value":"//a:t[text()='클라우드 보안투자']/ancestor::a:r//a:ea/@typeface"},{"kind":2,"language":"xpath","value":"boolean(//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE/FONTID/@Hangul]/@Name='바탕' and //CHARSHAPE/@Height='1000' and //PARASHAPE/PARAMARGIN/@LineSpacing='160' and //PARASHAPE/@Align='Justify')"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE/FONTID/@Hangul]/@Name='바탕'"},{"kind":2,"language":"xpath","value":"//PARASHAPE[@Id=//RECTANGLE/ancestor::P[last()]/@ParaShape]/@Align"},{"kind":2,"language":"xpath","value":"//RECTANGLE//LINESHAPE"},{"kind":2,"language":"xpath","value":"//RECTANGLE/SHAPEOBJECT/SIZE/@Width"},{"kind":2,"language":"xpath","value":"//HEADER//P"},{"kind":2,"language":"xpath","value":"//P[.//FIELDBEGIN[@Type='Hyperlink'] and .//CHAR[contains(., 'http')]]"},{"kind":2,"language":"xpath","value":"//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG']/@BinData]]/SHAPEOBJECT/POSITION[not(@TreatAsChar='true')]/@HorzOffset"},{"kind":2,"language":"xpath","value":"//CHAR[contains(string(.), '※')]/descendant-or-self::text()"},{"kind":2,"language":"xpath","value":"//P[@ParaShape=\"17\"]/TEXT[@CharShape='7']//CHAR[string(.)]"},{"kind":2,"language":"xpath","value":"//CHAR[contains(string(.), '기타')]/text()"}]
|
||||
Reference in New Issue
Block a user