import os import random import time import re import json import requests from bs4 import BeautifulSoup from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry import openai import gradio as gr from fpdf import FPDF as FPDF2 from datetime import datetime # API 키 설정 OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # OpenAI 설정 openai.api_key = OPENAI_API_KEY def setup_session(): try: session = requests.Session() retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504]) session.mount('https://', HTTPAdapter(max_retries=retries)) return session except Exception as e: return None def generate_naver_search_url(query): base_url = "https://search.naver.com/search.naver?" params = {"ssc": "tab.blog.all", "sm": "tab_jum", "query": query} url = base_url + "&".join(f"{key}={value}" for key, value in params.items()) return url def crawl_blog_content(url, session): try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", "Referer": "https://search.naver.com/search.naver", } delay = random.uniform(1, 2) time.sleep(delay) response = session.get(url, headers=headers) if response.status_code != 200: return "" soup = BeautifulSoup(response.content, "html.parser") content = soup.find("div", attrs={'class': 'se-main-container'}) if content: return clean_text(content.get_text()) else: return "" except Exception as e: return "" def crawl_naver_search_results(url, session): try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", "Referer": "https://search.naver.com/search.naver", } response = session.get(url, headers=headers) if response.status_code != 200: return [] soup = BeautifulSoup(response.content, "html.parser") results = [] count = 0 for li in soup.find_all("li", class_=re.compile("bx.*")): if count >= 10: break for div in li.find_all("div", class_="detail_box"): for div2 in div.find_all("div", class_="title_area"): title = div2.text.strip() for a in div2.find_all("a", href=True): link = a["href"] if "blog.naver" in link: link = link.replace("https://", "https://m.") results.append({"제목": title, "링크": link}) count += 1 if count >= 10: break if count >= 10: break if count >= 10: break return results except Exception as e: return [] def clean_text(text): text = re.sub(r'\s+', ' ', text).strip() return text def fetch_references(topic): search_url = generate_naver_search_url(topic) session = setup_session() if session is None: return ["세션 설정 실패"] * 3 results = crawl_naver_search_results(search_url, session) if len(results) < 3: return ["충분한 검색 결과를 찾지 못했습니다."] * 3 selected_results = random.sample(results, 3) references = [] for result in selected_results: content = crawl_blog_content(result['링크'], session) references.append(f"제목: {result['제목']}\n내용: {content}") return references def fetch_crawl_results(query): references = fetch_references(query) return references[0], references[1], references[2] def generate_blog_post(query, prompt_template): try: # 참고글 크롤링 references = fetch_references(query) ref1, ref2, ref3 = references combined_content = f"참고글1:\n{ref1}\n\n참고글2:\n{ref2}\n\n참고글3:\n{ref3}" # 랜덤 시드 생성 random_seed = random.randint(1, 10000) full_prompt = f"주제: {query}\n\n{prompt_template}\n\n참고 내용:\n{combined_content}" response = openai.ChatCompletion.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": prompt_template}, {"role": "user", "content": full_prompt} ], max_tokens=10000, temperature=0.75, top_p=1.0, frequency_penalty=0.3 ) return f"주제: {query}\n\n{response.choices[0].message['content']}", ref1, ref2, ref3 except Exception as e: return f"블로그 글 생성 중 오류 발생: {str(e)}", "", "", "" # PDF 클래스 및 관련 함수 정의 class PDF(FPDF2): def __init__(self): super().__init__() current_dir = os.path.dirname(__file__) self.add_font("NanumGothic", "", os.path.join(current_dir, "NanumGothic.ttf")) self.add_font("NanumGothic", "B", os.path.join(current_dir, "NanumGothicBold.ttf")) self.add_font("NanumGothicExtraBold", "", os.path.join(current_dir, "NanumGothicExtraBold.ttf")) self.add_font("NanumGothicLight", "", os.path.join(current_dir, "NanumGothicLight.ttf")) def header(self): self.set_font('NanumGothic', '', 10) def footer(self): self.set_y(-15) self.set_font('NanumGothic', '', 8) self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C') def save_to_pdf(blog_post, user_topic): pdf = PDF() pdf.add_page() lines = blog_post.split('\n') title = lines[0].strip() content = '\n'.join(lines[1:]).strip() # 현재 날짜와 시간을 가져옵니다 (대한민국 시간 기준) now = datetime.now() date_str = now.strftime("%y%m%d") time_str = now.strftime("%H%M") # 파일명 생성 filename = f"{date_str}_{time_str}_{format_filename(user_topic)}.pdf" pdf.set_font("NanumGothic", 'B', size=14) pdf.cell(0, 10, title, ln=True, align='C') pdf.ln(10) pdf.set_font("NanumGothic", '', size=11) pdf.multi_cell(0, 5, content) print(f"Saving PDF as: {filename}") pdf.output(filename) return filename def format_filename(text): text = re.sub(r'[^\w\s-]', '', text) return text[:50].strip() def save_content_to_pdf(blog_post, user_topic): return save_to_pdf(blog_post, user_topic) # 기본 프롬프트 템플릿 DEFAULT_PROMPT_TEMPLATE = """ [블로그 글 작성 기본 규칙] 1. 반드시 한글로 작성하라 2. 주어진 참고글을 바탕으로 1개의 정보성(Informativity) 블로그를 작성 3. 주제와 제목을 제외한 글이 1100단어 이상이 되도록 작성 4. 글의 제목을 정보성 블로그 형태에 맞는 적절한 제목으로 출력 - 참고글의 제목도 참고하되, 동일하게 작성하지 말 것 5. 반드시 마크다운 형식이 아닌 순수한 텍스트로만 출력하라 6. 다시한번 참고글을 검토하여 내용을 충분히 반영하되, 참고글의 표현을 그대로 작성하지는 말 것 [블로그 글 작성 세부 규칙] 1. 사용자가 입력한 주제와 주어진 참고글 3개를 바탕으로 블로그 글 1개를 작성하라 2. 주어진 모든 글을 분석하여 하나의 대주제를 선정하라 3. 대주제에 맞게 참고글 3가지의 내용이 모두 반영되게 작성 4. 독자가 읽고 주제의 정보를 충분히 습득 할 수 있도록 내용을 풍성하게 작성 5. 독자의 공감과 관심을 끌 수 있도록 작성하고 경험이나 일화, 흥미로운 사실, 전문가의 견해 등을 포함 6. 독자에게 이득이 되는 정보를 작성 7. 어투는 주어진 참고글 3가지의 어투를 적절히 반영하라 - 특히 문장의 끝 부분을 적절히 반영('~어요'), ('~니다' 제외) - 너무 딱딱하지 않게 편안하게 읽을 수 있도록 자연스러운 대화체를 반영 [제외 규칙] 1. 반드시 참고글의 포함된 링크(URL)는 제외 2. 참고글에서 '링크를 확인해주세요'와 같은 링크 이동의 문구는 제외 3. 참고글에 있는 작성자, 화자, 유튜버, 기자(Writer, speaker, YouTuber, reporter)의 이름, 애칭, 닉네임(Name, Nkickname)은 반드시 제외 """ # Gradio 앱 생성 with gr.Blocks() as iface: gr.Markdown("# 블로그 글 작성기_정보성_일반") gr.Markdown("주제를 입력하고 블로그 글 생성 버튼을 누르면 자동으로 블로그 글을 생성합니다.") query_input = gr.Textbox(lines=1, placeholder="블로그 글의 주제를 입력해주세요...", label="주제") prompt_input = gr.Textbox(lines=10, value=DEFAULT_PROMPT_TEMPLATE, label="프롬프트 템플릿", visible=False) generate_button = gr.Button("블로그 글 생성") output_text = gr.Textbox(label="생성된 블로그 글") ref1_text = gr.Textbox(label="참고글 1", lines=10, visible=False) ref2_text = gr.Textbox(label="참고글 2", lines=10, visible=False) ref3_text = gr.Textbox(label="참고글 3", lines=10, visible=False) save_pdf_button = gr.Button("PDF로 저장") pdf_output = gr.File(label="생성된 PDF 파일") generate_button.click( generate_blog_post, inputs=[query_input, prompt_input], outputs=[output_text, ref1_text, ref2_text, ref3_text], show_progress=True ) save_pdf_button.click( save_content_to_pdf, inputs=[output_text, query_input], outputs=[pdf_output], show_progress=True ) # Gradio 앱 실행 if __name__ == "__main__": iface.launch()