260 lines
8.1 KiB
Python
260 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
DXF 파일에서 TEXT, MTEXT, ATTRIB 엔티티를 추출하여 CSV 형식으로 변환하는 스크립트
|
|
의미 없는 텍스트는 필터링하고, 의미 있는 텍스트만 LLM에 전달
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from typing import List, Tuple
|
|
import csv
|
|
import io
|
|
|
|
|
|
@dataclass
|
|
class TextEntity:
|
|
"""DXF 텍스트 엔티티"""
|
|
entity_type: str # TEXT, MTEXT, ATTRIB
|
|
text: str
|
|
x: float
|
|
y: float
|
|
z: float
|
|
layer: str
|
|
height: float
|
|
style: str
|
|
|
|
|
|
def parse_dxf_text_entities(file_path: str) -> List[TextEntity]:
|
|
"""DXF 파일에서 TEXT, MTEXT, ATTRIB 엔티티를 파싱"""
|
|
entities = []
|
|
|
|
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
lines = f.readlines()
|
|
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
|
|
# TEXT, MTEXT, ATTRIB 엔티티 찾기
|
|
if line in ('TEXT', 'MTEXT', 'ATTRIB'):
|
|
entity_type = line
|
|
entity = {
|
|
'entity_type': entity_type,
|
|
'text': '',
|
|
'x': 0.0,
|
|
'y': 0.0,
|
|
'z': 0.0,
|
|
'layer': '',
|
|
'height': 0.0,
|
|
'style': ''
|
|
}
|
|
|
|
# 엔티티 속성 파싱 (다음 0이 나올 때까지)
|
|
i += 1
|
|
while i < len(lines):
|
|
code = lines[i].strip()
|
|
if code == '0':
|
|
break
|
|
|
|
if i + 1 < len(lines):
|
|
value = lines[i + 1].strip()
|
|
|
|
# 코드에 따라 값 파싱
|
|
if code == '1':
|
|
# 텍스트 내용 (MTEXT의 경우 여러 줄 가능)
|
|
if entity['text']:
|
|
entity['text'] += ' ' + value
|
|
else:
|
|
entity['text'] = value
|
|
elif code == '10':
|
|
entity['x'] = float(value)
|
|
elif code == '20':
|
|
entity['y'] = float(value)
|
|
elif code == '30':
|
|
entity['z'] = float(value)
|
|
elif code == '8':
|
|
entity['layer'] = value
|
|
elif code == '40':
|
|
entity['height'] = float(value)
|
|
elif code == '7':
|
|
entity['style'] = value
|
|
|
|
i += 1
|
|
|
|
i += 1
|
|
|
|
# 유효한 엔티티만 추가
|
|
if entity['text']:
|
|
entities.append(TextEntity(
|
|
entity_type=entity['entity_type'],
|
|
text=entity['text'],
|
|
x=entity['x'],
|
|
y=entity['y'],
|
|
z=entity['z'],
|
|
layer=entity['layer'],
|
|
height=entity['height'],
|
|
style=entity['style']
|
|
))
|
|
else:
|
|
i += 1
|
|
|
|
return entities
|
|
|
|
|
|
def filter_meaningful_text(entities: List[TextEntity]) -> List[TextEntity]:
|
|
"""
|
|
의미 있는 텍스트만 필터링
|
|
|
|
제거할 텍스트:
|
|
- 너무 짧은 텍스트 (2자 미만)
|
|
- 숫자만 있는 텍스트
|
|
- 특수문자만 있는 텍스트
|
|
- 반복되는 패턴 (예: "0", "1", "2" 등 단일 숫자)
|
|
- DXF 내부 메타데이터 (예: "$ACADVER", "$LIMMAX" 등)
|
|
"""
|
|
meaningful = []
|
|
|
|
# 제거할 패턴들
|
|
remove_patterns = [
|
|
r'^\$[A-Z]+$', # DXV 시스템 변수 ($ACADVER, $LIMMAX 등)
|
|
r'^[0-9]+$', # 숫자만 있는 텍스트
|
|
r'^[0-9.]+$', # 숫자와 점만 있는 텍스트
|
|
r'^[a-zA-Z0-9_]{1}$', # 1자 알파벳/숫자/언더스코어
|
|
r'^[ \t]+$', # 공백만 있는 텍스트
|
|
r'^[a-zA-Z0-9]{1,2}$', # 2자 이하의 알파벳/숫자 조합
|
|
]
|
|
|
|
# 허용할 패턴들 (의미 있는 텍스트)
|
|
allow_patterns = [
|
|
r'[가-힣]', # 한글 포함
|
|
r'[A-Z]{2,}', # 2자 이상 대문자 (예: P-101, PIC-6211)
|
|
r'[-_]{1}', # 하이픈/언더스코어 포함 (태그명 패턴)
|
|
r'[0-9]{3,}', # 3자 이상 숫자
|
|
]
|
|
|
|
for entity in entities:
|
|
text = entity.text.strip()
|
|
|
|
# 빈 텍스트 제거
|
|
if not text:
|
|
continue
|
|
|
|
# 시스템 변수 제거
|
|
is_system_var = False
|
|
for pattern in remove_patterns:
|
|
if re.match(pattern, text):
|
|
is_system_var = True
|
|
break
|
|
|
|
if is_system_var:
|
|
continue
|
|
|
|
# 의미 있는 텍스트인지 확인
|
|
is_meaningful = False
|
|
for pattern in allow_patterns:
|
|
if re.search(pattern, text):
|
|
is_meaningful = True
|
|
break
|
|
|
|
# 한글이 포함되어 있거나, 태그명 패턴(P-101, PIC-6211 등)이면 허용
|
|
if not is_meaningful:
|
|
# 태그명 패턴 확인 (예: P-101, PIC-6211, T-10101)
|
|
if re.match(r'^[A-Z]+[-_][A-Z0-9]+$', text):
|
|
is_meaningful = True
|
|
# 3자 이상이고 알파벳/숫자/한글이 포함된 경우
|
|
elif len(text) >= 3 and (re.search(r'[A-Z]', text) or re.search(r'[0-9]', text)):
|
|
is_meaningful = True
|
|
|
|
if is_meaningful:
|
|
meaningful.append(TextEntity(
|
|
entity_type=entity.entity_type,
|
|
text=text,
|
|
x=entity.x,
|
|
y=entity.y,
|
|
z=entity.z,
|
|
layer=entity.layer,
|
|
height=entity.height,
|
|
style=entity.style
|
|
))
|
|
|
|
return meaningful
|
|
|
|
|
|
def export_to_csv(entities: List[TextEntity], output_path: str):
|
|
"""CSV 형식으로 내보내기"""
|
|
with open(output_path, 'w', encoding='utf-8', newline='') as f:
|
|
writer = csv.writer(f)
|
|
writer.writerow(['entity_type', 'text', 'x', 'y', 'z', 'layer', 'height', 'style'])
|
|
|
|
for entity in entities:
|
|
writer.writerow([
|
|
entity.entity_type,
|
|
entity.text,
|
|
entity.x,
|
|
entity.y,
|
|
entity.z,
|
|
entity.layer,
|
|
entity.height,
|
|
entity.style
|
|
])
|
|
|
|
|
|
def export_to_llm_format(entities: List[TextEntity]) -> str:
|
|
"""LLM에 전달할 형식으로 변환 (CSV 문자열)"""
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(['entity_type', 'text', 'x', 'y', 'z', 'layer', 'height', 'style'])
|
|
|
|
for entity in entities:
|
|
writer.writerow([
|
|
entity.entity_type,
|
|
entity.text,
|
|
entity.x,
|
|
entity.y,
|
|
entity.z,
|
|
entity.layer,
|
|
entity.height,
|
|
entity.style
|
|
])
|
|
|
|
return output.getvalue()
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("사용법: python dxf_extractor.py <dxf_file_path> [output_csv_path]")
|
|
sys.exit(1)
|
|
|
|
dxf_path = sys.argv[1]
|
|
output_csv = sys.argv[2] if len(sys.argv) > 2 else None
|
|
|
|
print(f"DXF 파일 파싱 중: {dxf_path}")
|
|
entities = parse_dxf_text_entities(dxf_path)
|
|
print(f"총 {len(entities)}개 텍스트 엔티티 found")
|
|
|
|
print("의미 있는 텍스트 필터링 중...")
|
|
meaningful = filter_meaningful_text(entities)
|
|
print(f"의미 있는 텍스트: {len(meaningful)}개")
|
|
|
|
if output_csv:
|
|
export_to_csv(meaningful, output_csv)
|
|
print(f"CSV로 저장 완료: {output_csv}")
|
|
|
|
# LLM 포맷으로 출력
|
|
print("\n" + "="*80)
|
|
print("LLM에 전달할 CSV 형식:")
|
|
print("="*80)
|
|
print(export_to_llm_format(meaningful))
|
|
|
|
# 샘플 출력
|
|
print("\n" + "="*80)
|
|
print("샘플 텍스트 (상위 10개):")
|
|
print("="*80)
|
|
for i, entity in enumerate(meaningful[:10]):
|
|
print(f"{i+1}. [{entity.entity_type}] {entity.text} (layer: {entity.layer}, x:{entity.x}, y:{entity.y})")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|