selenium을 이용한 크롤링

2024년 4월 24일 수요일

Today I Learned

날짜

2024년 4월 24일 수요일

내용

동적 페이지 크롤링

결국 리뷰를 가져오기 위해선, SPU가 필요하다. 이 상품정보 페이지에서 SPU를 이용해 리뷰를 서버에서 받아오고 있으니, 분명 어딘가 SPU를 받은 요청이 있을텐데… 진짜 개발자도구에서 수백개를 뒤진 끝에 찾았다. 우선 Shein의 상품 ID에 관한 간략한 설명이다.

상품에는 여러가지 고유 ID가 존재하는데 shein에는 3가지가 대표적으로 있다.

  1. SPU : 상품의 고유 ID (ex. 아이폰 13 프로)
  2. SKC : 상품 + 색깔의 고유 ID (ex. 아이폰 13 프로 화이트골드)
  3. SKU : 상품 + 색깔 + 옵션의 고유 ID (ex. 아이폰 13 프로 화이트골드 1TB)

SPU를 변환해 여러 개의 SKC를 만들고, 여기서 또 변환해 SKU를 만든다. 정확한 변환 로직은 모르지만… 어제 말한 것처럼 하나 확실한건 여러 SKU 중의 하나는 반드시 SPU를 포함하고 있다. 다음은 내가 찾아간 과정이다.

1. SKU_CODE

상품 상세페이지가 열릴 때 우선 로그인한 회원에 관한 요청들이 오고간 후 페이지를 불러오는 요청을 들여다봤다. 수천 줄의 자바스크립트와 스타일이 포함된 코드가 불러와지는데 여기에 SKU_CODE가 포함되어 있었다. 확인해보니 해당 상품과 연관된 SKU 코드들이 맞았다. 만약 SPU를 찾을 수 없다면 이걸 탐색하여 SPU를 유추할 수 있겠다고 생각했다.

2. Free Trial Report SPU Code

열심히 찾아보니 SPU도 있었는데, 요청 자체가 Free Trial Report에 한정되어 있었다. 뭐 이따위로 만들어놨어.. 체험 리뷰가 없는 경우에는 이 SPU 요청이 없어서 딱히 나에겐 필요없었다.

3. goods_sn

내가 SKU에 집착했던 이유는, 여러 개의 SKU 중 하나는 반드시 SPU를 포함하고 있었기 떄문이다. 나는 이게 “기본 옵션”일 것이라고 생각했다. 옵션이 여러개더라도 분명히 디폴트 옵션은 있을게 분명했다. 상식적으로 상위 개념인 SPU를 포함하는 거라면 기본옵션의 SKU가 포함하고 있어야 하지 않겠는가? 내부에 goods_sn이라는 이름으로 SKU 코드 하나가 지정되어 있었는데 이 값이 기본 옵션이지 않을까 생각했다. 물론 아니었다.

4. main_attr

일단 생각의 깊이를 조금 얕게했다. 애초에 이 정보는 어떻게 불러오는가? 수천 줄의 코드를 불러오기 위해서 내가 필요한 데이터는 무엇인가? 애초에 리뷰 요청은 알고 있는데, 그때 필요한 payload가 없어서 이 고생을 하고있으니 이 방대한 정보를 위한 요청에는 무엇이 필요한지를 확인했다. URL에 있는 상품 ID였다. SKU,SPU,SKC가 아닌 goods_id 라는 또 다른 고윳값이다. 이 과정에서 발견한 건 다른 옵션들, 즉 SKU가 다른 녀석들은 이 goods_id도 다르다는 것이다. 상품 상세정보 페이지의 URL 형식은

https:// + {shein 도메인} + {상품 제목} + {goods_id} + { 기타 parameter } 이다. 같은 상품의 다른 옵션을 볼때 goods_id가 다르기 떄문에 URL도 다르다. parameter가 다른것과는 아예 다른 개념이라는 의미다. 그렇다면 분명, 맨 처음 진입할 때 하나의 ID를 입력하지만 다른 옵션의 goods_id도 불러올 것이라고 생각했다.

이 생각은 정확했고, 데이터 내부에 모든 옵션의 goods_id와 SKU가 담겨있었다. 최악의 경우로 SPU를 찾지 못할경우, 모든 SKU를 파싱해서 리뷰 처리 URL에 담아 보내보고 리뷰가 반환되는지 여부를 확인해볼 수 있다는 의미다. 물론 옵션이 100개면 100번 해봐야하는 비효율은 감당해야 겠지만… 각 옵션들의 정보들이 이 main_attr 내에 담겨있었다.

5. goods_relation_id

다른 옵션들은 각자의 goods_id, SKU, SKC를 가지지만 SPU만은 동일하다. 그럼 이 값이 정말 없을까? SPU는 없었다. 하지만 이름은 다른데 형식이 비슷한 녀석을 찾았다. “goods_relation_id” 였다. 혹시나 싶어 리뷰요청 형식에 payload로 넣어봤더니 리뷰가 불러와진다! 드디어 실마리를 찾은 듯했다. 다른 상품으로 테스트 해보는 과정에서, 어쩔때는 이 goods_relation_id가 없다는 사실을 알아냈다. 도대체 뭔데… 애초에 SPU와 다른 이름으로 존재하는걸 발견했으니, 또 goods_relation_id와 다른 이름으로도 존재할 수 있을거라 생각했다.

6. productRelationID

이 이름으로 똑같은 값이 들어오는 걸 확인했다. 케이스도 구분했는데, goods_relation_id는 상품에 색깔 옵션이 있을때만 있는 값이었다. 반면 productRelationID는 어떤 이유던 항상 들어오는 값이었다. 결국 SPU를 찾아냈다. 이름이 SPU가 아니라곤 말안했잖아 Shein아.. 이제 다 끝난 줄 알았다.

크롤링

이 productRelationID는 요청에 대한 응답에 들어오는 값이 아니었다. 페이지가 렌더링 되는 과정에서 내부에 있는 자바스크립트 코드가 가져오는 데이터였다. 따라서 크롤링을 이용해 이 스크립트를 읽어야 했다. 아마존에서도 그러고 있기 때문에 beatuifulsoup로 HTML을 텍스트로 바꾸고 productRelationID를 찾았다. 나타나지 않았다. 또 왜냐…

자바스크립트 코드여서 그랬다. 상품 상세정보 페이지는 기본적인 틀을 가져오고, 내부에 들어갈 데이터들(상품 이름부터 관련상품, 리뷰, 모든것이 다) 자바스크립트 코드로 불러와지는, 동적인 페이지였다. beautifulsoup는 이러한 형식을 처리하지 못했다. 따라서 데이터는 부르는 자바스크립트만 인식하지, 그 코드가 실행된 결과를 가져오지 못했다. 따라서 selenium이라고 하는 녀석을 이용했다. 이녀석은 Headless Browser로, 웹 페이지를 브라우저처럼 렌더링하여 시렞 사용자가 보는 것과 동일한 내용을 캡처한다.

따라서 진짜 브라우저를 줘야한다. 크롬을.. 방법이야 구글링을 하면 많이 나와있었다. 그대로 따라했는데 오류가 발생했다. 이유는 구체적인 크롬 버전을 지정해주지 않아서 그렇다. 아무 값이나 넣기엔, 크롬은 업데이트가 잦아 나중에 이 부분을 주기적으로 수정해야 하는 번거로움이 생길 수 있었다. 따라서 chromedriver_autoinstaller를 사용헀다. 크롬드라이버를 자동적으로 최신으로 적용해주는 기특한 녀석이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import chromedriver_autoinstaller

@router.get("/importer/shein")
async def importer_shein(page_url: str):
    async with httpx.AsyncClient(follow_redirects=True) as client:
        chromedriver_autoinstaller.install()
        options = webdriver.ChromeOptions()
        options.add_argument('--headless')  # 헤드리스 모드
        options.add_argument('--no-sandbox')
        driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
        try:
            # 웹 페이지 로드
            driver.get(page_url)
            # 페이지 로드를 기다림
            driver.implicitly_wait(10)
            # 페이지 소스 가져오기
            page_source = driver.page_source

            # BeautifulSoup을 사용하여 HTML 파싱
            soup = BeautifulSoup(page_source, 'html.parser')

            # 스크립트 태그 찾기
            script_tag = None
            for script in soup.find_all('script'):
                if 'window.gbRawData' in script.text:
                    script_tag = script.text
                    break
            
            # 스크립트 내에서 productRelationID 찾기
            if script_tag:
                match = re.search(r'"productRelationID":"(\w+)"', script_tag)
                if match:
                    product_relation_id = match.group(1)
                    start_index = script_tag.find(product_relation_id)
                    extract_text = script_tag[start_index:start_index+12]
                    review_url = f"https://asia.shein.com/api/comment/abcCommentInfo/query?_ver=1.1.8&_lang=ko&spu={extract_text}&goods_id=&page=2&limit=10&offset=3&sort=&size=&is_picture=&rule_id=recsrch_sort:A&tag_id=&local_site_abt_flag=1&shop_id=&query_rank=0&same_query_flag=1&not_need_img=1"
                    return {"extracted_text": extract_text}
                else:
                    return {"error": "Product Relation ID not found"}
            else:
                return {"error": "window.gbRawData script not found"}
        finally:
            driver.quit()  # 드라이버 종료

코드를 짰다. 페이지가 모두 렌더링되도록 10초를 기다린 후, 가져와서 Beautifulsoup을 이용해 HTML로 파싱한다. 스크립트 중에 내가 원하는 스크립트를 찾아 SPU가 있는 부분을 파싱한다. 추가로 오류가 발생했는데, 실행 할 때 크롬이 없다고 뜬다. 생각해보니 이 서버는 도커에서 돌아가니 도커 내에 크롬이 필요하다. 따라서 도커파일에 빌드할 때 크롬을 설치하도록 코드를 추가했다.

1
2
3
4
5
RUN apt-get update && apt-get install -y wget gnupg2 \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable

음.. 잘되긴 하는데 10번에 1번 정도 꼴로 오류가 발생한다.. 상당히 불안하다. 이렇게 하는게 맞을까?

회고

분명 1 영업일 안에 처리해준다해놓고 안해준다. 메일까지 보냈는데 왜 개발자 승인안해주냐 Shein.