셀레니움(Selenium)은 파이썬 코드로 실제 웹 브라우저(Chrome, Edge 등)를 자동으로 제어할 수 있게 해주는 웹 자동화 라이브러리입니다. 일반적인 requests 크롤링은 서버에서 받은 HTML만 분석하지만, Selenium은 브라우저를 직접 실행하여 버튼 클릭, 검색 입력, 스크롤, 로그인, 페이지 이동 등의 사용자 동작을 그대로 수행할 수 있기 때문에 JavaScript로 동적으로 생성되는 데이터까지 가져올 수 있습니다. 따라서 멜론, 유튜브, 쇼핑몰처럼 JavaScript 렌더링이 많은 사이트의 크롤링이나 웹 자동화 테스트에서 매우 많이 사용됩니다. 보통 webdriver.Chrome()으로 브라우저를 실행하고, find_element()로 요소를 찾으며, click(), send_keys() 등을 통해 자동화 작업을 수행합니다.
!python -m pip install selenium
JavaScript가 실행되어야 화면에 데이터가 나타나는 구조
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>자바스크립트 동적 페이지</title>
</head>
<body>
<h1>오늘의 과일 목록</h1>
<div id="fruit-list"></div>
<script>
const fruits = ["사과", "바나나", "오렌지", "딸기"];
const div = document.getElementById("fruit-list");
fruits.forEach((fruit) => {
div.innerHTML += `<p class="fruit">${fruit}</p>`;
});
</script>
</body>
</html>
import requests
from bs4 import BeautifulSoup
url = "http://127.0.0.1:5500/7.html"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
fruits = soup.select(".fruit")
print(fruits)
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
url = "http://127.0.0.1:5500/7.html"
driver.get(url)
# JavaScript 실행 대기
time.sleep(2)
fruits = driver.find_elements(By.CLASS_NAME, "fruit")
for fruit in fruits:
print(fruit.text)
driver.quit()
👉 find_elements()는 Selenium에서 HTML 요소를 여러 개 찾을 때 사용하는 메서드입니다. 하나의 요소만 찾는 find_element()와 달리, 조건에 맞는 요소들을 리스트 형태로 모두 반환합니다. 예를 들어 여러 개의 이미지, 게시글, 테이블 행(tr) 등을 반복해서 크롤링할 때 매우 자주 사용됩니다. 찾는 방식은 By.ID, By.CLASS_NAME, By.CSS_SELECTOR, By.XPATH 등 다양한 선택자를 사용할 수 있으며, 반환 결과는 리스트이므로 for문으로 반복 처리하는 경우가 많습니다. 또한 find_element()는 요소를 찾지 못하면 에러가 발생하지만, find_elements()는 빈 리스트([])를 반환하기 때문에 반복 크롤링에서 더 안정적으로 사용되는 경우도 많습니다.
import time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def melon_search_from_main(keyword):
options = Options()
options.add_argument("--start-maximized")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 10)
data = []
try:
driver.get("https://www.melon.com/")
time.sleep(2)
search_box = wait.until(
EC.presence_of_element_located((By.ID, "top_search"))
)
search_box.clear()
search_box.send_keys(keyword)
search_box.send_keys(Keys.ENTER)
time.sleep(3)
song_tab = wait.until(
EC.element_to_be_clickable(
(By.XPATH, '//*[@id="divCollection"]/ul/li[3]/a/span')
)
)
song_tab.click()
time.sleep(3)
song_table = wait.until(
EC.presence_of_element_located(
(By.XPATH, '//*[@id="frm_defaultList"]/div/table')
)
)
rows = song_table.find_elements(By.CSS_SELECTOR, "tbody tr")
print("찾은 행 개수:", len(rows))
for row in rows:
try:
cols = row.find_elements(By.TAG_NAME, "td")
if len(cols) < 5:
continue
# 곡명 td
title_text = cols[2].text.strip()
title_lines = [
line.strip()
for line in title_text.split("\n")
if line.strip()
]
title = ""
for line in title_lines:
if (
"재생" not in line
and "담기" not in line
and "상세정보" not in line
and not line.startswith("Title")
):
title = line
break
# Title 줄이 더 정확한 경우 보정
for line in title_lines:
if line.startswith("Title "):
title = line.replace("Title ", "").strip()
break
# 아티스트
artist_text = cols[3].text.strip()
artist_lines = [
line.strip()
for line in artist_text.split("\n")
if line.strip()
]
artist = artist_lines[0] if artist_lines else ""
# 앨범
album_text = cols[4].text.strip()
album_lines = [
line.strip()
for line in album_text.split("\n")
if line.strip()
]
album = album_lines[0] if album_lines else ""
# 좋아요 수
like = ""
try:
like = row.find_element(
By.CSS_SELECTOR,
"button.like span.cnt"
).text.strip()
except:
pass
if title:
data.append({
"곡명": title,
"아티스트": artist,
"앨범": album,
"좋아요수": like
})
except Exception as e:
print("행 처리 오류:", e)
df = pd.DataFrame(data)
if not df.empty:
df.index = df.index + 1
file_name = f"melon_{keyword}_songs.csv"
df.to_csv(file_name, encoding="utf-8-sig")
print(f"CSV 저장 완료: {file_name}")
print(f"총 {len(df)}곡 수집 완료")
return df
finally:
driver.quit()
👉 Options().add_argument()은 Selenium에서 Chrome 브라우저를 어떤 상태로 실행할지 설정하는 코드입니다. Options()는 크롬 실행 옵션을 담는 객체이고, add_argument()는 크롬을 실행할 때 추가 명령어를 넣는 메서드입니다. 여기서 "--start-maximized"는 브라우저를 처음부터 최대화된 상태로 실행하라는 의미입니다. 크롤링할 때 브라우저 창이 작으면 검색창, 버튼, 다음 페이지 버튼 등이 숨겨지거나 모바일 레이아웃으로 바뀔 수 있기 때문에, 보통 안정적인 크롤링을 위해 창을 크게 열어두는 경우가 많습니다.
👉 WebDriverWait는 Selenium에서 특정 요소가 나타날 때까지 일정 시간 동안 기다려주는 기능입니다. 웹 페이지는 JavaScript로 동적으로 로딩되는 경우가 많기 때문에, 페이지가 열리자마자 바로 요소를 찾으면 아직 화면에 생성되지 않아 에러가 발생할 수 있습니다. 이때 WebDriverWait를 사용하면 “최대 몇 초까지 기다리면서 조건이 만족되면 바로 다음 코드 실행” 방식으로 동작하여 크롤링 안정성을 높일 수 있습니다.
df = melon_search_from_main("신의키스")
df
import time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
def extract_current_page(driver, wait):
data = []
try:
song_table = wait.until(
EC.presence_of_element_located(
(By.XPATH, '//*[@id="frm_defaultList"]/div/table')
)
)
except TimeoutException:
return []
rows = song_table.find_elements(By.CSS_SELECTOR, "tbody tr")
for row in rows:
cols = row.find_elements(By.TAG_NAME, "td")
if len(cols) < 5:
continue
title_lines = [
line.strip()
for line in cols[2].text.split("\n")
if line.strip()
]
title = ""
for line in title_lines:
if line.startswith("Title "):
title = line.replace("Title ", "").strip()
break
if not title:
for line in title_lines:
if (
"재생" not in line
and "담기" not in line
and "상세정보" not in line
and not line.startswith("Title")
):
title = line
break
artist_lines = [
line.strip()
for line in cols[3].text.split("\n")
if line.strip()
]
artist = artist_lines[0] if artist_lines else ""
album_lines = [
line.strip()
for line in cols[4].text.split("\n")
if line.strip()
]
album = album_lines[0] if album_lines else ""
try:
like = row.find_element(
By.CSS_SELECTOR,
"button.like span.cnt"
).text.strip()
except:
like = ""
if title:
data.append({
"곡명": title,
"아티스트": artist,
"앨범": album,
"좋아요수": like
})
return data
def melon_search_all_pages(keyword, max_page=30):
options = Options()
options.add_argument("--start-maximized")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 10)
all_data = []
try:
driver.get("https://www.melon.com/")
time.sleep(2)
search_box = wait.until(
EC.presence_of_element_located((By.ID, "top_search"))
)
search_box.clear()
search_box.send_keys(keyword)
search_box.send_keys(Keys.ENTER)
time.sleep(3)
song_tab = wait.until(
EC.element_to_be_clickable(
(By.XPATH, '//*[@id="divCollection"]/ul/li[3]/a/span')
)
)
song_tab.click()
time.sleep(3)
for page in range(1, max_page + 1):
page_data = extract_current_page(driver, wait)
if not page_data:
print(f"{page}페이지 데이터가 없어 종료합니다.")
break
all_data.extend(page_data)
print(f"{page}페이지 크롤링 완료: {len(page_data)}곡")
next_start_index = page * 50 + 1
try:
driver.execute_script(
f"pageObj.sendPage('{next_start_index}');"
)
time.sleep(3)
except Exception as e:
print("다음 페이지 이동 실패:", e)
break
df = pd.DataFrame(all_data)
if not df.empty:
df = df.drop_duplicates(
subset=["곡명", "아티스트", "앨범"]
)
df.index = df.index + 1
file_name = f"melon_{keyword}_all_songs.csv"
df.to_csv(
file_name,
encoding="utf-8-sig"
)
print(f"CSV 저장 완료: {file_name}")
print(f"총 {len(df)}곡 수집 완료")
return df
finally:
driver.quit()
import time
import re
import pandas as pd
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
def fetch_starbucks():
url = "https://www.starbucks.co.kr/index.do"
driver = webdriver.Chrome()
driver.maximize_window()
driver.get(url)
time.sleep(2)
# 사과문 팝업 닫기
try:
close_btn = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable(
(By.XPATH, "/html/body/div[5]/p/a")
)
)
close_btn.click()
print("사과문 팝업 닫기 완료")
time.sleep(1)
except:
print("사과문 팝업이 없거나 이미 닫혀 있습니다.")
# 메뉴 이동
action = ActionChains(driver)
first_tag = driver.find_element(
By.CSS_SELECTOR,
"#gnb > div > nav > div > ul > li.gnb_nav03"
)
second_tag = driver.find_element(
By.CSS_SELECTOR,
"#gnb > div > nav > div > ul > li.gnb_nav03 > div > div > div > ul:nth-child(1) > li:nth-child(3) > a"
)
action.move_to_element(first_tag) \
.move_to_element(second_tag) \
.click() \
.perform()
# 서울 선택
seoul_tag = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((
By.CSS_SELECTOR,
"#container > div > form > fieldset > div > section > article.find_store_cont > article > article:nth-child(4) > div.loca_step1 > div.loca_step1_cont > ul > li:nth-child(1) > a"
))
)
seoul_tag.click()
# 구 목록 로딩 대기
WebDriverWait(driver, 5).until(
EC.presence_of_all_elements_located(
(By.CLASS_NAME, "set_gugun_cd_btn")
)
)
gu_elements = driver.find_elements(
By.CLASS_NAME,
"set_gugun_cd_btn"
)
# 전체 선택
gu_elements[0].click()
# 매장 목록 로딩 대기
WebDriverWait(driver, 5).until(
EC.presence_of_all_elements_located(
(By.CLASS_NAME, "quickResultLstCon")
)
)
# HTML 가져오기
req = driver.page_source
soup = BeautifulSoup(req, "html.parser")
stores = soup.find(
'ul',
'quickSearchResultBoxSidoGugun'
).find_all('li')
# 데이터 저장 리스트
store_list = []
addr_list = []
lat_list = []
lng_list = []
# 데이터 추출
for store in stores:
store_name = store.find("strong").text
store_addr = store.find("p").text
# 전화번호 제거
store_addr = re.sub(
r'\d{4}-\d{4}$',
'',
store_addr
).strip()
store_lat = store['data-lat']
store_lng = store['data-long']
store_list.append(store_name)
addr_list.append(store_addr)
lat_list.append(store_lat)
lng_list.append(store_lng)
# 데이터프레임 생성
df = pd.DataFrame({
'store': store_list,
'addr': addr_list,
'lat': lat_list,
'lng': lng_list
})
driver.quit()
return df
# 함수 실행
starbucks_df = fetch_starbucks()
# CSV 저장
starbucks_df.to_csv(
"starbucks_seoul.csv",
index=False,
encoding='utf-8-sig'
)
print("데이터가 starbucks_seoul.csv 파일로 저장되었습니다.")
print(starbucks_df.head())
| 크롤링을 위한 HTML과 CSS (0) | 2026.06.03 |
|---|---|
| pandas (1) | 2026.05.22 |
| numpy (0) | 2026.05.20 |