AWS Lambda에서 selenium 크롤링

유승우·2024년 6월 5일
1

맹그로브 남는 방을 주기적으로 체크하기 위해 12시간마다 페이지를 크롤링해서 알림이 오도록 하는 작업을 했다.

컴퓨터를 24시간 켜둘 수는 없기 때문에 AWS Lambda를 이용했다.

우선 https://sales.mangrove.city/sinseol/calendar 사이트가 노션으로 만들어져서 동적으로 작동하기 때문에 bs4로는 충분하지 않고 selenium을 이용해야 했다.

문제는 AWS Lambda 서버에는 셀레니움, 크롬이 모두 설치되어 있지 않기 때문에 넣어주어야 했다.

첫번째 삽질

우선 셀레니움 작동을 위해서는 selenium, chrome, chromedriver가 필요하다. AWS Lambda에서는 layer에 필요한 라이브러리 등을 넣어두면 그 layer를 활용해 라이브러리 등을 사용할 수 있다.

Chrome, Chrome driver

서버가 gui 환경이 아니기 때문에 headless-chrome이 필요하다. 구글링하면 headless-chrome을 https://github.com/adieuadieu/serverless-chrome에서 다운받으라고 많이 나오지만, chrome 112버전부터는 headless-chrome이 따로 나오지 않고 그냥 크롬에 headless 모드를 사용할 수 있다고 한다. (참고: https://developer.chrome.com/docs/chromium/new-headless?hl=ko)

그런데 기존의 헤드리스에 대한 수요가 많았는지 chrome-headless-shell 을 출시했다. (참고: https://developer.chrome.com/blog/chrome-headless-shell?hl=ko)

아래 URL에서 적당한 버전의 chromedriverchrome-headless-shell을 다운 받으면 된다.

Chrome for Testing availability

Untitled

두 파일을 압축하고 계층 생성을 눌러 압축파일을 올리면 된다. (그런데 용량이 커서 amazon s3를 이용했다.)

python package layer

mkdir python
pip install selenium-t .\python\

위 명령어로 만들어진 python 디렉토리를 압축하여 layer에 올리면 된다. 중요한 것은 가장 상위 디렉토리 이름이 반드시 python이어야 한다는 것이다.

마찬가지로 압축해서 계층을 생성해주면 된다.

문제점

ldd /opt/chromedriver-linux64/chromedriver

위 명령어로 chromedriver 실행에 필요한 라이브러리를 검색해보면 아래와 같이 몇 가지가 없는 것을 볼 수 있다.

linux-vdso.so.1 (0x00007ffed1bc4000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fb04dfa9000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb04dd8b000)
libglib-2.0.so.0 => not found
libnss3.so => not found
libnssutil3.so => /lib64/libnssutil3.so (0x00007fb04db5b000)
libnspr4.so => /lib64/libnspr4.so (0x00007fb04d91e000)
libm.so.6 => /lib64/libm.so.6 (0x00007fb04d5de000)
libxcb.so.1 => not found
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fb04d3c8000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb04d01b000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb04f177000)
libplc4.so => /lib64/libplc4.so (0x00007fb04ce16000)
libplds4.so => /lib64/libplds4.so (0x00007fb04cc12000)
librt.so.1 => /lib64/librt.so.1 (0x00007fb04ca0a000)

문제는 이 라이브러리를 설치할 방법이 없다는 것이다. 그래서 그냥 도커를 쓰기로 마음먹었다.

Docker 이미지

기본적으로 아래 링크를 참고했다.

selenium into aws lambda

도커 이미지는 https://github.com/uiandwe/lambda-selenium-docker를 이용했다.

chrome-deps.txt 파일을 보면 chrome 구동에 필요한 라이브러리들이 적혀있고, Dockerfile에서 RUN yum install -y $(cat /tmp/chrome-deps.txt) 과 같이 설치를 해준다. 이로써 첫번째 삽질 때의 문제는 해결되었다.

그리고 텔레그램 봇 파이썬 패키지도 필요하므로 requirement.txt에 추가해줬다.

ECR에 Docker 이미지 업로드

AWS Lambda에서 Docker 이미지를 이용하기 위해서는 Amazon Elastic Container Registry에 도커 이미지를 업로드하여야 한다. 우선 ECR을 이용하기 위해서는 IAM 사용자를 생성해야 한다.

IAM

Untitled

IAM에 들어가서 사용자를 생성하고 권한 정책으로 AdministratorAccess을 주면 된다. 그냥 귀찮아서 전체 권한을 준 것이지만 실전에서는 필요한 권한만 줘야겠지?

Untitled

그리고 엑세스키를 생성한다. 사용 사례로는 CLI를 체크해주면 된다.

Untitled

이 엑세스 키는 csv로 다운받거나 해서 잘 보관해 둬야 한다. (private 키 다시 못 봄)

그리고 도커파일을 다운 받아뒀던 디렉토리에 들어가서 aws를 설정해준다. (aws 없으면 apt-get으로 설치)

sudo aws configure
>AWS Access Key ID [None]: [방금 받은 Key ID]
>AWS Secret Access Key [None]: [방금 받은 Access Key]
>Default region name [None]: [내 region name ex)eu-north-1]
>Default output format [None]: [걍 None으로 내비둠]

이제 다시 ECR에 도커 이미지를 업로드해보자. 우선 리포지토리를 생성한다.

Untitled

이제 진짜 도커 이미지를 빌드하고 푸시해보자. 아래 명령어는 레포지토리에 들어가면 볼 수 있다. 여기서 레포지토리 이름은 selenium이다.

aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin 590184099944.dkr.ecr.eu-north-1.amazonaws.com
docker build -t selenium .
docker tag selenium:latest 590184099944.dkr.ecr.eu-north-1.amazonaws.com/selenium:latest
docker push 590184099944.dkr.ecr.eu-north-1.amazonaws.com/selenium:latest

여기서 권한 문제로 sudo 를 넣어준다면 docker login 앞에도 꼭 넣어주도록 하자. (이것 때문에 하루 생고생함)

Lambda 함수 생성

이제 lambda 함수를 생성하자. 도커 이미지를 활용하면 아래와 같이 함수를 생성할 수 있다.

Untitled

텔레그램 봇

크롤링 결과를 톡으로 받아보기 위해 텔레그램 봇을 이용했다.

Untitled

이 친구만 있으면 어렵지 않게 봇을 만들 수 있다.

톡방에 들어가서 /start 을 누르고 /newbot 을 입력하면 봇의 이름을 정해주면 HTTP API를 얻을 수 있다.

Untitled

그리고 chat_id가 필요한데 이건 봇한테 한 마디 남긴 뒤 https://api.telegram.org/bot봇Token값/getUpdates에 접속하면 얻을 수 있다.

Untitled

이제 필요한 정보를 얻었으니 아래와 같이 봇이 메세지를 보내게 할 수 있다. (print_template 은 그냥 출력 형식 적용한 스트링을 반환한다. MarkdownV2을 이용했다. https://core.telegram.org/bots/api#markdownv2-style)

import telegram, asyncio
from datetime import datetime

async def send_telegram(contents):
    token = "토큰 값"#os.environ.get('TELEGRAM_API_KEY')
    chat_id = "chat id 값"#os.environ.get('TELECRAM_CHAT_ID')
    bot = telegram.Bot(token = token)
    await bot.send_message(chat_id, print_template(contents), parse_mode='MarkdownV2')
    
asyncio.run(send_telegram(table_contents))

트리거

매일 오전 8시 반과 오후 8시 반에 알림이 오도록 트리거를 만들었다.

lambda 함수 페이지에서 아래와 같이 트리거를 생성하면 된다.

Untitled

주의할 것은 UTC 시간 기준이므로 9시간 시차를 고려해서 cron을 작성해야 한다.

결론

최종적인 Dokerfilemain.py는 아래와 같다.

Dockerfile:

FROM public.ecr.aws/lambda/python:3.9 as stage

RUN yum install -y -q sudo unzip
ENV CHROMIUM_VERSION=1002910

# Install Chromium
COPY install-browser.sh /tmp/
RUN /usr/bin/bash /tmp/install-browser.sh

FROM public.ecr.aws/lambda/python:3.9 as base

COPY chrome-deps.txt /tmp/
RUN yum install -y $(cat /tmp/chrome-deps.txt)

# Install Python dependencies for function
COPY requirements.txt /tmp/
RUN python3 -m pip install --upgrade pip -q
RUN python3 -m pip install -r /tmp/requirements.txt -q

COPY --from=stage /opt/chrome /opt/chrome
COPY --from=stage /opt/chromedriver /opt/chromedriver

# copy main.py
COPY main.py /var/task/

ENV TELEGRAM_API_KEY='어쩌구저쩌구' \
    TELECRAM_CHAT_ID=123456789

WORKDIR /var/task

CMD [ "main.handler" ]

main.py:

import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
import telegram, asyncio
from datetime import datetime
from pytz import timezone

def handler(event=None, context=None):
    print('시작합니다.')
    table_contents = get_available_rooms()
    asyncio.run(send_telegram(table_contents))
    return {
        "statusCode": 200,
        "room_num": len(table_contents['room_type'])-3
    }

async def send_telegram(contents):
    token = os.environ.get('TELEGRAM_API_KEY')
    chat_id = os.environ.get('TELECRAM_CHAT_ID')
    bot = telegram.Bot(token = token)
    await bot.send_message(chat_id, print_template(contents), parse_mode='MarkdownV2')

def get_available_rooms():
    chrome_options = webdriver.ChromeOptions()
    chrome_options.binary_location = "/opt/chrome/chrome"
    chrome_options.add_argument("--headless")
    chrome_options.add_argument('--no-sandbox')
    chrome_options.add_argument("--single-process")
    chrome_options.add_argument("--disable-dev-shm-usage")
    chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko")
    chrome_options.add_argument('window-size=1392x1150')
    chrome_options.add_argument("disable-gpu")
    service = Service(executable_path="/opt/chromedriver")
    driver = webdriver.Chrome(service=service, options=chrome_options)

    url = 'https://sales.mangrove.city/sinseol/calendar'
    driver.get(url)
    button = driver.find_element(By.XPATH, '//*[@id="__next"]/div/div/div/div/div/div[2]/div[3]/div[8]/div/div[2]/div[3]/div/div[1]')

    button.click()
    # time.sleep(0.5)
    table = driver.find_element(By.XPATH, '//*[@id="__next"]/div/div/div/div/div/div[2]/div[3]/div[8]/div/div[2]/div[3]/div/div[2]/div[2]/div/div/div/div/div/div/div/div[3]')
    table_lines = table.find_elements(By.XPATH, './*')
    table_contents = {'room_type': [], 'checkin_date': []}
    for tc in table_lines[:-3]:
        table_contents['room_type'].append(tc.find_element(By.XPATH, './div[1]/a[1]/span').text)
        table_contents['checkin_date'].append(tc.find_element(By.XPATH, './div[2]/div').text)
    driver.close()

    return table_contents

def print_template(contents):
    template = f"*\[{datetime.now(timezone('Asia/Seoul')).strftime('%m월 %d일 %H시 %M분')}\]*\n맹그로브 신설 객실 정보\n"
    template += f"남은 방: ||*{len(contents['room_type'])}*||"
    for room_type, checkin_date in zip(contents['room_type'], contents['checkin_date']):
        template += f'\n>*{room_type}*:\t{checkin_date}'
    template += '||'
    return template
    

if __name__ == '__main__':
    handler()

실행화면

테스트를 밤에 해서 그렇지 크론으로 지정해둔 시간에 잘 온다.

테스트를 밤에 해서 그렇지 크론으로 지정해둔 시간에 잘 온다.

아쉬운 점은 텔레그램 봇에 메세지를 보내면 크롤링이 작동해서 알림이 오도록 하고 싶었는데 그건 서버를 계속 켜둬야 해서 (봇 메세지 수신 서버든 크롤링 서버든) 실행하지 못했다.

profile
ㅎㅇㄹ

2개의 댓글

comment-user-thumbnail
2024년 10월 6일

제가 찾고있던 내용인데 이미지가 다 깨져나오네요. ㅜㅡㅜ

1개의 답글