4 분 소요

:octocat:Web Scraping

import pandas as pd
import requests
url = "https://www.melon.com/chart/index.htm"
# 로봇으로 인식하는 것 같음
# pd.read_html(url)

HTTPError: HTTP Error 406: Not Acceptable
사용자 에이전트에서 정해준 규격에 따른 어떠한 콘텐츠도 찾지 못했을 때
로봇으로 인식해 정보를 받지 못하므로 user-agent를 설정 해줘야 함

response = requests.get(url, headers={"user-agent": "Mozilla/5.0"})
response
table = pd.read_html(response.text)
temp = table[0]
temp.head(3)
temp.tail(3)

head
tail

temp.columns

[‘Unnamed: 0’, ‘순위’, ‘순위등락’, ‘앨범이미지’, ‘곡 상세가기’, ‘곡정보’, ‘앨범’, ‘좋아요’, ‘듣기’, ‘담기’, ‘다운’, ‘뮤비’]

‘Unnamed: 0’ : 선택버튼 무의미한 컬럼
[‘곡 상세가기’, ‘듣기’, ‘담기’, ‘다운’, ‘뮤비’] : 추가 기능

‘순위등락’ : 전처리 해서 사용가능
‘곡정보’ : 제목이랑 가수 구분 필요
‘좋아요’ : 모두 0으로 나오는 데 홈페이지상으론 수치가 나옴, 이유가 뭘까?
‘앨범이미지’ : NaN으로 받아와짐, 추가 처리 필요
[“순위”, “순위등락”, “곡정보”, “앨범”] : 사용

def rank_transform(str_rank):

    str_num = str_rank[0]
    
    return int(str_num)

순위 컬럼 수치형 데이터로 변환
ex) 1위 -> 1

df["순위_변환"] = df["순위"].apply(rank_transform)
df[["순위_변환"]].head(3)

rank

unique_list = df["순위등락"].unique()
unique_list

[‘순위 동일 0’, ‘단계 상승 1’, ‘단계 하락 2’, ‘단계 하락 1’, ‘단계 상승 2’, ‘단계 하락 3’, ‘단계 상승 4’, ‘단계 상승 3’, ‘단계 하락 4’, ‘단계 하락 9’, ‘순위 진입’]

# https://codingspooning.tistory.com/138
# https://kynk94.github.io/devlog/post/re-match-hangul
import re
def regex_hangul(s):

    hangul = re.compile('[^ㄱ-ㅣ가-힣+]') # 한글과 띄어쓰기를 제외한 모든 글자
    result = hangul.sub('', s) # 한글과 띄어쓰기를 제외한 모든 부분을 제거
    
    return result

슬라이싱을 통해 5글자를 추출해도 되겠지만, 정규표현식을 사용해서 추출해봤음
유니코드 ㄱ : 12593, ㅣ : 12643, 가 : 44032, 힣: 55203
ㄱ-힣으로 지정한다면 12644 ~ 44031의 문자도 포함되므로 ㄱ-ㅣ가-힣으로 지정한다.

for idx, unique in enumerate(unique_list):
    unique_list[idx] = regex_hangul(unique)
set(unique_list)

{‘단계상승’, ‘단계하락’, ‘순위동일’, ‘순위진입’}
set로 중복제거

def regex_num(s):

    num = re.compile(r'\d+')
    result = num.findall(s)[0] # 리스트 형식으로 나와서 0번째 인덱스 값을 가져옴

    return int(result)

숫자를 찾아오는 정규표현식

def rank_updown(rank_change):
    
    if regex_hangul(rank_change) == "단계상승":
        val = regex_num(rank_change)
    elif regex_hangul(rank_change) == "단계하락":
        val = -regex_num(rank_change)
    elif regex_hangul(rank_change) == "순위동일":
        val = 0
    else:
        val = "new"

    return val

단계하락 => 음수, 순위동일 => 0
새롭게 순위에 진입한 경우는 “new”로 변환. 추후 분석 시 수치형 데이터로 변환할 필요가 있어 보임

df["순위등락_변환"] = df["순위등락"].apply(rank_updown)
df[["순위", "순위_변환","순위등락_변환"]].head(20)

rank_change

from bs4 import BeautifulSoup as bs
html = bs(response.text)

Pandas로 추출하지 못한 내용들 BeautifulSoup으로 시작

# 제목
# #lst50 > td:nth-child(6) > div > div > div.ellipsis.rank01 > span > a

title = html.select("div.ellipsis.rank01 span a")

title_list = []
for t in title:
    title_list.append(t.text)
print(len(title_list))

df["제목"] = title_list

제목을 추출해와서 list로 모은 후 DataFrame 컬럼으로 추가

# 가수
#lst50 > td:nth-child(6) > div > div > div.ellipsis.rank02 > a
#lst50 > td:nth-child(6) > div > div > div.ellipsis.rank02 > span > a
#lst50 > td:nth-child(6) > div > div > div.ellipsis.rank02 > span 단 class="checkEllipsis"

# musician = html.select("div.ellipsis.rank02 a") # 여기로 접근하면 span에 있는 a 도 불러와져서 가수가 2번씩 뽑힘
# musician = html.select("div.ellipsis.rank02 span a") # 가수가 여러명이면 여러개 뽑힌다

musician = html.select("div.ellipsis.rank02 span.checkEllipsis")

musician_list = []
for m in musician:
    musician_list.append(m.text)    
print(len(musician_list))

df["가수"] = musician_list

가수를 추출해와서 list로 모은 후 DataFrame 컬럼으로 추가

df[["제목", "가수"]].head()

title_musician

# 앨범 이미지
#lst50 > td:nth-child(4) > div > a > img
# https://cdnimg.melon.co.kr/cm2/album/images/110/45/985/11045985_20220905151107_500.jpg/melon/resize/120/quality/80/optimize
# optimize를 없애야 큰 이미지를 얻을 수 있겠다

image = html.select("div a img")

img_list = []
for img in image[2:]: # 첫 줄 melon img, 둘째줄 불필요 
    img_list.append(img["src"].split("/melon")[0])
print(len(img_list))

/melon 뒤로 resize optimize를 없애줘야 본 크기의 이미지를 얻어올 수 있다.

df["앨범이미지_변환"] = img_list
df[["앨범이미지_변환"]].head()

image

#lst50 > td:nth-child(8) > div > button > span.cnt
# 이것도 0으로 뜸, 문제가 뭘까?
html.select("span.cnt")[9]

requests get을 사용해 불러오면 수치가 0으로 나온다.
BeautifulSoup으로 변환해도 마찬가지

# 멜론에서 Javascript를 사용해서 좋아요 수를 실시간으로 불러옴
# BeautifulSoup은 정적 페이지 정보만 가져옴 Selenium 같은 동적 정보 가져오는 툴이 필요
# BeautifulSoup만 사용해서 가져오는 방법이 없을까
# https://sdallman.medium.com/web-scraping-dynamic-content-only-using-beautiful-soup-631496473c0e

# 좋아요 javascript code
# likeAttr = data-song-no
# SUMMCNT가 좋아요 수 
html.select("script")[5]

like
(JavaScript Like Func)

위의 JavaScript Like Func를 보면 좋아요 데이터는 Javascript를 통해 동적으로 불러온다.
Selenium을 사용해 불러와도 되겠지만, 팀장님이 알려주신 위 출처 사이트를 통해 BeautifulSoup으로도 가능하다는 걸 알게 됐다.

#lst50 > td:nth-child(8) > div > button
button = html.select("button.button_etc.like")
data_song_no = []
for b in button:
    data_song_no.append(b["data-song-no"])
print(len(data_song_no))

Like Func의 attr인 data-song-no를 추출해와서 리스트에 넣어준다.

# 여러개를 한번에 뽑기 위해서는 "song_no1, song_no2,, song_noN"의 형태로 넣어줘야함  
song_no = ", ".join(data_song_no)
cashflow = f"https://www.melon.com/commonlike/getSongLike.json?contsIds={song_no}"
cf = requests.get(cashflow, headers={"user-agent": "Mozilla/5.0"})
cfdata = cf.json()

리스트에 있는 데이터를 콤마 구분자 형식의 문자열로 만들기 위해 join을 사용

df_like = pd.DataFrame(cfdata["contsLike"])
df_like.head()

like_table

df["좋아요_변환"] = df_like["SUMMCNT"]

필요한 SUMMCNT : 좋아요 수 만 뽑아서 DataFrame 컬럼에 추가한다.

df_last = df[["순위_변환", "순위등락_변환", "앨범이미지_변환", "제목", "가수", "앨범", "좋아요_변환"]]
df_last.head(3)

trans_table

df_last.columns = ["rank", "rank_change", "img_url", "title", "musician", "album", "like"]

알아보기 쉽게 컬럼명 바꿔준다.

# 이미지 저장
# https://www.delftstack.com/ko/howto/python/download-image-in-python/

# def jpg_load(name, url):
#     f = open(name + '.jpg','wb')
#     response = requests.get(url)
#     f.write(response.content)
#     f.close()

# https://stackoverflow.com/questions/16476924/how-to-iterate-over-rows-in-a-dataframe-in-pandas
# for idx, row in df_last.head().iterrows():
#     jpg_load(row["title"], row["img_url"])

저장할 이미지의 이름과 URL을 Parameter로 가지는 이미지 저장 함수
지금까지 만든 DataFrame으로 이미지를 저장하고 싶다면 위 코드를 실행하면 된다.

#conts > div.multi_row > div.calendar_prid.mt12 > span.yyyymmdd > span class="year"
#conts > div.multi_row > div.calendar_prid.mt12 > span.hhmm > span class="hour"

today = html.select("span.year")[0].text + "_" + html.select("span.hour")[0].text
file_name = f"top100-{today}.csv"
df_last.to_csv(file_name, index=False)

스크래핑한 URL의 날짜와 시간을 받아와서 만들 csv의 이름 설정
top100-yyyy.mm.dd_hh:mm.csv로 저장

pd.read_csv(file_name)

csv


:bookmark:출처 - 멜론차트 TOP100
https://www.melon.com/chart/index.htm
- 정규표현식 텍스트 전처리
https://codingspooning.tistory.com/138
https://kynk94.github.io/devlog/post/re-match-hangul
- Web scraping dynamic content only using beautiful soup
https://sdallman.medium.com/web-scraping-dynamic-content-only-using-beautiful-soup-631496473c0e
- 이미지 저장
https://www.delftstack.com/ko/howto/python/download-image-in-python/
- DataFrame iterrows
https://stackoverflow.com/questions/16476924/how-to-iterate-over-rows-in-a-dataframe-in-pandas


:mortar_board:포스팅 공지

작성한 포스팅은 멋쟁이 사자처럼 AI SCHOOl의 수업 내용입니다.

댓글남기기