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 from zoneinfo import ZoneInfo from langchain.chat_models import ChatOpenAI from langchain.prompts import ChatPromptTemplate from langchain.chains import LLMChain from langchain.callbacks import get_openai_callback import sys # 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: target_length = 1500 # 내부적으로 목표 글자수 설정 max_attempts = 2 # 최대 2번 실행 (초기 1번 + 재시도 1번) references = fetch_references(query) ref1, ref2, ref3 = references chat = ChatOpenAI( model_name="gpt-4o-mini", temperature=0.85, max_tokens=10000, top_p=0.9, frequency_penalty=0.5, presence_penalty=0, n=1, request_timeout=60 ) prompt = ChatPromptTemplate.from_template( prompt_template + """ 주제: {query} 참고글1: {ref1} 참고글2: {ref2} 참고글3: {ref3} 다음 표현은 사용하지 마세요: 여러분, 마지막으로, 결론적으로, 결국, 종합적으로, 따라서, 마무리, 요약 약 {target_length}자로 작성해주세요. """ ) chain = LLMChain(llm=chat, prompt=prompt) unwanted_patterns = [ r'\b여러분[,.]?\s*', r'\b(마지막으로|결론적으로|결국|종합적으로|따라서|마무리|요약)[,.]?\s*' ] for attempt in range(max_attempts): with get_openai_callback() as cb: result = chain.run(query=query, ref1=ref1, ref2=ref2, ref3=ref3, target_length=target_length) generated_post = result.strip() # 목표 글자수를 충족하고 원치 않는 표현이 없으면 루프 종료 if len(generated_post) >= target_length and not any(re.search(pattern, generated_post, re.IGNORECASE) for pattern in unwanted_patterns): break # 첫 번째 시도 후 재시도 시 프롬프트 수정 if attempt == 0: if len(generated_post) < target_length: prompt.template += f"\n\n현재 글자수는 {len(generated_post)}자입니다. 약 {target_length - len(generated_post)}자를 추가로 작성하여 총 {target_length}자가 되도록 해주세요." if any(re.search(pattern, generated_post, re.IGNORECASE) for pattern in unwanted_patterns): prompt.template += "\n\n원치 않는 표현이 포함되어 있습니다. 해당 표현을 제거하고 자연스럽게 글을 다시 작성해주세요." final_post = f"주제: {query}\n\n{generated_post}" actual_length = len(generated_post) return final_post, ref1, ref2, ref3, actual_length except Exception as e: return f"블로그 글 생성 중 오류 발생: {str(e)}", "", "", "", 0 # 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(ZoneInfo("Asia/Seoul")) 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개의 상품리뷰형(Product Review) 블로그를 작성 3. 주제와 제목을 제외한 글이 1500단어 이상이 되도록 작성 4. 글의 제목을 상품리뷰형 블로그 형태에 맞는 적절한 제목으로 출력 - 참고글의 제목도 참고하되, 동일하게 작성하지 말 것 5. 반드시 마크다운 형식이 아닌 순수한 텍스트로만 출력하라 6. 다시한번 참고글을 검토하여 내용을 충분히 반영하되, 참고글의 글을 그대로 재작성하지는 말 것 [블로그 글 작성 세부 규칙] 1. 사용자가 입력한 주제와 주어진 참고글 3개를 바탕으로 상품리뷰형 블로그 글 1개를 작성하라 2. 주어진 모든 글을 분석하여 하나의 대주제를 선정하라(1개의 참고글에 치우치지 말고 다양한 내용을 담을것) 3. 여러가지 상품이라면 상품 1개에 치우친 리뷰를 작성하지 말 것. 4. 대주제에 맞게 글의 맥락을 유지하라 5. 참고글에 작성된 상품과 기능에 집중하여 작성하라 6. 실제 내가 사용해보고 경험한 내용을 작성한 리뷰 형태로 글을 작성 7. 내용은 긍정적으로 작성하되, 상품이 돋보이도록 작성(제품이 여러개일 경우, 하나의 상품에 치우치지 말 것) 8. 상품의 가치를 고객에게 어필하라. 9. 글의 앞, 뒤 문장이 자연스럽게 이어지도록 작성 10. 어투는 주어진 참고글 3가지의 어투를 적절히 반영하라 - 특히 문장의 끝 부분을 적절히 반영(가급적 '~요'로 끝나도록 작성) - 너무 딱딱하지 않게 편안하게 읽을 수 있도록 자연스러운 대화체를 반영 - 단어 선택은 쉬운 한국어 어휘를 사용하고 사전식표현, 오래된 표현은 제외하라 [제외 규칙] 1. 반드시 참고글의 포함된 링크(URL)는 제외 2. 참고글에서 '링크를 확인해주세요'와 같은 링크 이동의 문구는 제외 3. 참고글에 있는 작성자, 화자, 유튜버, 기자(Writer, speaker, YouTuber, reporter)의 이름, 애칭, 닉네임(Name, Nkickname)은 반드시 제외 4. '업체로 부터 제공 받아서 작성', '쿠팡 파트너스'등의 표현을 반드시 제외하라. 5. 글의 구조가 드러나게 작성하지 말 것(시작, 끝에 대한 표현) - 여러분, - 마지막으로, 결론적으로, 결국, 종합적으로, 따라서, 마무리, 요약, """ # 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=True) generate_button = gr.Button("블로그 글 생성") output_text = gr.Textbox(label="생성된 블로그 글") ref1_text = gr.Textbox(label="참고글 1", lines=10, visible=True) ref2_text = gr.Textbox(label="참고글 2", lines=10, visible=True) ref3_text = gr.Textbox(label="참고글 3", lines=10, visible=True) 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()