개발/자동화

[티스토리 좋아요 댓글 자동화] 더 사람처럼 (이론) #10

kimchangmin02 2025. 7. 27. 07:38

더 사람처럼 보이기 위한 코드 개선 아이디어

1. 브라우저 환경 설정 고도화

웹사이트는 브라우저의 다양한 속성을 확인하여 자동화 봇을 탐지합니다. 따라서 일반 사용자 환경과 최대한 유사하게 설정하는 것이 중요합니다.

  • User-Agent 변경: 현재 접속하는 브라우저의 정보를 나타내는 User-Agent를 최신 버전의 일반적인 값으로 설정하면 봇으로 의심받을 확률을 줄일 수 있습니다.[1]
  • 자동화 제어 플래그 비활성화: Selenium으로 브라우저를 제어하면 navigator.webdriver라는 속성이 true로 설정됩니다. 이 값을 undefined로 변경하여 자동화 탐지를 우회할 수 있습니다.[1][2]
  • 기존 사용자 프로필 활용: 매번 새로운 브라우저를 띄우는 대신, 기존에 사용하던 크롬 프로필을 그대로 불러와 자동화에 사용하면 로그인 정보나 쿠키 등이 유지되어 일반 사용자처럼 보일 수 있습니다.

 

 

 

 

 

 자동화 제어 플래그 비활성화: "이마에 붙은 '로봇' 스티커 떼기"

  • 이게 뭔가요?
    • 비유: 신분증은 완벽하게 위조했는데, 당신의 이마에 "저는 로봇입니다" 라고 적힌 형광 스티커가 붙어 있다고 상상해 보세요. 문지기는 신분증을 보기도 전에 당신을 쫓아낼 겁니다. 이 스티커가 바로 **'자동화 제어 플래그'**입니다.
    • 실제: 셀레니움으로 브라우저를 제어하면, 브라우저 내부의 navigator.webdriver라는 속성이 자동으로 true(참)로 설정됩니다. 이것은 웹사이트에게 "이 브라우저는 지금 자동화 프로그램에 의해 원격 조종되고 있습니다"라고 대놓고 알려주는 신호입니다.
  • 왜 문제가 되나요?
    • 문지기(웹사이트)는 모든 손님의 이마(navigator.webdriver 속성)를 확인합니다. 여기에 스티커가 붙어있으면 다른 것은 볼 필요도 없이 바로 봇으로 판정합니다. 이것이 봇 탐지의 가장 확실하고 쉬운 방법 중 하나입니다.
  • 해결책은?
    • 코드를 통해 브라우저를 시작할 때, "이마에 로봇 스티커 절대 붙이지 마!" (excludeSwitches, AutomationControlled 옵션)라고 명령하고, 심지어 "누가 스티커 붙어있냐고 물어보면, '그런 거 없는데요?'라고 거짓말해!" (execute_script로 속성값 변경)라고 가르치는 것입니다.
    • 결론: 봇이라는 가장 명백한 증거인 '이마의 스티커'를 제거해서 정체를 숨기는 것입니다.

 

 

 


User-Agent는 딱히 변경할 필요없나
최신일거니깐?

 

1. 이상적인 상황 (사용자님께서 생각하신 그대로입니다)

사용자님께서 코드에 사용하신 ChromeDriverManager().install()은 정말 스마트한 도구입니다. 이 도구는 스크립트를 실행하는 시점에, 컴퓨터에 설치된 크롬 브라우저 버전에 맞는 최신 ChromeDriver(크롬 조종기)를 알아서 다운로드해줍니다.

  • 비유: 최신형 자동차(크롬 브라우저)를 사면, 그에 꼭 맞는 최신형 스마트키(ChromeDriver)를 받는 것과 같습니다.

이 스마트키로 시동을 걸면, 자동차는 자신의 신분증(User-Agent)을 "저는 최신형 2025년식 자동차입니다"라고 올바르게 제시할 가능성이 높습니다.

이 경우라면, 사실 User-Agent를 굳이 변경하지 않아도 큰 문제가 없을 수 있습니다. 대부분의 평범한 웹사이트에서는 이 정도만으로도 충분히 통과됩니다.

2. 현실적인 문제 상황 (우리가 대비해야 할 이유)

하지만 깐깐한 보안 시스템(나이트클럽의 엘리트 문지기)을 가진 웹사이트들은 더 깊이 파고들 수 있습니다. 이때 다음과 같은 미세한 차이점이 문제가 될 수 있습니다.

문제점 1: '자동화'라는 미세한 흔적

셀레니움으로 제어되는 브라우저가 제시하는 User-Agent는, 실제 사람이 사용하는 브라우저의 User-Agent와 겉보기엔 똑같지만, 아주 미세한 부분에서 '자동화된 환경'이라는 흔적을 포함할 수 있습니다.

  • 비유: 신분증은 최신형이 맞는데, 아주 자세히 들여다보면 오른쪽 구석에 육안으로는 거의 보이지 않는 '렌터카용'이라는 워터마크가 찍혀있는 것과 같습니다. 일반인은 못 보지만, 전문 감식반(봇 탐지 시스템)은 이 워터마크를 발견하고 "아, 이건 직접 구매한 차가 아니라 빌린 차(자동화된 브라우저)구나!"라고 알아챌 수 있습니다.

문제점 2: 미묘한 버전 불일치

내 컴퓨터의 크롬 브라우저는 백그라운드에서 자동으로 업데이트되곤 합니다. 그런데 자동화 스크립트가 사용하는 ChromeDriver가 이 업데이트 속도를 완벽하게 따라잡지 못하는 아주 짧은 순간이 있을 수 있습니다.

  • 비유: 내 자동차는 오늘 아침에 막 2025년 7월식으로 업데이트되었는데, 스마트키는 아직 2025년 6월식 정보를 가지고 있는 상황입니다. 이 사소한 불일치 자체를 '비정상적인 패턴'으로 감지하는 시스템도 있습니다.

결론: 왜 직접 설정하는 것이 '보험'이 되는가?

위와 같은 미세한 문제점들을 가장 확실하게 해결하는 방법이 바로 User-Agent를 직접 복사해서 붙여넣는 것입니다.

Generated python

# 내 실제 브라우저에서 복사해 온, 100% 리얼 User-Agent
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
chrome_options.add_argument(f"user-agent={user_agent}")

이렇게 하면 다음과 같은 강력한 효과를 얻습니다.

  • 완벽한 위장: '렌터카용' 워터마크가 있을지도 모른다는 의심 자체를 없앱니다. 우리는 실제 사람의 신분증을 그대로 복사해서 사용하므로, 감식반이 봐도 완벽한 '진짜' 신분증이 됩니다.
  • 일관성 유지: 스크립트를 언제 실행하든, 어떤 버전의 ChromeDriver가 사용되든, 항상 똑같은 신분증을 제시하게 됩니다. 이는 "매번 동일한 사용자가 접속한다"는 인상을 주어 신뢰도를 높입니다.

최종 요약:

User-Agent를 굳이 바꾸지 않아도 대부분의 경우 문제없을 수 있습니다. 하지만, 우리가 상대하는 웹사이트가 얼마나 똑똑한지 알 수 없으므로, 아주 쉽게 추가할 수 있는 이 한 줄의 코드는 '혹시 모를 탐지에 걸릴 위험'을 원천 차단해주는 가장 가성비 좋은 보험인 셈입니다.

따라서 전문가들은 봇 탐지를 피하는 것을 목표로 하는 자동화 코드를 짤 때, User-Agent를 직접 설정하는 것을 거의 필수적인 기본 예절처럼 여기고 있습니다.


 

 

 

 

또한 기존 사용자 프로필 사용 하면, 그걸로 좋아요나 댓글을 여러개 달순없으니깐
아니면, 

시크릿 탭으로 들어가는건 똑같은데, 랜덤으로 1~10중, 다른 글도 더 보게 하는거임

 


2. 행위 패턴의 무작위성 강화

사람은 매번 똑같은 순서와 시간 간격으로 행동하지 않습니다. 자동화 스크립트에도 이러한 불규칙성을 더 많이 추가하는 것이 좋습니다.

  • 다양한 스크롤 방식: 현재는 정해진 횟수만큼 스크롤하지만, 페이지 전체를 한 번에 내리거나, 특정 요소가 보일 때까지 스크롤하는 등 다양한 방식을 섞어 사용할 수 있습니다. 마우스 휠 스크롤, 키보드(Page Down, End 키) 입력 등도 좋은 방법입니다.
  • 의도적인 '딴짓' 추가: 실제 사람은 페이지를 읽다가 잠시 멈추거나, 마우스를 이리저리 움직이기도 합니다. time.sleep()만 사용하는 대신, 페이지 내의 특정 이미지나 링크 위로 마우스를 잠시 옮겨보는 동작을 추가할 수 있습니다.
  • 작업 순서 변경: '좋아요 -> 댓글' 순서가 고정되어 있다면, 가끔은 '댓글 -> 좋아요' 순서로 진행하거나, 둘 중 하나만 수행하는 경우를 추가하여 패턴을 깰 수 있습니다.
  • 입력 속도의 변화: 현재 타이핑 속도가 균일한 편인데, 사람처럼 오타를 냈다가 지우거나, 단어 입력 후 잠시 멈추는 등 더 현실적인 타이핑 패턴을 구현할 수 있습니다.

 


 

 

🥇 1순위: 작업 순서 변경

  • 이유: 이것은 '행동 패턴' 자체를 깨뜨리는 가장 강력한 방법입니다. 봇 탐지 시스템은 "이 사용자는 항상 A -> B -> C 순서로 행동하는가?"를 핵심적으로 분석합니다. 이 고정된 순서만 깨줘도 "기계가 아닌 것 같다"는 인상을 강하게 줄 수 있습니다.
  • 구현 난이도: 쉬움. random.choice()와 몇 개의 if문만으로 간단하게 구현할 수 있습니다.
  • 가성비: 최상. 가장 적은 코드로 가장 확실한 '패턴 파괴' 효과를 얻을 수 있습니다.

적용 예시:

Generated python

# 작업 시작 전에 오늘의 행동 패턴을 무작위로 결정
action_list = ['좋아요만', '댓글만', '좋아요_후_댓글', '댓글_후_좋아요']
today_action = random.choice(action_list)

if today_action == '좋아요만':
    # 좋아요 로직만 실행
elif today_action == '댓글만':
    # 댓글 로직만 실행
elif today_action == '좋아요_후_댓글':
    # 좋아요 -> 댓글 순서로 실행
elif today_action == '댓글_후_좋아요':
    # 댓글 -> 좋아요 순서로 실행
 
Use code with caution.Python

🥈 2순위: 다양한 스크롤 방식

  • 이유: 페이지에 진입한 후 가장 먼저 하는 행동이 바로 스크롤입니다. 매번 똑같은 방식으로만 스크롤한다면(예: 항상 3번에 걸쳐 1/3씩 내림) 기계적인 패턴으로 인식되기 쉽습니다. 스크롤 방식에 변화를 주는 것은 진입 직후의 행동을 인간적으로 만드는 효과적인 방법입니다.
  • 구현 난이도: 쉬움. random.choice() execute_script 또는 키보드 입력을 조합하면 됩니다.
  • 가성비: 상. 비교적 간단한 코드로 초기 행동 패턴을 다양화할 수 있습니다.

적용 예시:

Generated python

scroll_type = random.choice(['step_by_step', 'full_down', 'page_down_key'])

if scroll_type == 'step_by_step':
    # 기존 코드처럼 단계별로 스크롤
    ...
elif scroll_type == 'full_down':
    # 한번에 페이지 끝까지 스크롤
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
elif scroll_type == 'page_down_key':
    # Page Down 키를 여러 번 누르는 방식
    from selenium.webdriver.common.keys import Keys
    body = driver.find_element(By.TAG_NAME, 'body')
    for _ in range(random.randint(3, 7)):
        body.send_keys(Keys.PAGE_DOWN)
        time.sleep(random.uniform(0.5, 1.5))
 
Use code with caution.Python

🥉 3순위: 의도적인 '딴짓' 추가

  • 이유: 사람은 스크롤을 하거나 글을 읽는 중간에 잠시 멈춰서 마우스를 특정 이미지나 링크 위에 올려보는 등 '비효율적인' 행동을 합니다. 이러한 '딴짓'은 로봇에게서는 거의 나타나지 않는 매우 인간적인 특징입니다. time.sleep()만으로 멈춰있는 것보다 훨씬 자연스럽습니다.
  • 구현 난이도: 중간. 페이지 내에서 클릭 가능한 이미지나 링크를 찾아야 하고, 없을 경우를 대비한 예외 처리가 필요합니다.
  • 가성비: 중. 구현에 약간의 노력이 필요하지만, 행동 패턴을 훨씬 풍부하고 예측 불가능하게 만들어 줍니다.

적용 예시:

Generated python

# 스크롤 중간이나, 좋아요 누르기 전에 '딴짓'할지 말지 결정
if random.random() < 0.3: # 30% 확률로 딴짓 실행
    try:
        # 페이지에 있는 이미지 중 아무거나 하나를 찾음
        all_images = driver.find_elements(By.TAG_NAME, 'img')
        if all_images:
            random_image = random.choice(all_images)
            # 그 이미지 위로 마우스를 스르륵 이동시키고 잠시 머무름
            ActionChains(driver).move_to_element(random_image).perform()
            time.sleep(random.uniform(2, 4))
    except Exception:
        # 이미지를 못 찾거나 에러가 나도 그냥 조용히 넘어감
        pass
 
Use code with caution.Python

4순위: 입력 속도의 변화

  • 이유: 사람처럼 타이핑 중간에 잠시 멈추거나, 오타를 냈다가 지우는 행동은 매우 인간적인 패턴입니다. 하지만 현재 코드도 send_keys를 한 글자씩 보내면서 글자 사이에 랜덤한 sleep을 주고 있기 때문에, 이미 기본적인 인간적인 타이핑을 어느 정도 흉내 내고 있습니다.
  • 구현 난이도: 높음. 오타를 내고, 백스페이스를 누르고, 다시 정확한 글자를 입력하는 로직은 생각보다 복잡하고 길어집니다.
  • 가성비: 하. 이미 어느 정도 효과를 보고 있는 상태에서, 더 복잡한 로직을 추가하는 것은 노력 대비 효과 증가 폭이 크지 않을 수 있습니다. 다른 가성비 좋은 방법들을 모두 적용한 뒤, 완성도를 극한으로 끌어올리고 싶을 때 고려해볼 만한 마지막 단계입니다.


 

 

현재, 코드에서 마우스 랜덤으로 움직이나?

예전에 그랫다가 

out of bounds오류 자주 났엇던것같은데

 

현재 코드의 움직임: "목표가 명확한 사람"

지금의 코드는 마치 "나는 저기 있는 '좋아요' 버튼을 누를거야" 라고 명확하게 마음먹은 사람처럼 행동합니다.

  1. 목표 설정 (랜덤 아님):
    like_button = WebDriverWait(driver, 5).until(...)
    이 코드는 '좋아요' 버튼이라는 하나의 명확한 목표물을 찾습니다.
  2. 이동 (랜덤 아님):
    actions.move_to_element(like_button).perform()
    이 명령은 현재 마우스 커서 위치에서 '좋아요' 버튼의 위치까지 가장 효율적인 경로(보통 직선에 가깝습니다)로 마우스를 이동시킵니다. 이동하는 도중에 일부러 다른 곳으로 마우스를 돌리거나 흔드는 행동은 하지 않습니다. 마치 우리가 어떤 아이콘을 클릭하려고 할 때, 마우스를 곧장 그 아이콘으로 가져가는 것과 같습니다.
  3. 도착 후 대기 (랜덤 맞음!):
    time.sleep(random.uniform(0.5, 1.0))
    이 부분이 바로 랜덤 요소가 들어간 지점입니다. 목표물인 '좋아요' 버튼 위에 마우스 커서를 올려놓은 뒤, 바로 클릭하지 않고 0.5초에서 1.0초 사이의 무작위 시간 동안 잠시 머뭇거립니다. 어떤 때는 0.5초 만에, 어떤 때는 0.9초 만에 다음 행동으로 넘어가는 식입니다.

요약: 현재 코드는 A 지점에서 B 지점으로 가는 경로 자체는 랜덤이 아니지만, B 지점에 도착해서 머무는 시간은 랜덤입니다. 이것만으로도 기계적인 느낌을 상당히 줄여주는 매우 효과적인 방법입니다.


만약 "진짜로 마우스 움직임을 랜덤하게" 만든다면?

만약 움직이는 경로 자체를 랜덤하게 만들고 싶다면 훨씬 더 복잡한 코드가 필요합니다. 예를 들면 다음과 같은 방식이 있습니다.

  • 경로를 여러 조각으로 나누기: 출발점에서 도착점까지 한 번에 가는 것이 아니라, 중간에 여러 개의 가상 경유지를 무작위로 생성하고, 그 경유지들을 순서대로 들렀다 가는 방식입니다. (예: A -> 랜덤지점1 -> 랜덤지점2 -> B)
  • 인간의 손 떨림 흉내 내기: 마우스를 이동하면서 아주 미세하게 위아래, 또는 좌우로 좌표를 계속 흔들어주며 이동하는 방식입니다.

이러한 방식들은 코드가 매우 길고 복잡해지며, 투자하는 노력에 비해 봇 탐지를 피하는 효과가 극적으로 커지지는 않을 수 있습니다.

결론적으로, 현재 사용자님의 코드는 "마우스 움직임의 핵심적인 랜덤성"을 잘 포착하고 있습니다. 사람은 이동 경로를 랜덤하게 만들기보다는, 행동과 행동 사이의 간격(시간)을 랜덤하게 두는 경향이 더 강하기 때문입니다. 지금의 human_like_mouse_move 함수는 충분히 인간적인 행동 패턴을 잘 흉내 내고 있다고 볼 수 있습니다.

 

 


 

 

 

 

1. 이동 경로 랜덤 vs. 행동 간격 랜덤: 무엇이 더 봇 탐지에 유리한가?

결론부터 말씀드리면, 행동과 행동 사이의 간격(시간)을 랜덤하게 두는 것이 훨씬 더 중요하고, 탐지 회피에 유리합니다.

그 이유는 다음과 같습니다.

비교 항목 행동 간격(시간) 랜덤 (현재 코드 방식) 이동 경로 랜덤
탐지 시스템의 관점 매우 중요하게 보는 지표. "이 사용자는 각 행동을 몇 초 간격으로 하는가?" "패턴이 일정한가, 불규칙한가?"를 집중 분석합니다. 0.01초의 오차도 없이 매번 2.0초 간격으로 클릭한다면 100% 봇입니다. 상대적으로 덜 중요하게 보는 지표. 마우스가 직선으로 움직이든, 약간 곡선으로 움직이든 크게 신경 쓰지 않는 경우가 많습니다. 오히려 비정상적으로 어지럽게 움직이면 더 의심할 수 있습니다.
구현 난이도 매우 쉬움. time.sleep(random.uniform(a, b)) 한 줄이면 충분합니다. 매우 어려움. 화면 좌표계를 완벽히 이해해야 하고, 경유지를 계산하며, out of bounds 같은 예외를 모두 처리해야 합니다.
인간 행동과의 유사성 매우 높음. 사람은 글을 읽는 속도, 버튼을 누르기 전 망설이는 시간 등이 매번 다릅니다. 이 방식을 통해 그 불규칙성을 완벽히 흉내 낼 수 있습니다. 오히려 부자연스러울 수 있음. 실제 사람도 특정 버튼을 누를 땐 대부분 마우스를 직선에 가깝게 이동시킵니다. 의도적으로 빙빙 돌려서 클릭하는 경우는 거의 없습니다.
가성비 (투자 대비 효과) 최상. 아주 적은 노력으로 봇 탐지 회피에 가장 큰 효과를 볼 수 있습니다. 최하. 엄청난 노력을 들여 구현해도 탐지 회피에 미치는 영향은 미미하거나, 오히려 역효과가 날 수 있습니다.

결론: 자동화 봇을 사람처럼 보이게 만드는 데 있어서, '시간의 불규칙성'은 '경로의 불규칙성'보다 압도적으로 중요합니다. 따라서 현재 코드처럼 행동 사이의 sleep 시간을 랜덤하게 주는 전략에 집중하는 것이 가장 현명한 접근 방식입니다.


2. 마우스 랜덤 이동 시 out of bounds 오류가 자주 나는 이유

이 오류는 "경계를 벗어났습니다!"라는 뜻으로, 마우스 커서를 화면(또는 브라우저 창)의 물리적인 영역 밖으로 이동시키려고 할 때 발생합니다.

이 오류가 발생하는 주된 이유는 다음과 같습니다.

  • 비유: 당신이 방 안에서 눈을 가리고 술래잡기를 한다고 상상해 보세요. 당신은 방의 크기(화면 해상도)를 모릅니다. 무작위로 열 걸음을 앞으로 가라는 명령을 받았을 때, 운이 나쁘면 벽에 부딪히게 됩니다. 이 **"벽에 부딪히는 상황"**이 바로 out of bounds 오류입니다.

기술적인 원인:

  1. 화면 해상도 미고려: 코드가 마우스 커서를 이동시킬 (x, y) 좌표를 무작위로 생성할 때, 현재 사용자의 모니터 해상도(예: 1920x1080)나 브라우저 창의 크기를 고려하지 않고 너무 큰 숫자를 만들어내는 경우입니다. 예를 들어, x좌표를 2000으로 이동시키려고 하면, 1920 너비를 가진 화면에서는 당연히 "경계를 벗어났다"는 오류가 발생합니다.
  2. 브라우저 창의 상대 좌표: 셀레니움에서 마우스 이동은 보통 전체 모니터 화면이 아니라 현재 열려있는 브라우저 창 내부를 기준으로 작동합니다. 브라우저가 전체 화면이 아닐 경우, 창의 크기는 모니터 해상도보다 훨씬 작습니다. 이 작은 창의 크기를 넘어서는 좌표로 이동하려고 하면 오류가 발생합니다.
  3. 목표 지점 + 랜덤 값의 함정: 가장 흔한 실수 중 하나입니다. 예를 들어, "목표 버튼의 x좌표에 -50에서 +50 사이의 랜덤 값을 더해서 움직이자"라는 로직을 짰다고 가정해 봅시다.
    • 만약 목표 버튼이 화면 오른쪽 맨 끝에 있다면, x좌표는 거의 최대치(예: 1900)에 가까울 것입니다. 여기에 +50이 더해지면 1950이 되어 화면 밖으로 튕겨 나갑니다. (out of bounds)
    • 반대로 버튼이 왼쪽 맨 끝에 있다면 x좌표는 0에 가까울 것입니다. 여기에 -50을 더하면 음수 좌표가 되어 또다시 오류가 발생합니다.

해결책 (매우 복잡함):

이 문제를 해결하려면, 마우스를 움직이기 전에 항상 현재 브라우저 창의 정확한 크기와 위치를 가져와야 합니다. 그리고 무작위 좌표를 생성할 때, 그 좌표가 [0, 창의 너비] 그리고 [0, 창의 높이] 범위 안에 있는지 매번 검사하고, 범위를 벗어날 경우 값을 조정해주는 복잡한 로직이 추가되어야 합니다.

이것이 바로 제가 위에서 **"이동 경로 랜덤은 구현 난이도가 매우 높다"**고 말씀드린 이유입니다. out of bounds 오류는 그 과정에서 겪게 되는 가장 대표적인 난관입니다.

 

 

 


 

 

 

3. 댓글 생성 로직 개선

현재 댓글 생성 방식은 본문에서 단어를 무작위로 추출하는 방식입니다. 이는 나쁘지 않지만, 조금 더 자연스러운 문장을 만들 수 있습니다.

  • 문맥을 고려한 댓글: 단순히 단어 조합을 넘어, "OOO 단어에 대한 설명 잘 봤습니다.", "특히 OOO 부분이 인상 깊네요." 와 같이 특정 키워드를 포함한 문장 템플릿을 여러 개 만들어 사용하면 훨씬 자연스럽습니다.
  • 의미 없는 단어 제거: 조사, 전치사 등 의미 없는 단어(불용어)를 제거한 후 핵심 단어 위주로 댓글을 생성하면 더 나은 품질의 댓글을 만들 수 있습니다.

 

 

4. 예외 처리와 작업 간 휴식 시간 현실화

  • 오류 발생 시 대처: 지금도 오류 처리가 되어 있지만, 오류 발생 시 무조건 다음 작업으로 넘어가는 대신, 가끔은 해당 페이지에서 새로고침(F5)을 시도해보는 로직을 추가할 수 있습니다.
  • 불규칙한 작업 간 휴식: 긴 휴식을 주는 것도 좋은 방법입니다. 실제 사람은 계속해서 블로그만 보지 않기 때문입니다.

 

 

아마 좀더 완성된 문장을 만드는게 더 어렵겟지

생성형 ai가 어떤 원리로 문장만드는지 좀 공부해볼까나

 

근데 티스토리에서 봇 감지할때  댓글의 내용이 봇 탐지의 이유가 되진않을것같긴한데

 


 

Tistory와 같은 플랫폼이 봇을 탐지할 때는 주로 다음과 같은 행동 패턴과 기술적 지표를 분석합니다.

  • 속도와 간격: 사람이 할 수 없는 속도로 페이지를 이동하고, 댓글을 작성하고, '좋아요'를 누르는 행위
  • 입력 패턴: 매번 동일한 속도로 타이핑하거나, 마우스 움직임 없이 클릭만 하는 행위
  • IP 주소: 단기간에 비정상적으로 많은 활동을 보이는 특정 IP 주소 또는 대역
  • 브라우저 정보 (핑거프린팅): 자동화 도구가 남기는 흔적 (navigator.webdriver 속성 등)
  • 행동의 일관성: 항상 동일한 순서(게시물 방문 -> 스크롤 -> 좋아요 -> 댓글)로만 행동하는 기계적인 패턴

 


 

 

 

봇 탐지 지표 대응: 가성비 및 중요도 순위

 

🥇 1순위: 브라우저 정보 (핑거프린팅)

  • 가성비: 최상
  • 이유: 이것은 봇 탐지의 **'치트키'**와 같습니다. 만약 이 정보가 제대로 처리되지 않으면, 아무리 다른 행동을 인간처럼 흉내 내도 "이마에 '로봇' 스티커"가 붙어있는 것과 같아서 바로 탐지됩니다. 반대로, 이 부분만 완벽히 처리하면 다른 사소한 실수는 눈감아 줄 확률이 높아집니다. 몇 줄의 코드로 가장 확실한 방어막을 칠 수 있습니다.

하위 개선 항목 (모두 적용하는 것을 강력히 추천)

  1. 자동화 제어 플래그 비활성화 (필수 중의 필수)
    • 내용: navigator.webdriver 속성이 true로 설정되는 것을 막습니다.
    • 방법: options.add_experimental_option("excludeSwitches", ["enable-automation"])  options.add_argument("--disable-blink-features=AutomationControlled") 코드를 추가합니다.
  2. User-Agent 변경
    • 내용: 실제 일반 사용자의 최신 브라우저 정보로 위장합니다.
    • 방법: 내 실제 브라우저의 User-Agent 문자열을 복사하여 options.add_argument(...)로 설정합니다.
  3. 기존 사용자 프로필 활용 (가성비 최고급 옵션)
    • 내용: 매번 깨끗한 브라우저가 아닌, 로그인 정보와 쿠키가 저장된 실제 사용자의 프로필을 불러옵니다. '처음 보는 손님'이 아닌 '단골손님'으로 위장합니다.
    • 방법: options.add_argument("user-data-dir=...")로 내 컴퓨터의 크롬 프로필 경로를 지정합니다.

🥈 2순위: 행동의 일관성

  • 가성비: 상
  • 이유: 1순위 기술적 지표 다음으로, 탐지 시스템이 가장 쉽게 파악하는 것이 바로 '패턴'입니다. "항상 똑같은 순서로만 행동한다"는 것은 기계의 가장 명백한 특징입니다. 이 패턴을 깨는 것은 비교적 적은 코드로 구현 가능하며, 탐지 시스템을 혼란시키는 데 매우 효과적입니다.

하위 개선 항목 (하나 이상 선택 또는 조합하여 적용)

  1. 작업 순서 무작위화 (가장 효과적)
    • 내용: '좋아요 -> 댓글' 순서를 고정하지 않고, '댓글 -> 좋아요' 또는 둘 중 하나만 수행하는 경우를 섞습니다.
    • 방법: random.choice() if문을 사용하여 실행할 작업 조합을 무작위로 선택합니다.
  2. 작업 수행 확률 도입
    • 내용: 각 행동을 100% 수행하는 것이 아니라, 특정 확률(예: 80% 확률로만 '좋아요' 누르기)을 도입합니다.
    • 방법: if random.random() < 0.8: 과 같은 조건문을 추가합니다.

🥉 3순위: 속도와 간격

  • 가성비: 중
  • 이유: 이미 현재 코드에서 random.uniform()을 사용해 행동 사이의 간격을 무작위로 설정하고 있으므로, 기본적인 방어는 되어 있습니다. 여기서 더 나아가 간격의 범위를 더 넓고 다양하게 만들어주면 '인간적인 불규칙성'을 더욱 강화할 수 있습니다.
  • 구현 난이도: 이미 적용된 코드의 숫자만 바꾸면 되므로 매우 쉽습니다.

하위 개선 항목 (기존 코드를 수정/강화)

  1. 휴식 시간 범위 대폭 확대
    • 내용: 작업과 작업 사이의 휴식 시간(break_time)을 (10, 25)초가 아닌, (20, 180)초처럼 극단적으로 넓은 범위로 설정합니다. 가끔은 몇 분씩 쉬는 사람의 패턴을 흉내 냅니다.
    • 방법: random.uniform(20, 180)처럼 숫자를 변경합니다.
  2. 각 행동 사이 간격의 다양화
    • 내용: 모든 sleep (0.5, 1.5)초처럼 비슷하게 설정하는 대신, 어떤 행동은 더 길게, 어떤 행동은 더 짧게 쉬도록 차등을 둡니다. (예: 스크롤 후 13초, 좋아요 클릭 전 0.51.5초, 댓글 작성 후 3~7초)
    • 방법:  time.sleep() 함수의 인자 값을 상황에 맞게 다르게 설정합니다.

4순위: 입력 패턴

  • 가성비: 하
  • 이유: 이 역시 현재 코드에서 send_keys를 한 글자씩 보내면서 기본적인 방어는 하고 있습니다. 여기서 더 나아가 오타를 내고 지우는 등의 복잡한 패턴을 추가하는 것은 코드의 복잡도를 크게 높이는 것에 비해 탐지 회피 효과의 증가 폭은 크지 않습니다. 위의 1, 2, 3순위가 훨씬 더 중요합니다.

하위 개선 항목 (고급 과정, 완성도를 높이고 싶을 때)

  1. 다양한 스크롤 방식 적용
    • 내용: execute_script 외에, 키보드의 PAGE_DOWN, END 키를 사용하는 방식을 추가합니다.
    • 방법: body.send_keys(Keys.PAGE_DOWN) 와 같은 코드를 추가합니다.
  2. 의도적인 '딴짓' 추가
    • 내용: 행동 중간에 무의미해 보이는 이미지나 링크 위로 마우스를 옮겨보는 동작을 추가합니다.
    • 방법: 페이지 내의 특정 요소를 찾아 ActionChains move_to_element를 실행합니다.

번외: IP 주소

  • 가성비: 상황에 따라 다름
  • 이유: 이 문제는 코드 수정만으로는 해결이 어렵고, 프록시(Proxy) 서버나 VPN 같은 외부 도구의 도움이 필요하기 때문입니다. 만약 집이나 사무실의 고정된 IP 하나로 하루에 수백, 수천 건의 작업을 수행한다면, 위의 모든 노력을 무시하고 IP 자체가 차단될 수 있습니다.
  • 대응:
    • 단기/소량 작업: 크게 신경 쓰지 않아도 됩니다.
    • 장기/대량 작업: 유료 프록시 서비스를 이용하여 요청마다 IP를 변경해주는 것이 필수적입니다. 이는 코드의 복잡도를 높이지만, 대규모 작업에서는 가장 확실한 방어 수단이 됩니다.

결론적으로, 가장 가성비 높은 순서는 1순위(브라우저 정보 위장) -> 2순위(행동 패턴 파괴) -> 3순위(시간 간격 다양화) 순입니다. 이 세 가지만 제대로 적용해도 웬만한 봇 탐지 시스템은 효과적으로 우회할 수 있습니다.

 

 

 

 

 

 


 

 

하지만, 그렇다고 해서 댓글 내용이 전혀 상관없는 것은 아닙니다. 댓글 내용은 '보조적인 증거' 또는 **'스팸 필터링'**의 대상이 될 수 있습니다.

  1. 스팸 필터의 타겟: 의미 없는 문자열의 나열("asdfasdf"), 광고성 URL 포함, 특정 키워드의 과도한 반복 등은 Tistory 자체의 스팸 필터에 의해 자동으로 걸러지거나 숨김 처리될 수 있습니다. 이것이 직접적인 '봇 차단'은 아니지만, 활동에 제약을 받는다는 점에서는 결과가 비슷합니다.
  2. 패턴 분석의 단서: 여러 블로그에 걸쳐, 완전히 똑같거나 구조가 거의 동일한 댓글("포스팅 잘 보고 갑니다. 좋은 하루 되세요.")이 짧은 시간 안에 대량으로 달린다면, 시스템은 이를 비정상적인 활동으로 간주하고 해당 계정이나 IP의 다른 행동들을 더 면밀히 분석하기 시작할 수 있습니다. 즉, 댓글 내용의 '패턴'이 의심의 시작점이 될 수 있습니다.
  3. 사용자 신고: 가장 중요한 부분입니다. 자동화 시스템이 놓치더라도, 블로그 주인이 보기에 "이 댓글은 너무 성의가 없는데?", "본문 내용과 전혀 상관없는 이상한 댓글이네"라고 느껴서 신고 버튼을 누를 수 있습니다. 사용자 신고가 누적되면 Tistory 운영팀의 검토 대상이 되고, 결국 봇 활동으로 판명되어 제재를 받을 수 있습니다.

 

 

 

 

 

  • 현재 방식 (단어 조합): 프로그래밍 규칙(Rule-based)에 기반한 간단한 방식입니다. 구현은 쉽지만, 결과물이 어색하고 기계적인 느낌을 줍니다.
  • 자연스러운 문장 생성: 이는 자연어 생성(NLG, Natural Language Generation) 기술의 영역입니다. 사람이 문법과 의미를 이해하고 문장을 만드는 것처럼, AI가 데이터로부터 언어의 패턴을 학습하여 새로운 문장을 만들어내는 것을 의미합니다.

"생성형 AI가 어떤 원리로 문장 만드는지 좀 공부해볼까나"라고 하셨는데, 아주 좋은 생각입니다. 이 분야를 간단히 맛보신다면 왜 자연스러운 문장 생성이 어려운지 바로 이해하실 수 있을 겁니다.

생성형 AI의 문장 생성 원리 (아주 간단한 버전)

최신 생성형 AI (ChatGPT와 같은 LLM - 거대 언어 모델)는 '다음에 올 가장 확률이 높은 단어는 무엇일까?' 라는 질문을 수없이 반복하여 문장을 만듭니다.

  1. 대규모 학습: 인터넷의 수많은 텍스트(책, 뉴스, 블로그 등)를 읽고 단어와 단어 사이의 관계, 즉 통계적 패턴을 학습합니다. 예를 들어, "오늘 날씨가 참" 이라는 단어들 다음에는 "좋네요", "맑네요", "쌀쌀하네요" 같은 단어가 올 확률이 높다는 것을 배웁니다.
  2. 문맥 이해 (어텐션 메커니즘): '트랜스포머'라는 혁신적인 아키텍처를 사용하여, 문장 내에서 어떤 단어가 다른 단어와 더 중요한 관련이 있는지 파악합니다. 이를 통해 단순히 바로 앞 단어뿐만 아니라, 문장 전체의 문맥을 고려하여 다음 단어를 예측합니다.
  3. 확률적 생성: 학습된 확률에 따라 다음 단어를 선택하는 과정을 계속 반복하여 문장을 이어 나갑니다. "오늘 날씨가 참" -> "좋네요" -> "산책하기" -> "좋은" -> "날씨입니다" 와 같이, 마치 단어 릴레이 게임을 하듯 문장을 완성합니다.

이러한 과정 때문에, 방대한 데이터와 복잡한 모델 구조, 그리고 막대한 컴퓨팅 자원이 필요하며, 개인 프로젝트 수준에서 직접 구현하기는 매우 어렵습니다.

 

 

 

 

 

 

 

 

 

 

more)

 

뉴스 웹사이트에서 특정 키워드 기사 스크랩

  • 작업 내용: 특정 뉴스 사이트를 방문해 관심 있는 키워드(예: '파이썬', '인공지능')를 검색하고, 검색 결과 목록의 기사 제목과 링크를 수집하여 파일(txt, csv)로 저장합니다.
  • 필요 기술: 검색창에 텍스트 입력 및 검색 실행, 여러 개의 검색 결과(기사 제목, 링크)를 반복문으로 한 번에 수집하는 기술.

 

 

SNS 자동화 (인스타그램, 트위터)[3]

  • 어려운 점:
    • 캡차: "로봇이 아닙니다" 체크박스, 이미지 선택 등 다양한 형태의 캡차가 등장합니다.[4]
    • 로그인 감지: 자동화된 로그인을 감지하고 차단하는 경우가 많습니다.[5]
    • UI 변경: 웹사이트 구조가 자주 바뀌어 어제까지 잘 되던 코드가 오늘 안 될 수 있습니다.
  • 도전해볼 만한 방법:
    • 로그인 세션 유지: 직접 로그인한 후, 해당 브라우저의 프로필(쿠키 정보)을 코드에서 불러와 캡차나 로그인 과정을 건너뛰는 방법입니다.
    • 캡차 해결 서비스 이용: 유료 API 서비스를 통해 이미지를 보내면 인식 결과를 받아 입력하는 방식도 있지만, 비용이 발생하고 100% 성공을 보장하지는 않습니다.
    • 속도 조절: 실제 사람처럼 보이도록 코드 실행 중간에 time.sleep()을 충분히 길고 불규칙하게 주는 것이 중요합니다. 현재 작성하신 코드의 human_like_ 함수들이 좋은 예시입니다.

 

 

 

 

 


 

 

 

 

  • 코드 1: actions = ActionChains(driver)
    • 문법 설명: ActionChains라는 특별한 클래스(행동 전문가)의 새 인스턴스(개체)를 만듭니다. 이때 driver(웹 브라우저 제어기)를 알려주어, 어떤 브라우저에서 행동할지 지정합니다.
    • 동작 원리 (비유): "마우스와 키보드로 할 수 있는 복잡한 행동들(예: 드래그 앤 드롭, 더블 클릭 등)을 계획할 수 있는 행동 계획가를 고용한다"는 의미입니다. driver를 넘겨주는 것은 "이 계획가는 지금 열려있는 이 크롬 브라우저를 담당해!"라고 지정해주는 것과 같습니다.
  • 코드 2: actions.move_to_element(element).perform()
    • 문법 설명: 점(.)으로 연결된 체인(Chain) 형태의 명령어입니다.
      • actions.move_to_element(element): 행동 계획가(actions)에게 "마우스 커서를 element(목표물) 위로 이동시키는 계획을 세워줘"라고 지시합니다. element는 '좋아요' 버튼 등이 될 수 있습니다.
      • .perform(): "지금까지 세운 모든 계획을 즉시 실행해!"라고 명령합니다. 이 perform()이 없으면 계획만 하고 실제 움직이지는 않습니다.
    • 동작 원리 (비유): 군대에서 "목표 지점(element)으로 이동하라"는 작전(move_to_element)을 세운 뒤, "작전 개시!"(perform)라고 외치는 것과 같습니다. 이 명령을 통해 실제로 마우스 커서가 화면에서 스르륵 움직이는 것을 볼 수 있습니다.

driver가 뭔가요? (비유: 로봇 본체 또는 리모컨)

driver는 **웹 브라우저 자체를 제어하는 '리모컨' 또는 '로봇 본체'**라고 생각하시면 완벽합니다.

  • 우리가 코드를 작성할 때 driver = webdriver.Chrome() 이라는 명령어를 사용하죠.
  • 이것은 "이제부터 내가 제어할 크롬(Chrome) 브라우저 로봇을 하나 만들어줘. 그리고 그 로봇의 리모컨을 driver라는 변수에 담아둘게" 라는 의미입니다.

 driver 리모컨으로는 다음과 같은 기본적인 조작을 할 수 있습니다.

  • driver.get("https://... "): 특정 URL로 이동시켜!
  • driver.find_element(...): 페이지에서 특정 부품(요소)을 찾아!
  • driver.quit(): 로봇(브라우저)의 전원을 꺼!

즉, driver는 나의 파이썬 코드와 웹 브라우저를 연결하고 제어하는 가장 기본적인 핵심 객체입니다.

 

 


 

 

 

ActionChains는 뭔가요? (비유: 로봇 팔 조종 전문가)

driver 리모컨만으로는 단순한 행동(페이지 이동, 요소 찾기, 클릭)밖에 못 합니다. 하지만 실제 사람은 더 복잡한 행동을 하죠.

  • 마우스 커서를 특정 요소 위에 가만히 올려놓기 (hover)
  • 어떤 파일을 드래그해서 다른 곳에 놓기 (drag and drop)
  • 오른쪽 마우스 버튼 클릭하기
  • 더블 클릭하기

이런 단순 클릭을 넘어서는 '연속적이거나 복잡한 행동'을 시키기 위해 필요한 것이 바로 ActionChains(행동 사슬) 입니다.

ActionChains는 '로봇 팔 조종 전문가'와 같습니다. 이 전문가에게 여러 동작을 순서대로 알려주면, 그대로 실행해 줍니다.

 

 

 

 


  • ActionChains(driver): "로봇 팔 조종 전문가(ActionChains)를 한 명 새로 부를게. 그런데 이 전문가는 driver라는 리모컨으로 조종되는 바로 그 로봇을 담당해야 해!"
    • driver를 괄호 안에 넣어주는 이유는, 이 전문가가 어떤 로봇(어떤 브라우저 창)을 제어해야 하는지 알려주기 위함입니다. 만약 여러 브라우저를 동시에 띄웠다면, 각각의 driver에 맞는 ActionChains 전문가가 필요하겠죠.
  • actions = ...: "새로 부른 이 '로봇 팔 조종 전문가'에게는 actions라는 이름을 붙여주자. 앞으로 이 이름을 불러서 여러 가지 복잡한 명령을 내릴 거야."

 

 


 

 

1. actions.move_to_element(element) : "무엇을 할지 계획만 세워!"

이 부분은 이전에 만든 '로봇 팔 조종 전문가'(actions)에게 임무를 지시하는 단계입니다.

  • actions: 우리가 고용한 '로봇 팔 조종 전문가'입니다.
  • .move_to_element(): "마우스 커서를 이동시켜라" 라는 **동작(Action)**의 한 종류입니다.
  • (element): 어디로 이동시킬지에 대한 목표물입니다. (element는 이전에 driver.find_element(...)로 찾아둔 웹 페이지의 특정 버튼이나 이미지 같은 부품입니다.)

가장 중요한 점: 이 코드만으로는 브라우저에서 아무 일도 일어나지 않습니다.
이것은 마치 전문가에게 "자, 첫 번째 임무는 저기 element라는 버튼 위로 마우스를 옮기는 거야. 일단 알아둬." 라고 말로만 지시하고, 아직 '시작' 버튼은 누르지 않은 상태와 같습니다. ActionChains는 이렇게 여러 행동들을 차곡차곡 쌓아서 계획표를 만들 수 있습니다.

2. .perform() : "지금까지 계획한 모든 것을 실행해!"

이것이 바로 '실행(Perform)' 버튼입니다.

perform() 메소드가 호출되는 순간, 그동안 actions 전문가의 계획표에 쌓여있던 모든 임무들이 지시된 순서대로 한꺼번에 실행됩니다.

지금은 계획이 "마우스 이동" 하나뿐이므로, .perform()을 호출하면 브라우저에서 실제로 마우스 커서가 해당 element 위로 스르륵 움직이는 것을 볼 수 있습니다.

 

 


extract_post_text : 본문 내용 추출 전문가

 

def extract_post_text(driver):
    try:
        # 1. '인내심 많은 감시자'를 설정합니다.
        wait = WebDriverWait(driver, 10)

        # 2. 본문 영역이 나타날 때까지 최대 10초간 기다립니다.
        post_body = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.contents_style")))

        # 3. 본문 영역의 텍스트를 가져옵니다.
        text = post_body.text

        # 4. '문장부호 제거 규칙'을 만듭니다.
        translator = str.maketrans(string.punctuation, ' ' * len(string.punctuation))

        # 5. 규칙을 적용하고 단어 단위로 쪼갭니다.
        words = text.translate(translator).split()

        # 6. 한 글자가 넘는 단어만 골라서 돌려줍니다.
        return [word for word in words if len(word) > 1]
    except Exception:
        # 7. 위 과정 중 하나라도 실패하면, 빈 목록을 돌려줍니다.
        return []

 

  • By.CSS_SELECTOR: 웹페이지의 요소를 찾을 때 쓰는 방법 중 하나로, "스타일(CSS) 정보로 찾겠다"는 의미입니다. div.contents_style은 "div 태그 중에서 class 이름이 contents_style인 것을 찾아라"는 주소와 같습니다.

 

 

 

 

"페이지의 수많은 부품들 중에서 내가 원하는 부품을 정확하게 어떻게 찾아내는가?"

 

웹 페이지 = 하나의 큰 도시
HTML 태그 (<div>, <p>, <a> 등) = 도시의 건물 (아파트, 상가, 관공서 등)

여러분이 도시에서 '김씨네 가게'를 찾아가야 한다고 상상해 보세요. 어떻게 찾을 수 있을까요? 바로 주소가 필요합니다.

"강남구 테헤란로 123번지 1층에 있는 상가" 처럼요.

여기서 By.CSS_SELECTOR "div.contents_style"가 바로 그 '주소' 역할을 합니다.


By.CSS_SELECTOR가 뭔가요? (비유: 주소 찾는 방법)

By는 셀레니움에게 "어떤 방식으로" 요소를 찾을지 알려주는 메뉴판과 같습니다. 메뉴판에는 여러 가지 방법이 있습니다.

  • By.ID: '주민등록번호'처럼 페이지에서 유일한 ID 값으로 찾기
  • By.TAG_NAME: '아파트'라는 건물 종류로만 찾기
  • By.XPATH: "시청 앞 사거리에서 오른쪽으로 꺾어서 세 번째 건물"처럼 아주 상세한 경로로 찾기
  • By.CSS_SELECTOR: "스타일(CSS) 정보"를 이용해서 찾기

CSS는 원래 웹 페이지의 '인테리어'나 '디자인'을 담당하는 언어입니다. (글자색, 크기, 위치 등) 디자이너들은 특정 부분에 스타일을 적용하기 위해 아주 정교한 '선택자(Selector)' 문법을 사용합니다.

By.CSS_SELECTOR는 셀레니움이 바로 이 CSS 디자이너들이 사용하는 '선택자 문법'을 그대로 빌려와서 요소를 찾는 방법입니다. 가장 강력하고 널리 쓰이는 방법 중 하나입니다.


"div.contents_style"가 뭔가요? (비유: 상세 주소)

이것이 바로 CSS 선택자 문법으로 쓰인 '상세 주소'입니다. 이 주소를 두 부분으로 나누어 해석할 수 있습니다.

1. div

  • "도시에서 div라는 종류의 건물을 찾아라."
  • div는 HTML에서 특별한 의미 없이 구역을 나누기 위해 사용하는 가장 일반적인 '건물(태그)'입니다. 블로그에서는 보통 본문 전체를 하나의 div로 감싸는 경우가 많습니다.

2. .contents_style

  • 여기서 **점(.)**이 매우 중요합니다. CSS 문법에서 **점(.)은 '클래스(class) 이름'**을 의미합니다.
  • 클래스는 건물에 붙이는 '별명'이나 '그룹 이름'과 같습니다. (예: "프리미엄 아파트", "할인 매장")
  • 따라서 .contents_style은 "건물(태그)에 contents_style이라는 클래스 이름(별명)이 붙어 있는 것"을 찾아라는 의미입니다.

최종 결론: extract_post_text 함수 속 의미

이제 extract_post_text 함수 속의 코드를 다시 보겠습니다.

post_body = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.contents_style")))

이 한 줄을 한국말로 풀어서 번역하면 다음과 같습니다.

"셀레니움아, 지금 열려있는 웹 페이지에서 요소를 하나 찾을 건데..."
"찾는 방법(By)은 CSS_SELECTOR 문법을 사용할 거야."
"찾아야 할 주소("div.contents_style")는 'div라는 종류의 건물이면서, 동시에 contents_style이라는 클래스 이름을 가진 것'이야."
"그걸 찾아서 화면에 나타날 때까지 기다렸다가, 찾으면 post_body라는 변수에 저장해 줘!"