개발/자동화

티스토리 자동화 댓글 봇 만들기(2) (#8)

kimchangmin02 2025. 7. 26. 06:55

1. 작업 대상 및 내용 생성의 랜덤 요소

봇이 매번 똑같은 대상에게 똑같은 내용으로 작업하는 것을 막기 위한 랜덤입니다.

랜덤 요소 코드 설명
target_page = random.choice(TARGET_URLS) 어떤 게시물에 댓글을 달지 무작위로 선택합니다. TARGET_URLS 리스트에 있는 1번부터 54번까지의 게시물 주소 중 하나를 임의로 고릅니다.
generate_random_comment() 내부 댓글 내용을 매번 다르게 생성합니다. <br>  random.randint(4, 9): 댓글에 사용할 단어 수를 4개에서 9개 사이로 무작위 결정합니다. <br>  random.sample(word_list, num_words): 본문에서 추출한 단어들 중 위에서 정한 개수만큼 무작위로 단어를 뽑아 조합합니다.
generate_random_credentials() 내부 댓글 작성자 정보를 매번 다르게 생성합니다. <br>  random.randint(5, 8): 비로그인 이름을 5~8자리로 무작위 결정합니다. <br>  random.randint(8, 12): 비밀번호를 8~12자리로 무작위 결정합니다. <br>  random.choices(...): 해당 길이에 맞춰 알파벳과 숫자를 무작위로 조합합니다.

2. 행동 시간 지연(휴식)의 랜덤 요소

봇이 매번 동일한 간격으로 행동하는 것을 막고, 실제 사람이 생각하고 행동하는 것처럼 보이게 하는 휴식 시간입니다.

휴식(sleep) 코드 위치 및 목적 휴식 시간
time.sleep(random.uniform(1.5, 3.0)) 글 읽는 척 스크롤하는 중간중간에 쉽니다. 스크롤을 한 번 내리고 다음 스크롤을 내리기 전까지 1.5초에서 3초 사이로 불규칙하게 대기합니다.  
time.sleep(random.uniform(1, 2)) 스크롤을 마친 후, 댓글 입력창으로 이동하기 전에 쉽니다. 글을 다 읽고 "이제 댓글을 달아볼까" 하고 생각하는 듯한 지연 시간입니다. 1초에서 2초 사이로 대기합니다.  
time.sleep(random.uniform(0.05, 0.18)) 사람처럼 한 글자씩 타이핑하는 효과를 줍니다. 이름, 비밀번호, 댓글 내용을 입력할 때 각 글자 사이에 0.05초에서 0.18초 사이의 매우 짧은 랜덤 간격을 둡니다.  
time.sleep(random.uniform(0.5, 1.0)) 하나의 입력(이름)을 끝내고 다음 입력(비밀번호)으로 넘어가기 전에 쉽니다. 사람이 입력창을 마우스로 클릭해서 옮겨가는 듯한 짧은 멈춤입니다. 0.5초에서 1초 사이로 대기합니다.  
final_wait_time = random.uniform(3, 6) 댓글 등록을 완료한 후, 브라우저를 닫기 전에 쉽니다. 댓글이 잘 등록되었는지 확인하는 것처럼 3초에서 6초 사이로 페이지에 머뭅니다.  
error_sleep_time = random.uniform(5, 10) 오류가 발생했을 때 쉽니다. 예상치 못한 문제 발생 시 바로 재시도하면 서버에 부담을 주거나 차단될 수 있으므로, 5초에서 10초 사이로 대기 후 다음 작업을 시도합니다.  
break_time = random.uniform(30, 90) 하나의 완전한 작업을 끝내고 다음 작업을 시작하기 전의 긴 휴식 시간입니다. 이것이 가장 중요한 휴식으로, 봇 탐지를 피하기 위해 30초에서 90초(1분 30초) 사이의 긴 시간 동안 쉽니다.  

3. 인간적인 행동 모방의 랜덤 요소

마우스 움직임이나 스크롤 방식에 랜덤 요소를 추가하여 기계적인 패턴을 없애는 부분입니다.

랜덤 요소 코드 설명
scroll_count = random.randint(2, 4) 글을 읽을 때 스크롤을 내리는 횟수를 무작위로 결정합니다. 어떤 때는 2번, 어떤 때는 4번 등 2, 3, 4번 중 하나로 스크롤 횟수를 정합니다.
human_like_mouse_move() 내부 마우스 커서의 움직임을 사람처럼 만듭니다. <br>  random.randint(-20, 20): 목표(댓글창) 정중앙이 아닌, 상하좌우 -20px ~ +20px 범위 내의 살짝 빗나간 위치로 먼저 이동합니다. <br>  actions.pause(random.uniform(0.2, 0.6)): 마우스 이동 중간중간에 0.2초에서 0.6초 사이의 짧은 멈춤을 넣어 자연스러움을 더합니다.

 

 

--- [작업 시작] 'https://kimchangmin02.tistory.com/42' 게시물을 대상으로 새 작업을 시작합니다. ---
'https://kimchangmin02.tistory.com/42' 페이지로 성공적으로 이동했습니다.
사람처럼 보이기 위해 10.97초 동안 글을 읽는 척합니다...
페이지 스크롤을 완료했습니다.
게시물 본문 내용을 추출합니다...
    ㄴ 실패: 본문 내용 영역을 찾지 못했습니다. CSS 선택자를 확인하세요.
[오류 발생] 작업 중 예기치 않은 오류가 발생했습니다.
오류 내용: 본문 단어 추출에 실패하여 이번 작업을 중단합니다.
오류로 인해 6.25초 대기합니다.
브라우저를 종료합니다.

 

 

 

 

위의 주석으로 표시된 선택자들 역시, 본인 블로그 스킨의 HTML 구조에 맞게 정확한 값으로 수정해주셔야 합니다. 확인하는 방법은 다음과 같습니다.

  1. 본인 블로그 댓글 창에서 F12 키를 눌러 개발자 도구를 엽니다.
  2. 개발자 도구의 왼쪽 위 **요소 선택 아이콘(클릭 모양)**을 누릅니다.
  3. 이름 입력창, 비밀번호 입력창, 댓글 내용 입력창을 각각 클릭해보고, 개발자 도구에 하이라이트되는 HTML 태그의 class, id, name 등을 확인하여 코드의 선택자 값을 수정해주시면 됩니다.

 

 

 

 

 

 

 

 

 

1. 본문 영역 (Text Area)

Generated html

<div class="tt_article_useless_p_margin contents_style">...</div>
 
Use code with caution.Html
  • 찾으신 정보: 본문 전체를 감싸는 div 태그에 contents_style 이라는 클래스가 있습니다.
  • 적용할 코드: extract_post_text 함수 안의 CSS 선택자를 이것으로 바꿔주면 됩니다.

2. 이름 입력창 (Name Input)

Generated html

<input maxlength="32" placeholder="이름" title="이름" type="text" value="">```

*   **찾으신 정보:** `placeholder`나 `title`이 "이름"인 `input` 태그입니다. 이런 속성을 이용하면 정확하게 해당 요소를 찾을 수 있습니다.
*   **적용할 코드:** `name_input_selector` 변수를 `input[placeholder='이름']` 으로 설정하면 됩니다.

### 3. 비밀번호 입력창 (Password Input)

```html
<input maxlength="12" placeholder="비밀번호" title="비밀번호" type="password" value="">
 
Use code with caution.Html
  • 찾으신 정보: placeholder가 "비밀번호"인 input 태그입니다.
  • 적용할 코드: password_input_selector 변수를 input[placeholder='비밀번호'] 로 설정합니다.

4. 댓글 내용 입력창 (Comment Textarea)

Generated html

<div class="tt-inner-g"><div class="tt-cmt" contenteditable="true" data-placeholder="내용을 입력하세요."></div></div>
 
Use code with caution.Html
  • 찾으신 정보: 이 부분은 <textarea>가 아니라 contenteditable="true" 속성을 가진 div 태그입니다. 괜찮습니다. 셀레니움은 이런 요소에도 send_keys를 사용할 수 있습니다. tt-cmt라는 고유한 클래스가 있네요.
  • 적용할 코드: comment_textarea_selector 변수를 .tt-cmt 로 설정합니다.

5. 등록 버튼 (Submit Button)

Generated html

<button class="tt-btn_register" disabled="" type="submit" ...>등록</button>
 
Use code with caution.Html
  • 찾으신 정보: tt-btn_register 라는 매우 명확한 클래스를 가진 button 태그입니다.
  • 적용할 코드: submit_button_selector 변수를 .tt-btn_register 로 설정합니다.

 

 

 

 

 

 

 

 

==================================================
 TISTORY 자동 댓글 작성 봇을 시작합니다. (최종 버전) 
 총 54개의 게시물을 대상으로 무작위 작업을 반복합니다.
 프로그램을 종료하려면 이 창에서 Ctrl + C 를 누르세요.
==================================================

--- [작업 시작] 'https://kimchangmin02.tistory.com/26' 게시물을 대상으로 새 작업을 시작합니다. ---
'https://kimchangmin02.tistory.com/26' 페이지로 성공적으로 이동했습니다.
사람처럼 보이기 위해 9.77초 동안 글을 읽는 척합니다...
페이지 스크롤을 완료했습니다.
게시물 본문 내용을 추출합니다...
    ㄴ 성공: 251개의 의미있는 단어를 추출했습니다.
추출된 단어를 바탕으로 랜덤 댓글을 생성합니다...
    ㄴ 생성된 댓글: "12 num1 그러면 숫자가 def max 20 20부터"
비로그인용 랜덤 이름과 비밀번호를 생성합니다...
    ㄴ 생성된 정보: 이름=wwvdfhi, 비밀번호=N42oms6wd
댓글 작성 영역으로 이동합니다...
    ㄴ 사람처럼 마우스 커서를 목표 지점으로 이동했습니다.
댓글 폼에 정보를 입력합니다...
    ㄴ 이름, 비밀번호, 댓글 내용 입력을 완료했습니다.
댓글 '등록' 버튼을 클릭합니다...
[오류 발생] 작업 중 예기치 않은 오류가 발생했습니다.
오류 내용: 댓글 등록 버튼을 찾거나 클릭할 수 없습니다.
오류로 인해 7.84초 대기합니다.
브라우저를 종료합니다.

>>> 다음 작업을 위해 47.64초간 충분히 휴식합니다...

 

 

 

 

 

문제의 원인은 버튼을 **"못 찾는 것"**이 아니라, **"클릭할 수 없는 상태"**에 있기 때문입니다.

자세히 살펴보겠습니다.

 

<button class="tt-btn_register" disabled="" type="submit" ...>등록</button>```

여기서 가장 중요한 부분은 `disabled=""` 입니다.

*   `disabled`: 이 속성은 HTML에서 버튼, 입력창 등이 **비활성화**되었음을 의미합니다.
*   **비활성화된 요소는 사용자가 클릭할 수 없으며, 당연히 셀레니움도 클릭할 수 없습니다.**

### **왜 `disabled` 상태일까요?**

티스토리 댓글 시스템의 작동 방식을 생각해보면 간단합니다.

1.  처음 페이지가 로드되면 '이름', '비밀번호', '댓글 내용'이 모두 비어있습니다.
2.  이 상태에서는 실수로 빈 댓글을 등록하는 것을 막기 위해 '등록' 버튼이 **`disabled` (비활성화)** 상태로 시작합니다.
3.  사용자가 이름, 비밀번호, 그리고 **가장 중요하게는 댓글 내용을 한 글자라도 입력하는 순간**, 자바스크립트가 이를 감지하고 '등록' 버튼에서 `disabled` 속성을 제거하여 **활성화 상태로** 만듭니다.

### **현재 코드의 문제점**

현재 코드는 `send_keys`로 댓글 내용까지 모두 입력한 직후, 거의 0.001초의 딜레이도 없이 바로 `submit_button.click()`을 시도합니다.

이때, **아직 브라우저의 자바스크립트가 "어, 내용이 입력됐네? 버튼을 활성화해야지!" 라고 반응하기 전**일 가능성이 매우 높습니다. 그래서 셀레니움이 클릭을 시도하는 그 찰나의 순간에 버튼은 여전히 `disabled` 상태인 것입니다.

### **해결책: 버튼이 '클릭 가능'해질 때까지 기다리기**

이 문제를 해결하는 가장 확실한 방법은, `WebDriverWait`를 이용해 **"버튼에 `disabled` 속성이 없어질 때까지"** 또는 **"버튼이 클릭 가능한 상태가 될 때까지"** 명시적으로 기다려주는 코드를 추가하는 것입니다.

아래와 같이 '댓글 작성 버튼 클릭' 부분을 수정하면 됩니다.

**수정 전 코드:**
```python
        # 5. 댓글 작성 버튼 클릭
        print("댓글 '등록' 버튼을 클릭합니다...")
        try:
            submit_button_selector = "button.tt-btn_register"
            submit_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, submit_button_selector)))
            submit_button.click()
            print("    ㄴ 성공! 댓글을 성공적으로 등록했습니다.")
        except Exception:
            raise Exception("댓글 등록 버튼을 찾거나 클릭할 수 없습니다.")
 

수정 후 코드:

 

# 5. 댓글 작성 버튼 클릭
        print("댓글 '등록' 버튼을 클릭합니다...")
        try:
            submit_button_selector = "button.tt-btn_register"

            # [수정] 버튼이 활성화(클릭 가능) 상태가 될 때까지 최대 5초간 기다립니다.
            # EC.element_to_be_clickable는 요소가 화면에 보이고, 활성화되어 있을 때를 기다립니다.
            submit_button = WebDriverWait(driver, 5).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, submit_button_selector))
            )

            # 만약을 위해 JavaScript 클릭을 시도해볼 수도 있습니다.
            driver.execute_script("arguments[0].click();", submit_button)
            
            print("    ㄴ 성공! 댓글을 성공적으로 등록했습니다.")
        except Exception as e:
            # print(traceback.format_exc()) # 디버깅 시 오류 상세 내용 확인
            raise Exception("댓글 등록 버튼을 찾거나 클릭할 수 없습니다.")

수정된 내용의 핵심:

  1. EC.element_to_be_clickable 조건을 사용합니다. 이 조건은 해당 요소가 눈에 보일 뿐만 아니라 disabled 속성이 없는, 즉 실제로 클릭 가능한 상태가 될 때까지 기다려주는 가장 확실한 방법입니다.
  2. driver.execute_script("arguments[0].click();", submit_button): 간혹 일반 .click()이 다른 요소에 가려져 실패할 때가 있는데, 자바스크립트를 이용한 클릭은 이를 우회하여 더 안정적으로 클릭을 실행할 수 있습니다.

 

 

 

 

 

 

 

 

일단 현재까지의 코드는 다음과 같음

import time
import random
import string
import traceback
from selenium import webdriver
from seleniuhttp://m.webdriver.common.by import By
from seleniuhttp://m.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from seleniuhttp://m.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from seleniuhttp://m.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import NoSuchElementException, TimeoutException

# --- 헬퍼 함수 정의 ---

def human_like_mouse_move(driver, element):
    """지정한 요소로 사람처럼 마우스를 부드럽게 이동시킵니다."""
    actions = ActionChains(driver)
    
    element_location = element.location
    element_size = element.size
    center_x = element_location['x'] + element_size['width'] / 2
    center_y = element_location['y'] + element_size['height'] / 2

    start_x = random.randint(0, driver.get_window_size()['width'])
    start_y = random.randint(0, driver.get_window_size()['height'])
    
    actions.move_by_offset(start_x, start_y)
    actions.pause(random.uniform(0.2, 0.4))
    
    actions.move_to_element_with_offset(element, random.randint(-20, 20), random.randint(-20, 20))
    actions.pause(random.uniform(0.3, 0.6))

    actions.move_to_element(element)
    
    actions.perform()
    print("    ㄴ 사람처럼 마우스 커서를 목표 지점으로 이동했습니다.")
    time.sleep(random.uniform(0.5, 1.0))

def extract_post_text(driver):
    """게시물 본문 내용을 추출하여 단어 리스트로 반환합니다."""
    print("게시물 본문 내용을 추출합니다...")
    try:
        wait = WebDriverWait(driver, 10)
        post_body = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.contents_style")))
        
        text = post_body.text
        translator = str.maketrans(string.punctuation, ' ' * len(string.punctuation))
        words = text.translate(translator).split()
        
        meaningful_words = [word for word in words if len(word) > 1]
        
        if not meaningful_words:
            print("    ㄴ 경고: 의미있는 단어를 찾지 못했습니다.")
            return []
            
        print(f"    ㄴ 성공: {len(meaningful_words)}개의 의미있는 단어를 추출했습니다.")
        return meaningful_words
    except (NoSuchElementException, TimeoutException):
        print("    ㄴ 실패: 본문 내용 영역을 찾지 못했습니다. CSS 선택자를 확인하세요.")
        return []
    except Exception as e:
        print(f"    ㄴ 실패: 본문 내용 추출 중 오류 발생 - {e}")
        return []

def generate_random_comment(word_list):
    """단어 리스트에서 무작위로 단어를 선택해 댓글을 생성합니다."""
    print("추출된 단어를 바탕으로 랜덤 댓글을 생성합니다...")
    if not word_list:
        return "포스팅 잘 보고 갑니다. 좋은 하루 되세요."

    num_words = random.randint(4, 9)
    if len(word_list) < num_words:
        num_words = len(word_list)
        
    random_words = random.sample(word_list, num_words)
    comment = " ".join(random_words)
    
    print(f"    ㄴ 생성된 댓글: \"{comment}\"")
    return comment

def generate_random_credentials():
    """랜덤으로 비로그인용 이름과 비밀번호를 생성합니다."""
    print("비로그인용 랜덤 이름과 비밀번호를 생성합니다...")
    name = ''.join(random.choices(string.ascii_lowercase, k=random.randint(5, 8)))
    password = ''.join(random.choices(string.ascii_letters + string.digits, k=random.randint(8, 12)))
    print(f"    ㄴ 생성된 정보: 이름={name}, 비밀번호={password}")
    return name, password

# --- 메인 코드 ---
BASE_URL = "https://kimchangmin02.tistory.com"
TARGET_URLS = [f"{BASE_URL}/{i}" for i in range(1, 55)]
WAIT_TIMEOUT = 15

print("="*50)
print(" TISTORY 자동 댓글 작성 봇을 시작합니다. (v2 - 버튼 활성화 대기 기능 추가) ")
print(f" 총 {len(TARGET_URLS)}개의 게시물을 대상으로 무작위 작업을 반복합니다.")
print(" 프로그램을 종료하려면 이 창에서 Ctrl + C 를 누르세요.")
print("="*50)

while True:
    driver = None
    try:
        target_page = random.choice(TARGET_URLS)
        print(f"\n--- [작업 시작] '{target_page}' 게시물을 대상으로 새 작업을 시작합니다. ---")
        
        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument("--start-maximized")
        chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
        driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
        wait = WebDriverWait(driver, WAIT_TIMEOUT)
        
        driver.get(target_page)
        print(f"'{target_page}' 페이지로 성공적으로 이동했습니다.")
        
        # 1. 글을 읽는 것처럼 행동 (랜덤 스크롤 및 대기)
        reading_time = random.uniform(5, 12)
        print(f"사람처럼 보이기 위해 {reading_time:.2f}초 동안 글을 읽는 척합니다...")
        
        scroll_count = random.randint(2, 4)
        for i in range(scroll_count):
            scroll_depth = (i + 1) / (scroll_count + 1)
            driver.execute_script(f"window.scrollTo({{ top: document.body.scrollHeight * {scroll_depth}, behavior: 'smooth' }});")
            time.sleep(random.uniform(1.5, 3.0))
        
        print("페이지 스크롤을 완료했습니다.")

        # 2. 본문 텍스트 추출 및 댓글/신원 정보 생성
        words = extract_post_text(driver)
        if not words:
            raise Exception("본문 단어 추출에 실패하여 이번 작업을 중단합니다.")
            
        comment_text = generate_random_comment(words)
        user_name, user_password = generate_random_credentials()

        # 3. 댓글 입력창으로 이동
        print("댓글 작성 영역으로 이동합니다...")
        try:
            comment_area_selector = "button.tt-btn_register"
            comment_form_area = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, comment_area_selector)))
            driver.execute_script("arguments[0].scrollIntoView({block: 'center', behavior: 'smooth'});", comment_form_area)
            time.sleep(random.uniform(1, 2))
            
            human_like_mouse_move(driver, comment_form_area)

        except Exception:
             raise Exception("댓글 작성 영역을 찾지 못했습니다. CSS 선택자를 확인하세요.")

        # 4. 댓글 폼 요소 찾기 및 정보 입력
        print("댓글 폼에 정보를 입력합니다...")
        try:
            name_input_selector = "input[placeholder='이름']"
            password_input_selector = "input[placeholder='비밀번호']"
            comment_textarea_selector = "div.tt-cmt"

            name_input = driver.find_element(By.CSS_SELECTOR, name_input_selector)
            password_input = driver.find_element(By.CSS_SELECTOR, password_input_selector)
            comment_textarea = driver.find_element(By.CSS_SELECTOR, comment_textarea_selector)

            for char in user_name:
                name_input.send_keys(char)
                time.sleep(random.uniform(0.05, 0.15))
            time.sleep(random.uniform(0.5, 1.0))

            for char in user_password:
                password_input.send_keys(char)
                time.sleep(random.uniform(0.05, 0.15))
            time.sleep(random.uniform(0.5, 1.0))

            for char in comment_text:
                comment_textarea.send_keys(char)
                time.sleep(random.uniform(0.06, 0.18))
            
            print("    ㄴ 이름, 비밀번호, 댓글 내용 입력을 완료했습니다.")

        except Exception:
            raise Exception("이름/비밀번호/댓글 입력 필드를 찾지 못했습니다. CSS 선택자를 확인하세요.")

        # 5. 댓글 작성 버튼 클릭 (활성화 대기 기능 추가)
        print("댓글 '등록' 버튼을 클릭합니다...")
        try:
            submit_button_selector = "button.tt-btn_register"
            
            # 버튼이 '클릭 가능한' 상태가 될 때까지 최대 5초간 명시적으로 기다림
            submit_button = WebDriverWait(driver, 5).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, submit_button_selector))
            )
            
            # 일반 click()이 안될 경우를 대비해 JavaScript 클릭을 사용 (더 안정적)
            driver.execute_script("arguments[0].click();", submit_button)
            
            print("    ㄴ 성공! 댓글을 성공적으로 등록했습니다.")
        except TimeoutException:
            raise Exception("댓글 등록 버튼이 시간 내에 활성화되지 않았습니다.")
        except Exception:
            # traceback.print_exc() # 상세 오류를 보려면 이 줄의 주석을 해제
            raise Exception("댓글 등록 버튼을 찾거나 클릭하는 데 실패했습니다.")

        final_wait_time = random.uniform(3, 6)
        print(f"작업 완료! {final_wait_time:.2f}초 더 머문 뒤 브라우저를 닫습니다.")
        time.sleep(final_wait_time)

    except Exception as e:
        print(f"[오류 발생] 작업 중 예기치 않은 오류가 발생했습니다.")
        print(f"오류 내용: {e}")
        error_sleep_time = random.uniform(5, 10)
        print(f"오류로 인해 {error_sleep_time:.2f}초 대기합니다.")
        time.sleep(error_sleep_time)

    finally:
        if driver:
            print("브라우저를 종료합니다.")
            driver.quit()

    break_time = random.uniform(30, 90)
    print(f"\n>>> 다음 작업을 위해 {break_time:.2f}초간 충분히 휴식합니다...")
    time.sleep(break_time)