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 구조에 맞게 정확한 값으로 수정해주셔야 합니다. 확인하는 방법은 다음과 같습니다.
- 본인 블로그 댓글 창에서 F12 키를 눌러 개발자 도구를 엽니다.
- 개발자 도구의 왼쪽 위 **요소 선택 아이콘(클릭 모양)**을 누릅니다.
- 이름 입력창, 비밀번호 입력창, 댓글 내용 입력창을 각각 클릭해보고, 개발자 도구에 하이라이트되는 HTML 태그의 class, id, name 등을 확인하여 코드의 선택자 값을 수정해주시면 됩니다.

1. 본문 영역 (Text Area)
Generated 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="">
- 찾으신 정보: 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>
- 찾으신 정보: 이 부분은 <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>
- 찾으신 정보: 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("댓글 등록 버튼을 찾거나 클릭할 수 없습니다.")
수정된 내용의 핵심:
- EC.element_to_be_clickable 조건을 사용합니다. 이 조건은 해당 요소가 눈에 보일 뿐만 아니라 disabled 속성이 없는, 즉 실제로 클릭 가능한 상태가 될 때까지 기다려주는 가장 확실한 방법입니다.
- 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)
'개발 > 자동화' 카테고리의 다른 글
| [티스토리 좋아요 댓글 자동화] 더 사람처럼 (이론) #10 (15) | 2025.07.27 |
|---|---|
| [코드] 티스토리 자동 좋아요+비로그인 댓글(#9) (11) | 2025.07.26 |
| 티스토리 좋아요 자동화(외부 라이브러리 설치) (#7) (8) | 2025.07.26 |
| 티스토리 자동화 댓글 봇 만들기(이론) (#6) (13) | 2025.07.25 |
| 티스토리 좋아요 자동화 #5(가상 마우스가 움직이는거였음) (9) | 2025.07.25 |