Amazon Japan에서 Vinyl 상품정보를 Python으로 크롤링해서 DB에 넣어볼까?

d3fau1t·2021년 10월 4일
0

뻘짓

목록 보기
1/9

평소 Vinyl(LP)를 수집하는 취미가 있는데
중고로 구매하는 경우도 있지만 보통은 Amazon에서 둘러보다가 마음에 드는 것이 있으면 구매하는편이다.
매번 찾아보기 귀찮기도 해서 하루에 한번씩 확인하여, 기존에 없던 새로운 Vinyl이 올라오면 DB에 기록하고 나에게 문자(SMS)를 보내는 방식으로 알림을 날려보기로 했다.

크롤링하기

데이터 범위 정하기

일단.. Vinyl 항목만 긁어오려고한다.
모든 페이지를 다 긁어오기엔 너무 부담스럽고 쓸모없는 데이터가 많아지기 때문에 상품 페이지에서featured 항목으로 정렬하여 첫 페이지만 보기로 하였다.
잘 팔리거나 인기가 많다는건 트렌디하다는 것을 증명해주기 때문에 나름대로 필터를 걸어놓고 첫 페이지만 확인하면서 변동되는 정보만 기록하는 것이 합리적이라고 생각하였다.


어.. 아마존 페이지 개발자도 사람인가보다. 고양이는 귀엽지.

상품 페이지 주소
https://www.amazon.co.jp/s?i=popular&bbn=561956&rh=n:561956,p_n_format_browse-bin:81877051&s=featured-rank&dc&language=en&qid=1633267955&rnid=81872051&ref=sr_st_featured-rank

페이지에 있는 제품 이미지, 제품명, 아티스트, 평점, 가격 정도 긁어오면 되지 않을까?


위와 같이 css selector를 복사해두자

#search > div.s-desktop-width-max.s-desktop-content.s-opposite-dir.sg-row > div.s-matching-dir.sg-col-16-of-20.sg-col.sg-col-8-of-12.sg-col-12-of-16 > div > span:nth-child(4) > div.s-main-slot.s-result-list.s-search-results.sg-row > div

코드 작성

이슈 1: 선택 영역에 포함된 불필요한 정보 거르기

긁다보니 약간의 이슈가 있었다.

방금전에 css selector를 복사하여 해당 영역을 기준으로 안에있는 div를 긁어오면 상품정보만 긁어올 수 있겠다 싶었는데 하단의 페이지 선택영역이 포함되어있었다..
그런이유로 저 영역만 제외하고 데이터를 받아오기위해 약간 짱구를 굴리기 시작했는데
예상외로 간단했다.


모두가 가졌지만 페이지 선택영역은 가질 수 없는 것이 있을까 생각해보니 가격 정보가 제일 무난할 것 같다고 생각했다.
가격정보가 없는 영역은 무시하고 수집하기로 했다.

이슈 2: 하이퍼링크 유무에 따른 접근경로가 다른 경우

하이퍼링크가 없을 경우의 css selector

하이퍼링크가 있는 경우의 css selector

위와 같이 하이퍼링크 적용상황에 따라 예외적인 경우가 생겨버리니
예외처리 하기로 했다.

작성된 크롤러

import requests
from bs4 import BeautifulSoup

amazon_jp_vinyl_url = 'https://www.amazon.co.jp/s?i=popular&bbn=561956&rh=n:561956,p_n_format_browse-bin:81877051&s=featured-rank&dc&language=en&qid=1633267955&rnid=81872051&ref=sr_st_featured-rank'
headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get(amazon_jp_vinyl_url, headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')
vinyls = soup.select('#search > div.s-desktop-width-max.s-desktop-content.s-opposite-dir.sg-row > div.s-matching-dir.sg-col-16-of-20.sg-col.sg-col-8-of-12.sg-col-12-of-16 > div > span:nth-child(4) > div.s-main-slot.s-result-list.s-search-results.sg-row > div')

for vinyl in vinyls:
    try:
        vinyl_info = {
            'title': vinyl.select_one('span.a-text-normal').text,
            'price': vinyl.select_one('span.a-price-whole').text,
            'cover': vinyl.select_one('img.s-image').get('src'),
            'url': f"https://amazon.co.jp{vinyl.select_one('a').get('href')}",
            'artist': vinyl.select_one('a.a-size-base.a-link-normal').text 
                if vinyl.select_one('a.a-size-base.a-link-normal').text != 'Vinyl'
                else vinyl.select_one('div.a-row.a-size-base.a-color-secondary').text.replace('by ', '')
        }
    except Exception as e:
        # 불필요한 div를 거르기위한 트릭
        pass

실행 결과


대략 이런식으로 긁어왔다는 것을 보여주기 위해 url정보는 제외하고 이미지를 첨부하였다.
이정도면 잘 작동하는듯 하니 DB에 넣어주자.

수집한 데이터 DB에 보관하기

수집하고 그대로 날려버리면 자동알람을 보내려는 계획에 차질이 생긴다.
확인한 내용들은 DB에 저장되어있다는 가정하에 새로 기록되는 경우 기존에 기록된 내용들과 비교하여 알람을 보낼것이다.

MongoDB 써볼까

그러기 위해 데이터를 저장하려고하는데.. 기록된 데이터들끼리 서로 관계가 정의되어야 하는 것도 아니고.. 가볍게 사용할 목적으로 NoSQL 기반의 MongoDB를 사용하려고 한다.

나중에 별도의 서비스나 웹 페이지로 확장할 생각을 해보니 클라우드로 올리는게 좋을 것 같다고 판단하였고.. (사실 로컬에 설치하는게 싫어서..)
MongoDB Cloud를 사용하기로했다.


MongoDB Cloud를 사용하는법을 다루는 게시물이 아니기 때문에 자세한 설명은 생략한다.
가입하고 DB 생성하고 연결해서 데이터를 넣어보려고한다.

코드 작성

한 건씩 기록

from urllib.parse import quote_plus
from pymongo import MongoClient

password = '패스워드를 입력해주세요'
quote_password = quote_plus(password)
client = MongoClient(f'mongodb+srv://유저이름:{quote_password}@주소/DB이름?retryWrites=true&w=majority', 27017)
db = client.amazon_japan_lp

# 크롤링 하는 부분에서 한 건씩 DB에 기록
for vinyl in vinyls:
	...
    vinyl_info = { ... }
    ret = db.featured.update_many({'title': {'$eq': vinyl_info.get('title')}}, {"$set": vinyl_info}, upsert=True)

이후 DB에 기록할 때 중복인지 아닌지 한 건씩 확인하여 메시지를 발송할 생각에 위와 같이 DB에 기록하였다.

update_many 함수에 upsert 파라미터를 넘겨서 없으면 insert하고 있으면 update 하기 때문에, 없던 데이터가 들어온다면 뭔가 return 값이 다를 것이다.

한번에 기록

만약 너무 많은 요청이 거슬린다면 한번에 다 넣는것도 방법이다.

document = list()
for vinyl in vinyls:
	vinyl_info = { ... }
    document.append(vinyl_info)
db.featured.insert_many(document)

update_many함수의 반환값 뜯어보기

아래의 코드를 실행하여 데이터를 넣어보려고한다.

ret = db.featured.update_many({'title': {'$eq': vinyl_info.get('title')}}, {"$set": vinyl_info}, upsert=True)
print(ret.modified_count, ret.upserted_id, ret.matched_count)

넣으려는 데이터가 이미 기록된 데이터와 동일한 경우의 반환값

1 None 1

넣으려는 데이터가 기록되어있지 않을 경우의 반환값

0 6159ed58f58aed49123ff7b1 0

예상보다 쉽게 풀릴것같다.
어떤 값으로 비교해도 새로 들어왔는지 쉽게 알 수 있으니 해당 값을 참조하여 알림을 보낼 수 있어보인다.

실행 결과


잘 들어갔다.

없는 데이터가 들어오면 알림 보내기

IFTTT(IF This Then That) 써보자
이건 예전에 써봐서 익숙하기도 해서.. 써보기로했다.
이 글에서는 IFTTT의 자세한 사용법에대해 자세히 다루진 않을 것이다.

IFTTT Applet 생성



위와 같이 설정하였다.

WebHook URL에 전송할 JSON 데이터를 포함한 요청이 들어오면 ClickSend를 통해 SMS를 전달해주도록 작성하였다.

WebHook 사용법

지금까지 만들어놓은 내용을 보면
URL에 POST로 뭔가 찔러주면 SMS로 전송되는 구조로 보인다.

코드 작성

사용법을 참조하여 코드를 작성하였다.

ifttt_request = {
    'new_arrival_url': 'https://maker.ifttt.com/trigger/new_arrived_vinyl/json/with/key/WebHook_키값을_입력해주세요',
    'headers': {
        'Content-Type': 'application/json'
    }
}

for vinyl in vinyls:
	...
    vinyl_info = { ... }
    ret = db.featured.update_many({'title': {'$eq': vinyl_info.get('title')}}, {"$set": vinyl_info}, upsert=True)
    if ret.upserted_id:
    	# DB에 없는 상품정보가 들어왔을 경우 WebHook URL로 상품정보 전송
    	requests.post(
                ifttt_request.get('new_arrival_url'),
                {
                    "value1": f"{vinyl_info.get('artist', '?')} - {vinyl_info.get('title', '')[:16]}... 이 입고되었어요",
                    "value2": {vinyl_info.get('price')},
                    "value3": vinyl_info.get('url'),
                },
                ifttt_request.get('headers')
	)

실행결과

잘 작동한다.
URL에 상품정보를 담아서 전송하였고 SendSMS를 통해 문자가 발송되었다.
받은 문자에서 아티스트, 상품 이름, 가격, 구매링크를 확인할 수 있고 접속하여 상품을 확인할 수 있다.

작업하면서 SendSMS의 응답속도가 가끔 느릴 때가 있어서 답답했다.
(해외 서비스라서 그런듯..)
나중에는 IFTTT WebHook이 아니더라도 SMS API를 결제하거나 카카오톡연동을 해보는것도 괜찮을 듯 하다.

이제 1일 기준으로 매일 특정시간에 자동실행시켜주는 무언가 있으면 좋을 것 같다.

매일 특정시간에 작업을 수행시켜보자

특정 시간에 작업을 수행시키려고 찾아보니 Cron 이라는 키워드가 유난히 많이보인다.

클라우드 서비스를 사용하게된다면
AWS Lambda + EventBridge 조합으로 작업할 수도 있고
단순히 Github Action을 작성하는 것으로 끝낼 수도 있다.

로컬에서 작업할 예정이라면 Cron이나 시간정보 기반 이벤트를 걸어두어도 되지만 컴퓨터를 항상 켜놔야한다는 단점이 있다.

AWS도 괜찮지만 여기저기 분산시켜놓기도 뭐해서
GitHub Action을 Cron runner로 두면 어떨까 싶다.

Github Secret 추가


배포할 코드에 패스워드나 인증값이 그대로 노출되는건 좋지않아보인다.
그런이유로 필요한 정보는 환경변수로 지정해놓고 가져다 쓰기로 하였다.
어차피 Action 돌릴 때 환경변수 받아오려면 써야한다.

GitHub Action 작성


프로젝트 경로에 .github/workflows/what-you-want.yml 파일 생성해주고 배포하면 된다.

update-vinyl.yml 작성

# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Update vinyl info

on:
  schedule:
    - cron: '30 6 * * *' # every day at 15:30 (KST)
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.8']

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
        architecture: x64
    - name: Install Python dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Crawl and alert via SMS
      env:
        VINYL_IFTTT_WEBHOOK_KEY: ${{ secrets.VINYL_IFTTT_WEBHOOK_KEY }}
        VINYL_MONGO_COLLECTION: ${{ secrets.VINYL_MONGO_COLLECTION }}
        VINYL_MONGO_HOST: ${{ secrets.VINYL_MONGO_HOST }}
        VINYL_MONGO_PASSWORD: ${{ secrets.VINYL_MONGO_PASSWORD }}
        VINYL_MONGO_PORT: ${{ secrets.VINYL_MONGO_PORT }}
        VINYL_MONGO_USER: ${{ secrets.VINYL_MONGO_USER }}
      run: |
        python "run.py"
        

작성중 필요했던 Cron식은 Crontab.guru에서 이리저리 굴려보고 검증해볼 수 있다.

배포 완료

일하다가 어느정도 텐션이 떨어져서 루즈해지는 때가 언제일까 생각해보니
15시 30분 쯤이었다.

그래서 내일 15시 30분에 Action이 작동했는지 확인하고 문제가 발생한다면 수정할 생각이다.

작동 확인

잘된다. 문자도 잘온다.

결론

아마존(Japan)의 Vinyl 스토어의 제품목록을
매일 특정시간에 불러와서 DB에 저장하고
만약 DB에 없던 제품이 추가된다면 문자로 알림을 보내주는 프로젝트를 해봤다.

이게 잘 작동해주면
좀 더 풍성한 덕질이 가능하지 않을까? ㅎㅎ

후일담..


작업하다가 홀린듯 구매한 드럼스틱과 Vinyl 2장은 배송중이다.

profile
웹 백엔드 합니다.

0개의 댓글