본문 바로가기
Scraping

[파이썬으로 웹스크래핑] 스크래핑용 라이브러리로 다시 떡상하는 requests...

by 돈민찌 2021. 9. 15.
반응형

오늘은 스파르타 코딩클럽에서 웹개발종합반 심화반 수업 중에 크롤링 얘기가 나와서 좀 마침 쓰려고 하던 글과 어느 정도 맞아떨어지는 것이 있어서 올려본다ㅎㅎ 역쉬 스크래핑 짜릿해...

강의 내용에서 타깃으로 정한 것은 SBS TV맛집 이라는 사이트였다. 이 사이트는 SBS에서 만든 듯 하지만 TvN이나 olive 채널의 맛집 프로도 함께 소개되어 있어 소스로 아주 좋은 것 같았다. (식신원정대랑 맛있는 녀석들도 올려줘요...) 조회 순으로 훑어보니 아무래도 사람들이 수요미식회에 관심이 많은지 조회수가 비교적 높았다. 

 

SBS TV맛집

 

matstar.sbs.co.kr

수업에서는 기초반에서 배운 리퀘스츠+뷰티풀숲의 조합으로는 어떤 한계가 있는지, 멜론 같은 동적 웹페이지에서 텅빈 껍데기를 불러오는 모습으로 그 의미를 증명한 후에, 연습삼아 멜론차트를, 그리고 본격적으로 이 사이트에 접속해 스크래핑을 시도했다.

from selenium import webdriver
from bs4 import BeautifulSoup
import time
from selenium.common.exceptions import NoSuchElementException
from pymongo import MongoClient
import requests


client = MongoClient('내AWS아이피', 27017, username="아이디", password="비밀번호")
db = client.dbsparta_plus_week3

driver = webdriver.Chrome('./chromedriver')

url = "http://matstar.sbs.co.kr/location.html"

driver.get(url)
time.sleep(5)

req = driver.page_source
driver.quit()

soup = BeautifulSoup(req, 'html.parser')

# 이렇게 몽고디비와 뷰티풀숲, 셀레니움 크롬 드라이버를 각각 준비하고,
# 적절한 선택자를 탐색해 리스트에 접근한 다음
places = soup.select("ul.restaurant_list > div > div > li > div > a")

# 리스트를 돌면서 각 제목과 주소, 카테고리, 프로그램 제목 등을 스크랩한다.
for place in places:
    title = place.select_one("strong.box_module_title").text
    address = place.select_one("div.box_module_cont > div > div > div.mil_inner_spot > span.il_text").text
    category = place.select_one("div.box_module_cont > div > div > div.mil_inner_kind > span.il_text").text
    show, episode = place.select_one("div.box_module_cont > div > div > div.mil_inner_tv > span.il_text").text.rsplit(" ", 1)
    print(title, address, category, show, episode)
    
    # 여기서 모든 데이터를 몽고디비의 콜렉션에 쌓으려면 이렇게 해준다.
    db.restaurant.insert_one({
    'title':title, 
    'address':address, 
    'category':category, 
    'show':show, 
    'episode': episode
    })

이 과정에서 해당 페이지는 한번에 12개 정도의 게시물만 보여주고 아래로 더 보기 버튼을 눌러야 리스트를 더 불러오는 특성이 있었고, 방송 타이틀 별로 리스트를 필터링할 수는 있었지만 그것을 자동화하기는 어려운 점이 있었다. 그래서 이후의 수업은 셀레니움으로 마우스, 키보드를 제어하고 특정 자바스크립트 구문을 실행하는 내용이었다.

나는 왠지 호기심이 폭발해서 해당 페이지의 network 세션을 들여다보았고, 거기서 해당 사이트가 내부 서버(DB)에 보내고 받는 api 경로를 습득했다. 얼마 전에 퇴물 취급하며 손절했던 리퀘스츠가 다시 빛을 발할 타이밍이다...!!!ㅋㅋㅋㅋㅋ

셀레니움은 정말 획기적인 웹제어 도구이다. 하지만 오류가 생기면 중간에 멈춰서 다시 처음부터 시도해야 하는 점이나, 매 작업 과정이 리퀘스츠의 GET 메소드와 뷰티풀숲의 파싱의 시너지와 비교하자면 속도면에서 급격히 떨어진다. 하지만 자바스크립트 제어를 가능하게 해줘 스크롤, 특정 모달 띄우기 등의 다양한 조작을 할 수 있는 점에서 어느정도 advanced한 모듈로 생각하게 되지만, 리퀘스츠는 사실 쓸모가 매우 다양하고, 추천하는 사람들도 많은 파이썬에서는 없어선 안될 모듈 중 하나이다. 위의 코드는 강의의 정답안 같은 것이고, 나는 내 방식대로 풀어본 것을 올려본다.

programs = {"%M01_T40690": "생방송 오늘 저녁",
    "%S01_V2000008533": "백종원의 3대 천왕",
    "%S01_V0000338038": "생방송 투데이",
    "%C01_B120189770": "밥블레스유",
    "%MK1_PR752": "생생 정보마당",
    "%K02_T2014-0844": "2TV 생생정보",
    "%C01_B120144822": "수요미식회",
    "%MK1_PR769": "우리동네 맛집 탐방 미식클럽",
    "%K02_T2000-0037": "VJ특공대",
    "%J01_PR10010491": "밤도깨비",
    "%S11_22000012036": "외식하는 날 at Home",
    "%K01_T2000-0093": "6시 내고향",
    "%C01_B120145071": "2015 테이스티로드",
    "%S01_V0000305532": "생활의 달인",
    "%S01_V0000010090": "좋은아침",
    "%M01_T43347": "찾아라! 맛있는 TV",
    "%M01_T43003": "생방송 오늘 아침",
    "%J01_PR10010692": "TV정보쇼 오!아시스",
    "%M01_T80047": "#파워매거진",
    "%S06_V2000007049": "식객남녀 잘 먹었습니다",
    "%C01_B120159120": "2016 테이스티로드",
    "%C01_B120177486": "알쓸신잡  2",
    "%S11_22000011679": "외식하는 날 2",
    "%S01_V0000210215": "모닝와이드 3부",
    "%CA1_WPG2140064D": "먹거리X파일",
    "%S01_V2000010698": "백종원의 골목식당"}

네트워크 탭에서 들여다보니 필터링으로 선택할 수 있는 프로그램명들은 각각 대응하는 코드가 있었다. 그리고 이것을 데이터를 받을 때 아규먼트로 넘겨주는 방식으로 운영되고 있었다. 나는 개인적으로 선호하는 테이스티로드와 수요미식회, 생활의달인, 삼대천왕, 골목식당을 고르고 나머지는 무시했다.

limit = 741

def main():
    url = 'http://apis.sbs.co.kr/foodstar-api/1.1/search/list?location=11&offset=0&limit=' + str(limit)\
           + '&sort=view&latitude=0&longitude=0&programs='+','.join(programs.keys())
    headers = {'ETag': 'W/"7fa7-rQ1yXuy135GclM1qqYUozJQkdio"'}

    req = requests.get(url, headers=headers)
    results = req.json()
    
if __name__ == "__main__":
    main()

위의 코드가 어떤가 쿼리가 워낙 길어 가늠하기 어려울 수 있지만, 일단 위에 셀레니움 코드와 비교해보자면 훨씬 짧고 빠르다. 위의 limit 값이 741인 것은 이 api에 한번에 받아오는 컨텐츠 양의 제한이 따로 없어서 1000개 정도를 get 요청해봤더니, 내가 고른 프로그램들을 모두 합쳐도 741개만 나오는 것을 보고 바꿔둔 것이다. 근데 정말 이 코드만 입력하면 끝이다. 뷰티풀숲도, 셀레니움도 낄 틈이 없다. 대신 위에 셀레니움으로 "필요한 것만" 긁어오는 스크래핑과 다르게 쿼리를 복잡하게 써주지 않으면 거의 모든 데이터의 모든 키와 값을 받아야하는 상황이 생긴다. 별로 달가울 만한 일은 아니니까.

response 데이터의 형식을 들여다보면,

 {
    "_id": "599a92948994345bd9c7ffb2",
    "restaurantName": "이가네가마솥설렁탕",
    "foodCategory_code": "01",
    "address": {
      "postCode": "03378",
      "zoneCode": "",
      "emdCode": "11380102",
      "fullRoadAddr": "",
      "jibunAddress": "서울 은평구 녹번동 141-39",
      "jibunAddressEnglish": "141-39, Nokbeon-dong, Eunpyeong-gu, Seoul, Korea",
      "roadAddress": "서울 은평구 진흥로 130 (녹번동)",
      "roadAddressEnglish": "130, Jinheung-ro, Eunpyeong-gu, Seoul, Korea",
      "userSelectedType": "R",
      "roadnameCode": "",
      "roadname": "",
      "etc": "",
      "latitude": 37.6075107381,
      "longitude": 126.9246555847,
      "location": {
        "type": "Point",
        "coordinates": [
          126.9246555847,
          37.6075107381
        ]
      }
    },
    "phonenumber": "02-387-2246",
    "signatureMenu": "",
    "images": null,
    "description": "",
    "isDeliveable": null,
    "isPackageable": null,
    "closedDays": null,
    "promotions": [],
    "searchkeyword": [
      "가마솥설렁탕"
    ],
    "isUse": true,
    "programs": [
      {
        "cpid": "C2",
        "corporator": "MBC",
        "mediaKind": "smr",
        "programid": "M01_T40690",
        "channelid": "M01",
        "contentnumber": 756,
        "title": "생방송 오늘저녁",
        "subtitle": "2017.12.29 생방송 오늘 저녁 756회",
        "broaddate": "2017-12-29T18:10:00.000Z",
        "contentid": "M01_201712290009",
        "cornerid": 0,
        "mediaids": [
          {
            "mediaid": "M01_T9201712290025",
            "title": "소꼬리의 이색 요리가 있다?!'소꼬리찜 전골'",
            "viewcount": "92",
            "playtime": "152",
            "thumb": {
              "origin": "http://image.cloud.sbs.co.kr/smr/clip/201712/29/tGDxq3iCXw4wCPJBtAjcyR.jpg",
              "small": "http://image.cloud.sbs.co.kr/smr/clip/201712/29/tGDxq3iCXw4wCPJBtAjcyR_320.jpg",
              "medium": "http://image.cloud.sbs.co.kr/smr/clip/201712/29/tGDxq3iCXw4wCPJBtAjcyR_640.jpg",
              "large": "http://image.cloud.sbs.co.kr/smr/clip/201712/29/tGDxq3iCXw4wCPJBtAjcyR_1280.jpg"
            }
          }
        ],
        "actors": [],
        "keywords": [],
        "homeurl": "h"
      },

이게 저 741개의 데이터 중에 하나의 값이다. 저렇게 단순한 코드로 이런 결과물이 천개 가까이 돌아오는 기술이 있는 것이다. 처음 이걸 알았을 때(브런치 스크랩 중에 알게 됨)에는 셀레니움으로 오류 잡아가면서 하루종일 하던 스크래핑들이 무슨 의미가 있었나 싶을 정도였다. 위에 보면 알겠지만 썸네일 만으로 보이지 않는 이미지가 크기별로 존재하고, 어떤 프로그램들은 출연자들 명단까지 나온다...심각... 그래서 되도록이면 이런 방법을 널리 알리고 싶지는 않다.(라고 하면서 블로그에 쓰기) 고생해서 만든 컨텐츠는 좀 고생해서 가져와도 괜찮다....! 어차피 아무 사이트에나 다 사용할 수 있는 절대적인 방법도 아니니까 괜히 기대는 하지 마시길..

그럼 위의 셀레니움 코드가 얻어낸 결과처럼 나도 데이터에서 필요한 부분만 가져와야 하는 상황이다. 그래서 다음과 같은 코드를 짰다.

list_jip = []
for result in results:
    restaurant = dict()
    if result.get('restaurantName'):
        restaurant['name'] = result.get('restaurantName')
        restaurant['category'] = categories.get(result.get('foodCategory_code'))
        if result.get('address'):
            restaurant['address'] = result.get('address')['roadAddress']
            restaurant['eng_address'] = result.get('address')['roadAddressEnglish']
            restaurant['lat'] = result.get('address')['latitude']
            restaurant['long'] = result.get('address')['longitude']
        restaurant['phone'] = result.get('phonenumber')
        restaurant['desc'] = result.get('description')
        if result.get('programs'):
            restaurant['program'] = result.get('programs')[0]['title']
            restaurant['pro_title'] = result.get('programs')[0]['subtitle']
        list_jip.append(restaurant)
        db.restaurant.insert_one(restaurant)
print(len(list_jip))

보...ㄱ잡한가..? 그냥 크롬 개발자도구에 해당 json 객체를 받아온 다음에 열어서, 필요한 값만 꺼내와 새로운 딕셔너리를 만들어서 리스트에 넣는 것을 반복하는 것 뿐이다(이 때 몽고디비에도 인서트한다.).

* 참고로 저는 변수['키값'] 보다 변수.get('키값') 방법을 더 선호합니다. 에러를 줄여줌.

이렇게 놓고보면 셀레니움 코드나 리퀘스츠 코드가 복잡하긴 하네? 싶을 수도 있다. 하지만 셀레니움 코드에서는 latitude와 longitude를 전혀 습득하지 못했다. 그래서 또 한번 네이버 geocode에 주소를 보내고 위도경도를 반환받는 작업을 해야했다. 나는 이미 json에 그 값들이 있어서 바로 넘어갈 수 있었다 (데헷)

그리고 네이버 맵 api를 이용해서 (api 공식문서가 좀 엄격하더라...)

맵을 특정 div에 띄우고, 그 위에 마커들을 각 위치에 맞게 입력해서 금방 지도의 모습을 만들 수 있었다. 처음 써보는 api라 조금 버겁긴 했는데, 여유를 가지고 열심히 했다. 그래도 결과물도 빨리 봤고 만족할 만한 것이 나온 것 같다. 여태 만들어본 서비스 중에 "내가 써보고싶은" 것으로는 1등인 사이트...ㅋㅋㅋ재미있었다

튜터님의 맛집사이트
넘쳐나느 욕망의 항아리같은 내 맛집지도...

 

아직도 코딩 안배웠어? 5만원 깎아줄께 형아만 믿어

 

반응형

댓글