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}" # 2) 메인 페이지 파싱 soup = BeautifulSoup(response.text, "html.parser") debug_log("HTML 파싱(메인 페이지) 완료") # 3) 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를 찾을 수 없습니다." # 4) iframe src 보정 (절대경로 처리) parsed_iframe_url = urllib.parse.urljoin(url, iframe_src) debug_log(f"iframe 페이지 요청 URL: {parsed_iframe_url}") # 5) 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 페이지) 완료") # 6) 제목과 본문 추출 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] 형태소 분석 기능 def analyze_text(text: str): logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) 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] 네이버 광고 API 및 검색량/블로그문서수 조회 기능 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): debug_log(f"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월검색량", "모바일월검색량", "토탈월검색량"]] debug_log("fetch_related_keywords 완료") return result_df def fetch_blog_count(keyword): debug_log(f"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() debug_log(f"fetch_blog_count 결과: {data.get('total', 0)}") return data.get("total", 0) else: debug_log(f"fetch_blog_count 오류, 상태코드: {response.status_code}") 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) debug_log(f"Excel 파일 생성됨: {excel_path}") return excel_path def process_keyword(keywords: str, include_related: bool): debug_log(f"process_keyword 호출, 키워드들: {keywords}, 연관검색어 포함: {include_related}") 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) debug_log("process_keyword 완료") return result_df, create_excel_file(result_df) # [참조코드-1] 및 [참조코드-2]를 활용한 형태소 분석 및 검색량, 블로그문서수 추가 (빈도수1 제거 옵션 포함) def morphological_analysis_and_enrich(text: str, remove_freq1: bool): debug_log("morphological_analysis_and_enrich 함수 시작") df_freq, _ = analyze_text(text) if df_freq.empty: debug_log("형태소 분석 결과가 빈 데이터프레임입니다.") return df_freq, "" if remove_freq1: before_shape = df_freq.shape df_freq = df_freq[df_freq["빈도수"] != 1] debug_log(f"빈도수 1 제거 적용됨. {before_shape} -> {df_freq.shape}") # 형태소 분석 결과에서 키워드 추출 (각 단어를 엔터로 구분) keywords = "\n".join(df_freq["단어"].tolist()) debug_log(f"분석된 키워드: {keywords}") # [참조코드-2]를 활용하여 각 키워드의 검색량 및 블로그문서수 조회 (연관검색어 미포함) df_keyword_info, _ = process_keyword(keywords, include_related=False) debug_log("검색량 및 블로그문서수 조회 완료") # 형태소 분석 결과와 검색량 정보를 병합 (키워드 기준) merged_df = pd.merge(df_freq, df_keyword_info, left_on="단어", right_on="정보키워드", how="left") merged_df.drop(columns=["정보키워드"], inplace=True) # 병합 결과 Excel 파일 생성 merged_excel_path = create_excel_file(merged_df) debug_log("morphological_analysis_and_enrich 함수 완료") return merged_df, merged_excel_path # 새롭게 추가된 기능: 입력한 블로그 링크로부터 스크래핑하여 수정 가능한 텍스트 박스에 출력 def fetch_blog_content(url: str): debug_log("fetch_blog_content 함수 시작") content = scrape_naver_blog(url) debug_log("fetch_blog_content 함수 완료") return content # Gradio 인터페이스 구성 (단일 탭) with gr.Blocks(title="네이버 블로그 형태소 분석 스페이스", css=".gradio-container { max-width: 960px; margin: auto; }") as demo: gr.Markdown("# 네이버 블로그 형태소 분석 스페이스") with gr.Row(): blog_url_input = gr.Textbox(label="네이버 블로그 링크", placeholder="예: https://blog.naver.com/ssboost/222983068507", lines=1) with gr.Row(): scrape_button = gr.Button("스크래핑 실행") with gr.Row(): blog_content_box = gr.Textbox(label="블로그 내용 (수정 가능)", lines=10, placeholder="스크래핑된 블로그 내용이 여기에 표시됩니다.") with gr.Row(): remove_freq_checkbox = gr.Checkbox(label="빈도수1 제거", value=False) with gr.Row(): analyze_button = gr.Button("분석 실행") with gr.Row(): analysis_result = gr.Dataframe(label="분석 결과 (단어, 빈도수, 검색량, 블로그문서수 등)") with gr.Row(): analysis_excel = gr.File(label="Excel 다운로드") # 스크래핑 실행 시 URL로부터 블로그 본문 스크래핑 후 수정 가능한 텍스트 박스에 출력 scrape_button.click(fn=fetch_blog_content, inputs=blog_url_input, outputs=blog_content_box) # 분석 실행 시 수정된 블로그 내용을 대상으로 형태소 분석 및 검색량/블로그문서수 조회 진행 analyze_button.click(fn=morphological_analysis_and_enrich, inputs=[blog_content_box, remove_freq_checkbox], outputs=[analysis_result, analysis_excel]) if __name__ == "__main__": debug_log("Gradio 앱 실행 시작") demo.launch() debug_log("Gradio 앱 실행 종료")