상세 컨텐츠

본문 제목

피로그래밍 14기: day5 오후 <구글 COLAB, 파이썬 웹 크롤링>

환 codes web/PYTHON

by 퍼블리셔환 2021. 1. 13. 23:00

본문

 

 

이번 세션은 구글 colab을 이용한 웹 크롤링 강의이다. 

 

지금까지 했었던 내용들은 이해를 하고 따라가기가 비교적 수월했지만, 이번 세션부터는 밑천이 떨어지면서 이해하기부터 쉽지 않았다. 그래서 저번 팀 과제를 같이 했던 같은 조 친구들과 시간을 정해 줌으로 따로 자체 리뷰 세션을 가졌다. 그 덕분에 세션의 내용을 커버할 수 있게 되었다.

 

같이 코드 리뷰를 도와준 에게 감사를 보낸다:)


1. Google Colab

이번 세션은 vs code가 아닌, 구글 colab을 사용한다. colab을 사용하는 이유는 파이썬을 사용해서 웹 크롤링 협업을 하는 경우를 통해서 알아볼 수 있다. 참여하는 모든 사람들의 파이썬 버전을 똑같이 통일하면 좋겠지만, 현실적으로 쉽지 않다. 그래서 인터넷으로 항상 최신 버전 유지가 가능한 colab을 사용한다. 사용법은 간단하다. 구글 계정을 이용해서 colab에 접속하면 사용이 가능하다. 아래는 강의자님의 자료를 무단으로 공유하면 문제가 생길수도 있어서 내가 실습하면서 따라한 colab 자료의 링크이다. 이 링크에 접속해서 새 노트를 만들어서 실습할 수도 있다. 

 

colab.research.google.com/drive/1szl1bFvoe4fyl03tL1J0TMuqJBJmSvEx

에러 없이 모든 코드가 돌아감을 확인했다. 만약 따라하는데 안된다면 코드 블럭 나누는 것을 위 예시와 똑같이 해보는 것을 권장한다.

 

2. 웹 크롤링

웹 크롤링은 내가 코드에서 인터넷에 있는 정보를 임의로 긁어오는 것을 의미한다. 웹 크롤링을 이용하면, 오늘 뉴스의 헤드라인만 모두 긁어오거나, 웹툰의 이미지 파일을 모두 저장할 수도 있고, 기업들의 채용 공고를 모아볼 수도 있다. 

 

이 웹 크롤링을 잘 이용하면 생각보다 도움이 되는 무언가를 만들어 볼 수 있을 것 같다. 


3. 일반적인 경우의 크롤링 (SSR) - 네이버 뉴스스탠드, 네이버 웹툰 크롤링

크롤링을 하기 위해서는 라이브러리가 필요하다. 가장 기본적인, 웹페이지의 정보를 요청하는 request 라이브러리이다. 

pip install requests	# requests 라이브러리를 다운받는다

import requests		# requests 라이브러리를 import해온다

# 네이버 메인 페이지를 get method를 통해서 받아온다
response = requests.get('https://www.naver.com') 
print(response.text)

웹페이지는 사용자에게 html(text)의 형태로 제공된다. 따라서, 우리가 정의한 response를 text로 받아올 수 있다. 

 

colab에서 위를 실행시키면 다음과 같이 결과가 나온다. 

분명 html의 형태로 무언가 받아오기는 했지만, 이 자체로 의미를 갖는 데이터는 아닌 것 같다. 그래도 일단 첫 번째 크롤링이 성공했다는 것에 의의를 두면 좋겠다. 

 

그러면 이제 beautifulsoup이라는 라이브러리를 이용해서 위에서 정렬하지 않고 받아온 데이터를 우리가 실질적으로 볼 수 있는 데이터별로 정렬해보자. 아래는 네이버 메인 페이지 중에서 뉴스 스탠트 페이지를 가져오는 코드이다. 

from bs4 import BeautifulSoup as bs

soup = bs(response.text, "html.parser")
presses = soup.select('.thumb_area .thumb') #뉴스를 가져옴
for press in presses:
  print("- Soup 객체: ", press)
  print("- Attrs: ", press.attrs)
  print("- Select One: ", press.select_one("img.news_logo"))
  print("- Attribute: ", press.select_one("img.news_logo").get("src"), press.select_one("img.news_logo").get("alt"))
  print("- Text: ", press.text)
  print()

실행 결과값

위 코드에서 soup = bs(response.text, 'html.parser') 라는 코드가 있는데, 이는 bs 메서드로 html을 가져오는데, 가상의 환경을 구성해줌을 의미한다. 자세한건 구글링하자. 나도 정확하게는 잘 모르겠다. 그리고 정확히 알게 되면 댓글로 알려주면 감사하겠다:)

여기서 presses = soup.select('.thumb_area .thumb') 라는 코드를 잘 보면 중요한 사실 두 개를 알 수 있다. 

 

ㄱ. request와 beautifulsoup를 이용한 크롤링은 css 속성(class, id)를 토대로 정보를 받아온다. 

.thumb_area와 .thumb는 뉴스 스탠드의 클래스 명이다. 이 css속성(attributes, attrs)을 select메서드 뒤에 적어서 '네이버'라는 페이지에서 .thumb라는 속성을 가진 요소들을 가져올 수 있게 되는 것이다. 그래서 출력된 결과값의 - Attrs 내용을 보면 class: thumb라고 명시되어있다. 

ㄴ. soup.select는 리스트의 형태로 저장된다. 

바로 밑에 있는 for press in presses는 presses라는 리스트에 있는 엘리먼트 press 각각에 대한 내용을 지정한다는 의미이다. 따라서 우리는 따로 presses의 형태를 지정해주지 않았지만, 리스트의 형태로 반환된다는 것을 알 수 있다. 만약 내 말이 의심이 들면 colab에 들어가서 print(type(presses))를 입력해봐도 된다. 아니 그냥 직접 보여주겠다. 

인정?

뒤에 진행할 내용에서 이 두 개념이 중요하게 적용되니 기억해두면 좋다. 

 

그러면 이제 네이버 웹툰을 크롤링해보자. 겨울이라서 공포만화가 끌리기 때문에 원주민 공포만화를 크롤링하겠다. 

import requests
from bs4 import BeautifulSoup as bs
#원주민 공포만화 url
URL = 'https://comic.naver.com/webtoon/detail.nhn?titleId=698918&no=168&weekday=tue'

res = requests.get(URL)
soup = bs(res.text, 'html.parser')
cut_list = soup.select('.wt_viewer img')
print(cut_list)

해당 URL에 들어가서 웹툰의 이미지를 클릭해보면, wt_viewer라는 클래스 안에 img형태로 업로드 되어 있다는 것을 알 수 있다.

그런데, 분명 이미지를 가져왔는데 출력된 결과는 이미지의 형태가 아니다. 이를 우리가 볼 수 있는 이미지로 바꾸기 위해서는 ipyplot 모듈이 필요하다. 이것도 가져오자. 

pip install ipyplot

import ipyplot

image_src_list = [cut.get('src') for cut in cut_list]
print(image_src_list)

ipyplot.plot_images(image_src_list, img_width=100)

여기서 image_src_list = [cut.get('src') for cut in cut_list] 는 cut_list에 저장된 cut들마다 src 요소를 get 메서드로 가져옴을 의미한다. 그리고 앞서 설치하고 불러온 ipyplot을 이용하면 다음과 같이 잘 출력된다. 

지금까지 이렇게 크롤링이 잘 된 이유는 우리가 봤던 네이버 메인 페이지와 웹툰 페이지가 SSR방식으로 작성되었기 때문이다. 

SSR방식은 server-side rendering이다. 다시 말해, 사용자가 서버에 url을 통해서 request를 보내게 되면 서버에서 구성 요소가 rendering되어 html파일을 그려서 주는 것이다. 안타깝게도, 세상은 항상 쉬운 방법만 있는것은 아니다. 

CSR방식은 client-side rendering이다. 앞서 설명한 SSR방식과도 이름부터가 달라보인다. 이 방식은 서버에서 html을 구성해서 바로 주는것이 아니라 document를 불러온 사용자측에서 javascript코드를 통해서 엘리먼트의 형식으로 css정보를 나중에 외부 소스로 받는 방식이다. 다시 말해, javascript코드를 통해서 html을 사용자가 구성한다는 것이다. 앞서 우리가 진행했던 방식은 css에서 속성을 받아서 크롤링하는 것이었는데, 이렇게 되면 더 이상 이 방식을 쓸 수 없다. 

 

4. CSR방식의 크롤링 - 네이버 스포츠 크롤링

앞서 소개한 csr방식을 확인하기 위해서 네이버 스포츠 페이지를 크롤링해보자. 

URL = "https://sports.news.naver.com/kbaseball/news/index.nhn?isphoto=N&type=latest"
res = requests.get(URL)
soup = bs(res.text, "html.parser")
soup.select("#news_List ul li")

네이버 야구의 최신 기사를 가져오기 위해서 css속성이 id=news_List임을 확인했고, 그 하위 태그인 ul 안에 있는 li를 뽑아오는 코드이다. 그런데 이 코드를 실행하면 빈 리스트가 리턴된다.

 

이를 해결하기 위한 방법은 두 가지가 있다. 

ㄱ. 네트워크상에서 데이터를 가져오는 부분을 찾아서 가져오는 방법

ㄴ. 가상의 브라우저를 띄워서 실제 사용자 측에서의 렌더링을 기다린 후 데이터를 가져오는 방법

 

우선 ㄱ의 방법을 먼저 확인해보자. 

네이버 야구 페이지의 최신 기사 페이지에 들어가면 다음과 같이 보인다.

지금 우리는 야구의 최신 기사들을 가져오려고 하는데, 야구 기사들을 모아놓은 박스는 최신순/인기순을 클릭했을 때 전체 페이지가 새로 로드되지 않고, 박스 안에 있는 내용만 바뀐다. 다시 말하면, 최신순/인기순을 전환했을 때 새롭게 전송되는 데이터가 우리가 크롤링하고자 하는 야구의 최신 기사 목록일 것이다. 

 

실제 인터넷 도구에서 네트워크 탭을 누른 다음 최신순/인기순을 전환해보니 일부분만 새롭게 로드되는 것을 확인했다. 

2000ms~3000ms // 5000ms~6000ms 에서 일부분의 네트워킹이 발생하는 장면

이 부분을 드래그 하고 밑에 나온 정보를 확인하니, gif와 png등 이미지가 아닌 "xhr"형식의 데이터가 발생한 것을 확인할 수 있다. 

xhr타입의 데이터의 이름을 우클릭하고 copy-copy as cURL(bash)를 클릭하면 cURL데이터가 복사된다. 

이렇게 복사된 cURL 데이터를 파이썬의 requests로 바꿔주는 사이트가 있다. 

curl.trillworks.com/

이 사이트에 접속해서 curl command에 우리가 복사한 정보를 넣으면 자동으로 Pyton requests로 변환되는데, 변환된 requests를 다시 복사해서 코드로 실행시키면 된다. 

import requests

headers = {
    'authority': 'sports.news.naver.com',
    'pragma': 'no-cache',
    'cache-control': 'no-cache',
    'sec-ch-ua': '"Google Chrome";v="87", " Not;A Brand";v="99", "Chromium";v="87"',
    'accept': 'application/json, text/javascript, */*; q=0.01',
    'x-requested-with': 'XMLHttpRequest',
    'sec-ch-ua-mobile': '?0',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-mode': 'cors',
    'sec-fetch-dest': 'empty',
    'referer': 'https://sports.news.naver.com/kbaseball/news/index.nhn?isphoto=N&type=popular',
    'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
    'cookie': 'NNB=CURP4OVBWUEF4; ASID=7d8431450000016f5b3b86020000005a; NFS=2; MM_NEW=1; MM_NOW_COACH=1; repeatMode=list; NDARK=N; NRTK=ag#20s_gr#2_ma#-2_si#2_en#0_sp#0; NaverSuggestUse=use%26unuse; _fbp=fb.1.1605436066548.21295340; m_loc=e50ef315b5d3efb2cca584c6c893f178d6ff5028a1841be0c8924095fc3435e40a8fc4656e5d68744ed2193d1b747b265813f7a0835d01695d60c020c812bfa3fa5cef606eed7a7aec7c02c1b186ea1dc3d1852cd2f7a092be8166936b47d647; nx_ssl=2; _ga_4BKHBFKFK0=GS1.1.1609300597.4.1.1609300602.55; _ga=GA1.2.1357272616.1578502569; nid_inf=743521396; NID_AUT=75g0kaW55ZsTvX72vQl9ve92w2PhiTB45cjYmdbMIONcHZPlmgoPf3pdFpG0Z0Rv; NID_JKL=IKG6Oz5ZoxogiQozPQ/u8QRaO059l+8G8QXPSLXWdJM=; BMR=s=1610426557109&r=https%3A%2F%2Fpost.naver.com%2Fviewer%2FpostView.nhn%3FvolumeNo%3D30426520%26memberNo%3D49418894&r2=https%3A%2F%2Fwww.naver.com%2F; page_uid=U/2QrwprvmZssOy6TZZssssstWd-518514; NID_SES=AAABiMl+/ladjnXNWu9Nrvt/dKCrSxoMm9wzueMGeephfObYiM0kAUqfRnBr+9+oXps0jQTaPvSiqHCDQUEE7GoeyXUnVlas6aYCF0hKtVYW2aU+HZ+hcxlZrxZT4+1W4cv6U4MmTvdtp6vDUm9fEEjCWBI0DR6S2YvtGzRI+/0Z+GeKuMCaDYJFrPYRuy6cd07sSzeie1cTSNsxv2cJ3+r5n6bK0/XecdYhpkzNCUxWwH7kA31ezy5sDcL6g7itvwwYgClPQxTsw5v+AksInbKNryoBxelhXwgN8Pzj90Dyn1OQ2K02v11bD9lI7YWc8pwYgVWMYgPEH1Tmh1NQ36eOsfxAVvmkIs1AhU44WXIQrBHfdQAKPrTV89s75HUMZwi8BJNHu7r2a+yPY9c6MKtQVkOp2QUkowyP9cUcrhEjOgtH21ibWSsHzjylOprS8joApZA8GjBh+TdAu+f1MODmZYSLSkM/Cipqjm3QaEnmdUig7+iOR5ZNK36SrXD/ib/F6iinDYVNcHLo36XcDlUo68I=',
}

params = (
    ('isphoto', 'N'),
    ('type', 'latest'),
)

response = requests.get('https://sports.news.naver.com/kbaseball/news/list.nhn', headers=headers, params=params)

#NB. Original query string below. It seems impossible to parse and
#reproduce query strings 100% accurately so the one below is given
#in case the reproduced version is not "correct".
# response = requests.get('https://sports.news.naver.com/kbaseball/news/list.nhn?isphoto=N&type=latest', headers=headers)

복사한 코드를 실행하고, 밑에 코드를 한 줄 더 실행하면 우리가 원하는 최신 야구의 목록이 크롤링된다. 

response.json() # response를 받아오는 코드

 

선배님 내년에는 꼭 MLB 가세요ㅠ

이렇게 네트워크를 직접 확인해서 정보가 전송될 것이라고 예상한 지점에서 정보를 가져오는 ㄱ방식을 확인했다. 

 

그럼 이제 ㄴ의 방식을 확인해보자

우리 선배님의 MLB 진출이 좌절됐는데, 내년에는 꼭 성공하기를 바라는 마음에서 네이버에서 나성범을 검색해서 나성범선수의 이미지를 가져오자. 이 방식은 selenium 라이브러리가 추가로 필요하다. 

!pip install selenium
!apt-get update
!apt install chromium-chromedriver
from selenium import webdriver

# colab에서 selenium을 돌리기 위한 옵션들
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')

솔직히 말하면, 여기서 driver가 중요한 역할을 하는 것 같은데 나는 정확히 잘 이해를 못했다. 앞에서 가상 환경을 구성하지 않을때 requests의 역할을 여기서는 driver가 대신 하는 것으로 보여서, 그냥 가상 환경을 아래에서 생성하는데에 필요한 요소라고만 생각하고 넘어갔다. 만약 이 내용을 아는 사람이 있다면 댓글로 알려주시면 감사하겠다.

driver = webdriver.Chrome("chromedriver", options=chrome_options)
driver.get("https://search.naver.com/search.naver?where=image&sm=tab_jum&query=%EB%82%98%EC%84%B1%EB%B2%94")
driver.page_source

위 코드의 결과값이다. 무언가 가져와지기는 했지만, 우리가 원하는 이미지 형태는 아니다. 

images = driver.find_elements_by_css_selector('.photo_bx .thumb img')
print(images)
print(len(images))

이미지와, 이미지가 총 몇 장이나 리턴되는지를 알아보기 위해서 다음과 같이 코드를 작성했다. 결과를 보면, 리턴된 값의 형태가 이미지가 아니고, 한 번에 총 50장의 사진이 리턴되는 것을 알 수 있다. 

이미지를 사진의 형태로 받기 위해서 코드를 다음과 같이 다시 작성하자. 

from IPython.core.display import display, HTML

display(HTML(driver.page_source))

성공!

나성범 선수의 이미지를 가상의 브라우저에 가져오는데에 성공했다. 그런데 조금 아쉽다. 왜 50개 밖에 안될까? 그래서 욕심을 부려서 한 번에 더 많은 이미지를 가져오기 위해 다음과 같은 코드를 또 작성했다. 

from selenium.webdriver.common.keys import Keys
import time

body = driver.find_element_by_css_selector('body')

for i in range(20):
    body.send_keys(Keys.PAGE_DOWN)
    time.sleep(0.5)

images = driver.find_elements_by_css_selector('.photo_bx .thumb img')
print(images)
print(len(images))

다음 코드를 이용해서 이미지와 리턴되는 이미지를 출력해보면 다음 결과가 나온다. 

이전과 굉장히 흡사하지만, 한번에 가져와지는 이미지의 개수가 250개로 늘었다. 

위 코드에서 for문이 PAGE_DOWN을 0.5초 간격으로 총 20번을 실행하는데, 이 PAGE_DOWN이 브라우저에서 스크롤을 내리는 것과 같은 역할을 하는 것이다. 만약 이 상태에서 한 번 더 같은 코드를 실행하게 되면 아래의 250이라는 숫자가 또 바뀔 것이다. 수치가 누적되는데, 이미지의 종류에 따라서 일정한 패턴으로 수가 늘어나는 것은 아니다. 그런데, 아무리 위 코드를 여러 번 반복해도 숫자는 500을 넘지 않는데, 이는 네이버 이미지 페이지에서 스크롤을 끝까지 내린다고 가정하더라도 한 번에 500개까지의 이미지만을 보여주기 때문이다. 

img_src_list = [img.get_attribute("src") for img in images]
# print(img_src_list)
ipyplot.plot_images(img_src_list, img_width=100, max_images=len(img_src_list))

마찬가지로 이미지의 형태로 받아보면 다음과 같이 보인다.

크롤링의 모든 과정이 끝났다. 

 

일회성으로 진행되는 세션인지는 모르겠는데, 크롤링을 얕게나마 접해볼 수 있어서 굉장히 의미있었다고 생각한다. 짧은 시간동안 모든 내용을 파악하기 어려워서 이후에 다시 복습 세션을 팀원들끼리 진행했는데, 아주 좋았다.

 

앞으로도 이해가 잘 안되면 같이 복습을 해 봐야겠다.

'환 codes web > PYTHON' 카테고리의 다른 글

피로그래밍14기: day6 <PYTHON - class>  (0) 2021.01.14

관련글 더보기

댓글 영역