반응형
파이썬으로 웹 크롤링 하기!!
조원분들과 토이 프로젝트를 진행하고 있었다
주제는 Vector DB와 RAG를 구축하여 사연을 받고 해당 사연과 비슷한 감성의 노래를 Vector DB에서 찾아 추천해주는 시스템이다. LLM은 gpt-4o-mini를 썻던 걸로 기억한다
내가 맡은 부분은 Vector DB에 넣을 노래 자료, 제목, 가수, 가사를 크롤링해서 1000개 준비하기!!
일단 지니차트에서 TOP 200을 2025년,24년,23년,22년,21년 자료를 가지고 온다. 이 때 가사까지는 못 가지고 오니
제목과 가수만 가지고 온다
import requests
from bs4 import BeautifulSoup
import datetime
import time
import json
def crawl_genie_chart_page(page_num, date_str, songs_list):
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36'}
url = f'https://www.genie.co.kr/chart/top200?ditc=M&rtm=N&ymd={date_str}&pg={page_num}'
data = requests.get(url, headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')
trs = soup.select('#body-content > div.newest-list > div > table > tbody > tr')
count = 0
for tr in trs:
title = tr.select_one('a.title.ellipsis').text.strip()
artist = tr.select_one('a.artist.ellipsis').text.strip()
# 딕셔너리로 노래 정보 저장
song_info = {
"title": title,
"singer": artist
}
# 리스트에 추가
songs_list.append(song_info)
count += 1
print(f"{title} - {artist}")
return count
# JSON으로 저장할 노래 정보 리스트
songs_list = []
# 1000개의 곡을 가져오기 위해 여러 년도의 같은 날짜 데이터 크롤링
years = [2025, 2024, 2023, 2022, 2021] # 2025년부터 2021년까지
month_day = "0320" # 3월 20일
song_count = 0
max_songs = 1000
for year in years:
if song_count >= max_songs:
break
date_str = f"{year}{month_day}" # 예: 20250320, 20240320 등
# 각 년도별로 최대 20페이지까지 크롤링 (페이지당 50곡, 년도당 최대 1000곡)
page = 1
while song_count < max_songs and page <= 20: # 최대 20페이지(1000곡) 제한
# 페이지를 크롤링하고 songs_list에 노래 정보 추가
page_songs = crawl_genie_chart_page(page, date_str, songs_list)
song_count += page_songs
# 1000곡 도달하면 중단
if song_count >= max_songs:
break
# 서버 부하 방지를 위한 짧은 대기 시간
time.sleep(1)
page += 1
# 1000곡 도달하면 전체 크롤링 중단
if song_count >= max_songs:
break
# 연도간 대기 시간
time.sleep(3)
# 최대 1000곡까지만 저장
songs_list = songs_list[:max_songs]
# JSON 파일로 저장
with open('songs_data.json', 'w', encoding='utf-8') as f:
json.dump(songs_list, f, ensure_ascii=False, indent=2)
print(f"\n크롤링 완료! {len(songs_list)}개의 노래 정보를 songs_data.json 파일에 저장했습니다.")
성공적으로 1000개의 제목과 가수를 받아왔다.
이후 다시 해당 파일에서 제목과 가수를 뽑아내서 지니뮤직에 검색을 한 후 가사정보를 뽑아온다.
그리고 다시 JSON파일로 저장!!
물론 도중에 쓸데없는 정보들이 많이 들어가서 전처리가 어느 정도 필요하긴 하다
import requests
from bs4 import BeautifulSoup
import urllib.parse
import re
import json
import os
import time
def get_lyrics_from_genie(artist, song_title):
"""
지니뮤직에서 노래 가사를 검색하고 가져오는 함수
"""
print(f"{artist}의 {song_title} 가사를 지니뮤직에서 검색합니다...")
# 검색어 인코딩
query = urllib.parse.quote(f"{artist} {song_title}")
search_url = f"https://www.genie.co.kr/search/searchMain?query={query}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7'
}
try:
# 검색 결과 페이지 요청
response = requests.get(search_url, headers=headers)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# 첫 번째 검색 결과 찾기
song_items = soup.select('tr.list')
if not song_items:
print("검색 결과가 없습니다.")
return None
# 첫 번째 곡의 링크 찾기
first_song = song_items[0]
song_id_elem = first_song.select_one('a.btn-info')
if not song_id_elem:
print("곡 정보를 찾을 수 없습니다.")
return None
# 곡 ID 추출
song_info_link = song_id_elem['onclick']
song_id = song_info_link.split("'")[1]
# 가사 페이지 URL 생성
lyrics_url = f"https://www.genie.co.kr/detail/songInfo?xgnm={song_id}"
# 가사 페이지 요청
lyrics_response = requests.get(lyrics_url, headers=headers)
lyrics_response.raise_for_status()
lyrics_soup = BeautifulSoup(lyrics_response.text, 'html.parser')
# 가사 컨테이너 찾기
lyrics_container = lyrics_soup.select_one('div.lyrics')
if not lyrics_container:
print("가사를 찾을 수 없습니다.")
return None
# 가사 텍스트 추출 및 정제
lyrics = lyrics_container.get_text(separator='\n').strip()
# 불필요한 텍스트 제거
lyrics = re.sub(r'전체선택|프린트|담기|다운로드|더보기|선물하기|공유하기|오류신고하기', '', lyrics)
lyrics = re.sub(r'\d{2}:\d{2}', '', lyrics)
lyrics = re.sub(r'play|듣기|뮤비|티저', '', lyrics)
if '곡 정보' in lyrics:
lyrics = lyrics.split('곡 정보')[0]
lyrics = re.sub(r'\n\s*\n', '\n\n', lyrics)
lyrics = lyrics.strip()
# 제목과 가수명 정보 제거
lyrics = remove_song_info_from_lyrics(lyrics, song_title, artist)
# 과도한 요청 방지를 위한 딜레이
time.sleep(1)
return lyrics
except Exception as e:
print(f"오류 발생: {str(e)}")
return None
def remove_song_info_from_lyrics(lyrics, title, artist):
"""제목과 가수 반복되는 부분 제거"""
if title in lyrics:
lyrics = re.sub(f"{title}\\s*$", "", lyrics)
if artist in lyrics:
lyrics = re.sub(f"{artist}\\s*$", "", lyrics)
lyrics = re.sub(f"^{title}\\s*-\\s*{artist}\\s*\n", "", lyrics)
lyrics = re.sub(f"^{artist}\\s*-\\s*{title}\\s*\n", "", lyrics)
return lyrics.strip()
def find_lyrics(artist, song_title):
"""가사 찾기 함수"""
lyrics = get_lyrics_from_genie(artist, song_title)
return lyrics or "가사를 찾을 수 없습니다."
def create_lyrics_dataset():
input_file = "songs_data.json"
output_file = "title_singer_lyrics.json"
if not os.path.exists(input_file):
print(f"Error: {input_file} 파일이 존재하지 않습니다.")
return False
try:
# 입력 파일 읽기 - 파일 형식 오류 대응
with open(input_file, 'r', encoding='utf-8') as f:
file_content = f.read()
# 빈 파일 체크
if not file_content.strip():
print("파일이 비어 있습니다.")
return False
try:
# 일반적인 JSON 파싱 시도
data = json.loads(file_content)
print("일반 JSON 형식으로 데이터를 불러왔습니다.")
except json.JSONDecodeError as e:
# JSON 파싱 실패 시 수정 시도
print(f"JSON 파싱 오류: {str(e)}")
print("파일 형식 수정을 시도합니다...")
# 파일 내용 출력
print(f"파일 내용 미리보기: {file_content[:200]}...")
# 중괄호가 없는 경우 추가
if not file_content.strip().startswith('{') and not file_content.strip().startswith('['):
file_content = '[' + file_content + ']'
# 콤마 오류 수정 시도
file_content = file_content.replace('}{', '},{')
file_content = file_content.replace('}[', '},')
file_content = file_content.replace(']}', ']')
# 수정된 내용으로 다시 파싱 시도
try:
data = json.loads(file_content)
print("수정된 형식으로 데이터를 불러왔습니다.")
except json.JSONDecodeError:
# 그래도 실패하면 직접 예시 데이터 생성
print("JSON 형식 수정 실패. 직접 파일 내용을 확인해주세요.")
# 예시 데이터 (테스트용)
data = [{"title": "밤양갱", "singer": "비비"}]
print("테스트를 위한 예시 데이터를 생성했습니다.")
# 수정된 파일 백업
with open(input_file + ".backup", 'w', encoding='utf-8') as backup:
backup.write(file_content)
print(f"원본 파일을 {input_file}.backup으로 백업했습니다.")
# 정상적인 형식으로 파일 다시 쓰기
with open(input_file, 'w', encoding='utf-8') as fix:
json.dump(data, fix, ensure_ascii=False, indent=2)
print(f"파일을 정상적인 JSON 형식으로 수정했습니다.")
print("데이터 구조 확인...")
# 데이터 구조 확인 및 조정
if not isinstance(data, list):
print("데이터가 리스트 형식이 아닙니다. 변환합니다.")
if isinstance(data, dict):
if "songs" in data:
data = data["songs"]
print("'songs' 키에서 데이터를 추출했습니다.")
else:
data = [data]
print("단일 딕셔너리를 리스트로 변환했습니다.")
else:
print(f"지원되지 않는 데이터 형식입니다: {type(data)}")
return False
# 처리한 곡 수 카운터
processed_count = 0
total_count = len(data)
print(f"총 {total_count}개의 곡을 처리합니다.")
# 각 노래에 가사 추가
for i, song in enumerate(data):
# 진행 상황 표시
processed_count += 1
print(f"처리 중: {processed_count}/{total_count} ({processed_count/total_count*100:.1f}%)")
# 타입 체크 및 변환
if not isinstance(song, dict):
print(f"항목 {i}가 딕셔너리가 아닙니다. 건너뜁니다: {song}")
continue
# 이미 가사가 있으면 건너뛰기
if "lyrics" in song and song["lyrics"]:
print(f"{song.get('title', '알 수 없는 제목')}의 가사가 이미 존재합니다. 유지합니다.")
continue
title = song.get("title", "")
artist = song.get("singer", "")
# title과 singer가 모두 있는 경우에만 가사 찾기
if title and artist:
print(f"{artist}의 '{title}' 가사를 찾습니다.")
lyrics = find_lyrics(artist, title)
if lyrics and lyrics != "가사를 찾을 수 없습니다.":
song["lyrics"] = lyrics
print(f"가사를 성공적으로 추가했습니다.")
else:
song["lyrics"] = "가사를 찾을 수 없습니다."
print(f"가사를 찾을 수 없습니다.")
else:
print(f"제목 또는 가수 정보가 누락되었습니다: {song}")
# 100곡마다 중간 저장
if processed_count % 10 == 0: # 테스트를 위해 10곡마다 저장으로 변경
print(f"중간 저장 중... ({processed_count}/{total_count})")
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 최종 결과 저장
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"모든 작업이 완료되었습니다. 결과가 {output_file}에 저장되었습니다.")
return True
except Exception as e:
import traceback
print(f"오류 발생: {str(e)}")
print("상세 오류:")
traceback.print_exc()
return False
# 실행 코드
if __name__ == "__main__":
create_result = create_lyrics_dataset()
if create_result:
print("\n모든 작업이 완료되었습니다.")
else:
print("\n작업 중 오류가 발생했습니다.")
[
{
"title": "REBEL HEART",
"singer": "IVE (아이브)",
"lyrics": "REBEL HEART - \n\n시작은 항상 다 이룬 것처럼\r\n엔딩은 마치 승리한 것처럼\r\n겁내지 않고 마음을 쏟을래 \r\n내 모양대로\r\n\n이제 더 이상 신경 쓰지 않아\r\n어디에서도 내 맘을 지키기\r\n오해 받을 땐 자유에 맡겨둘래\r\n다 알게 될 거니까\r\n\nSo you can \r\n\nLove me, hate me\r\nYou will never be never be never be me\r\nTry me, I’ll break free\r\nYou will never be never be never be me\r\n\nWe are rebels in our heart,\r\nRebels in our heart \r\nWe are rebels in our heart\r\nWe are rebels in our heart,\r\n꺾이지 않아\r\nWe are rebels in our heart\r\n\n너는 어디가 조금 부족해\r\n너는 거기가 뭔가 좀 넘쳐\r\nI don’t care 뭐든 말은 참 쉽지\r\n그래 실행이 어려운 거야\r\n\nDo it, move it, do it \r\n우린 그냥 할게\r\n내 갈 길은 멀고 \r\n그 위에는 드라마가 있어 \r\n\n또 외로움이 너무 길어지는 밤엔 \r\n그 맘을 쏘아 올려\r\n\nLove me, hate me\r\nYou will never be never be never be me\r\nTry me, I’ll break free\r\nYou will never be never be never be me\r\n\nWe are rebels in our heart,\r\nRebels in our heart \r\nWe are rebels in our heart\r\nWe are rebels in our heart,\r\n꺾이지 않아\r\nWe are rebels in our heart\r\n\nRebels in our heart!\r\n\n우린 따로 이유를 묻지 않고 \r\n서로가 필요할 때가 있어\r\n그런 맘이 어떤 건지 잘 알기에\r\n영원을 바라는 사이보단\r\n지금을 이해해주고 싶어\r\nWe will always be the rebels \r\n\nCome, join as who you are\r\nWe are rebels, we are one\r\n이 마음만으로\r\nWe are rebels, we are one\r\n\nWe are rebels in our heart,\r\nRebels in our heart \r\nWe are rebels in our heart\r\nWe are rebels in our heart,\r\n꺾이지 않아\r\nWe are rebels in our heart\n\nREBEL HEART\n\nIVE (아이브)\n\nREBEL HEART (TEASER)\n\nIVE (아이브)\n\nIVE (아이브)\n\n1"
},
{
"title": "HOME SWEET HOME (Feat. 태양 & 대성)",
"singer": "G-DRAGON",
"lyrics": "가사를 찾을 수 없습니다."
},
{
"title": "나는 반딧불",
"singer": "황가람",
"lyrics": "나는 반딧불 - \n\n나는 내가 빛나는 별인 줄 알았어요\r\n한 번도 의심한 적 없었죠\r\n몰랐어요 난 내가 벌레라는 것을\r\n그래도 괜찮아 난 눈부시니까\r\n하늘에서 떨어진 별인 줄 알았어요\r\n소원을 들어주는 작은 별\r\n몰랐어요 난 내가 개똥벌레라는 것을\r\n그래도 괜찮아 나는 빛날 테니까\r\n나는 내가 빛나는 별인 줄 알았어요\r\n한 번도 의심한 적 없었죠\r\n몰랐어요 난 내가 벌레라는 것을\r\n그래도 괜찮아 난 눈부시니까\r\n한참 동안 찾았던 내 손톱\r\n하늘로 올라가 초승달 돼 버렸지\r\n주워 담을 수도 없게 너무 멀리 갔죠\r\n누가 저기 걸어놨어 누가 저기 걸어놨어\r\n우주에서 무주로 날아온\r\n밤하늘의 별들이 반딧불이 돼 버렸지\r\n내가 널 만난 것처럼 마치 약속한 것처럼\r\n나는 다시 태어났지 나는 다시 태어났지\r\n나는 내가 빛나는 별인 줄 알았어요\r\n한 번도 의심한 적 없었죠\r\n몰랐어요 난 내가 벌레라는 것을\r\n그래도 괜찮아 난 눈부시니까\r\n하늘에서 떨어진 별인 줄 알았어요\r\n소원을 들어주는 작은 별\r\n몰랐어요 난 내가 개똥벌레란 것을\r\n그래도 괜찮아 나는 빛날 테니까\n\n황가람\n\n1"
},
정상적으로??? 불러 올 줄 알았는데 HOME SWEET HOME은 못 불러 오네.
1000개의 데이터 중 90개 정도는 저렇게 "가사를 찾을 수 없습니다" 예외처리가 된다.
P.S
같은 코드, 같은 지니뮤직을 참조하는데 왜 90개는 안되는거지...?
혹시 이거 보시고 저작권 또는 기타 문제 생길 것 같으면 꼭 말씀해주세요 지니뮤직 관계자님들... 바로 삭제 조치할게요
반응형