import gradio as gr import requests from bs4 import BeautifulSoup import urllib.parse # iframe 경로 보정을 위한 모듈 import re import logging import tempfile import pandas as pd import mecab # python‑mecab‑ko 라이브러리 사용 import os import time import hmac import hashlib import base64 # 디버깅(로그)용 함수 def debug_log(message: str): print(f"[DEBUG] {message}") # ============================================================================= # [기본코드]: 네이버 블로그에서 제목과 본문을 추출하는 함수 # ============================================================================= def scrape_naver_blog(url: str) -> str: debug_log("scrape_naver_blog 함수 시작") debug_log(f"요청받은 URL: {url}") # 헤더 세팅(크롤링 차단 방지) headers = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/96.0.4664.110 Safari/537.36" ) } try: # 1) 네이버 블로그 메인 페이지 요청 response = requests.get(url, headers=headers) debug_log("HTTP GET 요청(메인 페이지) 완료") if response.status_code != 200: debug_log(f"요청 실패, 상태코드: {response.status_code}") return f"오류가 발생했습니다. 상태코드: {response.status_code}" soup = BeautifulSoup(response.text, "html.parser") debug_log("HTML 파싱(메인 페이지) 완료") # 2) iframe 태그 찾기 iframe = soup.select_one("iframe#mainFrame") if not iframe: debug_log("iframe#mainFrame 태그를 찾을 수 없습니다.") return "본문 iframe을 찾을 수 없습니다." iframe_src = iframe.get("src") if not iframe_src: debug_log("iframe src가 존재하지 않습니다.") return "본문 iframe의 src를 찾을 수 없습니다." # 3) iframe src가 상대경로인 경우 절대경로로 보정 parsed_iframe_url = urllib.parse.urljoin(url, iframe_src) debug_log(f"iframe 페이지 요청 URL: {parsed_iframe_url}") # 4) iframe 페이지 재요청 iframe_response = requests.get(parsed_iframe_url, headers=headers) debug_log("HTTP GET 요청(iframe 페이지) 완료") if iframe_response.status_code != 200: debug_log(f"iframe 요청 실패, 상태코드: {iframe_response.status_code}") return f"iframe에서 오류가 발생했습니다. 상태코드: {iframe_response.status_code}" iframe_soup = BeautifulSoup(iframe_response.text, "html.parser") debug_log("HTML 파싱(iframe 페이지) 완료") # 제목 추출 title_div = iframe_soup.select_one('.se-module.se-module-text.se-title-text') title = title_div.get_text(strip=True) if title_div else "제목을 찾을 수 없습니다." debug_log(f"추출된 제목: {title}") # 본문 추출 content_div = iframe_soup.select_one('.se-main-container') if content_div: content = content_div.get_text("\n", strip=True) else: content = "본문을 찾을 수 없습니다." debug_log("본문 추출 완료") # 결과 합치기 result = f"[제목]\n{title}\n\n[본문]\n{content}" debug_log("제목과 본문을 합쳐 반환 준비 완료") return result except Exception as e: debug_log(f"에러 발생: {str(e)}") return f"스크래핑 중 오류가 발생했습니다: {str(e)}" # ============================================================================= # [참조코드-1]: 형태소 분석 함수 (Mecab 이용) # ============================================================================= logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) def analyze_text(text: str): logger.debug("원본 텍스트: %s", text) # 1. 한국어만 남기기 (공백, 영어, 기호 등 제거) filtered_text = re.sub(r'[^가-힣]', '', text) logger.debug("필터링된 텍스트 (한국어만, 공백 제거): %s", filtered_text) if not filtered_text: logger.debug("유효한 한국어 텍스트가 없음.") return pd.DataFrame(columns=["단어", "빈도수"]), "" # 2. Mecab을 이용한 형태소 분석 (명사와 복합명사만 추출) mecab_instance = mecab.MeCab() # 인스턴스 생성 tokens = mecab_instance.pos(filtered_text) logger.debug("형태소 분석 결과: %s", tokens) freq = {} for word, pos in tokens: if word and word.strip(): if pos.startswith("NN"): freq[word] = freq.get(word, 0) + 1 logger.debug("단어: %s, 품사: %s, 현재 빈도: %d", word, pos, freq[word]) # 3. 빈도수를 내림차순 정렬 sorted_freq = sorted(freq.items(), key=lambda x: x[1], reverse=True) logger.debug("내림차순 정렬된 단어 빈도: %s", sorted_freq) # 4. 결과 DataFrame 생성 df = pd.DataFrame(sorted_freq, columns=["단어", "빈도수"]) logger.debug("결과 DataFrame 생성됨, shape: %s", df.shape) # 5. Excel 파일 생성 (임시 파일 저장) temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") df.to_excel(temp_file.name, index=False, engine='openpyxl') temp_file.close() logger.debug("Excel 파일 생성됨: %s", temp_file.name) return df, temp_file.name # ============================================================================= # [참조코드-2]: 키워드 검색량 및 블로그 문서수 조회 관련 함수 # ============================================================================= def generate_signature(timestamp, method, uri, secret_key): message = f"{timestamp}.{method}.{uri}" digest = hmac.new(secret_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).digest() return base64.b64encode(digest).decode() def get_header(method, uri, api_key, secret_key, customer_id): timestamp = str(round(time.time() * 1000)) signature = generate_signature(timestamp, method, uri, secret_key) return { "Content-Type": "application/json; charset=UTF-8", "X-Timestamp": timestamp, "X-API-KEY": api_key, "X-Customer": str(customer_id), "X-Signature": signature } def fetch_related_keywords(keyword): API_KEY = os.environ["NAVER_API_KEY"] SECRET_KEY = os.environ["NAVER_SECRET_KEY"] CUSTOMER_ID = os.environ["NAVER_CUSTOMER_ID"] BASE_URL = "https://api.naver.com" uri = "/keywordstool" method = "GET" headers = get_header(method, uri, API_KEY, SECRET_KEY, CUSTOMER_ID) params = { "hintKeywords": [keyword], "showDetail": "1" } response = requests.get(BASE_URL + uri, params=params, headers=headers) data = response.json() if "keywordList" not in data: return pd.DataFrame() df = pd.DataFrame(data["keywordList"]) if len(df) > 100: df = df.head(100) def parse_count(x): try: return int(str(x).replace(",", "")) except: return 0 df["PC월검색량"] = df["monthlyPcQcCnt"].apply(parse_count) df["모바일월검색량"] = df["monthlyMobileQcCnt"].apply(parse_count) df["토탈월검색량"] = df["PC월검색량"] + df["모바일월검색량"] df.rename(columns={"relKeyword": "정보키워드"}, inplace=True) result_df = df[["정보키워드", "PC월검색량", "모바일월검색량", "토탈월검색량"]] return result_df def fetch_blog_count(keyword): client_id = os.environ["NAVER_SEARCH_CLIENT_ID"] client_secret = os.environ["NAVER_SEARCH_CLIENT_SECRET"] url = "https://openapi.naver.com/v1/search/blog.json" headers = { "X-Naver-Client-Id": client_id, "X-Naver-Client-Secret": client_secret } params = {"query": keyword, "display": 1} response = requests.get(url, headers=headers, params=params) if response.status_code == 200: data = response.json() return data.get("total", 0) else: return 0 def create_excel_file(df): with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: excel_path = tmp.name df.to_excel(excel_path, index=False) return excel_path def process_keyword(keywords: str, include_related: bool): """ 여러 키워드를 엔터로 구분하여 리스트로 만들고, 각 키워드에 대해 네이버 광고 API로 검색량 정보를 조회하며, 첫 번째 키워드의 경우 옵션에 따라 연관검색어도 추가한 후, 각 정보키워드에 대해 블로그 문서수를 조회하여 DataFrame과 Excel 파일을 반환합니다. """ input_keywords = [k.strip() for k in keywords.splitlines() if k.strip()] result_dfs = [] for idx, kw in enumerate(input_keywords): df_kw = fetch_related_keywords(kw) if df_kw.empty: continue row_kw = df_kw[df_kw["정보키워드"] == kw] if not row_kw.empty: result_dfs.append(row_kw) else: result_dfs.append(df_kw.head(1)) if include_related and idx == 0: df_related = df_kw[df_kw["정보키워드"] != kw] if not df_related.empty: result_dfs.append(df_related) if result_dfs: result_df = pd.concat(result_dfs, ignore_index=True) result_df.drop_duplicates(subset=["정보키워드"], inplace=True) else: result_df = pd.DataFrame(columns=["정보키워드", "PC월검색량", "모바일월검색량", "토탈월검색량"]) result_df["블로그문서수"] = result_df["정보키워드"].apply(fetch_blog_count) result_df.sort_values(by="토탈월검색량", ascending=False, inplace=True) return result_df, create_excel_file(result_df) # ============================================================================= # 통합 처리 함수: 블로그 내용(텍스트)에 대해 형태소 분석을 수행한 후, # 키워드의 검색량 및 블로그 문서수를 추가하여 최종 결과를 반환함. # ============================================================================= def process_blog_content(text: str): debug_log("process_blog_content 함수 시작") # 1. 형태소 분석 실행 ([참조코드-1] 활용) df_morph, morph_excel = analyze_text(text) debug_log("형태소 분석 완료") if df_morph.empty: debug_log("형태소 분석 결과가 비어있음") return df_morph, "" # 2. 형태소 분석된 단어 목록 추출 (키워드 조회용) keywords = "\n".join(df_morph["단어"].tolist()) debug_log(f"추출된 단어 목록: {keywords}") # 3. 키워드 검색량 및 블로그 문서수 조회 ([참조코드-2] 활용) df_keyword, keyword_excel = process_keyword(keywords, include_related=False) debug_log("키워드 검색 정보 조회 완료") # 4. 형태소 분석 결과와 키워드 정보를 단어 기준으로 병합 df_merged = pd.merge(df_morph, df_keyword, left_on="단어", right_on="정보키워드", how="left") debug_log("데이터 병합 완료") df_merged.drop(columns=["정보키워드"], inplace=True) # 5. 병합 결과를 Excel 파일로 생성 merged_excel = create_excel_file(df_merged) debug_log(f"병합 결과 Excel 파일 생성됨: {merged_excel}") return df_merged, merged_excel # ============================================================================= # Gradio 인터페이스 구성 (허깅페이스 그라디오 환경) # ============================================================================= with gr.Blocks() as demo: gr.Markdown("# 블로그 글 형태소 분석 및 키워드 정보 조회") with gr.Tab("블로그 내용 입력 및 스크래핑"): with gr.Row(): blog_url = gr.Textbox(label="네이버 블로그 링크", placeholder="예: https://blog.naver.com/ssboost/222983068507") fetch_button = gr.Button("블로그내용가져오기") blog_content = gr.Textbox(label="블로그 내용 (제목 및 본문)", lines=10, placeholder="블로그 내용을 가져오거나 직접 입력하세요.") # '블로그내용가져오기' 버튼 클릭 시 스크래핑 실행하여 blog_content에 반영 fetch_button.click(fn=scrape_naver_blog, inputs=blog_url, outputs=blog_content) with gr.Tab("형태소 분석 실행"): with gr.Row(): analysis_button = gr.Button("형태소분석") # 분석 결과는 수정 가능하도록 interactive=True 설정 output_table = gr.Dataframe(label="분석 결과 (형태소 및 키워드 정보)", interactive=True) output_file = gr.File(label="Excel 다운로드") # '형태소분석' 버튼 클릭 시 process_blog_content 함수 실행 analysis_button.click(fn=process_blog_content, inputs=blog_content, outputs=[output_table, output_file]) if __name__ == "__main__": debug_log("Gradio 앱 실행 시작") demo.launch() debug_log("Gradio 앱 실행 종료")