diff --git a/score2.py b/score2.py new file mode 100644 index 0000000..65b82df --- /dev/null +++ b/score2.py @@ -0,0 +1,327 @@ +import json +import xml.etree.ElementTree as ET +import os +from pathlib import Path +import pandas as pd +from datetime import datetime +from Levenshtein import distance as levenshtein_distance + +class XMLScorer: + def __init__(self, scoring_criteria_path): + """ + 채점 기준표 JSON 파일을 로드하여 초기화합니다. + + Args: + scoring_criteria_path (str): 채점 기준표 JSON 파일 경로 + """ + self.scoring_criteria = self._load_scoring_criteria(scoring_criteria_path) + # 오탈자 감점 설정 + self.typo_penalties = { + 'slight': 0.9, # 90% 점수 (약간의 오탈자) + 'moderate': 0.7, # 70% 점수 (중간 정도의 오탈자) + 'severe': 0.0 # 0% 점수 (심각한 오탈자) + } + + def _load_scoring_criteria(self, file_path): + """ + JSON 채점 기준표를 로드합니다. + + Args: + file_path (str): JSON 파일 경로 + + Returns: + dict: 채점 기준표 데이터 + """ + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + + def _calculate_similarity_score(self, str1, str2): + """ + 두 문자열 간의 유사도를 계산합니다. + + Args: + str1 (str): 첫 번째 문자열 + str2 (str): 두 번째 문자열 + + Returns: + float: 유사도 점수 (0.0 ~ 1.0) + """ + if str1 is None or str2 is None: + return 0.0 + + max_len = max(len(str1), len(str2)) + if max_len == 0: + return 1.0 + + distance = levenshtein_distance(str1, str2) + similarity = 1 - (distance / max_len) + return similarity + + def _get_penalty_factor(self, similarity): + """ + 유사도에 따른 감점 계수를 반환합니다. + + Args: + similarity (float): 유사도 점수 + + Returns: + float: 감점 계수 + """ + if similarity >= 0.9: + return self.typo_penalties['slight'] + elif similarity >= 0.7: + return self.typo_penalties['moderate'] + else: + return self.typo_penalties['severe'] + + def _find_best_matching_element(self, root, target_element): + """ + 가장 유사한 요소를 찾습니다. + + Args: + root (Element): XML 루트 요소 + target_element (str): 찾고자 하는 요소 이름 + + Returns: + tuple: (가장 유사한 요소, 유사도 점수) + """ + best_match = None + best_similarity = 0.0 + + for element in root.iter(): + similarity = self._calculate_similarity_score(element.tag, target_element) + if similarity > best_similarity: + best_similarity = similarity + best_match = element + + return best_match, best_similarity + + def _find_element_value(self, root, element_name, attribute_name): + """ + XML에서 특정 요소와 속성값을 찾습니다. 오탈자를 고려합니다. + + Args: + root (Element): XML 루트 요소 + element_name (str): 찾을 요소 이름 + attribute_name (str): 찾을 속성 이름 + + Returns: + tuple: (속성값, 요소 유사도, 속성 유사도) + """ + element, element_similarity = self._find_best_matching_element(root, element_name) + + if element is not None: + # 속성 중 가장 유사한 것을 찾음 + best_attr_value = None + best_attr_similarity = 0.0 + + for attr_name, attr_value in element.attrib.items(): + attr_similarity = self._calculate_similarity_score(attr_name, attribute_name) + if attr_similarity > best_attr_similarity: + best_attr_similarity = attr_similarity + best_attr_value = attr_value + + return best_attr_value, element_similarity, best_attr_similarity + + return None, 0.0, 0.0 + + def score_xml_file(self, xml_path): + """ + 단일 XML 파일을 채점합니다. + + Args: + xml_path (str): XML 파일 경로 + + Returns: + dict: 채점 결과 + """ + try: + tree = ET.parse(xml_path) + root = tree.getroot() + + total_score = 0 + results = { + 'filename': os.path.basename(xml_path), + 'criteria_matches': [], + 'total_score': 0 + } + + # 각 채점 기준에 대해 검사 + for criterion_id, criterion in self.scoring_criteria.items(): + element_name = criterion['ele'] + attribute_name = criterion['arg'] + expected_value = criterion['value'] + points = criterion['points'] + + # 오탈자를 고려하여 값을 찾음 + actual_value, element_similarity, attr_similarity = self._find_element_value( + root, element_name, attribute_name) + + # 값 유사도 계산 + value_similarity = self._calculate_similarity_score(str(actual_value), str(expected_value)) + + # 전체 유사도 계산 (요소, 속성, 값의 유사도를 종합) + total_similarity = (element_similarity + attr_similarity + value_similarity) / 3 + + # 감점 계수 계산 + penalty_factor = self._get_penalty_factor(total_similarity) + + match = { + 'criterion': f"{element_name}.{attribute_name}", + 'expected': expected_value, + 'actual': actual_value, + 'element_similarity': round(element_similarity, 3), + 'attribute_similarity': round(attr_similarity, 3), + 'value_similarity': round(value_similarity, 3), + 'total_similarity': round(total_similarity, 3), + 'penalty_factor': penalty_factor, + 'points': round(points * penalty_factor, 2) + } + + total_score += match['points'] + results['criteria_matches'].append(match) + + results['total_score'] = round(total_score, 2) + return results + + except ET.ParseError as e: + return { + 'filename': os.path.basename(xml_path), + 'error': f"XML 파싱 오류: {str(e)}", + 'total_score': 0 + } + + def score_directory(self, xml_directory): + """ + 디렉토리 내의 모든 XML 파일을 채점합니다. + + Args: + xml_directory (str): XML 파일들이 있는 디렉토리 경로 + + Returns: + list: 모든 파일의 채점 결과 + """ + results = [] + xml_files = Path(xml_directory).glob('*.xml') + + for xml_file in xml_files: + result = self.score_xml_file(str(xml_file)) + results.append(result) + + return results + + def export_to_excel(self, results, output_path=None): + """ + 채점 결과를 엑셀 파일로 저장합니다. + + Args: + results (list): 채점 결과 리스트 + output_path (str, optional): 출력 파일 경로 + """ + if output_path is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_path = f"scoring_results_{timestamp}.xlsx" + + # 요약 시트용 데이터 준비 + summary_data = [] + detail_data = [] + + for result in results: + # 요약 정보 + summary_row = { + '파일명': result['filename'], + '총점': result.get('total_score', 0) + } + if 'error' in result: + summary_row['오류'] = result['error'] + summary_data.append(summary_row) + + # 상세 정보 + if 'criteria_matches' in result: + for match in result['criteria_matches']: + detail_row = { + '파일명': result['filename'], + '채점항목': match['criterion'], + '기대값': match['expected'], + '실제값': match['actual'], + '요소유사도': match['element_similarity'], + '속성유사도': match['attribute_similarity'], + '값유사도': match['value_similarity'], + '전체유사도': match['total_similarity'], + '감점계수': match['penalty_factor'], + '획득점수': match['points'] + } + detail_data.append(detail_row) + + # DataFrame 생성 + summary_df = pd.DataFrame(summary_data) + 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=False) + + # 열 너비 자동 조정 + for sheet_name in writer.sheets: + worksheet = writer.sheets[sheet_name] + for column in worksheet.columns: + max_length = 0 + column = [cell for cell in column] + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = (max_length + 2) + worksheet.column_dimensions[column[0].column_letter].width = adjusted_width + + return output_path + +# 메인 함수는 이전과 동일 + +# 사용 예시 +def main(): + # 채점기준표 파일 경로 + scoring_criteria_path = "scoring_criteria.json" + # XML 파일들이 있는 디렉토리 경로 + xml_directory = r"C:\Users\gzero-ser7-win11\Documents\hwpTest\Output" + + # 채점기 초기화 + scorer = XMLScorer(scoring_criteria_path) + + # 디렉토리 내 모든 XML 파일 채점 + results = scorer.score_directory(xml_directory) + + # 결과 출력 + for result in results: + print(f"\n파일: {result['filename']}") + if 'error' in result: + print(f"오류: {result['error']}") + continue + + print(f"총점: {result['total_score']}") + print("\n채점 세부사항:") + for match in result['criteria_matches']: + print(f"기준: {match['criterion']}") + print(f"기대값: {match['expected']}") + print(f"실제값: {match['actual']}") + print(f"획득 점수: {match['points']}") + print("---") + + # 결과를 엑셀 파일로 저장 + excel_path = scorer.export_to_excel(results) + print(f"\n채점 결과가 다음 경로에 저장되었습니다: {excel_path}") + +if __name__ == "__main__": + main() + + + + + + + + + diff --git a/score3.py b/score3.py new file mode 100644 index 0000000..ba5c318 --- /dev/null +++ b/score3.py @@ -0,0 +1,296 @@ +import json +import xml.etree.ElementTree as ET +import os +from pathlib import Path +import pandas as pd +from datetime import datetime +from difflib import SequenceMatcher +import re + +class XMLScorer: + def __init__(self, scoring_criteria_path): + self.scoring_criteria = self._load_scoring_criteria(scoring_criteria_path) + + def _load_scoring_criteria(self, file_path): + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + + def _calculate_string_similarity(self, str1, str2): + """ + 두 문자열 간의 유사도를 계산합니다. + + Args: + str1 (str): 첫 번째 문자열 + str2 (str): 두 번째 문자열 + + Returns: + float: 유사도 (0~1 사이의 값) + """ + return SequenceMatcher(None, str1, str2).ratio() + + def _count_differences(self, str1, str2): + """ + 두 문자열 간의 차이(오탈자, 띄어쓰기)를 계산합니다. + + Args: + str1 (str): 첫 번째 문자열 (기준값) + str2 (str): 두 번째 문자열 (비교값) + + Returns: + tuple: (전체 차이 개수, 띄어쓰기 차이 개수) + """ + # 띄어쓰기 차이 계산 + space_diff = abs(str1.count(' ') - str2.count(' ')) + + # 전체 글자 차이 계산 (Levenshtein 거리 기반) + total_diff = 0 + m, n = len(str1), len(str2) + dp = [[0] * (n + 1) for _ in range(m + 1)] + + for i in range(m + 1): + dp[i][0] = i + for j in range(n + 1): + dp[0][j] = j + + for i in range(1, m + 1): + for j in range(1, n + 1): + if str1[i-1] == str2[j-1]: + dp[i][j] = dp[i-1][j-1] + else: + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + + total_diff = dp[m][n] + + return total_diff, space_diff + + def _find_similar_element(self, root, target_element): + """ + 유사한 요소를 찾습니다. 완전 일치하지 않더라도 비슷한 이름의 요소를 찾습니다. + + Args: + root (Element): XML 루트 요소 + target_element (str): 찾고자 하는 요소 이름 + + Returns: + Element: 가장 유사한 요소 또는 None + """ + best_match = None + best_similarity = 0.7 # 최소 유사도 임계값 + + for element in root.iter(): + similarity = self._calculate_string_similarity(element.tag, target_element) + if similarity > best_similarity: + best_similarity = similarity + best_match = element + + return best_match + + def _find_element_value(self, root, element_name, attribute_name): + """ + XML에서 특정 요소와 속성값을 찾습니다. 유사한 요소도 고려합니다. + + Args: + root (Element): XML 루트 요소 + element_name (str): 찾을 요소 이름 + attribute_name (str): 찾을 속성 이름 + + Returns: + tuple: (찾은 속성값 또는 None, 요소 이름 오탈자 여부) + """ + # 정확한 요소 찾기 + element = root.find(f".//{element_name}") + + # 정확한 요소가 없으면 유사한 요소 찾기 + if element is None: + element = self._find_similar_element(root, element_name) + + if element is not None: + # 속성값 찾기 + value = element.get(attribute_name) + # 요소 이름이 정확히 일치하는지 확인 + has_typo = element.tag != element_name + return value, has_typo + + return None, False + + def score_xml_file(self, xml_path): + try: + tree = ET.parse(xml_path) + root = tree.getroot() + + total_score = 0 + results = { + 'filename': os.path.basename(xml_path), + 'criteria_matches': [], + 'total_score': 0, + 'deductions': [] # 감점 상세 내역 추가 + } + + for criterion_id, criterion in self.scoring_criteria.items(): + element_name = criterion['ele'] + attribute_name = criterion['arg'] + expected_value = criterion['value'] + points = criterion['points'] + + actual_value, has_element_typo = self._find_element_value( + root, element_name, attribute_name) + + match = { + 'criterion': f"{element_name}.{attribute_name}", + 'expected': expected_value, + 'actual': actual_value, + 'points': 0, + 'deductions': [] # 각 기준별 감점 내역 + } + + if actual_value is not None: + # 기본 점수 부여 + match['points'] = points + + # 요소 이름에 오탈자가 있는 경우 + if has_element_typo: + deduction = 1 + match['points'] -= deduction + match['deductions'].append( + f"요소 이름 오탈자 감점: -{deduction}점") + + # 속성값 비교 및 차이 계산 + if actual_value != expected_value: + total_diff, space_diff = self._count_differences( + expected_value, actual_value) + + # 띄어쓰기 차이당 1점 감점 + if space_diff > 0: + match['points'] -= space_diff + match['deductions'].append( + f"띄어쓰기 오류 감점: -{space_diff}점") + + # 나머지 차이(오탈자)당 1점 감점 + char_diff = total_diff - space_diff + if char_diff > 0: + match['points'] -= char_diff + match['deductions'].append( + f"글자 오류 감점: -{char_diff}점") + + # 음수 점수 방지 + match['points'] = max(0, match['points']) + + results['criteria_matches'].append(match) + total_score += match['points'] + + results['total_score'] = total_score + return results + + except ET.ParseError as e: + return { + 'filename': os.path.basename(xml_path), + 'error': f"XML 파싱 오류: {str(e)}", + 'total_score': 0 + } + + def export_to_excel(self, results, output_path=None): + if output_path is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_path = f"scoring_results_{timestamp}.xlsx" + + summary_data = [] + detail_data = [] + + for result in results: + # 요약 정보 + summary_row = { + '파일명': result['filename'], + '총점': result.get('total_score', 0) + } + if 'error' in result: + summary_row['오류'] = result['error'] + summary_data.append(summary_row) + + # 상세 정보 + if 'criteria_matches' in result: + for match in result['criteria_matches']: + detail_row = { + '파일명': result['filename'], + '채점항목': match['criterion'], + '기대값': match['expected'], + '실제값': match['actual'], + '획득점수': match['points'], + '감점내역': '; '.join(match.get('deductions', [])) + } + detail_data.append(detail_row) + + # DataFrame 생성 + summary_df = pd.DataFrame(summary_data) + 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=False) + + # 열 너비 자동 조정 + for sheet_name in writer.sheets: + worksheet = writer.sheets[sheet_name] + for column in worksheet.columns: + max_length = 0 + column = [cell for cell in column] + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = (max_length + 2) + worksheet.column_dimensions[column[0].column_letter].width = adjusted_width + + return output_path + + def score_directory(self, xml_directory): + results = [] + xml_files = Path(xml_directory).glob('*.xml') + + for xml_file in xml_files: + result = self.score_xml_file(str(xml_file)) + results.append(result) + + return results + + + +# 사용 예시 +def main(): + # 채점기준표 파일 경로 + scoring_criteria_path = "scoring_criteria.json" + # XML 파일들이 있는 디렉토리 경로 + xml_directory = r"C:\Users\gzero-ser7-win11\Documents\hwpTest\Output" + + # 채점기 초기화 + scorer = XMLScorer(scoring_criteria_path) + + # 디렉토리 내 모든 XML 파일 채점 + results = scorer.score_directory(xml_directory) + + # 결과 출력 + for result in results: + print(f"\n파일: {result['filename']}") + if 'error' in result: + print(f"오류: {result['error']}") + continue + + print(f"총점: {result['total_score']}") + print("\n채점 세부사항:") + for match in result['criteria_matches']: + print(f"기준: {match['criterion']}") + print(f"기대값: {match['expected']}") + print(f"실제값: {match['actual']}") + print(f"획득 점수: {match['points']}") + print("---") + + # 결과를 엑셀 파일로 저장 + excel_path = scorer.export_to_excel(results) + print(f"\n채점 결과가 다음 경로에 저장되었습니다: {excel_path}") + +if __name__ == "__main__": + main() + + diff --git a/scoring_results_20241111_170638.xlsx b/scoring_results_20241111_170638.xlsx new file mode 100644 index 0000000..0d89223 Binary files /dev/null and b/scoring_results_20241111_170638.xlsx differ diff --git a/scoring_results_20241111_170945.xlsx b/scoring_results_20241111_170945.xlsx new file mode 100644 index 0000000..6a3570e Binary files /dev/null and b/scoring_results_20241111_170945.xlsx differ diff --git a/scoring_results_20241111_172707.xlsx b/scoring_results_20241111_172707.xlsx new file mode 100644 index 0000000..b0b295d Binary files /dev/null and b/scoring_results_20241111_172707.xlsx differ diff --git a/scoring_results_20241112_161225.xlsx b/scoring_results_20241112_161225.xlsx new file mode 100644 index 0000000..902c249 Binary files /dev/null and b/scoring_results_20241112_161225.xlsx differ diff --git a/~$scoring_results_20241107_170915.xlsx b/~$scoring_results_20241112_161225.xlsx similarity index 100% rename from ~$scoring_results_20241107_170915.xlsx rename to ~$scoring_results_20241112_161225.xlsx