본문 바로가기
제로베이스 데이터 스쿨/일일 스터디 노트

25일차 스터디노트 / 파이썬 웹데이터 수집하고 정리하기, Beutiful Soup, 네이버금융·위키백과·시카고 맛집·네이버영화 데이터 수집 및 정리

by 김뎀뎀 2023. 2. 6.

※제로베이스 데이터 취업스쿨 11기 수강 중

📗25일차 공부 내용 요약

 

파이썬을 활용해 웹데이터를 수집하고, 정리하여, 시각화하는 법을 학습했다

 

1. BeautifulSoup : BeautifulSoup으로 html 불러오고, 원하는 태그를 찾는 법을 학습했다.

2. 네이버 금융 데이터 수집/정리 : urllib의 requests 모듈을 활용해 웹주소에 접근하는 법을 학습하고, 네이버 금융데이터를 수집해보았다.

3. 위키백과 데이터 수집/정리 : URL값이 깨질 때 인코딩하며 불러오는 법과, 동일한 태그가 많을 때 원하는 값을 찾는 법을 학습하고 위키백과에서 데이터를 수집했다.

4. 시카고 맛집 데이터 수집/정리 : fake_useragent를 활용하는 법, Regular Expression을 학습하고, 시카고 맛집 데이터를 수집하고 시각화하였다.

5. 네이버 영화 평점 데이터 수집/정리 : format(),date_range,query를 활용하는 법을 학습하고, 네이버 영화 평점 데이터를 수집하고 정리하였다.

 


📖  25일차 공부 내용 자세히

 

1. BeautifulSop

 

■ BeautifulSoup으로 html 불러오기 

from bs4 import BeautifulSoup

page = open("../data/03. zerobase.html", "r").read()
soup = BeautifulSoup(page, "html.parser") #BeautifulSoup 객체로 html을 불러온다
print(soup.prettify()) #prettify메소드로 보기 편하게 표현한다

 

■ 원하는 태그 확인

1) 원하는 태그 확인

 

① 단일 선택

  • 객체.태그명
  • find()
    • find(”태그”,”속성값”)
    • find(’태그’, 속성 = “값”)
    • find(’태그’, {”속성” = “값”})
  • select_one()
    • select

② 다중 선택

  • find_all()
    • find_all(속성=’값’)[n]
      • 리스트로 반환되기 때문에 offset index로 선택 필요
  • select()
    • slect(”#id값”)
    • slect(”태그”)
    • slect(”.속성(class)값”)
    • 바로 아래라는 의미

 

2) 텍스트 추출

 

  • find().text
  • find().stirng
  • find().get_text
  • find().text.strip()

 

# find

soup.find("p")
'''
<p class="inner-text first-item" id="first">
                Happy ZeroBase.
                <a href="http://www.zero-base.co.kr" id="pw-link">ZeroBase</a>
</p>
'''

soup.find("p",{"class":"outer-text first-item"}).text.strip()
'''
'Data Science is funny.']


# find_all

soup.find_all(class_="outer-text")
'''
[<p class="outer-text first-item" id="second">
 <b>Data Science is funny.</b>
 </p>,
 <p class="outer-text">
 <i>All I need is Love.</i>
 </p>]
'''

#리스트로 반환되기 때문에, text로 뽑기 위해서는 offset index로 가져와야 한다
print(soup.find_all("p")[0].text)
print(soup.find_all("p")[1].string)
print(soup.find_all("p")[2].get_text())
'''
								Happy ZeroBase.
                ZeroBase

None

Data Science is funny.
'''

 

2. 네이버 금융데이터 수집/정리

 

■ urllib 라이브러리, request 모듈 ⇒ 웹 주소(URL) 접근

웹 주소(URL)에 접근할 때는 urllib의 request 모듈이 필요

from urllib.request import urlopen
from bs4 import BeautifulSoup

url = "https://finance.naver.com/marketindex/"
page = urlopen(url) 
soup = BeautifulSoup(page, "html.parser")
print(soup.prettify()) #불러온 url 출력

###

import requests
#from urllib.request.Request
from bs4 import BeautifulSoup

url = "https://finance.naver.com/marketindex/"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
response.status
# response = urlopen(url) => request 모듈을 통해 요청하고, response로 받는다
# response.status => 200이 나오면 정상적으로 받았다는 뜻 
# 이러한 숫자를 http 상태 코드라고 한다. 숫자에 땨른 http 상태 표현

 

■ 네이버 금융 데이터 수집하기

import requests
#from urllib.request.Request
from bs4 import BeautifulSoup

url = "https://finance.naver.com/marketindex/"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")

#환전 고시 환율들이 있는 태그 값들 리스트
exchangeList = soup.select("#exchangeList > li")

exchange_datas = []
baseUrl = "https://finance.naver.com"

for item in exchangeList:
    #상승, 하락일 때 클래스값이 다르기 때문에 조건부로 걸어줌
    if item.select_one(".head_info.point_up") == None:
        updown = item.select_one(".head_info.point_dn > .blind")
    else:
        updown = item.select_one(".head_info.point_up > .blind")

    data = {
        "title" : item.select_one(".h_lst").text, 
        "exchange" : item.select_one(".value").text,
        "change" : item.select_one(".change").text,
        "updown" : updown.text,
        "link" : baseUrl + item.select_one("a").get("href")
    }
    
    exchange_datas.append(data)

df = pd.DataFrame(exchange_datas)
df.to_excel("./naverfinance.xlsx", encoding='utf-8-sig')

 

3. 위키백과 데이터 수집/정리

 

■ URL 값이 깨질 때 인코딩하며 불러오기

import urllib
from urllib.request import urlopen, Request

html = "https://ko.wikipedia.org/wiki/{search_words}"

#글자를 URL로 인코딩
req = Request(html.format(search_words = urllib.parse.quote("여명의_눈동자"))) 
response = urlopen(req)

soup = BeautifulSoup(response, "html.parser")
print(soup.prettify())

 

■ 위키백과 '여명의눈동자'데이터 수집

동일한 태그가 많을 때 반복문으로 원하는 값 찾기

#1. find_all이 반환하는 리스트에서 몇 번째 인덱스에 원하는 값이 있는지 반복문으로 출력해 확인

n = 0

for each in soup.find_all("ul"):
    print("=>" + str(n)  +"=================")
    print(each.get_text())
    n += 1

#2. 반복문으로 찾은 인덱스의 값에서 원하는 내용 추출

#strip() 공백삭제
#replace("원하는 값", "바꿀내용")

soup.find_all("ul")[32].text.strip().replace("\xa0","").replace("\n","")
'''
'채시라: 윤여옥 역 (아역: 김민정)박상원: 장하림(하리모토 나츠오) 역 (아역: 김태진)최재성: 최대치(사카이) 역 (아역: 장덕수)'
'''

 

4. 시카고 맛집 데이터 수집/정리

 

■ fake_useragent 라이브러리 활용해 사이트 불러오기

  • fake_useragent 설치 필요(pip 활용)
  • hear의 user-agent 확인 하는 법
    • 개발자도구 > 네트워크 > 첫번째 페이지 > header > user-agent 확인
# !pip install fake-useragent
from urllib.request import Request, urlopen
from fake_useragent import UserAgent
from bs4 import BeautifulSoup

url_base = "https://www.chicagomag.com/"
url_sub = "chicago-magazine/november-2012/best-sandwiches-chicago/"
url = url_base + url_sub

#원래는 html = urlopen(url)만 해도 되는데, 403에러가 떠서 아래처럼 해준다
ua = UserAgent()
req = Request(url, headers = {"user-agent" : ua.ie}) #ua.ie = 임의의 user-agent 값 생성
html = urlopen(req)
html.status

#임의의 user-agemt 값 없이 "Chrome"dmfheh rksmd
'''
req = Request(url, headers={"User-agent":"Chrome"}) 
html = urlopen(req)
'''

 

■  Regular Expression

 

■ 시카고 맛집 데이터 불러오기 / 메인 페이지 / 순위, 식당명, 메뉴, url까지 

  • re 모듈 : split() 메서드
    • splet(”기준”, 대상) ⇒ 대상을 기준으로 나눔
    • 리스트로 반환
  • urlib모듈 : urljoin() 메서드
    • 상대주소를 절대주소로 변환하는 함수
    • urljoin(a, b)
    • 절대 경로를 기준으로 상대 경로가 잡힘
    • a에 url_base을 절대 주소로 지정, b에 상대 주소를 지정하면 a를 기준으로 b를 합쳐 절대주소로 변환해줌
#list 자료형으로 데이터 뽑아내기
soup = BeautifulSoup(html, "html.parser")

from urllib.parse import urljoin
import re #split() 메서드 활용

#상대주소, 절대주소 대응을 위한 명령
#시카고 매거진의 페이지에서 연결하는 하위 50 페이지의 주소가 상대주소와 절대주소가 혼용되어 있음
url_base = "https://www.chicagomag.com/"

# 필요한 내용을 담을 빈 리스트
# 리스트로 하나씩 컬럼을 만들고, DataFrame으로 합칠 예정
rank = []
main_menu = []
cafe_name = []
url_add = []

#div의 sammy 태그 가져오기
list_soup = soup.find_all("div", "sammy")

for item in list_soup:
    rank.append(item.find(class_="sammyRank").get_text())
    tmp_string = item.find(class_="sammyListing").get_text() #메뉴와 식당이 있는 구간
    main_menu.append(re.split(("\n|\r\n"),tmp_string)[0]) #regular expression
    cafe_name.append(re.split(("\n|\r\n"),tmp_string)[1])
    url_add.append(urljoin(url_base, item.find("a")["href"])) #urljoin 활용


#데이터 프레임으로 만들기
import pandas as pd

data = {
    "Rank" : rank,
    "Menu" : main_menu,
    "Cafe" : cafe_name,
    "URL" : url_add
}

df = pd.DataFrame(data)

df = pd.DataFrame(data, columns=["Rank", "Cafe", "Menu", "URL"]) #컬럼 순서 변경

#데이터 저장
df.to_csv(
    "../data/03. best_sandwiches_list_chicago.csv", sep=",", encoding="utf-8"
)

 

■ 시카고 맛집 데이터 불러오기 / 하위 페이지 / 순위, 식당명, 메뉴, url까지 

하나의 데이터로 테스트
  • 가격과 주소가 한 줄에 있음, 구분해 추출이 필요
  • re.split(), re.search(), regular expression 활용
price_tmp = soup_tmp.find("p","addy").text
# '\n$10. 2109 W. Chicago Ave., 773-772-0406, theoldoaktap.com'


import re
re.split(".,", price_tmp)
# ['\n$10. 2109 W. Chicago Ave', ' 773-772-040', ' theoldoaktap.com']
price_tmp = re.split(".,", price_tmp)[0]

re.search("\$\d+\.(\d+)?", price_tmp)
# <re.Match object; span=(1, 5), match='$10.'>
# serach로 찾은 값이 match object로 반됨

re.search("\$\d+\.(\d+)?", price_tmp).group() 
# '$10.'
# #grpup()을 통해 search로 찾은 문자열을 반환함

tmp = re.search("\$\d+\.(\d+)?", price_tmp).group()
price_tmp[len(tmp) + 2 : ]
# '2109 W. Chicago Ave'
# 가격이 끝나는 지점에서 주소가 시작된다는 점을 활용,가격의 문자열 길이를 알아내서 그 뒤부터 주소로 추출

 

반복문으로 모든 데이터 불러오기 ⇒ itterow + tqdm 활용
#conda install -c conda-forge tqdm
from tqdm import tqdm

price = []
address = []

for idx,row in tqdm(df.iterrows()): 
    req = Request(row["URL"], headers={"user-agent":"Chrome"}) #임의의 user-agent값에서 오류가 나서, chrom으로
    html = urlopen(req).read()
    soup_tmp = BeautifulSoup(html, "html.parser")

    #하위페이지에서 가격이 기재된 태그 부분 찾기
    gettings = soup_tmp.find("p","addy").get_text()

    #가격부분만 추출하기
    price_tmp = re.split(".,", gettings)[0]
    tmp = re.search("\$\d+\.(\d+)?", price_tmp).group()

    #가격 추가
    price.append(tmp)
    
    #주소 추가
    address.append(price_tmp[len(tmp) + 2 : ])
    print(idx)
    
    
#데이터 프레임 만들기
 
df["Price"] = price
df["Address"] = address

df.set_index("Rank", inplace=True)

df.to_csv("../data/03. best_sandwiches_list_chicago2.csv", sep=",", encoding="utf-8")

 

■ 시카고 맛집 데이터 지도 시각화

#requirements

import folium
import pandas as pd
import numpy as np
import googlemaps
from tqdm import tqdm

gmaps_key = 'AIzaSyAs5Gs6qCzAaqwKaZq9RiaBkL3FW6EwV_Y'
gmaps = googlemaps.Client(key=gmaps_key)

#주소로 위도 경도 찾기
lat = []
lng = []

for idx, row in tqdm(df.iterrows()):
    if not row["Address"] == "Multiple location":
        target_name = row["Address"] + ", " + "Chicago"

        #구글맵에서 각 주소 위도 경도값 찾기
        gmaps_output = gmaps.geocode(target_name)
        location_output = gmaps_output[0].get("geometry")
        lat.append(location_output["location"]["lat"])
        lng.append(location_output["location"]["lng"])

    else:
        lat.append(np.nan)
        lng.append(np.nan)

#데이터 프레임에 위도, 경도 추가하기
df["lat"] = lat
df["lng"] = lng

#지도 표기하기
mapping = folium.Map(location = [41.8781136, -87.6297982], zoom_start = 11)

for idx, row in df.iterrows():
    if not row["Address"] =="Multiple location":
        folium.Marker(
            location = [row["lat"], row["lng"]],
            popup = row["Cafe"],
            tooltip = row["Menu"],
            icon = folium.Icon(
                icon = "coffee",
                color = "orange",
                prefix = "fa"
            )
        ).add_to(mapping)

mapping

 

 

5. 네이버 영화 평점 데이터 수집/정리

 

■ format() 함수

  • 문자열 포맷을 지정 가능
  • {이름}포함한 포맷팅할 내용.formant(이름=값)
  • {0,1}포함한 포맷팅할 내용.format(값,값)
    ⇒ {}에 들어가는 0,1,2,,,숫자는 인덱스에 해당. format함수의 값과 차례로 대응
#이름으로 넣기
test_string = "Hi, I'm {name}"
test_string.format(name = "ZeroBase")
#"Hi, I'm ZeroBase"

#인덱스로 넣기
number = 10
day = "three"
"I ate {0} apples. so I was sick for {1} days.".format(number, day)
#'I ate 10 apples. so I was sick for three days.'

 

■ date와 strftime()메서드 활용

  • pd.date_range(”시작날짜”, perids=100, freq =”D”)
    • 시작날짜를 기준으로 100개의 기간을 D(하루)기준으로 만들어 리스트로 반환
    • 반환되는 데이터 타입은 dtype='datetime64[ns]
  • strftime()
    • date, datetime, time 객체에서 활용 가능
    • 주어진 포맷에 따라 객체를 문자열로 변환
    • 객체.strftime()
import pandas as pd

date = pd.date_range("2022.09.01",periods=100, freq="D")

date[0]
#Timestamp('2022-09-01 00:00:00', freq='D')

date[0].strftime("%Y-%m-%d")
#'2022-09-01'
#%Y 연도를 10진수로, #m 월을 0으로 채운 10진수로, %d 일을 10으로 채운 10진수로

date[0].strftime("%Y.%m.%d")
#'2022.09.01'

 

■ 날짜별 영화 평점 데이터 불러오기

날짜별 영화 평점 불러와 데이터프레임 만들기
import pandas as pd
from urllib.request import urlopen
from bs4 import BeautifulSoup
import time
from tqdm import tqdm

#100개의 날짜 생성
date = pd.date_range("2022.09.01",periods=100, freq="D")

#반복문으로 리스트에 날짜, 영화제목, 평점 담기
movie_date = []
movie_name = []
movie_point = []

for today in tqdm(date):
    #각 날짜에 따른 사이트 불러오기
    url = "https://movie.naver.com/movie/sdb/rank/rmovie.naver?sel=cur&date={date}"
    response = urlopen(url.format(date=today.strftime("%Y%m%d")))
    soup = BeautifulSoup(response, "html.parser")
    
    #길이 구하기
    end = len(soup.find_all("td", "point"))
    
    #영화 제목, 평점 구하기
    movie_date.extend([today for _ in range(0,end)])
    movie_name.extend([soup.select("div.tit5")[n].a.text for n in range(0,end)])
    movie_point.extend([soup.select("td.point")[n].text for n in range(0,end)])
    
    time.sleep(0.5)

#데이터 프레임으로 만들기
movie = pd.DataFrame({
    "date" : movie_date,
    "name" : movie_name,
    "point": movie_point    
})

평점 문자열 데이터 타입으로 바꾸기
movie["point"] = movie["point"].astype(float)

 

■ 베스트10 & 워스트10

movie_unique = pd.pivot_table(
    data=movie, 
    index = 'name',  
    values = 'point',
    aggfunc = np.sum)

movie_best = movie_unique.sort_values(by="point", ascending=False)#내림차순

#베스트 10
movie_best.head(10)

#워스트10
movie_best.tail(10)

 

■ 특정 영화의 날짜별 평점 변화 시각화

query()로 특정 영화 데이터 추출
tmp = movie.query("name == ['코다']")
한 영화의 날짜별 데이터 시각화
import matplotlib.pyplot as plt
from matplotlib import rc

rc("font", family = "Malgun Gothic")
%matplotlib inline

plt.figure(figsize = (20,8))
plt.plot(tmp["date"], tmp["point"])#선 그래프 x축 날짜, y축 평점 => 날짜에 따른 평점 변화를 선그래프로 표현(시계열)
plt.title("날짜별 평점")
plt.xlabel("날짜")
plt.ylabel("평점")
plt.xticks(rotation ="vertical") #x축 라벨을 세로로 표시
plt.legend(labels=["평점 추이"], loc="best")
plt.grid(True)
plt.show()

 

■ 여러 영화의 날짜별 평점 변화 시각화

날짜를 인덱스로 피봇테이블
movie_pivot = pd.pivot_table(data=movie, index="date", columns="name", values="point")

 

여러 영화 날짜별 데이터 시각화
import platform
import seaborn as sns
from matplotlib import font_manager, rc

#한글 환경 설정
path = "C:/Windows/Fonts/malgun.ttf"

if platform.system() == "Darwin":
    rc("font", family = "Arial Unicode MS")
elif platform.system() == "Windows":
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc("font", family=font_name)
else:
    print("Unknown system.sorry")

#시각화할 영화만 지정
target_col = ["인생은 아름다워", "탑건: 매버릭", "헌트", "고양이를 부탁해", "하우스 오브 구찌"]

#그래프 그리기
plt.figure(figsize = (20,8))
plt.title("날짜별 평점")
plt.xlabel("날짜")
plt.ylabel("평점")
#plt.xticks(rotation = 'vertical')
plt.tick_params(bottom="off", labelbottom="off")
plt.plot(movie_pivot[target_col])
plt.legend(target_col, loc = 'best')
#plt.grid(True)
plt.show()

 


➰ 25일차 후기

웹데이터를 크롤링해보는 게 신기하고 재미있었는데,

새롭게 학습하는 내용들이라, 완벽하게 이해가 어려운 부분들이 있어 검색하며 진도를 나가느라 속도가 조금 더뎠다.

그래도 하나의 개념을 반복적으로 코드를 작성하며 해서 조금은 익숙해질 수 잇었다.

네이버 금융 데이터를 불러올 때, 강의와는 다르게 환율이 상승/하락이 섞여있어서 상승/하락에 따라 클래스가 달라 조건문을 활용해 스스로 해결해보았고, 시카고 맛집 데이터를 불러올 때도 user-agent값을 임의로 설정했을 때 중간에 자꾸 수집을 못해서 그냥 "Chrome"으로 설정을 바꾸는 등 혼자 이것저것 해결해 보려 했던 게 나름 뿌듯했다.


※본 내용은 제로베이스 데이터 취업 스쿨에서 제공하는 학습 내용에 기반합니다.