All Articles

Python - 웹 크롤링 실습

빌보드 TOP 100 순위, 가수 이름, 곡 제목, 앨범 아트 이미지 주소 크롤링

# Selenium 임포트
from selenium import webdriver
# 키보드 down키(↓)를 누르게 하기 위해 Keys 임포트
from selenium.webdriver.common.keys import Keys
# Beautiful Soup 임포트
from bs4 import BeautifulSoup

# csv 파일 생성을 위해 임포트
import csv


# 생성할 csv 파일 이름
csv_filename = 'billboard.csv'
# csv 파일을 쓰기 방식으로 열기, 인코딩 형식 지정
csv_open = open(csv_filename, "w+", encoding='utf-8')
csv_writer = csv.writer(csv_open)

# csv 파일 열 이름 지정. 쓴 개수만큼 열 생성
csv_writer.writerow( ('rank', 'title', 'artist', 'image') )

# 크롬드라이버가 저장된 경로
PATH = "/Users/NAON/myprojects/chromedriver"
driver = webdriver.Chrome(PATH)

# 소스를 긁어올 사이트 주소
driver.get("https://www.billboard.com/charts/hot-100")

# 앨범 아트 이미지 로딩을 위해 키보드 down키(↓) 반복
body = driver.find_element_by_css_selector("body")
for i in range(100):
    keys = body.send_keys(Keys.PAGE_DOWN)

# 사이트에서 긁어온 소스를 html로 만들기
htmlsrc = driver.page_source
bs = BeautifulSoup(htmlsrc, 'html.parser')

# 특정 태그 중 class가 특정 이름인 것 전부 찾기
ranks = bs.find_all('span', {"class" : "chart-element__rank__number"})
titles = bs.find_all('span', {"class" : "chart-element__information__song"})
artists = bs.find_all('span', {"class" : "chart-element__information__artist"})
wrapper = bs.find_all('button', {"class" : "chart-element__wrapper"})

ranks_wrap = []
titles_wrap = []
artists_wrap = []
images_wrap = []

# 위에서 찾아낸 내용을 각 리스트에 하나씩 밀어넣기
for rank in ranks:
    rank_text = rank.get_text()
    ranks_wrap.append(rank_text)

for title in titles:
    title_text = title.get_text()
    titles_wrap.append(title_text)

for artist in artists:
    artist_text = artist.get_text()
    artists_wrap.append(artist_text)

for images in wrapper:
    # span 태그 중 특정 클래스 이름을 가진 span 태그 추출 후 style attribute 값 부분 추출
    image = images.find('span', class_="chart-element__image")['style']
    # 이미지를 style 속성의 background-image 값으로 넣었으므로, background-image: url(" 부분 먼저 제거하고 뒷부분 "); 제거 
    url_text = image.split('background-image: url("')[1][:-3]
    images_wrap.append(url_text)

# 순위, 곡 제목, 가수 이름, 앨범 아트 이미지 주소 리스트에서 n번째 요소끼리 튜플로 묶은 리스트 생성
wrap = zip(ranks_wrap, titles_wrap, artists_wrap, images_wrap)

# n번째 튜플을 n번째 행에 넣음
for i in wrap:
   csv_writer.writerow((i))

# 열어준 csv 파일 닫기
csv_open.close()

# 소스 긁어오느라 실행했던 드라이버 종료
driver.quit()

for 반복문 부분을 list comprehension으로 더 간결하게 표현할 수도 있다.

ranks_wrap = [rank.get_text() for rank in ranks]
titles_wrap = [title.get_text() for title in titles]
artists_wrap = [artist.get_text() for artist in artists]

스타벅스 전 메뉴 이름과 이미지 주소 크롤링

# Selenium 임포트
from selenium import webdriver
# Beautiful Soup 임포트
from bs4 import BeautifulSoup

# csv 파일 생성을 위해 임포트
import csv

# 생성할 csv 파일 이름
csv_filename = 'starbucks.csv'

# csv 파일을 쓰기 방식으로 열기, 인코딩 형식 지정
csv_open = open(csv_filename, "w+", encoding='utf-8')
csv_writer = csv.writer(csv_open)

# csv 파일 열 이름 지정. 쓴 개수만큼 열 생성
csv_writer.writerow( ('메뉴', '이미지 주소') )

# 크롬드라이버가 저장된 경로
PATH = "/Users/NAON/myprojects/chromedriver"
driver = webdriver.Chrome(PATH)
# 웹사이트의 모든 자원 로드를 기다리기 위해 암묵적으로 5초 대기. 그 전에 로딩이 끝나면 5초가 안 됐어도 다음 코드로 넘어감
driver.implicitly_wait(5)

# 소스를 긁어올 사이트 주소
driver.get("https://www.starbucks.co.kr/menu/drink_list.do")
htmlsrc = driver.page_source

# 사이트에서 긁어온 소스를 html로 만들기
bs = BeautifulSoup(htmlsrc, "html.parser")

# <a> 태그 중 class 이름이 "goDrinkView"인 것 전부 찾기
images = bs.find_all("a", {"class" : "goDrinkView"})

# <image> 태그 속성 중 alt 속성에 (운 좋게도) 메뉴 이름이 있어서 그것을 메뉴 이름 열에 넣고 이미지 소스 주소를 다음 열에 넣음
for image in images:
    img = image.find('img')
    csv_writer.writerow( (img['alt'], img['src']) )

# 열어준 csv 파일 닫기
csv_open.close()

# 소스 긁어오느라 실행했던 드라이버 종료
driver.quit()

실습하면서 어려웠던 점

빌보드 순위, 곡 이름, 가수 이름은 뷰티풀 수프만으로도 크롤링이 가능했다. 하지만 앨범 아트 이미지는 페이지 최초 호출 시 한 번에 뜨는 것이 아니라 스크롤을 내려서 해당 앨범 아트가 뷰포트 영역에 들어온 후에야 로딩이 됐다. 따라서 동적인 페이지까지 긁어올 수 있는 셀레니움을 사용했다.

문제는 인터넷 환경에 따라, 그리고 컴퓨터 성능에 따라 앨범 아트가 로딩되는 시간이 조금 더 걸린다는 점이었다. 다행히 페이스북처럼 스크롤을 계속 내려서 콘텐츠가 무한 로딩되는 스타일이 아니기 때문에 스크롤을 페이지 끝까지 내려주고 일정 시간 기다리면 해결할 수 있을 거라 생각했다. 하지만 스크롤을 화면 높이 0에서부터 사이트 끝까지 한 번에 내려주니 중간에 있는 앨범 아트는 로딩되지 않았다. 무조건 뷰포트에 한 번씩은 들어와야 로딩되는 것이다.

처음엔 2000px씩 끊어서 5초 기다린 후 또 2000px씩 내려가는 방법으로 시도했다. 그런데 2000px도 간격이 너무 넓었던 것인지 누락되는 이미지가 있었다.. 그래서 1000px 간격으로 다시 시도했는데, 내가 봐도 정말 정말 비효율적인 코드가 나왔다.

driver.execute_script("window.scrollTo(0, 1000);")
time.sleep(5)
driver.execute_script("window.scrollTo(1000, 2000);")
time.sleep(5)
driver.execute_script("window.scrollTo(2000, 3000);")
time.sleep(5)
driver.execute_script("window.scrollTo(3000, 4000);")
time.sleep(5)
driver.execute_script("window.scrollTo(4000, 5000);")
time.sleep(5)
driver.execute_script("window.scrollTo(5000, 6000);")
time.sleep(5)
driver.execute_script("window.scrollTo(6000, 7000);")
time.sleep(5)
driver.execute_script("window.scrollTo(7000, 8000);")
time.sleep(5)
driver.execute_script("window.scrollTo(8000, 9000);")
time.sleep(5)
driver.execute_script("window.scrollTo(9000, 10000);")
time.sleep(5)
driver.execute_script("window.scrollTo(10000, 11000);")
time.sleep(5)
driver.execute_script("window.scrollTo(11000, document.body.scrollHeight);")
time.sleep(5)

이 못생긴 코드는 무엇인지… 게다가 로딩 시간을 5초씩 준 덕분에 한 번 크롤링 하려면 1분이 걸린다. 테스트 할 때도 이만저만 불편한 게 아니었고 더 큰 문제는 이렇게 긁어온 이미지 주소가 리스트에 들어가지 않았다는 것이다. 개별로 출력하면 잘 나오고, 곡 제목이나 가수 이름이랑 마찬가지로 문자열 형태였는데 왜 에러가 떴는지는 아직도 의문이다.

# print(url_text) 했을 때
https://charts-static.billboard.com/img/2020/05/lady-gaga-b8x-rain-on-me-n1e-155x155.jpg
https://charts-static.billboard.com/img/2020/03/megan-thee-stallion-z0z-savage-7q2-155x155.jpg
https://charts-static.billboard.com/img/2017/01/dababy-sfn-155x155.jpg

# 코드 후략

하지만 빈 리스트에 이 주소를 하나씩 넣으려고 하니 IndexError 발생. 왜죠?

Traceback (most recent call last):
  File "crawling.py", line 78, in <module>
    url_text = image.split('background-image: url("')[1][:-3]
IndexError: list index out of range

혹시 url_text 결과값은 문자열이 아닌가 싶어서 이렇게도 확인해보았다.

print("artist_text=", end=""), print(type(artist_text))
print("url_text=", end=""), print(type(url_text))
artist type=<class 'str'>
url type=<class 'str'>

둘 다 문자열인데 왜 가수 이름은 리스트로 잘 append되고 이미지 주소는 안 되는지…

한참 씨름하다 테스트 한 번 할 때마다 1분씩 기다리는 게 어이없어서 스크롤 다운 방식을 DOWN 키를 누르는 방식으로 바꿨다. 그런데 의외로 코드가 동작하는 것이다! 문제는 스크롤을 내리면서 이미지를 로딩시키는 방식에 있었나보다. 아직 이유를 모르기 때문에 확인 후 내용을 보충할 것이다.