파이썬 라이브러리를 활용해서 간단한 웹 페이지 스크래핑을 해보자.

0. 웹 스크래핑

먼저, 웹 스크래핑이 무엇인지 간단히 짚고 넘어가자. 크롤링과는 어떻게 다른가? 웹 스크래핑은 신문이나 잡지 스크랩처럼 웹 사이트에서 원하는 정보를 따로 추출하여 저장해두는 것을 말한다.
엄청난 분량의 웹 문서를 사람이 일일이 모으는 일이 거의 불가능하기에 이를 자동으로 해주는 작업이다. 모든 순간 모든 자료를 모두 업데이트하지는 않고, 그 순간의 데이터를 저장할 수 있다.

이 외에도 데이터가 업데이트될 때마다 혹은 주기적으로 특정 시간마다 데이터를 수집하는 과정을 반복하여 업데이트 된 정보를 추출해올 수도 있다. 이런 프로그램을 웹 크롤러 라고 한다.

  • Billboard chart 200
  • Netflix FAQ list

두 페이지에서 웹 스크래핑을 실습해보려한다. 다시 말해, 두 페이지에서 원하는 정보를 가져오려고 한다.

1. 라이브러리 설치 & 간단 소개

bs4sqlalchemy, sqlalchemy 세 라이브러리가 필요하다. 혹시 모르니 가상환경 아래에서 설치해주었다.

conda creat -n "scrap01"  # 가상환경 생성
conda activate scrap01    # 가상환경 활성화

pip install bs4           # bs4 설치
pip install requests      # requests 설치
pip install sqlalchemy    # sqlalchemy 설치

각 모듈의 역할을 아주 간단히 기술해보자.

name function
requests Python에서 HTTP 요청을 보내는 모듈. 웹에서 정보를 가져오려면, 웹에 요청을 보내고 그 응답을 받아야하니까.
BeautifulSoup html에서 데이터를 추출할 때 사용할 모듈.
sqlalchemy 데이터베이스를 만들어주는 역할.

두루뭉실해도 이정도로 이해하고 과정을 진행해가며 조금씩 이해 범위를 넓혀보자.

2. 웹 기본 정보 가져오기

먼저 파이썬 파일을 열어서 설치한 라이브러리와 모듈을 불러온다.

# filename : billboard-scraping.py
import sys
import requests

from bs4            import BeautifulSoup
from sqlalchemy     import *
from sqlalchemy.sql import *
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

큰 어려움 없이 import import import !
가장 기본적인 정보인 홈페이지 url을 가져오고, 페이지를 구성하는 html을 뽑아내보자.
빌보드 웹페이지에 접속하여 ‘BILLBOARD 200’ 리스트가 있는 페이지의 url을 알아낸다.

이것이다.
https://www.billboard.com/charts/billboard-200

req = requests.get('https://www.billboard.com/charts/billboard-200')
# get 해오기
html = req.text
# get 해온 내용의 'text'를 가져온다
soup = BeautifulSoup(html, 'html.parser')
# html 소스를 파이썬으로 불러옴 (parse)

이렇게, 기초 작업이 되었다. soup이라는 변수에 빌보드차트 200 홈페이지를 구성하는 모든 html 구조가 할당되어있다. 이 가운데 우리가 원하는 요소를 콕 집어서 가져올 것이다.

3. 원하는 요소(정보) 가져오기

빌보드 차트에서 내가 원하는 자료는 아래 네가지다.

  1. 순위
  2. 곡 이름
  3. 가수 이름

먼저 빌보드 웹페이지 개발자도구에서, 내가 원하는 자료의 요소(element)를 택한다.
개발자 도구 > 요소 선택 > 태그 우클릭 > copy > copy selector

개발자도구-요소선택

복사한 정보는 아래. #charts > div > div.chart-list.container > ol > li:nth-child(1) > button > span.chart-element__information > span.chart-element__information__song.text--truncate.color--primary
이렇게나 길다니
이 정보를 아래 코드 안에 argument로 넘겨줄 것이다.

rank            = soup.select('') # 순위
song            = soup.select('') # 곡 이름
singer          = soup.select('') # 가수 이름
song = soup.select('#charts > div > div.chart-list.container > ol > li:nth-child(1) > button > span.chart-element__information > span.chart-element__information__song.text--truncate.color--primary')

확인할 겸 프린트를 해본다 (print(song)). 결과는 아래.

[<span class="chart-element__information__song text--truncate color--primary">Please Excuse Me For Being Antisocial</span>]

우선 리스트가 도출되었다는 것을 알 수 있다. 당황스러운 결과가 표시되긴 하지만, 노래 제목인 Please Excuse Me For Being Antisocial이 보이니 반갑다.
당황스러운 결과에서 우리가 원하는 ‘text’만 가져와야한다. song이 리스트인 것을 다시 한 번 인식하자.

song[0].text

결과는 ?
Please Excuse Me For Being Antisocial
Oh Yeah 😻 노래 이름이 별로 마음에 들진 않지만 어쨌든 1위곡이다. 그런데 우리가 필요한 것은 1위곡 뿐만 아니라, 차트 200에 올라있는 모든 곡의 이름이다. 여기서 우리가 복사해 온 셀렉터의 이름을 다시 한 번 보자.
#charts > div > div.chart-list.container > ol > li:nth-child(1) > button > span.chart-element__information > span.chart-element__information__song.text—truncate.color—primary’

의심스러운 친구를 발견한다. li:nth-child(1)로 리스트 하나만 선택한 것이다. 리스트 내부에 모든 요소를 선택하려면 li 로 바꾸면 된다.
rank와 singer도 같은 방법으로 가져온다.

🌞 지금 까지 코드
  import sys
  import requests

  from bs4            import BeautifulSoup
  from sqlalchemy     import *
  from sqlalchemy.sql import *
  from sqlalchemy.orm import relationship, sessionmaker
  from sqlalchemy.ext.declarative import declarative_base

  req = requests.get('https://www.billboard.com/charts/billboard-200')
  html = req.text
  soup = BeautifulSoup(html, 'html.parser')

  rank    = soup.select('#charts > div > div.chart-list.container > ol > li > button > span.chart-element__rank.flex--column.flex--xy-center.flex--no-shrink > span.chart-element__rank__number')
  song    = soup.select('#charts > div > div.chart-list.container > ol > li > button > span.chart-element__information > span.chart-element__information__song.text--truncate.color--primary')
  singer  = soup.select('#charts > div > div.chart-list.container > ol > li > button > span.chart-element__information > span.chart-element__information__artist.text--truncate.color--secondary')

4. 저장할 데이터 베이스 살펴보기

html 코드에서 자료를 콕 집어 가져왔다면, 그 자료를 따로 저장할 데이터베이스가 필요하다. 보통, 자료를 가져오기 전에 먼저 모델(표)을 만들어둔다. 해야할 일을 아래와 같다.

  1. 저장할 데이터베이스와 내부 표(모델) 만들기
  2. 뽑아온 자료 표에 저장하기 (rank song singer)

4-1. 데이터베이스 만들기

engine = create_engine('sqlite:///billboard_chart_200.db')
Base   = declarative_base()

class Music(Base):
  __tablename__ = 'musics'
  id = Column(Integer, primary_key=True)
  rank = Column(String(50))
  song = Column(String(50))
  singer = Column(String(50))

Music.__table__.create(bind=engine, checkfirst=True)
Session = sessionmaker(bind=engine)
session = Session()

실행하면 디렉토리 안에 billboardchart200.db 라는 데이터베이스가 만들어진다. 거기에 저장할 Music이라는 테이블을 생성하였다. 클래스 형태로 만들고, 그 내부 속성으로 우리가 넣고싶은 정보의 이름을 넣는다. 이 이름이 컬럼의 이름이 될 것이다.
아래 세 줄은 우선은 그냥 따라했음.

4-2. 저장하기

music_chart = []

for item in zip(rank, song, singer):
  music_chart.append(
  {
    'rank'  : item[0].text,
    'song'  : item[1].text,
    'singer': item[2].text,
  })

for element in music_chart:
  result = Music(
    rank = element['rank'],
    song = element['song'],
    singer = element['singer'],
  )
  session.add(result)
  session.commit()

request = session.query(Music).all()

for row in request:
  print(row.rank,'|', row.song,'|' ,row.singer)

끝! 디렉토리를 보면

billboard-scraping.py
billboardchart200.db

두 파일이 포함되어 있고, billboardchart200.db에는 내 musics 표가 저장되있다. 확인하는 방법은 아래.

sqlite3 billboard_chart_200.db
>>> .tables
>>> select * from musics;

Netflix FAQ 스크래핑하기

🌞 Netflix FAQ scraping code
import requests
import sys
from bs4 import BeautifulSoup

from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.sql import *
e = sys.exit

engine = create_engine('sqlite:///netflix.db')
Base = declarative_base()

class FAQ(Base):
    __tablename__ = 'FAQs'
    id = Column(Integer, primary_key=True)
    question = Column(String(50))

FAQ.__table__.create(bind=engine, checkfirst=True)
Session = sessionmaker(bind=engine)
session = Session()

req = requests.get(
    'https://www.netflix.com/kr/'
)
html = req.text
soup = BeautifulSoup(html, 'html.parser')

faq = soup.select(
    '#faq > div.our-story-card-text > ul > li > button'
)

FAQ_list = []

for item in faq:
    FAQ_list.append(
    {
        'question' : item.text
    })

for element in FAQ_list:
    print('question : ',element['question'])
    result = FAQ(
        question = element['question'],
    )
    session.add(result)
    session.commit()

request = session.query(FAQ).all()
for row in request:
    print(row.name,'|', row.song,'|' ,row.singer)
🌜 Netflix FAQ 저장 결과
sqlite3 netflix.db
.tables
select * from FAQs;

netflix_db_img


현재 우리가 beautifulsoup으로 가져올 수 있는 데이터는 html에 있는 요소에 접근해오기 때문에, React로 만들어져 있는 등 javascript 렌더링 결과가 표출되는 웹페이지에서는 지금 상태로는 데이터를 추출해올 수 없다. SELENIUM 과 같은 도구로 사용자의 동적 반응을 추가해준 뒤 추출해오는 방법을 추천한다.