""" 키워드 검색량 조회 관련 기능 - 네이버 API를 통한 키워드 검색량 조회 - 검색량 배치 처리 """ import requests import time import random from concurrent.futures import ThreadPoolExecutor, as_completed import api_utils import logging # 로깅 설정 logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) def exponential_backoff_sleep(retry_count, base_delay=0.3, max_delay=5.0): """지수 백오프 방식의 대기 시간 계산""" delay = min(base_delay * (2 ** retry_count), max_delay) # 약간의 랜덤성 추가 (지터) jitter = random.uniform(0, 0.5) * delay time.sleep(delay + jitter) def fetch_search_volume_batch(keywords_batch): """키워드 배치에 대한 네이버 검색량 조회""" # 1. 스페이스바 제거 개선 - 배치 키워드들 전처리 cleaned_keywords_batch = [] for kw in keywords_batch: cleaned_kw = kw.strip().replace(" ", "") if kw else "" cleaned_keywords_batch.append(cleaned_kw) keywords_batch = cleaned_keywords_batch result = {} max_retries = 3 retry_count = 0 while retry_count < max_retries: try: # 순차적으로 API 설정 가져오기 (배치마다 한 번만 호출) api_config = api_utils.get_next_api_config() API_KEY = api_config["API_KEY"] SECRET_KEY = api_config["SECRET_KEY"] CUSTOMER_ID_STR = api_config["CUSTOMER_ID"] logger.debug(f"=== 환경 변수 체크 (시도 #{retry_count+1}) ===") logger.info(f"배치 크기: {len(keywords_batch)}개 키워드") # API 설정 유효성 검사 is_valid, message = api_utils.validate_api_config(api_config) if not is_valid: logger.error(f"❌ {message}") retry_count += 1 exponential_backoff_sleep(retry_count) continue # CUSTOMER_ID를 정수로 변환 try: CUSTOMER_ID = int(CUSTOMER_ID_STR) except ValueError: logger.error(f"❌ CUSTOMER_ID 변환 오류: '{CUSTOMER_ID_STR}'는 유효한 숫자가 아닙니다.") retry_count += 1 exponential_backoff_sleep(retry_count) continue BASE_URL = "https://api.naver.com" uri = "/keywordstool" method = "GET" headers = api_utils.get_header(method, uri, API_KEY, SECRET_KEY, CUSTOMER_ID) # 키워드 배치를 한 번에 API로 전송 params = { "hintKeywords": keywords_batch, "showDetail": "1" } logger.debug(f"요청 파라미터: {len(keywords_batch)}개 키워드") # API 호출 response = requests.get(BASE_URL + uri, params=params, headers=headers, timeout=10) logger.debug(f"응답 상태 코드: {response.status_code}") if response.status_code != 200: logger.error(f"❌ API 오류 응답 (시도 #{retry_count+1}):") logger.error(f" 본문: {response.text}") retry_count += 1 exponential_backoff_sleep(retry_count) continue # 응답 데이터 파싱 result_data = response.json() logger.debug(f"응답 데이터 구조:") logger.debug(f" 타입: {type(result_data)}") logger.debug(f" 키들: {result_data.keys() if isinstance(result_data, dict) else 'N/A'}") if isinstance(result_data, dict) and "keywordList" in result_data: logger.debug(f" keywordList 길이: {len(result_data['keywordList'])}") # 배치 내 각 키워드와 매칭 for keyword in keywords_batch: found = False for item in result_data["keywordList"]: rel_keyword = item.get("relKeyword", "") if rel_keyword == keyword: pc_count = item.get("monthlyPcQcCnt", 0) mobile_count = item.get("monthlyMobileQcCnt", 0) # 숫자 변환 try: if isinstance(pc_count, str): pc_count_converted = int(pc_count.replace(",", "")) else: pc_count_converted = int(pc_count) except: pc_count_converted = 0 try: if isinstance(mobile_count, str): mobile_count_converted = int(mobile_count.replace(",", "")) else: mobile_count_converted = int(mobile_count) except: mobile_count_converted = 0 total_count = pc_count_converted + mobile_count_converted result[keyword] = { "PC검색량": pc_count_converted, "모바일검색량": mobile_count_converted, "총검색량": total_count } logger.debug(f"✅ '{keyword}': PC={pc_count_converted}, Mobile={mobile_count_converted}, Total={total_count}") found = True break if not found: logger.warning(f"❌ '{keyword}': 매칭되는 데이터를 찾을 수 없음") # 성공적으로 데이터를 가져왔으므로 루프 종료 break else: logger.error(f"❌ keywordList가 없음 (시도 #{retry_count+1})") logger.error(f"전체 응답: {result_data}") retry_count += 1 exponential_backoff_sleep(retry_count) except Exception as e: logger.error(f"❌ 배치 처리 중 오류 (시도 #{retry_count+1}): {str(e)}") import traceback logger.error(traceback.format_exc()) retry_count += 1 exponential_backoff_sleep(retry_count) logger.info(f"\n=== 배치 처리 완료 ===") logger.info(f"성공적으로 처리된 키워드 수: {len(result)}") return result def fetch_all_search_volumes(keywords, batch_size=5): """키워드 리스트에 대한 네이버 검색량 병렬 조회""" results = {} batches = [] # 키워드를 5개씩 묶어서 배치 생성 for i in range(0, len(keywords), batch_size): batch = keywords[i:i + batch_size] batches.append(batch) logger.info(f"총 {len(batches)}개 배치로 {len(keywords)}개 키워드 처리 중…") logger.info(f"배치 크기: {batch_size}, 병렬 워커: 3개, API 계정: {len(api_utils.NAVER_API_CONFIGS)}개 순차 사용") with ThreadPoolExecutor(max_workers=3) as executor: # 워커 수 제한 futures = {executor.submit(fetch_search_volume_batch, batch): batch for batch in batches} for future in as_completed(futures): batch = futures[future] try: batch_results = future.result() results.update(batch_results) logger.info(f"배치 처리 완료: {len(batch)}개 키워드 (성공: {len(batch_results)}개)") except Exception as e: logger.error(f"배치 처리 오류: {e}") # API 레이트 리밋 방지를 위한 지수 백오프 사용 exponential_backoff_sleep(0) # 초기 지연 적용 logger.info(f"검색량 조회 완료: {len(results)}개 키워드") return results