Selenium을 통한 크롤링 시, 마주하는 reCAPTCHA 해결기

2025. 2. 19. 00:03·AlgoMate

기존 프로젝트를 리팩토링 하면서 크롤링을 기능을 자동화하는 걸 목표로 했다. 크롤링하는 곳에 접근하기 위해서 해당 사이트에 로그인을 수행해야 했는데 로그인 과정에서 자주 등장하는 recaptcha가 말썽이었다. 기존에 프로젝트를 진행할 때는 워낙 바쁘기도 했고, 전체 서비의 프로토타입을 만드는 걸 목적으로 했기에, 로그인 과정 중에 reCAPTCHA를 만나면 내가 수기로 해결하곤 했다. 

바빳던 11월,, 프로젝트들에 시달리던 나의 모습이다,, ㅋㅋ

해당 시기에도 크롤링을 자동화 하려고 했는데, 결국 해결하지 못했다. 전체 프로젝트에서 크롤링이 일부분이었기에 일단 전체 프로세스가 돌아가도록 설계하는 것이 우선이라고 생각하여 포기하게 되었다.

 

이후 방학을 맞이해서 해당 서비스를 고도화해서 배포해보자라는 목표를 가지고 요즘 리팩토링을 진행하고 있는데, 잊고 있었던 그녀석,, reCAPTCHA를 또 마주하게 되었다. 

 

1. reCAPTCHA 란?

 

reCAPTCHA 같은 경우에 요청하는 쪽을 서버가 사람이 아니라고 판단하거나, 로그인 시도가 너무 잦게, 반복적으로 일어날 때 등장하곤 한다. 아래 사진으로 보면 한 번에 아! 할 것이라고 생각한다. 대부분 신호등이나, 차를 고르라고 한다. 전에 보안문자 입력 같은 경우에는 컴퓨터가 해결할 수 없는 걸 사람이 대신 보안문자로 입력하게 한다는 소문이 있었는데 믿거나 말거나이다.

종류도 여러가가지로 예정 방식은 텍스트, 연산, 비디오 등 다양한 것들이 이용되었는데 요즘에는 간단한 로봇이 아닙니다 체크박스를 자주 사용하곤 한다. 이 체크박스에서 뭔가 이상한 점이 발견된다면 눈으로 보고 고를 수 밖에 없는 것을 출동시켜 사람인지 매크로인지 판단하는 것이다.

이녀석이다.

 

 

2. 쿠키를 통한 부분적 해결

대부분 잦은 로그인 시도에서 해당 매크로를 만났기 때문에, 로그인 시도를 안하면 되지 않을까? 란 생각에 첫 로그인 때 쿠키를 받아오고 이걸 계속 활용하는 방식으로 해결을 시도했다. 우리가 흔히 다른 페이지에 갔다와도 브라우저를 닫지 않으면 로그인이 돼 있는 것이 쿠키를 활용한 방식이다.

# 쿠키 저장 함수
def save_cookies(driver, filename="cookies.pkl"):
    with open(filename, "wb") as f:
        pickle.dump(driver.get_cookies(), f)

# 쿠키 불러오기 함수
def load_cookies(driver, filename="cookies.pkl"):
    with open(filename, "rb") as f:
        cookies = pickle.load(f)
        for cookie in cookies:
            driver.add_cookie(cookie)

# 쿠키 갱신 함수
def refresh_cookies(driver):
    # 쿠키를 다시 로드하고 로그인 절차를 통해 갱신
    login(driver)  # 로그인 함수 호출
    save_cookies(driver)  # 새 쿠키 저장

def tryCookieThenLogin(driver):
    """ ✅ 쿠키 로그인 시도 → 실패하면 새 로그인 """
    try:
        if login_using_cookies(driver):
            print("✅ 쿠키로 로그인 성공")
            return True
        elif login(driver):
            print("✅ 새로 로그인 성공")
            return True
        else:
            print("🚨 로그인 실패1")
            return False

    except Exception as e:
        logging.error(f"🚨 로그인 중 오류 발생: {e}")
        return False

 

쿠키를 저장하고, 저장한 쿠키를 바탕으로 다음 로그인 시 쿠키를 가져다가 쓰도록 로그인 로직을 짰다. 

쿠키는 TTL(time-to-live)가 존재하여 일정 시간 이후에는 쿠키로 로그인을 시도해도 로그인이 불가능하기 때문에, 일단 한번 로그인하면, 브라우저를 닫기 전 쿠키를 새로 저장하는 방식으로 구현하였다. 이렇게 해서 TTL 이 5분이라도 4분 49초에 다시 저장하고 사용하고, 저장하고 사용하고 이런 방식으로 지속적으로 쿠키가 유효하게 유지하려고 했다.

 

로컬에서 테스트할 때는 충분히 이 방식으로도 reCAPTCHA를 우회할 수 있었다. 

로컬에서는 chromedriver 를 GUI가 있는 방식, 즉 직접 브라우저를 우리 눈으로 볼 수 있는 방식으로 띄웠기 때문에 이러한 방식을 사용하더라도, 간혹가다가 reCAPTCHA가 발생하면 내가 수기로 해결해주면 됐었다. 해결해주면 쿠키를 새로 발급받고 해당 쿠키는 꽤나 오래 refresh 되면서 사용 가능했다.

 

그러나 해당 기능을 하는 서버를 컨테이너에 올리면서 문제가 발생했다. 컨테이너에서 크롬 브라우저가 돌아가게 하기 위해서는 GUI 없이 해당 webdriver를 띄워야했다. GUI를 있게 띄우려고 하니 webdriver 를 실행하는 부분에서 --user-data-dir이 중복돼서 webdriver를 띄울 수가 없다고 에러가 났다. 이걸 해결하려고 독립적인 uuid를 주고 tempfilefolder를 만들어서 --user-data-dir에 지정해주기도 하고눈물의 똥꼬쑈를 했지만, 저 에러는 고치지 못했다. 해당 에러 내용이 GUI를 있게 실행하는 것과 전혀 상관없어 보여서 더 헤맸던 것 같다. 그러나 GUI를 없게 설정하자 해당 에러는 해결 된 걸 보니, GUI를 띄우는 과정에서 뭔가 --user-data-dir이 연관돼있는 것 같다.

fastapi_app    | INFO:     172.18.0.1:55156 - "POST /api/scrape HTTP/1.1" 200 OK
celery_worker  | [2025-02-18 09:33:17,497: INFO/MainProcess] Task scrape_baekjoon[9fe74d04-56e3-4c96-80c9-13042f3cbecb] received
celery_worker  | [2025-02-18 09:33:17,498: INFO/ForkPoolWorker-8] 🚀 크롤링 시작: 문제 ID=1027, 언어 ID=1003
celery_worker  | [2025-02-18 09:33:17,624: ERROR/ForkPoolWorker-8] 🚨 크롤링 중 오류 발생: Message: session not created: 
probably user data directory is already in use, please specify a unique value for --user-data-dir argument, or don't use --user-data-dir

사실 webdriver를 GUI 있게 띄워봤자 컨테이너 내부에 띄워진 브라우저를 볼 수도 없기에 의미가 없다. 그런데 로컬에서는 GUI 있게 띄웠었는데 없이 띄우자 에러가 발생해,, 저기에 너무 집작했던 것 같다.

결과적으로 짜게 된 코드는 아래와 같다.

def get_driver():
    chrome_options = Options()
    chrome_options.add_argument("--headless")  # ✅ GUI 없이 실행
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-dev-shm-usage")
    
    # ✅ Chromedriver 경로 명확하게 지정
    chromedriver_path = "/usr/bin/chromedriver"
    service = Service(chromedriver_path)

    return webdriver.Chrome(service=service, options=chrome_options)

@celery_app.task(name="scrape")
def scrape_baekjoon(problem_id, language_id):
    """ ✅ 크롤링을 백그라운드에서 실행하는 Celery Task """
    driver = None  # ✅ 예외 발생 시에도 driver.quit()을 호출하기 위해 선언

    try:
        logger.info(f"🚀 크롤링 시작: 문제 ID={problem_id}, 언어 ID={language_id}")

        driver = get_driver()
        .
        .
        .

 

그런데 컨테이너에 요청을 보냈을 때 이번에는 전혀 로그인을 하지 못하는 일이 발생했다. 지속적으로 로그엔 로그인 실패만 뜨고 무슨 에러가 났는지도 표시를 안해주고 있었다. --user-data-dir 이라는 큰 산을 넘어오면서 에너지 소비를 많이 했는데 바로 다음 에러를 만나고, 바로 로그인 문제가 발생하니,, 너무 답답했다... 왜 항상 컨테이너에만 올리면 이렇게 안되는 것인가.. 어엉ㅇ어엉어어어엉

 

이 로그인 문제를 해결하기 위해 별 시도를 다해봤다.코드를 계속 바꿔보았는데 코드가 반영안됐나 해서 docker-comose up --build를 하루종일 쳤던 것 같다. 그러다가 아예 페이지에 접근을 하고 있는 건 맞나? 에 대한 의심이 들어서 

페이지 소스를 직접 로그에 출력하도록 해보았다. 그런데 유레카!!! 페이지 소스가 전혀 보이지 않고 403 forbidden 만 응답으로 받아오고 있었다. webdriver에서 요청을 보낼 때 사람이 아니라고 생각하여 아예 막고 있었던 것이다.

# User-Agent 설정 (일반적인 브라우저로 설정)
chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")

 

이 옵션을 추가하여 해결할 수 있었다.. 휴 

이제 webdriver 잘 띄웠고, 로그인 페이지 잘 로딩됐고, 내가 구현한 쿠키방식으로의 로그인만 잘 작동하면 됐었다. 그런데 아뿔싸 

또 문제가 발생했다. 

바로 "reCAPTCHA" 문제이다... 애초에 요청헤더나 바디에 담긴 것들이 사람이 컴퓨터에서 보낼 때와 selenium을 이용해서 요쳥을 보낼때 구성이 너무 달라서, 무조건 봇이라고 판단하는 것 같았다. 1번은 로그인을 수행하야 쿠키를 새로고침해가면서 로그인 세션을 유지할 수 있는데, 1번도 로그인을 수행을 못하니 이건 reCAPTCHA문제를 해결하지 못하면 영원히 크롤링 자동화는 실패로 돌아가는 것이었다.

 

3. 익스텐션을 활용한 해결

reCAPTCHA를 해결할 수 있는 방법을 찾아 열심히 돌아다닌 것 가 같아. 여기 저기 웹사이트도 가보고 하였는데 대부분 보이는건 reCAPTCHA를 돈받고 해결해주는 api를 제공하는 업체들 뿐이었다. 대부분의 사이트가 reCAPTCHA를 해결하는 것을 메인 솔루션으로 하고 있지도 않았고, 정말 작동하는 영상도 제공해준 게 없기에 선뜻 결제해서 내 프로젝트에 사용하긴 고민이 됐다.

그러다가 reCAPTCHA를 해결하는 익스텐션을 찾았다. 오 이 익스텐션을 내 chromedriver()에 설치하면 되겠군! 이라고 생각하고 찾아봤는데 직접 익스텐션의 실행파일을 폴더에 넣고 설치해야하는 방식으로만 익스텐션이 설치가능했는데, 내가 찾은 익스텐션은 웹스토어에 올라와 있는거라서 내가 직접 실행파일을 다운받을 순 없었다.

그래서 개발자의 놀이터,, Github를 뒤져보기 시작했다. ㄷㄱㄷㄱ 뭔가 같은 문제를 느끼고 무조건 해결한 repo가 있을거라고 생각했다. 여러 검색어를 통해 검색하다가 Bypass reCAPTCHA라고 치니 reCAPTCHA를 해결하는 여러가지 레포가 나왔다. 

대부분의 사람들의 솔루션은 GUI가 필요했다. 이미지를 인식하고 해당 이미지에서 지정된 그림을 찾아서 클릭하는 방식같았다. 그러나 나는  GUI 없이 돌아가고 었기에 요청만으로 해결할 수 있는 repo르 계속 찾았다.

그러다 발견한 repo!

https://github.com/FaustRen/bypass_recaptcha

이 사람은 신이다. 깃헙 readme를 읽어보고 당장 익스텐션 실행파일을 받아 내 프로젝트에 적용했다.

결과적으로 이런 모양이 됐다. 5-6줄에 보이는 crx 파일이 익스텐셔의 실행파일이다.

def get_driver():
    chrome_options = Options()
    chrome_options.add_argument("--headless")  # ✅ GUI 없이 실행
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-dev-shm-usage")
    chrome_options.add_extension("/app/crx/auto_recaptcha_solver.crx")
    chrome_options.add_extension("/app/crx/recaptcha_autoclick.crx")
    # User-Agent 설정 (일반적인 브라우저로 설정)
    chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")

    # ✅ Chromedriver 경로 명확하게 지정
    chromedriver_path = "/usr/bin/chromedriver"
    service = Service(chromedriver_path)

    return webdriver.Chrome(service=service, options=chrome_options)

@celery_app.task(name="scrape_baekjoon")
def scrape_baekjoon(problem_id, language_id):
    """ ✅ 백준 크롤링을 백그라운드에서 실행하는 Celery Task """
    driver = None  # ✅ 예외 발생 시에도 driver.quit()을 호출하기 위해 선언

    try:
        logger.info(f"🚀 크롤링 시작: 문제 ID={problem_id}, 언어 ID={language_id}")

        driver = get_driver()

기도하며 실행해보니!!

fastapi_app    | INFO:     172.18.0.1:63680 - "POST /api/scrape?problem_id=1027&language_id=1003 HTTP/1.1" 200 OK
celery_worker  | [2025-02-18 17:00:46,059: INFO/MainProcess] Task scrape_baekjoon[360a9611-59e9-41f7-93e9-69e6ab0c9075] received
celery_worker  | [2025-02-18 17:00:46,062: INFO/ForkPoolWorker-8] 🚀 크롤링 시작: 문제 ID=1027, 언어 ID=1003
celery_worker  | [2025-02-18 17:00:47,336: WARNING/ForkPoolWorker-8] 1
celery_worker  | [2025-02-18 17:00:47,757: WARNING/ForkPoolWorker-8] ✅ 해당 문제 푼적 있음
celery_worker  | [2025-02-18 17:00:47,765: ERROR/ForkPoolWorker-8] 쿠키 로드 실패1: Message: invalid cookie domain
celery_worker  |   (Session info: chrome=133.0.6943.98)

celery_worker  | [2025-02-18 17:00:49,708: WARNING/ForkPoolWorker-8] reCAPTCHA detected. Please solve it manually.
celery_worker  | [2025-02-18 17:01:47,621: WARNING/ForkPoolWorker-8] 로그인 성공
celery_worker  | [2025-02-18 17:01:47,621: WARNING/ForkPoolWorker-8] ✅ 새로 로그인 성공
celery_worker  | [2025-02-18 17:01:47,621: WARNING/ForkPoolWorker-8] ✅ 로그인: True

드디어 성공이다!! 
그러나 해보니 쿠키를 활용한 방식이 안먹히는 것 같다. 계속 로그인을 새로 수행하는 데 이 과정은 개선해나가야겠다.

 

 

 

+ 2025.2.19
쿠키를 통한 로그인 구현 
오류 로그를 보면 

쿠키 로드 실패1: Message: invalid cookie domain

이런 오류가 보인다.
1. 해당 사이트 접속 -> 쿠키로드
2. 쿠키 로드 -> 해당 사이트 접속 
이 두가지 중에 나는 2 번으로 지속적으로 하고 있어서 위와 같은 에러가 났다. 따라서 1번의 로직으로 바꿔주었다.

# 기존 코드
def login_using_cookies(driver):
    try:
        load_cookies(driver)  # 쿠키 불러오기
        driver.get("https://www.acmicpc.net/login")  # 페이지를 새로고침하여 로그인된 상태 확인
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "username"))
        )
        print("이미 로그인된 상태입니다.")
        return True
    except Exception as e:
        logging.error(f"쿠키 로드 실패1: {e}")
        return False
# 수정된 코드
def login_using_cookies(driver):
    try:
        driver.get("https://www.acmicpc.net")  # ✅ 먼저 로그인 페이지 방문
        load_cookies(driver)  # ✅ 쿠키 불러오기 (도메인 확인 후 추가)

        driver.get("https://www.acmicpc.net/login")  # ✅ 새로고침하여 쿠키 적용
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "username"))
        )
        print("이미 로그인된 상태입니다.")
        return True
    except Exception as e:
        logging.error(f"쿠키 로드 실패1: {e}")
        return False

 

또한 쿠키는 celery_worker 에서만 사용하지만 혹시 fastapi 에서도 사용할까봐서 둘의 볼륨을 공유할 수 있도록 설정을 변경하였다.

# 추가한 부분
 volumes:
  - ./cookie:/app/cookie
# 기존의 docker-compose.yml
services:
  redis:
    image: "redis:latest"
    container_name: redis_celery
    ports:
      - "6379:6379"
    networks:
      - celery_network

  fastapi_app:
    build: .
    container_name: fastapi_app
    ports:
      - "8000:8000"
    depends_on:
      - redis
    environment:
      - CELERY_BROKER_URL=redis://redis_celery:6379/0
    command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
    networks:
      - celery_network

  celery_worker:
    build: .
    container_name: celery_worker
    depends_on:
      - redis
    environment:
      - CELERY_BROKER_URL=redis://redis_celery:6379/0
    command: ["celery", "-A", "workers.scraping_tasks", "worker", "--loglevel=info"]
    networks:
      - celery_network

networks:
  celery_network:
# 수정된 docker-compose.yml
services:
  redis:
    image: "redis:latest"
    container_name: redis_celery
    ports:
      - "6379:6379"
    networks:
      - celery_network

  fastapi_app:
    build: .
    container_name: fastapi_app
    ports:
      - "8000:8000"
    volumes:
      - ./cookie:/app/cookie
    depends_on:
      - redis
    environment:
      - CELERY_BROKER_URL=redis://redis_celery:6379/0
    command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
    networks:
      - celery_network

  celery_worker:
    build: .
    container_name: celery_worker
    volumes:
      - ./cookie:/app/cookie
    depends_on:
      - redis
    environment:
      - CELERY_BROKER_URL=redis://redis_celery:6379/0
    command: ["celery", "-A", "workers.scraping_tasks", "worker", "--loglevel=info"]
    networks:
      - celery_network

networks:
  celery_network:

'AlgoMate' 카테고리의 다른 글

host.docker.internal은 왜 운영환경에서 사용하기 어려울까⁉️  (0) 2025.02.19
🛠️ 컨테이너 내부에서 로컬 서버로 API 요청 보내는 방법 🚀  (0) 2025.02.19
Redis만으로도 비동기 처리는 가능하잖아‼️ 근데 왜 Celery+Redis를 사용해야 할까  (0) 2025.02.18
🚀 크롤링 서버 vs 메인 서버, 크롤링한 데이터를 어디서 저장해야 할까?  (0) 2025.02.17
🔑 크롤링&스크래핑에서 쿠키를 사용하여 로그인 상태 유지하기  (0) 2025.02.17
'AlgoMate' 카테고리의 다른 글
  • host.docker.internal은 왜 운영환경에서 사용하기 어려울까⁉️
  • 🛠️ 컨테이너 내부에서 로컬 서버로 API 요청 보내는 방법 🚀
  • Redis만으로도 비동기 처리는 가능하잖아‼️ 근데 왜 Celery+Redis를 사용해야 할까
  • 🚀 크롤링 서버 vs 메인 서버, 크롤링한 데이터를 어디서 저장해야 할까?
SungHoJung
SungHoJung
  • SungHoJung
    HOLOUD
    SungHoJung
  • 전체
    오늘
    어제
    • 분류 전체보기 (42)
      • AlgoMate (13)
      • TroubleShooting (0)
      • 여러가지 모음집 (4)
      • Infra (18)
  • 링크

    • github
  • 인기 글

  • 태그

    recaptcha 우회
    스왑 메모리 설정
    ci-cd
    host.docker.internal
    AWS
    ECS
    docker-compose
    크롤링
    EC2
    redis
    Kubernetes
    bypass recaptcha
    k8s
    Celery
    celery+redis
    로컬 서버와 통신
    컨테이너 간 통신
    크롤링한 데이터
    메세지 브로커
    IAM
  • hELLO· Designed By정상우.v4.10.3
SungHoJung
Selenium을 통한 크롤링 시, 마주하는 reCAPTCHA 해결기
상단으로

티스토리툴바