.
드디어 “문과생도 AI” 부트 캠프의 마지막 프로젝트를 진행하게 되었습니다.
바로 “네이버 블로그 포스팅 자동화 프로그램 개발”입니다.
이 프로그램은 사용자한테 글의 소재(주제, 의도, 키워드 등)를 받아서 ChatGPT의 도움으로 블로그 글을 작성하고, 작성된 글을 네이버 블로그에 자동으로 등록(임시저장)하는 것입니다.
이 프로젝트에는 그동안 “문과생도 AI” 방에서 진행했던 일련의 미션들의 결과물과 @박정기 파트너님의 미니 특강의 내용이 모두 포함될 듯 합니다.
pyperclip 활용 네이버 자동 로그인
OpenAI API를 활용한 블로그 글 자동 생성
Selenium 활용 동적 크롤링
Pyautogui 라이브러리 활용
기타 등등..
일단은 ChatGPT의 도움과 박정기 파트너님의 예시 답안 코드를 참고하여 꾸역 꾸역.. 네이버 블로그에 임시 저장까지 하는 것 까지 기능적으로는 성공을 하였지만.. 짧은 기간에 이 프로젝트를 완벽하게 이해하는 것은 한계가 있는 듯 합니다.
그래도, 한 땀 한 땀 진해한 과정을 나름대로 정리해 보도록 하겠습니다.
중간 중간 ChatGPT의 급발진 고급 스킬 시전으로 인해 이해가 힘든 고난도 코드가 나오더라도 그건 저의 실력이 절대 아님을 감안하고 봐 주세요~^^
먼저, 전체적인 그림을 그려 보는 것 부터 시작을 하였습니다.
이제 두구두구.. 챗씨가 플로우차트를 그려 줍니다..
다들 이런 경험을 한번씩 있으실 거 같습니다. 잔뜩 기대했는데 정말 엉뚱한 결과를…
그래서, 그냥 무시하고 다음 단계로 넘어 갑니다.
나름대로 단계별 모듈화가 나온 듯 한데.. 거창하게 만들 게 아니라서 유효성 검사 등 부수적인 것은 제외하고 꼭 필요한 4가지 모듈로 정리를 하였고.. 이것을 whimsical 툴을 이용해서 제가 보기 편한 방식으로 플로우 차트를 간단하게 그려 봤습니다. (초기 버전은 아니고, 중간 중간 수정을 한 버전입니다.)
이제 Module별로 작업을 진행하겠습니다.
[Module 1 : get_user_input()]
주요 기능 : 사용자로 부터 필요한 정보를 입력 받는다.
input : topic(블로그 글의 주제), purpose(블로그 글의 작성 의도, 키워드 등), naver_id, naver_pw
output : input과 동일
4개의 정보를 한 번에 하나씩 입력창을 띄우는 방식인데.. 이것을 아래와 같이 그림까지 그려 가며 몇 번의 수정 작업을 거쳐서 다음 같은 결과물을 얻을 수 있었습니다.
실행 화면.
마지막으로 입력 받은 값을 다른 모듈의 입력 값으로 활용할 수 있게 수정해 달라고 했더니, class까지 등장해서 당황했으나.. 일단은 chatGPT가 만들어 준 코드를 사용하기로 하였습니다.
** GetUserInput Module(테스트 출력 코드 포함)
import tkinter as tk
class Get_User_Input:
def __init__(self, master):
self.master = master
self.setup_gui()
def setup_gui(self):
self.master.title("Blog Posting Automation")
# 라벨과 입력 필드의 정렬을 위한 padding
label_padx = 10
entry_padx = (0, 10)
pady = 5
# Topic 입력 필드
tk.Label(self.master, text="Topic").grid(row=0, column=0, sticky='w', padx=label_padx, pady=pady)
self.entry_topic = tk.Entry(self.master)
self.entry_topic.grid(row=0, column=1, sticky='w', padx=entry_padx, pady=pady)
# Purpose 입력 필드 (여러 줄의 텍스트)
tk.Label(self.master, text="Purpose").grid(row=1, column=0, sticky='nw', padx=label_padx, pady=pady)
self.text_purpose = tk.Text(self.master, height=4, width=50)
self.text_purpose.grid(row=1, column=1, sticky='w', padx=entry_padx, pady=pady)
# Naver ID 입력 필드
tk.Label(self.master, text="Naver ID").grid(row=2, column=0, sticky='w', padx=label_padx, pady=pady)
self.entry_naver_id = tk.Entry(self.master)
self.entry_naver_id.grid(row=2, column=1, sticky='w', padx=entry_padx, pady=pady)
# Naver PW 입력 필드
tk.Label(self.master, text="Naver PW").grid(row=3, column=0, sticky='w', padx=label_padx, pady=pady)
self.entry_naver_pw = tk.Entry(self.master, show="*")
self.entry_naver_pw.grid(row=3, column=1, sticky='w', padx=entry_padx, pady=pady)
# 실행 버튼
tk.Button(self.master, text="Submit", command=self.submit).grid(row=4, column=1, sticky='e', padx=entry_padx, pady=pady)
def submit(self):
# 사용자 입력값을 속성으로 저장
self.topic = self.entry_topic.get()
self.purpose = self.text_purpose.get("1.0", "end-1c")
self.naver_id = self.entry_naver_id.get()
self.naver_pw = self.entry_naver_pw.get()
# GUI 창을 닫습니다.
self.master.quit()
def run(self):
# GUI 창을 실행
self.master.mainloop()
# GUI 인스턴스 생성 및 실행
root = tk.Tk()
app = Get_User_Input(root)
app.run()
# GUI가 종료된 후에 입력값에 접근
print("Submitted Topic:", app.topic)
print("Submitted Purpose:", app.purpose)
print("Submitted Naver ID:", app.naver_id)
print("Submitted Naver PW:", app.naver_pw)
# 이제 이 값들을 다른 모듈에서 사용할 수 있습니다.
# 예를 들어, 다른 모듈의 함수를 호출하고 결과를 처리할 수 있습니다.
# result = some_module.process(app.topic, app.purpose, app.naver_id, app.naver_pw)
[Module 2 : generate_blog_content()]
주요 기능 : OpenAI API 를 활용하여 ChatGPT로 블로그 글을 작성한다.
input : topic, purpose
output : Title(제목), Contents(본문), Hashtags(해시태그) —> JSON 형식의 텍스트로 출력
{“Title” : <title>, “Contents” : <contents>, “Hashtags” : <hashtags>}
이 모듈은 이전 미션인 “ChatGPT API 활용 블로그 글 자동 생성” 코드를 활용하여 모듈화를 진행하였습니다.
이전 코드를 ChatGPT에게 주고, 1) input parameter : topic / purpose(keyword), 2) 사용 모델 : GPT-3.5-turbo, 3) 별도 파일 출력 없음을 반영하여 함수로 만들어 달라고 하고, JSON 형식의 output 출력 부분을 추가하여 다음과 같은 결과를 얻었습니다.
(OpenAI API Key는 시스템 환경 변수 설정)
** generate_blog_content() // test 부분 포함
import os
import openai
def generate_blog_content(topic, keyword):
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("API Key not found in environment variables")
openai.api_key = api_key
# (기존)
# # 시스템 메시지를 설정합니다.
# system_prompt = f"당신은 {topic}를 잘 알고 있는 네이버 블로그 SEO 전문가입니다."
# # 사용자 메시지를 설정합니다.
# user_prompt = f"블로그 글의 목적은 {keyword}입니다. 이 목적을 참고하여 {topic}에 대한 블로그 글을 작성해 주세요. "
# # 보조자 메시지를 설정합니다.
# assistant_prompt = '1) SEO가 적용된 블로그 글을 작성해 주세요. \
# 2) 네이버 SEO를 고려하여 글의 제목을 추천해 주세요. \
# 3) 추천 해시태그를 작성된 글의 끝 부분에 추가해 주세요. \
# 4) 친근한 어투를 사용해 주세요. \
# 5) 자기 소개를 넣지 마세요.(예 : 000 전문가입니다, SEO 전문가입니다) \
# 6) SEO를 고려해서 글을 작성한다는 것을 밝히지 마세요. \
# 7) 제목은 최대 20자, 블로그 글은 4~5개의 문단으로 구성하고 최대 1500자 정도로 작성해 주세요. \
# 8) Key 값이 Title, Contents, Hashtags인 Json 형식으로 [Template]을 참고하여 작성해 주세요. \
# [Template] {"Title":<title>, "Contents":<contents>, "Hashtags":<hashtags>} '
# (변경 프롬프트 코드) : assistant_promp 부분 축소!!
# 시스템 메시지를 설정합니다.
system_prompt = f'''
당신은 {topic}을 잘 알고 있는 네이버 블로그 SEO 전문가입니다.
1) SEO가 적용된 블로그 글과 해시태그를 작성해 주세요.
2) 제목은 SEO에 맞게 20자를 넘지 않게 해 주세요.
4) 블로그 <contents>는 전체 1000자 이상으로 작성해 주세요.
'''
# 사용자 메시지를 설정합니다.
user_prompt = f'블로그 글의 키워드는 {keyword}입니다. 이 키워드를 참고하여 {topic}에 대한 블로그 글을 작성해 주세요.'
# 보조자 메시지를 설정합니다.
assistant_prompt = '''
[IMPORTANT] Key 값이 Title, Contents, Hashtags인 Json 형식으로 [Template] 참고하여 작성해 주세요.
[Template] ---{"Title":<title>, "Contents":<contents>, "Hashtags":<hashtags>}---
'''
text_length= 0
iteration_no = 0
while text_length <= 1000:
iteration_no += 1
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
{"role": "assistant", "content": assistant_prompt},
]
)
blog_post = response.choices[0].message.content
text_length = len(blog_post)
print(f"Blog post generated successfully.: {iteration_no}회 생성!!")
return blog_post
# 'topic'과 'keyword'는 예시 입력입니다. 실제로는 사용자로부터 받은 입력값을 사용합니다.
blog_post = generate_blog_content("인공 지능", "기술의 발전")
print(blog_post)
(추가) 아무리 프롬프트에.. 글 길이를 1000자 이상, 1500자 이상 이렇게 적어 놔도.. ChatGPT 맘이라..
가끔 정말 짧은 글을 만들어 주는 경우가 있다. 그래서, 생성된 글 길이를 체크해서 500자 보다 작으면 다시 생성해 달라고 요청하는 while 문을 추가했습니다. (500자도 적을려나…)
(추가) 욕심을 부려 assistant prompt에 이런 저런 조건을 많이 넣으니, 결과적으로 답변의 길이가 짧아지는 문제점이 발견되었습니다. 최총 버전에서는 assistant prompt의 길이를 많이 줄였습니다.
[Module 3 : login_to_naver()]
주요 기능 : pyperclip 을 이용하여 네이버 자동 로그인
input : naver_id, naver_pw
output : driver(Selenium WebDriver의 인스턴스/웹 페이지 접근, 요소찾기, 클릭 등 다양한 동작 수행)
이 모듈은 2월 13일 특강 주제 “pyperclip 라이브러리를 활용한 네이버 자동 로그인하기 ” 결과를 활용하였습니다.
** log_to_naver()
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options
import pyperclip
import time
def login_to_naver(username, password):
# 크롬 옵션 설정
chrome_options = Options()
# 크롬 드라이버 초기화
chrome_service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=chrome_service, options=chrome_options)
# 자동 로그인할 웹사이트 열기
driver.get('https://nid.naver.com/nidlogin.login')
# 로딩 대기
time.sleep(2)
# 아이디 입력 필드 찾기
id_field = driver.find_element(By.XPATH, "//*[@id='id']")
# 아이디 입력
pyperclip.copy(username)
id_field.click()
id_field.send_keys(Keys.CONTROL, 'v')
time.sleep(1) # 로딩 대기
# 패스워드 입력 필드 찾기
password_field = driver.find_element(By.XPATH, "//*[@id='pw']")
# 패스워드 입력
pyperclip.copy(password)
password_field.click()
password_field.send_keys(Keys.CONTROL, 'v')
time.sleep(1) # 로딩 대기
# 로그인 버튼 클릭
login_button = driver.find_element(By.XPATH, '//*[@id="log.login"]')
login_button.click()
# 로그인 후의 추가적인 처리가 필요한 경우를 위해 대기
time.sleep(5)
# 여기서 추가적인 작업을 수행할 수 있습니다.
# 예를 들어, 로그인 후 페이지의 정보를 검사하거나 다른 작업을 수행합니다.
# 함수가 끝날 때 웹드라이버를 닫지 않고, 이를 호출한 곳에서 제어할 수 있도록 driver를 반환합니다.
return driver
# 함수 사용 예:
# driver = login_to_naver('your_naver_username', 'your_naver_password')
[Module 4 : publish_blog_post()]
여기서가 진짜 난관이였습니다.
[Module 3]에서 네이버 자동 로그인을 성공한 이후에
1) “블로그” 버튼 누르기
2) “글쓰기” 버튼 누르기
를 작동 시키면 블로그 글쓰기 페이지가 새창에 열리게 됩니다.
여기서 새창으로 열린다는 것을 염두해 두어야 하는데요.
driver.switch_to.window(driver.window_handles[-1])
이 코드를 고려해야 합니다. 이는 웹 브라우저를 제어하는 Selenium Webdriver가 현재 열려 있는 브라우저 창 중에서 가장 최근에 열린 창(방금 열린 네이버 글쓰기 페이지 창)으로 포커스를 전환한다는 의미입니다.
이 코드가 없으면 제대로 동작을 하지 않더라구요..
이는 박정기 파트너님이 특강 중에서 강조한 부분입니다.
이 페이지에서 3가지 이슈가 있는데요.
이 페이지가 iframe으로 구성되어 있다.
중간에 “작성 중인 글이 있습니다.” 팝업이 뜬 경우가 있다.
우측에 “도움말”이라는 팝업이 뜨는 경우가 있다.
첫 번째 이슈인 iframe 부분도 웹 드라이버를 iframe으로 전환을 시켜 줘야 하는데요. ChatGPT가 다음의 코드를 알려 줍니다.
# iframe으로 포커스 전환
WebDriverWait(driver, 10).until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'mainFrame')))
중앙과 우측의 팝업은 try / except 구문과 “취소” 버튼의 XPATH 주소로 요소를 찾아서 클릭하도록 코드를 작성하면 됩니다.
중간 중간 테스트로 실행 시켜 보면 오류가 뜨는 것은 오류 메세지를 ChatGPT에게 던져서 해결책을 받는 방향으로 수정을 진행하였습니다.
이제 마지막으로 제목과 본문 입력, 임시 수정 버튼 클릭의 과정만 남았는데요.
여기서도.. 난관이 있었습니다.
ChatGPT는 제목과 본문 입력 필드에 값을 넣을 때 selenium의 send_key()라는 메소드를 사용하도록 코드를 작성해 줬는데.. 제대로 동작을 안 합니다.
하다 하다 안 되어. 결국 파트너님의 소스를 ChatGPT에게 넣고 물 어 보니.. 다음과 같이 안내를 해 줏ㅂ니다.
위의 가이드에 따라.. pyperclip, pyautogui 라이브러리를 활용한 코드로 전환하였고.
여기서 단축키는 ctrl + V로 설정하였습니다.
추가로.. 테스트 하는 중간에 TimeoutException 오류가 자주 나와서 중간 중간에 WebDriverWait와 time.sleep()의 을 이용해서 대기 시간을 늘려 주었습니다.
이제 마지막으로 생성된 blog 글에서 제목과 본문, 해시태그를 편하게 뽑아 오기 위해서
generate_blog_content()에서 api로 글을 생성할 때, 프롬프트에 json 형태로 출력하는 부분을 넣었고,
이 결과를 publish_blog_post()에 넘겨 받아 dictionary 형식으로 바꾸는 작업을 추가하였습니다.
(이 과정도 그리 쉽지 않았습니다…ㅎㅎ)
» (추가) publish_blog_post() 코드(streamlit 버전)
def publish_blog_post(driver: Any, contents: Dict[str, Any]) -> None :
# 네이버 메인 페이지로 이동
driver.get('https://www.naver.com')
# "블로그" 버튼을 찾고 클릭
blog_button = driver.find_element(By.XPATH, '/html/body/div[2]/div[2]/div/div[2]/div/div[1]/div[1]/div[2]/div/div/ul/li[3]/a/span[1]')
blog_button.click()
st.write('* 블로그 버튼 클릭 성공')
time.sleep(2)
# "글쓰기" 버튼을 찾고 클릭
# 실제 사이트의 구조에 따라 XPath는 달라질 수 있음
write_post_button = driver.find_element(By.XPATH, '/html/body/div[2]/div[2]/div/div[2]/div/div[1]/div[1]/div[3]/div[2]/div[2]/a')
write_post_button.click()
time.sleep(2)
st.write('* 글쓰기 버튼 클릭 성공')
# 새 창으로 이동 (블로그 글쓰기 창이 새 탭/창에서 열렸다고 가정)
driver.switch_to.window(driver.window_handles[1])
time.sleep(2)
st.write('* 블로그 글쓰기 새 창으로 이동')
# iframe으로 포커스 전환
WebDriverWait(driver, 5).until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'mainFrame')))
time.sleep(2)
st.write('* iframe으로 포커스 전환 성공')
# "작성 중인 글이 있습니다." 팝업 처리
try:
cancel_button = WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div/div[3]/div/div/div[1]/div/div[4]/div[2]/div[3]/button[1]')))
cancel_button.click()
except Exception as e:
st.write("* No draft popup appeared.")
st.write('* 작성 중인 글 팝업 처리 성공')
# 우측 팝업 처리
try:
close_button = WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div/div[3]/div/div/div[1]/div/div[1]/article/div/header/button')))
close_button.click()
except Exception as e:
print("No right side popup appeared.")
time.sleep(2)
st.write('* 우측 팝업 처리 성공')
# 글쓰기 페이지 내 요소들의 XPATH
title_field_xpath = '/html/body/div[1]/div/div[3]/div/div/div[1]/div/div[1]/div[2]/section/article/div[1]/div[1]/div/div/p/span[2]'
content_field_xpath = '/html/body/div[1]/div/div[3]/div/div/div[1]/div/div[1]/div[2]/section/article/div[2]/div/div/div/div/p'
publish_button_xpath = '/html/body/div[1]/div/div[1]/div/div[2]/button[1]'
text_image_xpath = '/html/body/div[1]/div/div[3]/div/div/div[1]/div/header/div[1]/ul/li[17]/button'
image_keyword_xpath = '/html/body/div[1]/div/div[3]/div/div/div[1]/div/div[1]/aside/div/div[1]/input'
first_image_xpath = '/html/body/div[1]/div/div[3]/div/div/div[1]/div/div[1]/aside/div/div[3]/div/ul/div/li[1]/div/div[2]'
# 이미지 넣기(글감 무료 이미지 활용)
# 글감 버튼 클릭
image_button = driver.find_element(By.XPATH, text_image_xpath )
image_button.click()
time.sleep(1)
st.write('* 글감 버튼 클릭 성공')
# 키워드 입력 후 이미지 검색
keyword_field = WebDriverWait(driver, 5).until(EC.element_to_be_clickable((By.XPATH, image_keyword_xpath)))
pyperclip.copy(contents['Title'])
time.sleep(1)
keyword_field.click()
pyautogui.hotkey('ctrl', 'v') # 클립보드 내용 붙여넣기
pyautogui.press('enter')
time.sleep(2)
# 첫 이미지 클릭
login_button = driver.find_element(By.XPATH, first_image_xpath)
login_button.click()
# 제목 입력
title_field = WebDriverWait(driver, 5).until(EC.element_to_be_clickable((By.XPATH, title_field_xpath)))
pyperclip.copy(contents['Title'])
time.sleep(1)
title_field.click()
pyautogui.hotkey('ctrl', 'v') # 클립보드 내용 붙여넣기
time.sleep(1)
# 내용 입력
content_field = WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH, content_field_xpath)))
pyperclip.copy(contents['Contents'] + '\n\n\n\n\n' + contents['Hashtags'])
time.sleep(1)
content_field.click()
pyautogui.press('enter')
pyautogui.press('enter')
pyautogui.hotkey('ctrl', 'v') # 클립보드 내용 붙여넣기
pyautogui.press('enter')
time.sleep(1)
# "발행" 버튼 클릭
publish_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, publish_button_xpath)))
publish_button.click()
# 발행 후의 처리가 필요한 경우를 위해 대기
time.sleep(2)
# 글쓰기 작업 종료 이후 드라이버 종료
driver.quit() # 드라이버 종료
[Module 5 : parse_contents_to_dict()]
주요 기능 : JSON 형식으로 된 텍스트를 Dictionary로 변환하여 반환
input : blog_post(ChatGPT가 생성한 글)
output : 키 값이 “Title”, “Contents”, “Hashtags”인 Dictionary"
def parse_contents_to_dict(contents):
title = contents.split('"Title":')[1].split('"Contents":')[0].strip().strip(',').strip('"')
content = contents.split('"Contents":')[1].split('"Hashtags":')[0].strip().strip(',').strip('"')
hashtags = contents.split('"Hashtags":')[1].split('}')[0].strip().strip('"')
dict_contents = {
'Title': title,
'Contents': content,
'Hashtags': hashtags
}
return dict_contents
※ 프롬프트에서 지정한 형식에 맞춰 일반적으로 코드를 짜 보기는 했는데.. ChatGPT가 늘 동일한 포맷의 답변만 하는 게 아니라서.. 가끔 오류가 생길 수도 있을 거 같습니다..
[5개 모듈의 최종 결합 코드]
# ---------------------------------------------------------------------------
# Blog Automation Posting Program
# ---------------------------------------------------------------------------
# 1) Get User input
root = tk.Tk()
app = GetUserInput(root)
app.run()
# 2) generate blog content
blog_post = generate_blog_content(app.topic, app.purpose)
# 3) login to naver
driver = login_to_naver(app.naver_id, app.naver_pw)
# 4) publish blog post
input_blog_post = parse_contents_to_dict(blog_post)
publish_blog_post(driver, input_blog_post)
[네이버 블로그 임시 저장 화면]
(임시 저장이라서.. 편집의 과정이 필요해 보입니다.. )
이렇게 “네이버 블로그 포스팅 자동화 프로그램”의 개발 과정을 총 5개의 모듈로 구성하여 작성해 보았습니다.
개발이라고 하기에는 좀.. 부끄러운 수준이네요..ㅎㅎ
늘 원하는 대도 작동할 거라는 보장이 없기에.. 안정성을 높이기 위해 이미 작성된 모듈 뿐만 아니라 원래 Flow Chart에서 뺐던 중간 중간 오류나 실패 등 예외 상황에 대응하는 부분을 좀 더 검토해 봐야 하지 않을까 싶습니다.
머리 속으로는 이렇게 이렇게 하면 되는거지.. 언제 한번 해 봐야지 라고만 생각했던 프로그램인데..
이번 “문과생도 AI” 부트 캠프에 참여하지 않았다면 결코 시도도 못하고, 완성은 꿈도 꾸지 못했을 듯 합니다.
프로그램의 완결성 여부를 떠나, 이런 결과물을 만들 수 있게 도와 준 우리 @박정기 파트너님께 감사의 말씀을 드립니다~^^
(선 등록 후 수정 예정입니다. )
#9기문과생도AI