From 9ab761eff726479b70efa9bf9aad2d05a23c371f Mon Sep 17 00:00:00 2001 From: Envy_PC Date: Tue, 30 Apr 2024 09:01:24 +0900 Subject: [PATCH] first commit --- .gitignore | 8 ++ README.md | 1 + api.py | 24 ++++ categories.json | 47 +++++++ kipris.py | 40 ++++++ kipris_api_from_publicdata.py | 65 +++++++++ kkapi.py | 47 +++++++ main.py | 182 +++++++++++++++++++++++++ main_async.py | 248 ++++++++++++++++++++++++++++++++++ main_with_re.py | 111 +++++++++++++++ src/kipo_api_client.py | 86 ++++++++++++ test.py | 14 ++ test_api.py | 42 ++++++ test_main.py | 48 +++++++ test_web.py | 20 +++ web_scraper.py | 199 +++++++++++++++++++++++++++ web_scraper_async.py | 228 +++++++++++++++++++++++++++++++ web_scraper_with_re.py | 110 +++++++++++++++ xmltest.py | 55 ++++++++ 19 files changed, 1575 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api.py create mode 100644 categories.json create mode 100644 kipris.py create mode 100644 kipris_api_from_publicdata.py create mode 100644 kkapi.py create mode 100644 main.py create mode 100644 main_async.py create mode 100644 main_with_re.py create mode 100644 src/kipo_api_client.py create mode 100644 test.py create mode 100644 test_api.py create mode 100644 test_main.py create mode 100644 test_web.py create mode 100644 web_scraper.py create mode 100644 web_scraper_async.py create mode 100644 web_scraper_with_re.py create mode 100644 xmltest.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..847e21f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +dist/ +build/ +Lib/ +.vscode/ +Include/ +Scripts/ +pyvenv.cfg diff --git a/README.md b/README.md new file mode 100644 index 0000000..95d02b8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +##키프리스에서 API 또는 웹방식으로 상표권을 검색하는 프로그램 \ No newline at end of file diff --git a/api.py b/api.py new file mode 100644 index 0000000..50a7b04 --- /dev/null +++ b/api.py @@ -0,0 +1,24 @@ +# Python3 샘플 코드 # + + +import requests +apikey ='X9Tz3JqC%2FJcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6%2Bt3dyLZV3gh2TPXdNhxsRQwaKP673Q%3D%3D' +apikey2 = 'X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TPXdNhxsRQwaKP673Q==' +apikey3 = "X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TPXdNhxsRQwaKP673Q==" + +# 디코딩된 API 키 사용 +# apikey2 ='X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TPXdNhxsRQwaKP673Q==' +# url = 'http://kipo-api.kipi.or.kr/openapi/service/trademarkInfoSearchService' +url = 'http://kipo-api.kipi.or.kr/openapi/service/trademarkInfoSearchService/getWordSearch' +params = { + 'serviceKey' : apikey3, # 디코딩된 키 사용 + 'searchString' : 'love*롯데', + 'searchRecentYear' : '0', + 'title' : '사랑 : LOVE', + 'fullText' : '', + 'drawing' : '', + 'bigDrawing' : '' +} + +response = requests.get(url, params=params) +print(response.content) diff --git a/categories.json b/categories.json new file mode 100644 index 0000000..4a62423 --- /dev/null +++ b/categories.json @@ -0,0 +1,47 @@ +{ + "01": "공업/과학 및 사진용 및 농업/원예 및 임업용 화학제; 미가공 인조수지, 미가공 플라스틱; 소화 및 화재예방용 조성물; 조질제 및 땜납용 조제; 수피용 무두질제; 공업용 접착제; 퍼티 및 기타 페이스트 충전제; 퇴비, 거름, 비료; 산업용 및 과학용 생물학적 제제", + "02": "페인트, 니스, 래커; 방청제 및 목재 보존제; 착색제, 염료; 인쇄, 표시 및 판화용 잉크; 미가공 천연수지; 도장용/장식용/인쇄용/미술용 금속박(箔) 및 금속분(紛)", + "03": "비의료용 화장품 및 세면용품; 비의료용 치약; 향료, 에센셜 오일; 표백제 및 기타 세탁용 제제; 세정/광택 및 연마재", + "04": "공업용 오일 및 그리스, 왁스; 윤활제; 먼지흡수제, 먼지습윤제 및 먼지흡착제; 연료 및 발광체; 조명용 양초 및 심지", + "05": "약제, 의료용 및 수의과용 제제; 의료용 위생제; 의료용 또는 수의과용 식이요법 식품 및 제제, 유아용 식품; 인체용 및 동물용 식이보충제; 플라스터, 외상치료용 재료; 치과용 충전재료, 치과용 왁스; 소독제; 해충구제제; 살균제, 제초제", + "06": "일반금속 및 그 합금, 광석; 금속제 건축 및 구축용 재료; 금속제 이동식 건축물; 비전기용 일반금속제 케이블 및 와이어; 소형금속제품; 저장 또는 운반용 금속제 용기; 금고", + "07": "기계, 공작기계, 전동공구; 모터 및 엔진(육상차량용은 제외); 기계 커플링 및 전동장치 부품(육상차량용은 제외); 농기구(수동식 수공구는 제외); 부란기(孵卵器); 자동판매기", + "08": "수동식 수공구 및 수동기구; 커틀러리; 휴대 무기(화기는 제외); 면도기", + "09": "과학, 연구, 항법, 측량, 사진, 영화, 시청각, 광학, 계량, 측정, 신호, 탐지, 시험, 검사, 구명 및 교육용 기기; 전기 분배 또는 전기 사용의 전도, 전환, 변형, 축적, 조절 또는 통제를 위한 기기; 음향/영상 또는 데이터의 기록/전송/재생 또는 처리용 장치 및 기구; 기록 및 내려받기 가능한 미디어, 컴퓨터 소프트웨어, 빈 디지털 또는 아날로그 기록 및 저장매체; 동전작동식 기계장치; 금전등록기, 계산기; 컴퓨터 및 컴퓨터주변기기; 잠수복, 잠수마스크, 잠수용 귀마개, 다이버 및 수영용 노즈클립, 잠수용 장갑, 잠수용 호흡장치; 소화기기", + "10": "외과용, 내과용, 치과용 및 수의과용 기계기구; 의지(義肢), 의안(義眼) 및 의치(義齒); 정형외과용품; 봉합용 재료; 장애인용 치료 및 재활보조장치; 안마기; 유아수유용 기기 및 용품; 성활동용 기기 및 용품", + "11": "조명용, 가열용, 냉각용, 증기발생용, 조리용, 건조용, 환기용, 급수용, 위생용 장치 및 설비", + "12": "수송기계기구; 육상, 항공 또는 해상을 통해 이동하는 수송수단", + "13": "화기(火器); 탄약 및 발사체; 폭약; 폭죽", + "14": "귀금속 및 그 합금; 보석, 귀석 및 반귀석; 시계용구", + "15": "악기; 악보대 및 악기용 받침대; 지휘봉", + "16": "종이 및 판지; 인쇄물; 제본재료; 사진; 문방구 및 사무용품(가구는 제외); 문방구용 또는 가정용 접착제; 제도용구 및 미술용 재료; 회화용 솔; 교재; 포장용 플라스틱제 시트, 필름 및 가방; 인쇄활자, 프린팅블록", + "17": "미가공 및 반가공 고무, 구타페르카, 고무액(gum), 석면, 운모(雲母) 및 이들의 제품; 제조용 압출성형형태의 플라스틱 및 수지; 충전용, 마개용 및 절연용 재료; 비금속제 신축관, 튜브 및 호스", + "18": "가죽 및 모조가죽; 수피; 수하물가방 및 운반용 가방; 우산 및 파라솔; 걷기용 지팡이; 채찍 및 마구(馬具); 동물용 목걸이, 가죽끈 및 의류", + "19": "건축용 및 구축용 비금속제 건축재료; 건축용 비금속제 경질관(硬質管); 아스팔트, 피치, 타르 및 역청; 비금속제 이동식 건축물; 비금속제 기념물", + "20": "가구, 거울, 액자; 보관 또는 운송용 비금속제 컨테이너; 미가공 또는 반가공 뼈, 뿔, 고래수염 또는 나전(螺鈿); 패각; 해포석(海泡石); 호박(琥珀)(원석)", + "21": "가정용 또는 주방용 기구 및 용기; 조리기구 및 식기(포크, 나이프 및 스푼은 제외); 빗 및 스펀지; 솔(페인트 솔은 제외); 솔 제조용 재료; 청소용구; 비건축용 미가공 또는 반가공 유리; 유리제품, 도자기제품 및 토기제품", + "22": "로프 및 노끈; 망(網); 텐트 및 타폴린; 직물제 또는 합성재료제 차양; 돛; 하역물운반용 및 보관용 포대; 충전재료(고무/플라스틱/종이 및 판지제는 제외); 직물용 미가공 섬유 및 그 대용품", + "23": "직물용 실(絲)", + "24": "직물 및 직물대용품; 가정용 린넨; 직물 또는 플라스틱제 커튼", + "25": "의류, 신발, 모자", + "26": "레이스, 장식용 끈 및 자수포, 의류장식용 리본 및 나비매듭리본; 단추, 훅 및 아이(hooks and eyes), 핀 및 바늘; 조화(造花); 머리장식품; 가발", + "27": "카펫, 융단, 매트, 리놀륨 및 기타 바닥깔개용 재료; 비직물제 벽걸이", + "28": "오락용구, 장난감; 비디오게임장치; 체조 및 스포츠용품; 크리스마스트리용 장식품", + "29": "식육, 생선, 가금 및 엽조수; 고기진액; 보존처리/냉동/건조 및 조리된 과일 및 채소; 젤리, 잼, 콤폿; 달걀; 우유, 치즈, 버터, 요구르트 및 기타 유제품; 식용 유지(油脂)", + "30": "커피, 차(茶), 코코아 및 그 대용물; 쌀, 파스타 및 국수; 타피오카 및 사고(sago); 곡분 및 곡물 조제품; 빵, 페이스트리 및 과자; 초콜릿; 아이스크림, 셔벗 및 기타 식용 얼음; 설탕, 꿀, 당밀(糖蜜); 식품용 이스트, 베이킹 파우더; 소금, 조미료, 향신료, 보존처리된 허브; 식초, 소스 및 기타 조미료; 얼음", + "31": "미가공 농업, 수산양식, 원예 및 임업 생산물; 미가공 곡물 및 종자; 신선한 과실 및 채소, 신선한 허브; 살아 있는 식물 및 꽃; 구근(球根), 모종 및 재배용 곡물종자; 살아있는 동물; 동물용 사료 및 음료; 맥아", + "32": "맥주; 비알코올성 음료; 광천수 및 탄산수; 과실음료 및 과실주스; 시럽 및 비알코올성 음료용 제제", + "33": "알코올성 음료(맥주는 제외); 음료제조용 알코올성 제제", + "34": "담배 및 대용담배; 권연 및 여송연; 흡연자용 전자담배 및 기화기; 흡연용구; 성냥", + "35": "광고업; 사업관리/조직 및 경영업; 사무처리업", + "36": "금융, 통화 및 은행업; 보험서비스업; 부동산업", + "37": "건축서비스업; 설치 및 수리서비스업; 채광업/석유 및 가스 시추업", + "38": "통신서비스업", + "39": "운송업; 상품의 포장 및 보관업; 여행알선업", + "40": "재료처리업; 폐기물 재생업; 공기 정화 및 물 처리업; 인쇄 서비스업; 음식 및 음료수 보존업", + "41": "교육업; 훈련제공업; 연예오락업; 스포츠 및 문화활동업", + "42": "과학적, 기술적 서비스업 및 관련 연구, 디자인업; 산업분석, 산업연구 및 산업디자인 서비스업; 품질 관리 및 인증 서비스업; 컴퓨터 하드웨어 및 소프트웨어의 디자인 및 개발업", + "43": "식음료제공서비스업; 임시숙박시설업", + "44": "의료업; 수의업; 인간 또는 동물을 위한 위생 및 미용업; 농업, 수산양식, 원예 및 임업 서비스업", + "45": "법무서비스업; 유형의 재산 및 개인을 물리적으로 보호하기 위한 보안서비스업; 이성(異性) 소개업, 온라인 소셜 네트워킹 서비스업; 장례업; 베이비시팅업" +} diff --git a/kipris.py b/kipris.py new file mode 100644 index 0000000..8e24bc0 --- /dev/null +++ b/kipris.py @@ -0,0 +1,40 @@ +import requests +class KiprisApiClient: + def __init__(self, service_key): + self.service_key = service_key + self.base_url = "http://plus.kipris.or.kr/" + + def search_patents(self, word, year, patent, utility, numOfRows=30, pageNo=1): + params = { + 'accessKey': self.service_key, + 'word': word, + 'year': year, + 'patent': str(patent).lower(), + 'utility': str(utility).lower(), + 'numOfRows': numOfRows, + 'pageNo': pageNo + } + response = requests.get(self.base_url, params=params) + return response.json() + + +if __name__ == "__main__": + # API 키와 검색하고자 하는 단어 설정 + service_key = '/loM8C4yXZsTLAmp7PLoq2UCl5zg/OFhZCibzO/D968=' + word = '스키에이트' + client = KiprisApiClient(service_key) + + # API 호출 예제 + # word = '전자회로' + year = '0' # 검색 년도 범위 + patent = True + utility = False + numOfRows = 30 + pageNo = 1 + + result = client.search_patents(word, year, patent, utility, numOfRows, pageNo) + if 'items' in result: + for item in result['items']: + print(f"발명의 명칭: {item['inventionTitle']}, 등록번호: {item['registerNumber']}") + else: + print("검색 결과가 없습니다.") diff --git a/kipris_api_from_publicdata.py b/kipris_api_from_publicdata.py new file mode 100644 index 0000000..313bb1f --- /dev/null +++ b/kipris_api_from_publicdata.py @@ -0,0 +1,65 @@ +import xml.etree.ElementTree as ET +import requests + +class Kipris: + def __init__(self, url, params=None): + self.url = url + self.params = params + self.results = [] + + def fetch_and_decode(self): + # API 요청 및 응답 받기 + response = requests.get(self.url, params=self.params) + decoded_data = response.content.decode('utf-8') + return decoded_data + + def parse_xml(self, xml_data): + # XML 데이터 파싱 + root = ET.fromstring(xml_data) + total_items = 0 + status_registered = 0 + status_published = 0 + + # 'body/items/item' 경로에 맞춰 'item' 태그를 순회하면서 필요한 데이터 추출 + for item in root.findall('.//body/items/item'): + total_items += 1 + application_status = item.find('applicationStatus').text if item.find('applicationStatus') is not None else None + + # 각 상태의 개수를 카운트 + if application_status == "등록": + status_registered += 1 + if application_status == "공개": + status_published += 1 + + if application_status in ["등록", "공개"]: + result = { + "index_no": item.find('indexNo').text if item.find('indexNo') is not None else None, + "application_number": item.find('applicationNumber').text if item.find('applicationNumber') is not None else None, + "application_date": item.find('applicationDate').text if item.find('applicationDate') is not None else None, + "publication_number": item.find('publicationNumber').text if item.find('publicationNumber') is not None else None, + "publication_date": item.find('publicationDate').text if item.find('publicationDate') is not None else None, + "registration_date": item.find('registrationDate').text if item.find('registrationDate') is not None else None, + "registration_number": item.find('registrationNumber').text if item.find('registrationNumber') is not None else None, + "applicant_name": item.find('applicantName').text if item.find('applicantName') is not None else None, + "agent_name": item.find('agentName').text if item.find('agentName') is not None else None, + "title": item.find('title').text if item.find('title') is not None else None, + "drawing_url": item.find('drawing').text if item.find('drawing') is not None else None, + "big_drawing_url": item.find('bigDrawing').text if item.find('bigDrawing') is not None else None, + "full_text": item.find('fullText').text if item.find('fullText') is not None else None, + "application_status": application_status, + "classification_code": item.find('classificationCode').text if item.find('classificationCode') is not None else None + } + self.results.append(result) + + # 상태 개수와 총 아이템 개수 출력 + print(f"검색된 item 총 개수: {total_items}") + print(f"등록 상태인 item 개수: {status_registered}") + print(f"공개 상태인 item 개수: {status_published}") + + def get_results(self): + return self.results + + def run(self): + xml_data = self.fetch_and_decode() + self.parse_xml(xml_data) + return self.get_results() diff --git a/kkapi.py b/kkapi.py new file mode 100644 index 0000000..7164cf7 --- /dev/null +++ b/kkapi.py @@ -0,0 +1,47 @@ +import requests +import xml.etree.ElementTree as ET + +# 디코딩된 API ' +apikey2 = 'X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TPXdNhxsRQwaKP673Q==' + +# 인코딩 api +# apikey1 = 'X9Tz3JqC%2FJcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6%2Bt3dyLZV3gh2TPXdNhxsRQwaKP673Q%3D%3D' + +url = 'http://kipo-api.kipi.or.kr/openapi/service/trademarkInfoSearchService/getWordSearch' + +params = { + 'serviceKey': apikey2, # 디코딩된 키 사용 + 'searchString': '스키에이트', + 'searchRecentYear': '0', + 'title': '', + 'fullText': '', + 'drawing': '', + 'bigDrawing': '' +} + +response1 = requests.get(url, params=params) +print(f"response1 : {response1.content}") +root = ET.fromstring(response1.content) +# response2 = requests.get( +# 'http://kipo-api.kipi.or.kr/openapi/service/patUtiModInfoSearchSevice/getWordSearch?serviceKey=X9Tz3JqC%2FJcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6%2Bt3dyLZV3gh2TPXdNhxsRQwaKP673Q%3D%3D&word=%EC%8A%A4%ED%82%A4%EC%97%90%EC%9D%B4%ED%8A%B8&year=0&patent=true&utility=true' +# ) +# print(f"response2 : {response2.content}") + + +# Extracting items +items = [] +for item in root.findall(".//item"): + data = { + "applicantName": item.find("applicantName").text, + "applicationNumber": item.find("applicationNumber").text, + "applicationDate": item.find("applicationDate").text, + "applicationStatus": item.find("applicationStatus").text, + "publicationNumber": item.find("publicationNumber").text, + "publicationDate": item.find("publicationDate").text, + "registrationNumber": item.find("registrationNumber").text, + "registrationDate": item.find("registrationDate").text, + "agentName": item.find("agentName").text if item.find("agentName") is not None else "", + "drawing": item.find("drawing").text, + "bigDrawing": item.find("bigDrawing").text + } + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..1ab48be --- /dev/null +++ b/main.py @@ -0,0 +1,182 @@ +import sys +from PyQt5.QtWidgets import QApplication, QMessageBox, QSizePolicy, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QGridLayout +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt, QByteArray, QBuffer, QIODevice +from web_scraper import WebScraper + +class MainApp(QWidget): + def __init__(self): + super().__init__() + self.initUI() + self.scraper = WebScraper() + self.scraper.setup_browser() + + + def initUI(self): + # 검색 레이아웃 + searchLayout = QHBoxLayout() + self.searchLabel = QLabel('검색어 입력:', self) + self.searchLineEdit = QLineEdit(self) + self.searchLineEdit.returnPressed.connect(self.start_search) + self.searchButton = QPushButton('검색 실행', self) + self.searchButton.clicked.connect(self.start_search) + + searchLayout.addWidget(self.searchLabel) + searchLayout.addWidget(self.searchLineEdit) + searchLayout.addWidget(self.searchButton) + + # 로그 레이아웃 + logLayout = QVBoxLayout() + self.logTextEdit = QTextEdit(self) + self.logTextEdit.setReadOnly(True) + + logLayout.addWidget(self.logTextEdit) + + # 메인 레이아웃 + mainLayout = QVBoxLayout() + mainLayout.addLayout(searchLayout) + mainLayout.addLayout(logLayout) + + self.setLayout(mainLayout) + self.setWindowTitle('Web Scraper') + self.setGeometry(300, 300, 300, 400) + + def wrap_text(self, text, width=40): + """주어진 너비에 맞게 텍스트를 줄바꿈합니다.""" + words = text.split() + wrapped_text = '' + line_length = 0 + + for word in words: + if line_length + len(word) + 1 > width: + wrapped_text += '\n' + line_length = 0 + wrapped_text += word + ' ' + line_length += len(word) + 1 + + return wrapped_text.strip() + + def start_search(self): + term = self.searchLineEdit.text() + if not term: + self.logTextEdit.append("검색어를 입력해주세요.") + return + + self.logTextEdit.append("검색을 시작합니다...") + results = self.scraper.search_for_term(term) + if results: + self.logTextEdit.append("검색 완료. 결과를 처리합니다...") + self.show_results(results) + else: + self.logTextEdit.append("검색 결과가 없거나 오류가 발생했습니다.") + QMessageBox.information(self, "검색 결과 없음", "검색 결과가 없으므로 지재권에 안심하시면 됩니다.", QMessageBox.Ok) + + + def show_results(self, results): + + try: + # 결과 위젯 생성 + self.results_widget = QWidget() + layout = QVBoxLayout() + self.results_widget.setLayout(layout) + + # 결과 갯수 확인 및 레이아웃 동적 생성 + total_count = int(results['total_count']) + set_count = min(total_count, 10) + grid_layout = QGridLayout() + layout.addLayout(grid_layout) + grid_index = 0 + grid_columns = 5 + + for i in range(1, set_count + 1): + result_key = f"result_{i}" + if result_key in results: + result = results[result_key] + + # 테두리 설정 + border_style = '' + if result['admin_status'] == '등록': + border_style = 'border: 4px solid red;' + elif result['admin_status'] == '공고': + border_style = 'border: 3px solid black;' + + # 각 결과에 대한 레이아웃 생성 + item_layout = QVBoxLayout() + item_widget = QWidget() # 위젯 생성 + + # item_layout의 크기 정책 설정 + item_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) + + # 이미지 처리 + image_label = QLabel() + image_label.setFixedSize(150, 150) + image_data = self.scraper.fetch_image_data(result['IDimageURL']) + pixmap = QPixmap() + pixmap.loadFromData(image_data) + # QLabel의 크기에 맞게 이미지 크기 조정 + scaled_pixmap = pixmap.scaled(image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) + image_label.setPixmap(scaled_pixmap) + # QLabel의 가로 세로 중앙에 이미지 표시 + image_label.setAlignment(Qt.AlignCenter) + # 이미지 표시 위젯의 크기 조정 정책 설정 + image_label.setScaledContents(True) + + #이미지 중앙배치를 위해 + horizontal_layout = QHBoxLayout() + horizontal_layout.addWidget(image_label) + horizontal_layout.setAlignment(Qt.AlignCenter) + item_layout.addLayout(horizontal_layout) + + # item_layout.addWidget(image_label) + + # 정보 텍스트 + # info_text = f"상표권명: {result['title']}\n등록상태: {result['admin_status']}\nCategory: {result['product_category']}\nApplicant: {result['applicant']}\nPublication Date: {result['publication_date']}\nRegistration Date: {result['registration_date']}" + info_text = f"상표권명: {result['title']}
\n" \ + f"등록상태: {result['admin_status']}
\n" \ + f"카테고리: {result['product_category']}
\n" \ + f"권리자: {result['applicant']}
\n" \ + f" {result['publication_date']}
\n" \ + f" {result['registration_date']}" + + info_label = QLabel(info_text) + info_label.setToolTip(self.wrap_text(result['category_description'], 50)) + image_label.setToolTip(self.wrap_text(result['category_description'], 50)) + item_layout.addWidget(info_label) + + image_label.setStyleSheet(border_style) + info_label.setStyleSheet(border_style) + + # 레이아웃에 위젯 추가 + row = grid_index // grid_columns + col = grid_index % grid_columns + grid_layout.addLayout(item_layout, row, col) + grid_index += 1 + + # 결과 위젯에 닫기 버튼 추가 + close_button = QPushButton("Close") + close_button.clicked.connect(self.close_results_widget) + layout.addWidget(close_button) + + # 결과 위젯을 메인 윈도우에 추가 + self.results_widget.setGeometry(300, 300, 600, 300) # 위치와 크기 설정 + self.results_widget.setWindowTitle('Search Results') # 타이틀 설정 + self.results_widget.show() + + except Exception as e: + print(f"Error displaying results: {e}") + + + def close_results_widget(self): + # 결과 위젯닫기 함수를 호출할 때 사용하는 메서드 + self.results_widget.close() + + + def closeEvent(self, event): + self.scraper.close_browser() + event.accept() + +if __name__ == '__main__': + app = QApplication(sys.argv) + ex = MainApp() + ex.show() + sys.exit(app.exec_()) diff --git a/main_async.py b/main_async.py new file mode 100644 index 0000000..21d3101 --- /dev/null +++ b/main_async.py @@ -0,0 +1,248 @@ +import sys +import asyncio +from tkinter import EXCEPTION +import qasync +from PyQt5.QtWidgets import QApplication, QGridLayout, QSizePolicy, QProgressDialog, QMessageBox, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +from web_scraper_async import WebScraper + +class MainApp(QWidget): + def __init__(self): + super().__init__() + self.initUI() + self.scraper = WebScraper() + self.results_widget = None # 초기에는 결과 위젯이 없음 + + + def initUI(self): + searchLayout = QHBoxLayout() + self.searchLabel = QLabel('검색어 입력:', self) + + self.searchButton = QPushButton('검색 실행', self) + # 검색 버튼 클릭 시 start_search를 비동기로 실행 + self.searchButton.clicked.connect(self.on_search_button_clicked) + self.searchButton.setEnabled(False) # 초기에 버튼을 비활성화합니다. + + self.searchLineEdit = QLineEdit(self) + # 엔터 키를 누를 때 start_search를 비동기로 실행 + self.searchLineEdit.returnPressed.connect(self.on_search_button_clicked) + # self.searchLineEdit.setEnabled(False) # 초기에 입력 필드도 비활성화 + + + searchLayout.addWidget(self.searchLabel) + searchLayout.addWidget(self.searchLineEdit) + searchLayout.addWidget(self.searchButton) + + logLayout = QVBoxLayout() + self.logTextEdit = QTextEdit(self) + self.logTextEdit.setReadOnly(True) + + logLayout.addWidget(self.logTextEdit) + + mainLayout = QVBoxLayout() + mainLayout.addLayout(searchLayout) + mainLayout.addLayout(logLayout) + + self.setLayout(mainLayout) + self.setWindowTitle('Web Scraper') + self.setGeometry(300, 300, 300, 400) + + def start_search_process(self): + # 검색 프로세스 시작 시 QProgressDialog 설정 + self.progress_dialog = QProgressDialog("검색 진행 중... 잠시만 기다려 주세요.", "취소", 0, 100, self) + self.progress_dialog.setWindowTitle("검색 진행 상태") + self.progress_dialog.setWindowModality(Qt.WindowModal) + self.progress_dialog.setAutoClose(True) + self.progress_dialog.setAutoReset(True) + self.progress_dialog.canceled.connect(self.cancel_search) + self.progress_dialog.show() + + # 검색 시작 + asyncio.ensure_future(self.perform_search(self.searchLineEdit.text())) + + async def perform_search(self, term): + # 검색어를 사용하여 실제 검색 수행을 시뮬레이션합니다. + for i in range(101): + if self.progress_dialog.wasCanceled(): + break + self.progress_dialog.setValue(i) # 진행 상황 업데이트 + await asyncio.sleep(0.05) # 검색 시간을 시뮬레이션 + if not self.progress_dialog.wasCanceled(): + self.progress_dialog.setValue(100) # 작업 완료 + + def cancel_search(self): + # 검색 취소 로직 + print("검색이 사용자에 의해 취소되었습니다.") + + def on_search_button_clicked(self): + asyncio.ensure_future(self.start_search()) + + async def prepare_search(self): + # setup_browser에서 페이지 로딩을 관리하므로 여기서는 로딩 완료를 기다립니다. + print(f"prepare_search Start : is_page_loaded : {self.scraper.is_page_loaded}") + while not self.scraper.is_page_loaded: + print(f"is_page_loaded : {self.scraper.is_page_loaded}") + await asyncio.sleep(1) # 로딩 상태를 주기적으로 체크합니다. + + self.searchButton.setEnabled(True) + # self.searchLineEdit.setEnabled(True) + + async def start_search(self): + term = self.searchLineEdit.text() + if not term: + self.logTextEdit.append("검색어를 입력해주세요.") + QMessageBox.warning(self, "경고", "검색어를 입력해주세요.") + return + + self.logTextEdit.append("검색을 시작합니다...") + try: + results = await self.scraper.search_for_term(term) + if results: + self.logTextEdit.append("검색 완료. 결과를 처리합니다...") + await self.show_results(results) + else: + self.logTextEdit.append("검색 결과가 없거나 오류가 발생했습니다.") + QMessageBox.information(self, "검색 결과 없음", "검색 결과가 없으므로 지재권에 안심하시면 됩니다.", QMessageBox.Ok) + except Exception as e: + self.logTextEdit.append(f"검색 중 오류 발생: {str(e)}") + + async def show_results(self, results): + try: + # 이전 결과 위젯이 있으면 닫기 + if self.results_widget is not None: + self.results_widget.close() # 기존 위젯 닫기 + self.results_widget = None # 참조 제거 + + # 새 결과 위젯 생성 + self.results_widget = QWidget() + + layout = QVBoxLayout() + self.results_widget.setLayout(layout) + total_count = results['total_count'] + if total_count is not int: + total_count = int(results['total_count']) + set_count = min(total_count, 10) + grid_layout = QGridLayout() + layout.addLayout(grid_layout) + grid_index = 0 + grid_columns = 5 + + for i in range(1, set_count + 1): + result_key = f"result_{i}" + if result_key in results: + result = results[result_key] + print(f"result num {i}: {result}") + + # 테두리 설정 + border_style = '' + if result['admin_status'] == '등록': + border_style = 'border: 4px solid red;' + elif result['admin_status'] == '공고': + border_style = 'border: 3px solid black;' + + item_layout = QVBoxLayout() + item_widget = QWidget() + item_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) + print("item_widget set") + + try: + image_label = QLabel() + image_label.setFixedSize(150, 150) + image_data = await self.scraper.fetch_image_data(result['IDimageURL']) + print(f"image_data : {image_data}") + + pixmap = QPixmap() + if image_data: + pixmap.loadFromData(image_data) + scaled_pixmap = pixmap.scaled(image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) + image_label.setPixmap(scaled_pixmap) + image_label.setAlignment(Qt.AlignCenter) + image_label.setScaledContents(True) + print(f"image URL : [{result['IDimageURL']}]") + print("image set") + horizontal_layout = QHBoxLayout() + horizontal_layout.addWidget(image_label) + horizontal_layout.setAlignment(Qt.AlignCenter) + item_layout.addLayout(horizontal_layout) + print("horizontal_layout set") + except Exception as e: + print(f" 이미지 데이터 처리 중 에러 : {e}") + + info_text = f"상표권명: {result['title']}
\n" \ + f"등록상태: {result['admin_status']}
\n" \ + f"카테고리: {result['product_category']}
\n" \ + f"권리자: {result['applicant']}
\n" \ + f" {result['publication_date']}
\n" \ + f" {result['registration_date']}" + + print(f"info_text : {info_text}") + info_label = QLabel(info_text) + print(f"result['category_description'] : {result['category_description']}") + tooltip_text = self.wrap_text(result['category_description'], 50) + info_label.setToolTip(tooltip_text) + image_label.setToolTip(tooltip_text) + item_layout.addWidget(info_label) + + image_label.setStyleSheet(border_style) + info_label.setStyleSheet(border_style) + + print("item_layout set") + + # 레이아웃에 위젯 추가 + row = grid_index // grid_columns + col = grid_index % grid_columns + grid_layout.addLayout(item_layout, row, col) + # grid_layout.addLayout(item_layout, grid_index // grid_columns, grid_index % grid_columns) + grid_index += 1 + + close_button = QPushButton("Close") + close_button.clicked.connect(self.close_results_widget) + layout.addWidget(close_button) + print("close_button set") + + self.results_widget.setGeometry(300, 300, 800, 600) + self.results_widget.setWindowTitle('Search Results') + print("Search Results results_widget set") + self.results_widget.show() + + except Exception as e: + print(f"Error displaying results: {e}") + + def wrap_text(self, text, width=40): + """주어진 너비에 맞게 텍스트를 줄바꿈합니다.""" + words = text.split() + wrapped_text = '' + line_length = 0 + + for word in words: + if line_length + len(word) + 1 > width: + wrapped_text += '\n' + line_length = 0 + wrapped_text += word + ' ' + line_length += len(word) + 1 + + return wrapped_text.strip() + + + def close_results_widget(self): + self.results_widget.close() + + + def closeEvent(self, event): + asyncio.create_task(self.scraper.close_browser()) + super().closeEvent(event) # QWidget의 기본 closeEvent 처리 + +if __name__ == '__main__': + app = qasync.QApplication(sys.argv) + ex = MainApp() + loop = qasync.QEventLoop(app) + asyncio.set_event_loop(loop) + ex.show() + + # `setup_browser`가 완료될 때까지 기다립니다. + loop.create_task(ex.scraper.setup_browser()) + loop.create_task(ex.prepare_search()) + + # `loop.run_forever()`를 사용하여 이벤트 루프를 시작합니다. + loop.run_forever() diff --git a/main_with_re.py b/main_with_re.py new file mode 100644 index 0000000..0b905cf --- /dev/null +++ b/main_with_re.py @@ -0,0 +1,111 @@ +import sys +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QMessageBox, QGridLayout, QSizePolicy +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +from web_scraper_with_re import WebScraper + +class MainApp(QWidget): + def __init__(self): + super().__init__() + self.initUI() + self.scraper = WebScraper() + + def initUI(self): + self.searchLayout = QHBoxLayout() + self.searchLabel = QLabel('검색어 입력:', self) + self.searchLineEdit = QLineEdit(self) + self.searchButton = QPushButton('검색 실행', self) + self.searchButton.clicked.connect(self.start_search) + self.searchLineEdit.returnPressed.connect(self.start_search) + self.searchButton.setEnabled(False) # 버튼을 초기에 비활성화합니다. + + self.searchLayout.addWidget(self.searchLabel) + self.searchLayout.addWidget(self.searchLineEdit) + self.searchLayout.addWidget(self.searchButton) + + self.logLayout = QVBoxLayout() + self.logTextEdit = QTextEdit(self) + self.logTextEdit.setReadOnly(True) + self.logLayout.addWidget(self.logTextEdit) + + self.mainLayout = QVBoxLayout() + self.mainLayout.addLayout(self.searchLayout) + self.mainLayout.addLayout(self.logLayout) + + self.setLayout(self.mainLayout) + self.setWindowTitle('Web Scraper') + self.setGeometry(300, 300, 300, 400) + + self.prepare_search() # 페이지 로딩과 관련된 초기 설정 + + def prepare_search(self): + # self.scraper.setup_browser() # 브라우저 설정 + self.searchButton.setEnabled(True) # 페이지 로드 후 버튼 활성화 + + def start_search(self): + term = self.searchLineEdit.text() + if not term: + QMessageBox.warning(self, "경고", "검색어를 입력해주세요.") + return + + self.logTextEdit.append("검색을 시작합니다...") + results = self.scraper.search_for_term(term) + if results: + self.logTextEdit.append("검색 완료. 결과를 처리합니다...") + self.show_results(results) + else: + self.logTextEdit.append("검색 결과가 없거나 오류가 발생했습니다.") + QMessageBox.information(self, "검색 결과 없음", "검색 결과가 없으므로 지재권에 안심하시면 됩니다.", QMessageBox.Ok) + + def show_results(self, results): + # 이전 결과 위젯이 있으면 닫기 + if self.results_widget is not None: + self.results_widget.close() + + self.results_widget = QWidget() + layout = QVBoxLayout() + self.results_widget.setLayout(layout) + total_count = results['total_count'] + set_count = min(total_count, 10) + grid_layout = QGridLayout() + layout.addLayout(grid_layout) + grid_index = 0 + grid_columns = 5 + + for i in range(1, set_count + 1): + result_key = f"result_{i}" + if result_key in results: + result = results[result_key] + item_layout = QVBoxLayout() + item_widget = QWidget() + item_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) + + image_label = QLabel() + image_label.setFixedSize(150, 150) + image_data = self.scraper.fetch_image_data(result['IDimageURL']) + if image_data: + pixmap = QPixmap() + pixmap.loadFromData(image_data) + scaled_pixmap = pixmap.scaled(image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) + image_label.setPixmap(scaled_pixmap) + image_label.setAlignment(Qt.AlignCenter) + image_label.setScaledContents(True) + item_layout.addWidget(image_label) + + info_text = f"{result['title']} - {result['status']}" + info_label = QLabel(info_text) + item_layout.addWidget(info_label) + + grid_layout.addLayout(item_layout, grid_index // grid_columns, grid_index % grid_columns) + grid_index += 1 + + self.results_widget.setGeometry(300, 300, 800, 600) + self.results_widget.setWindowTitle('Search Results') + self.results_widget.show() + +if __name__ == '__main__': + app = QApplication(sys.argv) + ex = MainApp() + ex.show() + sys.exit(app.exec_()) + diff --git a/src/kipo_api_client.py b/src/kipo_api_client.py new file mode 100644 index 0000000..c50999c --- /dev/null +++ b/src/kipo_api_client.py @@ -0,0 +1,86 @@ +import requests +import xml.etree.ElementTree as ET + +class KipoApiClientError(Exception): + """Custom exception class for Kipo API client errors, includes the error code.""" + def __init__(self, message, code=None): + super().__init__(message) + self.code = code + print(f"Error : {code} - {message}") + +class KipoApiClient: + def __init__(self, service_key): + self.service_key = service_key + self.base_url = 'http://kipo-api.kipi.or.kr/openapi/service/patUtiModInfoSearchSevice/getWordSearch' + + def search_patents(self, word, year, patent=True, utility=True): + params = { + 'serviceKey': self.service_key, + 'word': word, + 'year': year, + 'patent': str(patent).lower(), + 'utility': str(utility).lower() + } + response = requests.get(self.base_url, params=params) + if response.status_code != 200: + raise KipoApiClientError("HTTP 오류: 상태 코드 {}".format(response.status_code)) + + return self.parse_response(response.content) + + def parse_response(self, xml_response): + root = ET.fromstring(xml_response) + if root.find('.//cmmMsgHeader') is not None: + return self.handle_error(root) + else: + return self.extract_data(root) + + def handle_error(self, root): + err_msg = root.find('.//errMsg').text + return_auth_msg = root.find('.//returnAuthMsg').text + return_reason_code = int(root.find('.//returnReasonCode').text) + error_message = f"API Error {return_reason_code}: {return_auth_msg} ({err_msg})" + + if return_reason_code == 30: + raise KipoApiClientError("Unregistered Service Key Error\n등록되지 않은 서비스키", code=return_reason_code) + elif return_reason_code == 20: + raise KipoApiClientError("Access Denied Error\n서비스 접근거부", code=return_reason_code) + elif return_reason_code == 22: + raise KipoApiClientError("Service Request Limit Exceeded Error\n서비스 요청제한횟수 초과에러", code=return_reason_code) + elif return_reason_code == 32: + raise KipoApiClientError("Unregistered IP Error\n등록되지 않은 IP", code=return_reason_code) + elif return_reason_code == 1: + raise KipoApiClientError("Application Error\n어플리케이션 에러", code=return_reason_code) + elif return_reason_code == 4: + raise KipoApiClientError("HTTP Error\nHTTP 에러", code=return_reason_code) + elif return_reason_code == 12: + raise KipoApiClientError("No OpenAPI Service Error\n해당 오픈 API 서비스가 없거나 폐기됨", code=return_reason_code) + elif return_reason_code == 31: + raise KipoApiClientError("Deadline Expired Error\n활용기간 만료", code=return_reason_code) + elif return_reason_code == 99: + raise KipoApiClientError("Unknown Error\n기타에러", code=return_reason_code) + else: + raise KipoApiClientError("Unexpected Error: " + error_message, code=return_reason_code) + + def extract_data(self, root): + items = [] + for item in root.findall('.//item'): + data = { + 'registerStatus': item.find('registerStatus').text if item.find('registerStatus') is not None else None, + 'inventionTitle': item.find('inventionTitle').text if item.find('inventionTitle') is not None else None, + 'ipcNumber': item.find('ipcNumber').text if item.find('ipcNumber') is not None else None, + 'registerNumber': item.find('registerNumber').text if item.find('registerNumber') is not None else None, + 'registerDate': item.find('registerDate').text if item.find('registerDate') is not None else None, + 'applicationNumber': item.find('applicationNumber').text if item.find('applicationNumber') is not None else None, + 'applicationDate': item.find('applicationDate').text if item.find('applicationDate') is not None else None, + 'openNumber': item.find('openNumber').text if item.find('openNumber') is not None else None, + 'openDate': item.find('openDate').text if item.find('openDate') is not None else None, + 'publicationNumber': item.find('publicationNumber').text if item.find('publicationNumber') is not None else None, + 'publicationDate': item.find('publicationDate').text if item.find('publicationDate') is not None else None, + 'abstractContent': item.find('astrtCont').text if item.find('astrtCont') is not None else None, + 'bigDrawing': item.find('bigDrawing').text if item.find('bigDrawing') is not None else None, + 'drawing': item.find('drawing').text if item.find('drawing') is not None else None, + 'applicantName': item.find('applicantName').text if item.find('applicantName') is not None else None + } + items.append(data) + print(f"성공적인 응답결과 : {items}") + return items diff --git a/test.py b/test.py new file mode 100644 index 0000000..ed7d52f --- /dev/null +++ b/test.py @@ -0,0 +1,14 @@ +import requests +from src.kipo_api_client import KipoApiClient + +if __name__ == "__main__": + service_key = 'X9Tz3JqC%2FJcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6%2Bt3dyLZV3gh2TPXdNhxsRQwaKP673Q%3D%3D' + client = KipoApiClient(service_key) + + try: + # 테스트 실행: 'view'라는 단어로 특허 검색을 시도합니다. + result = client.search_patents('view', '2022', True, True) + # 결과를 콘솔에 출력합니다. + print("Test Success:", result) + except Exception as e: + print("Test Failed:", e) diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..26ae570 --- /dev/null +++ b/test_api.py @@ -0,0 +1,42 @@ +from kipris_api_from_publicdata import Kipris + +# 테스트를 위한 URL 및 파라미터 설정 +apikey = 'X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TPXdNhxsRQwaKP673Q==' +search_keyword = "써지오" +url = 'http://kipo-api.kipi.or.kr/openapi/service/trademarkInfoSearchService/getWordSearch' + +params = { + 'serviceKey': apikey, + 'searchString': search_keyword, + 'searchRecentYear': '0', + 'title': '', + 'fullText': '', + 'drawing': '', + 'bigDrawing': '' +} + +# XMLParser 인스턴스 생성 및 실행 +parser = Kipris(url, params) +results = parser.run() + +# 모든 아이템의 결과 출력 +if results: + print("결과가 있는 아이템 목록:") + for index, item in enumerate(results, start=1): + print(f"\n아이템 {index} 정보:") + for key, value in item.items(): + print(f"{key}: {value}") +else: + print("결과가 없습니다.") + + +# ※ 행정 상태 도움말 +# 거절 : 출원 후 특허 심사과정에서 실체적인 특허 등록요건을 만족하지 못할 경우에 심사관이 취하는 행정처분 +# 등록 : 심사관이 심사한 결과 등록요건에 적합하여 설정등록을 받을 수 있다는 내용의 행정처분 +# 소멸 : 특허등록 후 존속기간이 만료되어 권리가 소멸된 상태 +# 무효 : 출원 또는 등록된 상태에 대하여 특정 사유로 인해 그 권리나 행위가 무효화 된 상태 +# 취하 : 출원한 특허가 등록되기전 여러 사유로 인하여 출원이 취소된 상태 +# 포기 : 출원인의 포기서 제출, 등록료 불납 등으로 등록결정이나 권리를 포기한 상태 +# 공개 : 출원이나 등록사실이 일반 공중에게 공표된 상태로 출원 후 18개월이 지난 건 +# *조기공개신청시 18개월 미만도 공개가능 +# \ No newline at end of file diff --git a/test_main.py b/test_main.py new file mode 100644 index 0000000..de464ac --- /dev/null +++ b/test_main.py @@ -0,0 +1,48 @@ +import unittest +from unittest.mock import patch +from src.kipo_api_client import KipoApiClient, KipoApiClientError + +class TestKipoApiClient(unittest.TestCase): + def setUp(self): + apikey = '''X9Tz3JqC%2FJcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6%2Bt3dyLZV3gh2TPXdNhxsRQwaKP673Q%3D%3D''' + self.client = KipoApiClient(apikey) + + def test_search_patents_successful(self): + # 예상되는 성공적인 응답을 시뮬레이션합니다. + xml_response = ''' + + + 1000319440000 + 회전체의센서와센서의엔코더 + G01D 5/00 + ... + + '''.encode('utf-8') # 문자열을 UTF-8로 인코딩 + + with patch('requests.get') as mocked_get: + mocked_get.return_value.status_code = 200 + mocked_get.return_value.content = xml_response + response = self.client.search_patents('view', '0', True, True) + self.assertEqual(len(response), 1) + self.assertEqual(response[0]['inventionTitle'], '회전체의센서와센서의엔코더') + + def test_handle_error(self): + # 에러 상황을 시뮬레이션하여 에러 처리를 테스트합니다. + xml_response = ''' + + + 서비스 오류 + 등록되지 않은 서비스키 오류 + 30 + + '''.encode('utf-8') # 문자열을 UTF-8로 인코딩 + + with patch('requests.get') as mocked_get: + mocked_get.return_value.status_code = 200 + mocked_get.return_value.content = xml_response + with self.assertRaises(KipoApiClientError) as context: + self.client.search_patents('view', '0', True, True) + self.assertIn('등록되지 않은 서비스키 오류', str(context.exception)) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test_web.py b/test_web.py new file mode 100644 index 0000000..f44847f --- /dev/null +++ b/test_web.py @@ -0,0 +1,20 @@ +from web_scraper import WebScraper + +def test_scraper(): + scraper = WebScraper() + scraper.setup_browser() + try: + scraper.navigate_to_page("http://kdtj.kipris.or.kr/kdtj/searchLogina.do?method=loginTM#page1") + keyword = "스키에이트" + results = scraper.search_for_term(keyword) + if results: + print("Search Results:") + for key, value in results.items(): + print(f"{key}: {value}") + else: + print("No results found or an error occurred.") + finally: + scraper.close_browser() + +if __name__ == "__main__": + test_scraper() diff --git a/web_scraper.py b/web_scraper.py new file mode 100644 index 0000000..8b4f3df --- /dev/null +++ b/web_scraper.py @@ -0,0 +1,199 @@ +# import asyncio +from playwright.sync_api import sync_playwright +import random, requests, json +from PIL import Image +from io import BytesIO + +class WebScraper: + def __init__(self): + self.results = {} + self.playwright = None + self.browser = None + self.context = None + self.page = None + filename = 'categories.json' + self.category_description = self.load_category_descriptions(filename) + self.url = "http://kdtj.kipris.or.kr/kdtj/searchLogina.do?method=loginTM#page1" + + def setup_browser(self): + """브라우저 설정 및 인스턴스 생성""" + self.playwright = sync_playwright().start() + self.browser = self.playwright.chromium.launch(headless=True) # For testing, set headless to True + self.context = self.browser.new_context(user_agent=random.choice([ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/85.0.0.0" + ])) + self.page = self.context.new_page() + self.navigate_to_page(self.url) + + def navigate_to_page(self, url): + """주어진 URL로 이동""" + self.page.goto(url) + + # def search_for_term_success(self, term): + # """검색어로 검색하고 결과 수집""" + # try: + # self.page.fill("#queryText", term) + # self.page.click(".input_btn") + # # JavaScript에 의해 결과가 동적으로 로드되기를 기다립니다. + # loaded = self.page.wait_for_function("document.querySelector('form#listForm section.search_section article') != null", timeout=10000) + # if not loaded: + # print("검색 결과가 시간 내에 로드되지 않았습니다.") + # return None + + # # 결과 로딩이 확인된 후, 실제 요소를 수집 + # self.page.wait_for_selector("form#listForm section.search_section", state="visible", timeout=10000) + # total_count = self.page.text_content("form#listForm section.search_section div p span.total") + # if total_count: + # self.results['total_count'] = total_count.strip() + # articles = self.page.query_selector_all("form#listForm section.search_section article") + # for i, article in enumerate(articles): + # img_element = article.query_selector("div.thumb a img") + # if img_element: + # trademark_image = img_element.get_attribute("src") + # name_element = article.query_selector("div.search_section_title h1.stitle a b") + # if name_element: + # trademark_name = name_element.text_content() + # self.results[f"result_{i+1}"] = {"image": trademark_image, "name": trademark_name} + # else: + # print(f"이미지 요소가 없는 기사: {i+1}") + # return self.results + # except Exception as e: + # print(f"오류 발생 : {e}") + # return None + + + def search_for_term(self, term): + """검색어로 검색하고 결과 수집""" + try: + self.page.fill("#queryText", term) + self.page.click(".input_btn") + + # JavaScript에 의해 결과가 동적으로 로드되기를 기다립니다. + loaded = self.page.wait_for_function("document.querySelector('form#listForm section.search_section article') != null") + if not loaded: + print("검색 결과가 시간 내에 로드되지 않았습니다.") + return None + + # 검색 결과가 없는지 확인 + nodata_info = self.page.query_selector(".nodata_info") + if nodata_info: + print("검색 결과가 없습니다.") + # 특정 메서드 호출 및 함수 종료 + # self.handle_no_search_results() + return None + + + # 결과 로딩이 확인된 후, 실제 요소를 수집 + self.page.wait_for_selector("form#listForm section.search_section", state="visible", timeout=10000) + total_count = self.page.text_content("form#listForm section.search_section div p span.total") + if total_count: + self.results['total_count'] = total_count.strip() + articles = self.page.query_selector_all("form#listForm section.search_section article") + for i, article in enumerate(articles): + id_and_name_element = article.query_selector("div:nth-child(1) span input[type='checkbox']") + trademark_name = id_and_name_element.get_attribute("title") if id_and_name_element else "No name found" + + applno = id_and_name_element.get_attribute("value") if id_and_name_element else "No ID found" + if applno: + trademark_image_url_by_id = f"http://kdtj.kipris.or.kr/kdtj/remoteFile.do?method=bigImageTM&applno={applno}&no={applno}_tm000001.jpg" + + img_element = article.query_selector("div:nth-child(2) div a img") + trademark_image = img_element.get_attribute("src") if img_element else "No image found" + + admin_status_element = article.query_selector("div:nth-child(1) h1 a:nth-child(1) span") + admin_status = admin_status_element.text_content() if admin_status_element else "No status found" + + product_category_element = article.query_selector("div:nth-child(2) ul li:nth-child(1) a span") + product_category = product_category_element.text_content() if product_category_element else "No category found" + if product_category: + category_desc = self.add_category_description(product_category) + + applicant_element = article.query_selector("div:nth-child(2) ul li:nth-child(2) span[title]") + applicant = applicant_element.get_attribute("title") if applicant_element else "No applicant found" + + publication_date_element = article.query_selector("div:nth-child(2) ul li:nth-child(8)") + publication_date = publication_date_element.text_content().strip() if publication_date_element else "No publication date found" + + registration_date_element = article.query_selector("div:nth-child(2) ul li:nth-child(6)") + registration_date = registration_date_element.text_content().strip() if registration_date_element else "No registration date found" + + # name_element = article.query_selector("div:nth-child(1) h1 a:nth-child(2) font b") + # trademark_name = name_element.text_content() if name_element else "No name found" + + # Use the title attribute of the checkbox input for the trademark name + name_element = article.query_selector("div:nth-child(1) span input[type='checkbox']") + trademark_name = name_element.get_attribute("title") if name_element else "No name found" + + if not admin_status == "소멸": + self.results[f"result_{i+1}"] = { + "ID": applno, + "title": trademark_name, + "admin_status": admin_status, + "imageURL": trademark_image, + "IDimageURL": trademark_image_url_by_id, + "product_category": product_category, + "category_description": category_desc, + "applicant": applicant, + "publication_date": publication_date, + "registration_date": registration_date, + } + return self.results + else: + print("No total count element found, possibly incorrect selector or page structure has changed.") + except Exception as e: + print(f"오류 발생 : {e}") + return None + + def download_image(url, applno): + """이미지를 다운로드하고 applno를 파일 이름으로 사용하여 저장합니다.""" + response = requests.get(url) + if response.status_code == 200: + filename = f"{applno}.jpeg" # 파일 이름을 ID로 설정 + with open(filename, 'wb') as file: + file.write(response.content) + print(f"이미지가 성공적으로 저장되었습니다: {filename}") + else: + print(f"이미지 다운로드 실패: HTTP {response.status_code}") + + def load_category_descriptions(self, filename): + """JSON 파일에서 카테고리 설명을 로드합니다.""" + with open(filename, 'r', encoding='utf-8') as file: + return json.load(file) + + def add_category_description(self, category_code): + """주어진 카테고리 코드에 따라 설명을 반환합니다.""" + return self.category_description.get(category_code, "카테고리 설명을 찾을 수 없습니다.") + + def fetch_image_data(self, url): + """주어진 URL로부터 이미지 데이터를 가져와 반환합니다.""" + response = requests.get(url) + if response.status_code == 200: + # 서버 응답 헤더에서 Content-Type 확인 + content_type = response.headers.get('Content-Type', '') + if 'image' in content_type: + return response.content + else: + # Content-Type이 이미지가 아니면, 데이터를 이미지로 변환 + try: + image = Image.open(BytesIO(response.content)) + with BytesIO() as buffer: + image.save(buffer, 'JPEG') # 예시로 JPEG 포맷을 사용 + return buffer.getvalue() + except Exception as e: + print(f"이미지 변환 실패: {e}") + return None + else: + print(f"이미지 다운로드 실패: HTTP {response.status_code}") + return None + + def close_browser(self): + """브라우저 리소스 정리""" + if self.context: + self.context.close() + self.browser.close() + self.playwright.stop() + diff --git a/web_scraper_async.py b/web_scraper_async.py new file mode 100644 index 0000000..4443a8b --- /dev/null +++ b/web_scraper_async.py @@ -0,0 +1,228 @@ +import asyncio, aiofiles, aiohttp +from playwright.async_api import async_playwright +import random, requests, json +from PIL import Image +from io import BytesIO + +class WebScraper: + def __init__(self): + self.results = {} + self.playwright = None + self.browser = None + self.context = None + self.page = None + filename = 'categories.json' + self.category_description = self.load_category_descriptions(filename) + self.url = "http://kdtj.kipris.or.kr/kdtj/searchLogina.do?method=loginTM#page1" + self.is_page_loaded = False + + async def setup_browser(self): + """브라우저 설정 및 인스턴스 생성""" + self.playwright = await async_playwright().start() + self.browser = await self.playwright.chromium.launch(headless=False) + self.context = await self.browser.new_context( + user_agent=random.choice([ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/85.0.0.0", + ])) + # ]), + # cache_enabled=True # 캐시 활성화 + # ) + self.page = await self.context.new_page() + await self.page.route('**/*.{png,jpg,jpeg,svg,gif,css}', lambda route: route.abort()) + + try: + await self.page.goto(self.url, wait_until='networkidle') + self.is_page_loaded = True + print("Page loaded successfully.") + except Exception as e: + print(f"Failed to load the page: {e}") + self.is_page_loaded = False # 로드 실패 처리 + + + async def navigate_to_page(self, url): + try: + await self.page.goto(url, wait_until='networkidle') + self.is_page_loaded = True + print("Page loaded successfully.") + except Exception as e: + print(f"Failed to load the page: {e}") + self.is_page_loaded = False # 로드 실패 처리 + + + async def search_for_term(self, term): + """검색어로 비동기적으로 검색하고 결과를 수집합니다.""" + try: + self.results = {} + # await self.page.fill("#queryText", term) + await self.page.fill("#keywordTextarea", term) + + print(f"검색어 입력 : {term}") + await self.page.click(".input_btn") + print(f"검색버튼 클릭") + + # JavaScript에 의해 결과가 동적으로 로드되기를 기다립니다. + loaded = await self.page.wait_for_function( + "document.querySelector('form#listForm section.search_section article') != null" + ) + if not loaded: + print("검색 결과가 시간 내에 로드되지 않았습니다.") + return None + print(f"결과가 동적으로 로드되기를 기다림 : {loaded}") + + # 검색 결과가 없는지 확인 + nodata_info = await self.page.query_selector(".nodata_info") + if nodata_info: + print("검색 결과가 없습니다.") + return None + + # 결과 로딩이 확인된 후, 실제 요소를 수집 + await self.page.wait_for_selector("form#listForm section.search_section", state="visible", timeout=10000) + total_count = await self.page.text_content("form#listForm section.search_section div p span.total") + total_count = total_count.strip() + total_count = int(total_count.replace(',', '')) + print(f"total_count : {total_count}") + + if total_count: + self.results['total_count'] = total_count + articles = await self.page.query_selector_all("form#listForm section.search_section article") + print(f"articles : {len(articles)} 개") + for i, article in enumerate(articles): + id_and_name_element = await article.query_selector("div:nth-child(1) span input[type='checkbox']") + trademark_name = await id_and_name_element.get_attribute("title") if id_and_name_element else "No name found" + print(f"trademark_name : {trademark_name}") + + applno = await id_and_name_element.get_attribute("value") if id_and_name_element else "No ID found" + trademark_image_url_by_id = ( + f"http://kdtj.kipris.or.kr/kdtj/remoteFile.do?method=bigImageTM&applno={applno}&no={applno}_tm000001.jpg" + if applno else None + ) + print(f"trademark_image_url_by_id : {trademark_image_url_by_id}") + + img_element = await article.query_selector("div:nth-child(2) div a img") + trademark_image = await img_element.get_attribute("src") if img_element else "No image found" + print(f"trademark_image : {trademark_image}") + + admin_status_element = await article.query_selector("div:nth-child(1) h1 a:nth-child(1) span") + admin_status = await admin_status_element.text_content() if admin_status_element else "No status found" + print(f"admin_status : {admin_status}") + + product_category_element = await article.query_selector("div:nth-child(2) ul li:nth-child(1) a span") + product_category = await product_category_element.text_content() if product_category_element else "No category found" + category_desc = self.add_category_description(product_category) if product_category else "No category description" + print(f"product_category : {product_category}") + print(f"category_desc : {category_desc}") + + applicant_element = await article.query_selector("div:nth-child(2) ul li:nth-child(2) span[title]") + applicant = await applicant_element.get_attribute("title") if applicant_element else "No applicant found" + print(f"applicant : {applicant}") + + publication_date_element = await article.query_selector("div:nth-child(2) ul li:nth-child(8)") + if publication_date_element: + publication_date_content = await publication_date_element.text_content() + publication_date = publication_date_content.strip() if publication_date_content else "No publication date found" + else: + publication_date = "No publication date found" + print(f"publication_date : {publication_date}") + + registration_date_element = await article.query_selector("div:nth-child(2) ul li:nth-child(6)") + if registration_date_element: + registration_date_content = await registration_date_element.text_content() + registration_date = registration_date_content.strip() if registration_date_content else "No registration date found" + else: + registration_date = "No registration date found" + print(f"registration_date : {registration_date}") + + if not (admin_status == "소멸" or admin_status == "거절"): + self.results[f"result_{i+1}"] = { + "ID": applno, + "title": trademark_name, + "admin_status": admin_status, + "imageURL": trademark_image, + "IDimageURL": trademark_image_url_by_id, + "product_category": product_category, + "category_description": category_desc, + "applicant": applicant, + "publication_date": publication_date, + "registration_date": registration_date, + } + # print(f"results : {self.results}") + await self.navigate_to_page(self.url) + return self.results + else: + print("No total count element found, possibly incorrect selector or page structure has changed.") + except Exception as e: + print(f"오류 발생 : {e}") + return None + + async def download_image(self, url, applno): + """이미지를 비동기적으로 다운로드하고 applno를 파일 이름으로 사용하여 저장합니다.""" + async with aiohttp.ClientSession() as session: + print(f"download_image session Start!!") + async with session.get(url) as response: + print(f"download_image url : {url}") + if response.status == 200: + filename = f"{applno}.jpeg" + async with aiofiles.open(filename, 'wb') as file: + content = await response.read() + await file.write(content) + print(f"이미지가 성공적으로 저장되었습니다: {filename}") + else: + print(f"이미지 다운로드 실패: HTTP {response.status}") + + async def fetch_image_data(self, url): + """주어진 URL로부터 이미지 데이터를 비동기적으로 가져와 반환합니다.""" + async with aiohttp.ClientSession() as session: + # print(f"download_image session Start!!") + async with session.get(url) as response: + print(f"download_image url : {url}") + if response.status == 200: + # print(f"response : {response}") + content_type = response.headers.get('Content-Type', '') # await 제거 + print(f"content_type : {content_type}") + if 'image' in content_type or 'octet-stream' in content_type: + # print(f"image content type or octet-stream : {content_type}") + return await response.read() + else: + try: + # Content-Type이 이미지가 아니면, 데이터를 이미지로 변환 + data = await response.read() + # print(f"Content-Type이 이미지가 아님 : {data}") + image = Image.open(BytesIO(data)) + with BytesIO() as buffer: + image.save(buffer, 'JPEG') + print(f"image 를 JPEG로 저장") + return buffer.getvalue() + except Exception as e: + print(f"이미지 변환 실패: {e}") + return None + else: + print(f"이미지 다운로드 실패: HTTP {response.status}") + return None + + # async def load_category_descriptions(self, filename): + # """JSON 파일에서 카테고리 설명을 비동기적으로 로드합니다.""" + # async with aiofiles.open(filename, 'r', encoding='utf-8') as file: + # content = await file.read() + # print(f"JSON 파일에서 카테고리 설명을 비동기적으로 로드합니다: {content}") + # return json.loads(content) + + def load_category_descriptions(self, filename): + """JSON 파일에서 카테고리 설명을 로드합니다.""" + with open(filename, 'r', encoding='utf-8') as file: + return json.load(file) + + def add_category_description(self, category_code): + """주어진 카테고리 코드에 따라 설명을 반환합니다.""" + print(f"add_category_description => category_code: {category_code}") + return self.category_description.get(category_code, "카테고리 설명을 찾을 수 없습니다.") + + + async def close_browser(self): + if self.context: + await self.context.close() # context를 닫습니다. + await self.browser.close() # browser를 닫습니다. + await self.playwright.stop() # playwright 세션을 종료합니다. diff --git a/web_scraper_with_re.py b/web_scraper_with_re.py new file mode 100644 index 0000000..447d492 --- /dev/null +++ b/web_scraper_with_re.py @@ -0,0 +1,110 @@ +from requests_html import HTMLSession +import json +import requests + +class WebScraper: + def __init__(self): + self.session = HTMLSession() + self.url = "http://kdtj.kipris.or.kr/kdtj/searchLogina.do?method=loginTM#page1" + self.results = {} + self.category_description = self.load_category_descriptions('categories.json') + + def add_category_description(self, category_code): + """주어진 카테고리 코드에 따라 설명을 반환합니다.""" + description = self.category_description.get(category_code, "카테고리 설명을 찾을 수 없습니다.") + return description + + def load_category_descriptions(self, filename): + """JSON 파일에서 카테고리 설명을 로드합니다.""" + with open(filename, 'r', encoding='utf-8') as file: + return json.load(file) + + def navigate_to_page(self, url): + """지정된 URL로 이동하고 페이지를 로드합니다.""" + response = self.session.get(url) + response.html.render() # 필요 시 JavaScript 실행 + print("Page loaded successfully.") + return response + + def search_for_term(self, term): + """검색어로 검색하고 결과를 수집합니다.""" + response = self.navigate_to_page(self.url) # 로그인 페이지 로드 + # await self.page.fill("#queryText", term) + # 검색어 입력과 폼 제출을 시뮬레이션 + try: + form = response.html.find('form', containing='검색', first=True) + if form: + search_url = form.attrs.get('action') + if search_url: + data = { + 'keywordTextarea': term, # form에서 요구하는 필드 이름에 맞추어야 함 + } + search_response = self.session.post(search_url, data=data) + search_response.html.render() + return search_response + else: + print("폼의 action URL을 찾을 수 없습니다.") + else: + print("검색 폼을 찾을 수 없습니다.") + except Exception as e: + print(f"검색 실행 중 오류 발생: {e}") + + # response.html.render(script=script, reload=False) + + # # response.html.find('#keywordTextarea', first=True).fill(term) + # self.page.evaluate(f"document.querySelector('#keywordTextarea').value = '{term}';") + # print(f"검색어 입력: {term}") + # input_field = response.html.find('#keywordTextarea', first=True) + # script = f"document.querySelector('#keywordTextarea').value = '{term}';" + # response.html.page.evaluate(script) # JavaScript 실행 + + # search_button = response.html.find('.input_btn', first=True) + # search_button.click() + response.html.render(wait=10, timeout=20) # 결과 로드를 위해 대기 + + articles = response.html.find('form#listForm section.search_section article') + if not articles: + print("검색 결과가 없습니다.") + return None + + # Store results in a structured format + for i, article in enumerate(articles, 1): + title = article.find('.title', first=True).text + status = article.find('.status', first=True).text + image_url = article.find('img', first=True).attrs.get('src', '') + + self.results[f'result_{i}'] = { + 'title': title, + 'status': status, + 'image_url': image_url + } + return self.results + + def download_image(self, url, applno): + """이미지를 다운로드하고 applno를 파일 이름으로 사용하여 저장합니다.""" + response = requests.get(url) # 이미지 URL에 대한 요청 + if response.status_code == 200: + filename = f"{applno}.jpeg" + with open(filename, 'wb') as file: + file.write(response.content) + print(f"이미지가 성공적으로 저장되었습니다: {filename}") + else: + print(f"이미지 다운로드 실패: HTTP {response.status_code}") + + def fetch_image_data(self, url): + """주어진 URL로부터 이미지 데이터를 직접 가져와 반환합니다.""" + response = requests.get(url) + if response.status_code == 200: + return response.content + else: + print(f"이미지 다운로드 실패: HTTP {response.status_code}") + return None + + def close_browser(self): + """세션을 종료합니다.""" + self.session.close() + +# # 사용 예시 +# scraper = WebScraper() +# results = scraper.search_for_term("특허") +# print(results) diff --git a/xmltest.py b/xmltest.py new file mode 100644 index 0000000..b4586e3 --- /dev/null +++ b/xmltest.py @@ -0,0 +1,55 @@ +import xml.etree.ElementTree as ET +import requests + +# XML 응답 데이터 (예시) +# xml_response = b'\xec\xa3\xbc\xec\x8b\x9d\xed\x9a\x8c\xec\x82\xac202301024020230000430\xea\xb3\xb5\xea\xb3\xa0http://plus.kipris.or.kr/kiprisplusws/fileToss.jsp?arg=ad7a17eeeef6e4ea4b5e22ef00dd3e2991ba5d9d09407d4bf69fd0e282d93ba4822ea711d1c0014105513d5758c737d00c9e62115cad0aef03http://plus.kipris.or.kr/kiprisplusws/fileToss.jsp?arg=ed43a0609e94d6e251697a9d72a913440be47db3b03c4b733d99b169217b523f0853e0d6dd613d455ccd291c28fd8817ed949b84dd8bb0c3Y4202404034020240052090\xec\x8a\xa4\xed\x82\xa4\xec\x97\x90\xec\x9d\xb4\xed\x8a\xb81014' +apikey = 'X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TPXdNhxsRQwaKP673Q==' +url = 'http://kipo-api.kipi.or.kr/openapi/service/trademarkInfoSearchService/getWordSearch' +params = { + 'serviceKey': apikey, # 디코딩된 키 사용 + 'searchString': '스키에이트', + 'searchRecentYear': '0', + 'title': '', + 'fullText': '', + 'drawing': '', + 'bigDrawing': '' +} +response = requests.get(url, params=params) + +decoded_data = response.content.decode('utf-8') +print(f"decoded_data : {decoded_data}") + +root = ET.fromstring(decoded_data) + +# 결과 저장을 위한 딕셔너리 생성 +results = [] + +# 'item' 태그를 순회하면서 필요한 데이터 추출 +for item in root.findall('.//body/items/item'): + result = { + "index_no": item.find('indexNo').text if item.find('indexNo') is not None else None, + "application_number": item.find('applicationNumber').text if item.find('applicationNumber') is not None else None, + "application_date": item.find('applicationDate').text if item.find('applicationDate') is not None else None, + "publication_number": item.find('publicationNumber').text if item.find('publicationNumber') is not None else None, + "publication_date": item.find('publicationDate').text if item.find('publicationDate') is not None else None, + "registration_date": item.find('registrationDate').text if item.find('registrationDate') is not None else None, + "registration_number": item.find('registrationNumber').text if item.find('registrationNumber') is not None else None, + "applicant_name": item.find('applicantName').text if item.find('applicantName') is not None else None, + "agent_name": item.find('agentName').text if item.find('agentName') is not None else None, + "title": item.find('title').text if item.find('title') is not None else None, + "drawing_url": item.find('drawing').text if item.find('drawing') is not None else None, + "big_drawing_url": item.find('bigDrawing').text if item.find('bigDrawing') is not None else None, + "full_text": item.find('fullText').text if item.find('fullText') is not None else None, + "application_status": item.find('applicationStatus').text if item.find('applicationStatus') is not None else None, + "classification_code": item.find('classificationCode').text if item.find('classificationCode') is not None else None + } + results.append(result) + +# 결과 출력 (첫 번째 아이템 예시) +if results: + first_item = results[0] + print("첫 번째 아이템 정보:") + for key, value in first_item.items(): + print(f"{key}: {value}") +else: + print("결과가 없습니다.")