Spaces:
Sleeping
Sleeping
| """ | |
| νκ΅κ±΄μ€κΈ°μ€μΌν°(KCSC) API ν΄λΌμ΄μΈνΈ λͺ¨λ | |
| """ | |
| import time | |
| import json | |
| import logging | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from typing import Dict, List, Any, Optional, Union, Tuple | |
| from src import config | |
| from src.utils import ( | |
| HTTPClient, | |
| APIError, | |
| ConfigError, | |
| DataProcessingError, | |
| save_json_data, | |
| normalize_doc_type | |
| ) | |
| logger = config.setup_logger("kcsc_api") | |
| class KCSCApiClient: | |
| """ | |
| νκ΅κ±΄μ€κΈ°μ€μΌν°(KCSC) APIμ ν΅μ νλ ν΄λΌμ΄μΈνΈ | |
| """ | |
| def __init__(self, api_key: str = None, base_url: str = None): | |
| """ | |
| KCSC API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν | |
| Args: | |
| api_key: KCSC API ν€ | |
| base_url: KCSC API κΈ°λ³Έ URL | |
| """ | |
| self.api_key = api_key or config.KCSC_API_KEY | |
| if not self.api_key: | |
| raise ConfigError("KCSC API ν€κ° μ€μ λμ§ μμμ΅λλ€. .env νμΌμ νμΈνμΈμ.") | |
| self.base_url = base_url or "https://www.kcsc.re.kr/OpenApi" | |
| self.data_dir = config.DATA_DIR | |
| # HTTP ν΄λΌμ΄μΈνΈ μ΄κΈ°ν | |
| self.http_client = HTTPClient( | |
| base_url=self.base_url, | |
| default_headers={ | |
| "Accept": "application/json", | |
| "Content-Type": "application/json" | |
| } | |
| ) | |
| logger.info(f"KCSC API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μλ£ (κΈ°λ³Έ URL: {self.base_url})") | |
| def get_code_list(self, doc_type: str = "KDS") -> List[Dict[str, Any]]: | |
| """ | |
| μ½λ λͺ©λ‘ κ°μ Έμ€κΈ° | |
| Args: | |
| doc_type: λ¬Έμ μ ν (μ: "KDS", "KCS") | |
| Returns: | |
| μ½λ λͺ©λ‘ | |
| Raises: | |
| APIError: API μμ² μ€ν¨ μ | |
| """ | |
| doc_type = normalize_doc_type(doc_type) | |
| params = { | |
| "Type": doc_type, | |
| "key": self.api_key | |
| } | |
| try: | |
| logger.info(f"{doc_type} μ½λ λͺ©λ‘ μμ² μ€...") | |
| response = self.http_client.get("CodeList", params=params) | |
| data = response.json() | |
| if "List" not in data: | |
| raise APIError(f"{doc_type} μ½λ λͺ©λ‘μ 'List' νλκ° μμ΅λλ€.", response=data) | |
| code_list = data["List"] | |
| logger.info(f"{doc_type} μ½λ λͺ©λ‘ μμ² μ±κ³΅ (μ΄ {len(code_list)}κ° νλͺ©)") | |
| return code_list | |
| except APIError: | |
| raise | |
| except Exception as e: | |
| raise APIError(f"{doc_type} μ½λ λͺ©λ‘ μμ² μ€ν¨: {str(e)}") | |
| def get_code_details(self, doc_type: str, code: str) -> Dict[str, Any]: | |
| """ | |
| μ½λ μμΈ μ 보 κ°μ Έμ€κΈ° | |
| Args: | |
| doc_type: λ¬Έμ μ ν (μ: "KDS", "KCS") | |
| code: μ½λ λ²νΈ | |
| Returns: | |
| μ½λ μμΈ μ 보 | |
| Raises: | |
| APIError: API μμ² μ€ν¨ μ | |
| """ | |
| doc_type = normalize_doc_type(doc_type) | |
| params = { | |
| "key": self.api_key | |
| } | |
| try: | |
| logger.info(f"{doc_type}/{code} μμΈ μ 보 μμ² μ€...") | |
| endpoint = f"CodeViewer/{doc_type}/{code}" | |
| response = self.http_client.get(endpoint, params=params) | |
| data = response.json() | |
| logger.info(f"{doc_type}/{code} μμΈ μ 보 μμ² μ±κ³΅") | |
| # μλ΅μ λ¬Έμ μ ν μ 보 μΆκ° | |
| if data and isinstance(data, dict): | |
| data["DocType"] = doc_type | |
| return data | |
| except APIError: | |
| raise | |
| except Exception as e: | |
| raise APIError(f"{doc_type}/{code} μμΈ μ 보 μμ² μ€ν¨: {str(e)}") | |
| def collect_codes(self, doc_types: List[str] = None, save: bool = True) -> Dict[str, List[Dict[str, Any]]]: | |
| """ | |
| λ¬Έμ μ νλ³ μ½λ μμ§ | |
| Args: | |
| doc_types: μμ§ν λ¬Έμ μ ν λͺ©λ‘ | |
| save: μμ§ν λ°μ΄ν°λ₯Ό νμΌλ‘ μ μ₯ν μ§ μ¬λΆ | |
| Returns: | |
| λ¬Έμ μ νλ³ μ½λ λͺ©λ‘ λμ λ리 | |
| Raises: | |
| APIError: API μμ² μ€ν¨ μ | |
| """ | |
| if doc_types is None: | |
| doc_types = ["KDS", "KCS"] | |
| all_codes = {} | |
| for doc_type in doc_types: | |
| normalized_type = normalize_doc_type(doc_type) | |
| logger.info(f"{normalized_type} μ½λ μμ§ μμ") | |
| try: | |
| code_list = self.get_code_list(normalized_type) | |
| if not code_list: | |
| logger.warning(f"{normalized_type} μ½λ λͺ©λ‘μ΄ λΉμ΄ μμ΅λλ€.") | |
| continue | |
| all_codes[normalized_type] = code_list | |
| if save: | |
| filename = f"{normalized_type}_codes.json" | |
| save_json_data(code_list, filename) | |
| except Exception as e: | |
| logger.error(f"{normalized_type} μ½λ μμ§ μ€ μ€λ₯ λ°μ: {str(e)}") | |
| return all_codes | |
| def fetch_code_details( | |
| self, | |
| doc_type: str, | |
| codes: List[Dict[str, Any]], | |
| save: bool = True, | |
| max_workers: int = 5 | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| νΉμ λ¬Έμ μ νμ μ¬λ¬ μ½λμ λν μμΈ μ 보 κ°μ Έμ€κΈ° | |
| Args: | |
| doc_type: λ¬Έμ μ ν | |
| codes: μ½λ λͺ©λ‘ | |
| save: μμ§ν λ°μ΄ν°λ₯Ό νμΌλ‘ μ μ₯ν μ§ μ¬λΆ | |
| max_workers: λ³λ ¬ μ²λ¦¬ν μ΅λ μμ μ μ | |
| Returns: | |
| μ½λ μμΈ μ 보 λͺ©λ‘ | |
| Raises: | |
| APIError: API μμ² μ€ν¨ μ | |
| """ | |
| doc_type = normalize_doc_type(doc_type) | |
| details = [] | |
| failed_codes = [] | |
| logger.info(f"{doc_type} μμΈ μ 보 μμ§ μμ (μ΄ {len(codes)}κ° μ½λ)") | |
| # λ³λ ¬ μ²λ¦¬λ‘ μ¬λ¬ μ½λ μ 보 μμ§ | |
| with ThreadPoolExecutor(max_workers=max_workers) as executor: | |
| # κ° μ½λμ λν μμ μμ± | |
| future_to_code = { | |
| executor.submit(self._fetch_single_code_detail, doc_type, code.get("Code")): code | |
| for code in codes if code.get("Code") | |
| } | |
| # μμ κ²°κ³Ό μ²λ¦¬ | |
| for i, future in enumerate(as_completed(future_to_code)): | |
| code = future_to_code[future] | |
| try: | |
| detail = future.result() | |
| if detail: | |
| details.append(detail) | |
| # μ§ν μν© λ‘κΉ (10% λ¨μ) | |
| completion = (i + 1) / len(codes) * 100 | |
| if completion % 10 < 100 / len(codes): | |
| logger.info(f"{doc_type} μμΈ μ 보 μμ§ μ§ν μ€: {i+1}/{len(codes)} ({completion:.1f}%)") | |
| except Exception as e: | |
| logger.error(f"{doc_type}/{code.get('Code')} μμΈ μ 보 μμ§ μ€ν¨: {str(e)}") | |
| failed_codes.append(code.get("Code")) | |
| logger.info(f"{doc_type} μμΈ μ 보 μμ§ μλ£: {len(details)}κ° μ±κ³΅, {len(failed_codes)}κ° μ€ν¨") | |
| if failed_codes: | |
| logger.warning(f"μ€ν¨ν μ½λ: {', '.join(failed_codes[:10])}{' μΈ {}κ°'.format(len(failed_codes)-10) if len(failed_codes) > 10 else ''}") | |
| if save and details: | |
| filename = f"{doc_type}_details.json" | |
| save_json_data(details, filename) | |
| return details | |
| def _fetch_single_code_detail(self, doc_type: str, code: str) -> Optional[Dict[str, Any]]: | |
| """ | |
| λ¨μΌ μ½λ μμΈ μ 보 κ°μ Έμ€κΈ° (λ΄λΆ λ©μλ) | |
| Args: | |
| doc_type: λ¬Έμ μ ν | |
| code: μ½λ λ²νΈ | |
| Returns: | |
| μ½λ μμΈ μ 보 λλ None (μ€λ₯ μ) | |
| """ | |
| try: | |
| # API μμ² | |
| detail = self.get_code_details(doc_type, code) | |
| # API μλ² λΆν λ°©μ§λ₯Ό μν μ§μ° | |
| time.sleep(config.API_RETRY_DELAY) | |
| return detail | |
| except Exception as e: | |
| logger.error(f"{doc_type}/{code} μμΈ μ 보 κ°μ Έμ€κΈ° μ€ν¨: {str(e)}") | |
| return None | |
| # μ€ν μμ | |
| if __name__ == "__main__": | |
| # μ€μ μ΄κΈ°ν | |
| config.init() | |
| # API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν | |
| kcsc = KCSCApiClient() | |
| # μ½λ λͺ©λ‘ μμ§ | |
| all_codes = kcsc.collect_codes() | |
| # μ½λλ³ μμΈ μ 보 μμ§ | |
| for doc_type, codes in all_codes.items(): | |
| logger.info(f"{doc_type} μμΈ μ 보 μμ§ μμ (μ΄ {len(codes)}κ° μ½λ)") | |
| kcsc.fetch_code_details(doc_type, codes) | |