프로그래밍

[Gitistory] Chrome driver를 통해 소프트웨어로 블로그 자동 로그인하기

손가든 2024. 7. 29. 16:39

개발공부를 목적을 두고 수행하고 싶어서 사이드 프로젝트를 하나 진행하려한다.
 
프로젝트 기능은 github의 PR 내용을 블로그에 자동 업로드하여, 코드와 주요 변경사항을 손쉽게 파악할 수 있도록 하는것이다.
 
이름은 Gitistory
 
그 과정에서 발생하는 트러블 슈팅 및 개발 과정을 정리하도록 하겠다.
 


Flask 개발 환경 세팅

 
개발환경은 지난 포스팅을 통해 세팅을 완료했다.
 
다음은 이제 내 블로그에 소프트웨어 코드로 포스팅을 하는 방법을 찾는 것이었다.
 
처음엔 당연히 tistory open api를 사용하려고 했다.
 
하지만
 

 
왠지 올해부터 자동 댓글 생성하는 봇이 활동을 안하더라..
 
아무튼 그래서 다른 방법을 생각해내야 했다.
크래프톤 정글에서 코치님이 '개발자에게 불가능한 것은 없다. 다만 시간이 얼마나 걸리는지의 차이일 뿐' 이라고 해주신 말을 기억하며.
 
 
방법은 역시 있었다.
 
원격 소프트웨어 코드를 활용해서 웹을 켜서 블로그 게시물 게시 과정을 모두 코드화 하는 것
 
그 조력을 수행해주는 chrome driver라는 웹 소프트웨어 드라이버를 찾아냈다.
 

from flask import Flask, Response
import os
from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

# .env 파일에서 환경 변수 로드
load_dotenv()

app = Flask(__name__)

# 환경 변수 읽기
TISTORY_USERNAME = os.getenv('TISTORY_USERNAME')
TISTORY_PASSWORD = os.getenv('TISTORY_PASSWORD')
TISTORY_BLOGNAME = os.getenv('TISTORY_BLOGNAME')

@app.route('/')
def home():
    # Tistory 로그인 URL
    login_url = 'http://www.tistory.com/auth/login'
    is_success = 'test started but not success'
    
    # 브라우저 꺼짐 방지
    chrome_options = Options()
    chrome_options.add_experimental_option("detach", True)

    # 불필요 메세지 없애기
    chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"])

    # 드라이버 생성
    driver = webdriver.Chrome(options=chrome_options)
    
    driver.implicitly_wait(3)
    debug = "nothing"
        
    # 명시적 대기 예시
    wait = WebDriverWait(driver, 10)  # 최대 10초 대기

    try:
        # Tistory 로그인 페이지 접속
        driver.get(login_url)
        time.sleep(2)  # 페이지 로딩 대기

        # 로그인 정보 입력
        driver.find_element(By.CLASS_NAME,'btn_login.link_kakao_id').click()
        username = driver.find_element(By.XPATH, '//*[@id="loginId--1"]')
        password = driver.find_element(By.XPATH, '//*[@id="password--2"]')
        username.click()
        username.send_keys(TISTORY_USERNAME)
        password.click()
        password.send_keys(TISTORY_PASSWORD)
        password.send_keys(Keys.ENTER)
        time.sleep(2)  # 로그인 후 페이지 로딩 대기
        
        # 새 게시글 작성 페이지 접속
        new_post_url = f'https://{TISTORY_BLOGNAME}.tistory.com/manage/newpost'
        driver.get(new_post_url)
        time.sleep(2)  # 페이지 로딩 대기

 
초기 수행 코드를 다음과 같이 짰다
 
먼저 chrome driver의 설정을 수행해주고, driver로부터 url에 get 요청을 수행한다.
 
이후 카카오톡 로그인을 클릭해서 아이디 패스워드 로그인을 수행한다.
 
이때, 해당 태그를 가져온뒤 input에 데이터를 입력하고 진입하려는 경우 (로그인의 경우) 해당 input 태그를 실제 사용하는거처럼 클릭한 후 입력해주어야 한다.
 

 
 
근데 이때, 티스토리는 게시물을 작성하기 전에 이어서 작성하실건지 묻는 alert가 있다.
 
나는 코드로 이 alert를 고려해줘야 했다.
 

alert = driver.switch_to.alert
        if(alert.text) :
            alert.dismiss()

 
그래서 driver를 alert로 전환한 뒤, 만약에 alert가 있다면 해당 alert를 무시하도록 코드를 구현했다.
 
이후, 카테고리 설정 -> 게시물 제목 -> 게시물 content 순으로 기록한 후, 완료 -> 최종 완료 버튼을 통해 게시물 post가 완료된다.
 
이때, 게시물 content는 내부 프레임 요소여서 driver를 alert와 같이 switch 해야 한다.
 

#iframe 으로 driver 전환 필요 (게시물 내용)
        iframe=driver.find_element(By.CSS_SELECTOR, 'iframe')
        driver.switch_to.frame(iframe)
        
        content = driver.find_element(By.ID, 'tinymce')
        content.click()
        content.send_keys(post_content)
        
        driver.switch_to.default_content()

 
따라서 iframe이라는 내부 요소로 switch 한 후, content를 제어해야 한다.
 
이후, 반드시 iframe에서부터 다시 본 frame으로 재 스위치하여 완료버튼으로 접근할 수 있도록 한다.
 
 
최종 코드는 다음과 같다.
 

from flask import Flask, Response
import os
from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

# .env 파일에서 환경 변수 로드
load_dotenv()

app = Flask(__name__)

# 환경 변수 읽기
TISTORY_USERNAME = os.getenv('TISTORY_USERNAME')
TISTORY_PASSWORD = os.getenv('TISTORY_PASSWORD')
TISTORY_BLOGNAME = os.getenv('TISTORY_BLOGNAME')

@app.route('/')
def home():
    # Tistory 로그인 URL
    login_url = 'http://www.tistory.com/auth/login'
    is_success = 'test started but not success'
    
    # 브라우저 꺼짐 방지
    chrome_options = Options()
    chrome_options.add_experimental_option("detach", True)

    # 불필요 메세지 없애기
    chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"])

    # 드라이버 생성
    driver = webdriver.Chrome(options=chrome_options)
    
    driver.implicitly_wait(3)
        
    # 명시적 대기 예시
    wait = WebDriverWait(driver, 10)  # 최대 10초 대기
    
    post_title = 'test post title'
    
    post_content = 'test post content'

    try:
        # Tistory 로그인 페이지 접속
        driver.get(login_url)
        time.sleep(2)  # 페이지 로딩 대기

        # 로그인 정보 입력
        driver.find_element(By.CLASS_NAME,'btn_login.link_kakao_id').click()
        username = driver.find_element(By.XPATH, '//*[@id="loginId--1"]')
        password = driver.find_element(By.XPATH, '//*[@id="password--2"]')
        username.click()
        username.send_keys(TISTORY_USERNAME)
        password.click()
        password.send_keys(TISTORY_PASSWORD)
        password.send_keys(Keys.ENTER)
        time.sleep(2)  # 로그인 후 페이지 로딩 대기

        # 새 게시글 작성 페이지 접속
        new_post_url = f'https://{TISTORY_BLOGNAME}.tistory.com/manage/newpost'
        driver.get(new_post_url)
        time.sleep(2)  # 페이지 로딩 대기
        alert = driver.switch_to.alert
        if(alert.text) :
            alert.dismiss()
            
        category = driver.find_element(By.XPATH, '//*[@id="category-btn"]')
        category.click()
        gitistory_category = driver.find_element(By.XPATH, '//*[@id="category-item-707114"]/span')
        gitistory_category.click()
    
        title = driver.find_element(By.XPATH, '//*[@id="post-title-inp"]')
        title.click()
        title.send_keys(post_title)
        
        #iframe 으로 driver 전환 필요 (게시물 내용)
        iframe=driver.find_element(By.CSS_SELECTOR, 'iframe')
        driver.switch_to.frame(iframe)
        
        content = driver.find_element(By.ID, 'tinymce')
        content.click()
        content.send_keys(post_content)
        
        driver.switch_to.default_content()

        
        
        confirm = driver.find_element(By.XPATH, '//*[@id="publish-layer-btn"]')
        confirm.click()
        
        final_confirm = driver.find_element(By.XPATH, '//*[@id="publish-btn"]')
        final_confirm.click()
        


    finally:
        # 종료
        time.sleep(2)
        driver.quit()

    return "POST DONE"

if __name__ == '__main__':
    app.run(debug=True, port=5001)

 
 
++++
 

작성 중이던 글이 없어서 Alert가 발생하지 않을 경우 에러 문제 해결

 
저장된 글이 없을 때, 게시물 post 시 alert가 발생하지 않았을 때는, 위 코드가 에러를 일으킨다.
NoSuchAlertException은 Alert를 참조하는데 alert가 없을 때 발생한다.
 
따라서 alert를 무시하는 함수를 구현하여 try-except문으로 해결했다.
 

def handle_alert(driver):
            try:
                alert = driver.switch_to.alert
                alert.dismiss()  # 알림 닫기
                return True
            except NoAlertPresentException:
                return False

        handle_alert(driver)