한 줄 요약 · requests/BeautifulSoup로 네이버 블로그의 목록과 본문을 수집하는 방법, 그리고 정적 크롤링의 한계를 넘어 Selenium으로 브라우저를 직접 제어해 동적 콘텐츠까지 수집하는 전체 흐름을 정리한다.
목차
- Part 1. 다중 페이지 수집 코드 함수화
- Part 2. 네이버 블로그 목록 정적 크롤링
- Part 3. 네이버 블로그 본문 텍스트 추출
- Part 4. Selenium 동적 크롤링 기초
- Part 5. 스크롤·탭 이동·동적 목록 수집 실습
Part 1. 다중 페이지 수집 코드 함수화
정적 크롤링에서 여러 페이지를 수집할 때의 핵심은 URL에 포함된 페이지 번호를 바꿔가며 요청하는 것이다. 페이지 정보가 URL에 들어 있는 사이트는 동일한 패턴으로 확장할 수 있으므로, 반복문 안에서 URL을 포맷팅해 원하는 페이지 범위를 순회하도록 설계한다.
셀 단위로 실행하던 코드를 함수로 통합할 때는 "입력(매개변수) → 처리(수집·정제) → 출력(반환)" 구조를 고정하는 것이 중요하다. 함수 기반이 셀 기반보다 유지보수와 재사용에 훨씬 유리하기 때문이다.
함수 설계의 핵심 요소
| 요소 | 예시 | 의미 |
|---|---|---|
| 매개변수 1 | end_page: int | 마지막 페이지 번호를 정수로 받는다. |
| 매개변수 2 | search: str = '' | 기본은 검색어 없음이며, 입력 시 필터링에 사용한다. |
| 리턴 힌트 | -> pd.DataFrame | 최종 반환 타입을 데이터프레임으로 명시한다. |
★ 핵심 · 검색어 필터링은 "검색어가 있는 경우 / 없는 경우"를 조건문으로 분기하는 것이 안정적이다. 파이썬에서 빈 문자열은False로 평가되므로if search:패턴이 디폴트 값 처리에 적합하다. 조건이 참이면str.contains(search)로 포함 여부를 필터링하고, 아니면 원본을 그대로 사용한다.
⚠️ 주의 · 반복 범위는range(1, end_page + 1)로 작성한다.range는 종료값을 포함하지 않으므로,end_page까지 수집하려면+ 1보정이 반드시 필요하다.range(1, end_page)로 쓰면 마지막 페이지가 누락된다.
함수 골격
def search_recipe(end_page: int, search: str = '') -> pd.DataFrame:
# 1) 리스트 초기화
# 2) for page in range(1, end_page + 1): URL 포맷팅 → requests.get → 파싱 → select
# 3) DataFrame 생성
# 4) if search: contains 필터링 + copy, else: 전체 copy
# 5) 인덱스 정리 후 return
...
💡 실무 팁 ·copy()는 원본 데이터프레임이 의도치 않게 변형되는 위험을 줄이기 위한 방어적 복사로 이해하면 된다. 또한 함수 선언부의 자료형 힌트(end_page: int,-> pd.DataFrame)는 실행에 필수는 아니지만, "이 매개변수는 정수가 기대된다", "리턴은 데이터프레임이다"를 명시하는 문서화 효과가 있어 협업·장기 유지보수에서 가독성을 높인다.
Part 2. 네이버 블로그 목록 정적 크롤링
네이버 블로그 검색 결과도 GET 방식이며, 검색어가 URL의 query 파라미터에 포함된다. 따라서 검색어를 변수로 치환한 URL 템플릿을 만들면 재사용성이 좋아진다.
⚠️ 주의 · 브라우저에서 복사한 URL은 한글이 인코딩되어("암호화처럼 보이는 문자열") 나타날 수 있다. 또한 같은 콘텐츠 페이지라도 유입 경로 추적(마케팅·분석 목적) 때문에 URL이 조금씩 달라진다. 그러므로 query={keyword} 형태로 검색어만 치환 가능한 구조로 정리하고, 같은 경로·같은 페이지로 접근하도록 URL 형태를 통일해 두면 재현성에 유리하다.
목록 요소 선택과 수집
네이버 검색 결과 DOM은 클래스가 길고 다중 클래스가 흔하다. 공백으로 구분된 클래스를 CSS 선택자에서는 .으로 연결해 사용한다. 타이틀·블로거명·게시일 세 가지를 각각 선택해 리스트로 모은 뒤 DataFrame으로 만든다.
| 항목 | 특징 | 해결 접근 |
|---|---|---|
| 타이틀 | 긴 클래스가 정상 매칭됨 | 다중 클래스를 .으로 연결 후 .text 사용 |
| 블로거명 | 중복·불필요 텍스트가 함께 잡혀 개수가 어긋남 | 상위(부모) 요소로 한 단계 올려 더 타이트하게 선택 |
| 게시일 | 비교적 단순하게 매칭됨 | 동일 방식으로 .text 추출 |
import requests as req
import pandas as pd
from bs4 import BeautifulSoup as bs
def search_blog(keyword: str) -> pd.DataFrame:
# 통신 설정
headers = {"User-Agent": "Mozilla/5.0 ... Chrome/148.0.0.0 Safari/537.36"}
url = f"https://search.naver.com/search.naver?ssc=tab.blog.all&sm=tab_jum&query={keyword}"
res = req.get(url, headers=headers)
soup = bs(res.text, "lxml")
# 타이틀 / 블로거명 / 게시일 선택자 (실제 클래스는 길어 축약 표기)
title = soup.select(".sds-comps-text...headline1...")
bloger = soup.select(".fender-ui_... > .sds-comps-text...") # 부모로 한 단계 올려 타이트하게
date = soup.select(".sds-comps-text...profile-info-subtext")
title_list, bloger_list, date_list = [], [], []
for i in range(len(title)):
title_list.append(title[i].text)
bloger_list.append(bloger[i].text)
date_list.append(date[i].text)
blog_dic = {"타이틀": title_list, "블로거": bloger_list, "게시일": date_list}
blog_df = pd.DataFrame(blog_dic, index=range(1, len(title_list) + 1))
blog_df.index.name = "게시순서"
return blog_df
search_blog("아토피")
★ 핵심 · 블로거명처럼 동일 선택자가 과도하게 매칭(예: 90개, 중복·불필요 텍스트 포함)될 수 있는 요소는, 상위(부모) 요소로 한 단계 올려 범위를 좁힌 뒤 하위 요소를 선택하는 전략이 필요하다. 세 리스트의 길이가 어긋나면 DataFrame 생성 단계에서 깨지므로 개수 정합성을 먼저 확인하는 습관이 중요하다.
Part 3. 네이버 블로그 본문 텍스트 추출
블로그 글 내부로 들어가면 본문 텍스트는 글꼴·크기·볼드·기울임 등 편집 요소에 따라 태그와 클래스가 달라져 선택자가 분산된다. 이때 개별 텍스트 요소를 일일이 선택하기보다, 본문 전체를 감싸는 컨테이너를 잡아 .text를 호출하면 하위의 다양한 태그에 포함된 텍스트를 한 번에 가져올 수 있다.
⚠️ 주의 · 네이버 블로그 글은 iframe 구조가 흔하다. "겉 URL"로 접근하면 원하는 본문이 보이지 않을 수 있으므로, iframe이 적용된 실제 글 URL을 기준으로 접근해야 한다.
url = "https://blog.naver.com/PostView.naver?blogId=...&logNo=..."
res_blog = req.get(url, headers=headers)
soup_blog = bs(res_blog.text, "lxml")
# 본문 영역은 하나이므로 select_one 사용 (인덱싱 생략 가능)
main_content = soup_blog.select_one("div.se-main-container")
print(main_content.text.strip()) # 양끝 공백·개행 제거
print(main_content.text.replace("\n", "")) # 본문 내 개행 일괄 제거
💡 실무 팁 · 단일 요소는select_one을 사용하면[0]인덱싱을 줄일 수 있다. 텍스트 정제는 문자열 처리로 접근한다. 블로그 특성상 줄바꿈이 많으므로strip()만으로는 부족할 수 있으며, 분석 목적이라면replace('\n', '')로 개행을 일괄 제거한다. 특정 태그만 필요하면 컨테이너 하위에서 추가 선택으로 확장한다.
Part 4. Selenium 동적 크롤링 기초
정적 크롤링(requests + BeautifulSoup)은 "요청 시점의 HTML"에서만 추출이 가능하다. 따라서 스크롤 이후 로드되는 콘텐츠나 클릭으로 전환되는 화면은 다루기 어렵다. Selenium은 실제 브라우저를 띄우고 제어하므로, 사람이 인터넷을 사용하는 것과 동일하게 페이지를 탐색하며 동적 콘텐츠까지 수집할 수 있다.
★ 핵심 · 정적/동적의 가장 큰 차이는 "수집 가능한 범위"다. 정적은 최초 응답 HTML에 한정되고, 동적(Selenium)은 사용자 상호작용(클릭·스크롤)으로 추가 로드되는 콘텐츠까지 포함한다. 면접 단골 질문이므로 한 문장으로 답할 수 있어야 한다.
⚠️ 주의 · Selenium은 페이지 전환 시 DOM이 초기화·변경된다. 즉, 페이지가 바뀌면 이전에 찾아둔 요소 참조가 무효가 될 수 있으므로 "요소를 다시 찾아야 하는 타이밍"을 항상 고려해 코드를 작성해야 한다.
요소 선택 방식과 권장 접근
| 목적 | 권장 접근 | 비고 |
|---|---|---|
| 검색창 찾기 | By.ID 또는 By.CSS_SELECTOR | #query처럼 ID는 안정적 |
| 버튼 클릭 | By.CSS_SELECTOR 또는 By.XPATH | 단일 고정 버튼은 XPath도 편리 |
| 다량 목록 수집 | find_elements + CSS_SELECTOR | 반복 수집에는 구조 기반 선택이 적합 |
| 절대경로 접근 | By.XPATH | 잘 바뀌지 않는 고정 요소에 한해 권장 |
!pip install selenium
from selenium import webdriver as wb
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
driver = wb.Chrome() # 브라우저 실행 ('자동화 소프트웨어가 제어 중' 문구 확인)
driver.get("https://www.naver.com") # 페이지 이동
# 검색창 찾기 → 검색어 입력 → 엔터
search = driver.find_element(By.CSS_SELECTOR, "#query")
search.send_keys("부트캠프")
search.send_keys(Keys.ENTER)
driver.back() # 뒤로가기: 웹 요소가 아닌 브라우저 기능 → driver에서 수행
# 엔터 대신 돋보기 버튼 클릭으로 검색
search = driver.find_element(By.ID, "query")
search.send_keys("오늘의 날씨")
btn = driver.find_element(By.CSS_SELECTOR, "#search-btn-svg")
btn.click()
# XPATH 절대경로로 동일 버튼 접근
btn = driver.find_element(By.XPATH, '//*[@id="search-btn"]/span[1]')
btn.click()
# 스크롤: body 태그를 잡아 END 키 전송
body = driver.find_element(By.TAG_NAME, "body")
body.send_keys(Keys.END)
driver.quit() # 브라우저 종료 + 세션·메모리 정리
★ 핵심 ·find_element는 조건에 맞는 첫 요소 하나,find_elements는 조건에 맞는 모든 요소를 리스트로 반환한다. 단수/복수의s차이를 명확히 구분해야 한다. 키보드 입력은send_keys로 처리하며,Keys.ENTER같은 특수키도 보낼 수 있다.
⚠️ 주의 · XPath 절대경로는 하나의 요소를 정확히 찍을 때 편리하지만, DOM이 바뀌면 깨지기 쉽다. 따라서 검색·메뉴 버튼처럼 잘 바뀌지 않는 고정 요소에 사용하는 것이 적합하다. 자주 바뀌거나 다수를 수집해야 하는 경우에는 구조 기반의 CSS 선택자가 유리하다.
💡 실무 팁 · 브라우저를 닫을 때quit()은 모든 창을 닫고 세션·연결·메모리까지 종료한다. 반면close()는 현재 창만 닫아 세션이 남을 수 있다. 리소스 정리 관점에서 작업 종료 시에는quit()을 사용하는 것이 안전하다.
Part 5. 스크롤·탭 이동·동적 목록 수집 실습
동적 페이지는 스크롤을 내리는 순간 추가 통신이 발생해 DOM이 늘어날 수 있다. 즉 스크롤 전후로 수집 가능한 요소 수가 달라지며, Selenium은 이를 이용해 "처음에는 없던 목록"까지 확보할 수 있다. 아래는 네이버 검색 → 블로그 탭 이동 → 스크롤 → 제목 수집 → CSV 저장으로 이어지는 실습 흐름이다.
탭 이동: nth-child로 형제 요소 지정
네이버 검색 결과 상단 탭(이미지·쇼핑·블로그 등)은 동일 구조의 형제 요소로 구성된다. "세 번째 탭"처럼 순서가 고정되어 있다면 가상 클래스 선택자 :nth-child(n)로 특정 탭을 안정적으로 지정할 수 있다. 부모 컨테이너를 먼저 잡고 >로 자식을 내려가며 지정한다.
# 1) 브라우저 실행 + 네이버 이동
driver = wb.Chrome()
driver.get("https://www.naver.com")
# 2) 검색어 입력 + 돋보기 클릭
search = driver.find_element(By.CSS_SELECTOR, "#query")
search.send_keys("텀블러")
btn = driver.find_element(By.CSS_SELECTOR, "#search-btn-svg")
btn.click()
# 3) 블로그 탭 클릭 (형제 중 3번째 → nth-child(3))
blog_tab = driver.find_element(
By.CSS_SELECTOR,
"#lnb ... .api_flicking_wrap._conveyer_root > div > div:nth-child(3)"
)
blog_tab.click()
# 4) 스크롤로 동적 콘텐츠 추가 로드
body = driver.find_element(By.TAG_NAME, "body")
body.send_keys(Keys.END)
import time
time.sleep(1) # 로드 대기 (고정 대기)
# 5) 제목 목록 수집: 다수 대상이므로 find_elements
titles = driver.find_elements(By.CSS_SELECTOR, ".sds-comps-text...headline1...")
title_list = [t.text for t in titles] # 각 요소에서 .text로 사람이 읽는 제목 추출
blog_df = pd.DataFrame(
title_list, columns=["블로그 제목"],
index=range(1, len(title_list) + 1)
)
# 6) CSV 저장
import os
os.makedirs("data", exist_ok=True) # 폴더 없으면 생성
blog_df.to_csv("data/blog1.csv", encoding="utf-8-sig") # 엑셀에서 한글 깨짐 방지
driver.quit()
⚠️ 주의 ·to_csv("data/blog1.csv")는data/폴더가 없으면FileNotFoundError가 발생한다. 저장 전에os.makedirs("data", exist_ok=True)로 폴더를 만들어 두면 안전하다. 또한 인코딩을 단순utf-8로 저장하면 윈도우 Excel에서 한글이 깨질 수 있으므로, BOM이 포함된utf-8-sig를 권장한다.
💡 실무 팁 ·time.sleep()은 무조건 정해진 시간을 멈추는 "고정 대기"라 페이지가 빨리 떠도 기다리고, 느리면 그래도 실패할 수 있다. 대신 특정 요소가 나타날 때까지만 기다리는 "명시적 대기"가 안정적이다. 강의 노트에도WebDriverWait+expected_conditions활용이 함께 제시되었다.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 제목 요소가 실제로 나타날 때까지 최대 10초 대기
WebDriverWait(driver, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, ".sds-comps-text...headline1...")
)
)
★ 핵심 · 동적 목록 수집의 정석은 "스크롤로 콘텐츠를 추가 로드 →find_elements로 복수 요소 수집 →.text로 텍스트 추출 → DataFrame 구성 → CSV 저장 →quit()으로 정리"의 순서다. 정적 크롤링과 데이터 처리(DataFrame·CSV) 단계는 동일하고, 앞단의 수집 방식만 브라우저 제어로 바뀐 것이라는 점을 이해하면 흐름이 자연스럽게 연결된다.
핵심 요약
- 정적 vs 동적 · 정적(requests/BS4)은 최초 응답 HTML만, 동적(Selenium)은 클릭·스크롤로 추가 로드되는 콘텐츠까지 수집한다.
- 다중 페이지 함수화 ·
range(1, end_page + 1)로 끝값을 보정하고,search: str = ''+if search:로 검색어 유무를 분기한다. - 선택자 전략 · 다중 클래스는
.으로 연결하고, 과매칭되는 요소는 부모로 한 단계 올려 타이트하게 좁힌다. 본문 전체는 컨테이너를select_one으로 잡아.text로 일괄 추출한다. - Selenium 기본 제어 ·
find_element(단수)/find_elements(복수),By로 선택 방식 지정, 페이지 이동·뒤로가기·스크롤은driver·body단위로 수행한다. - 안정성·정리 · 고정 대기(
sleep)보다 명시적 대기(WebDriverWait)가 안정적이며, 저장은utf-8-sig, 종료는quit()으로 리소스를 정리한다.
참고
- 그로스 마케팅 부트캠프 5기 — 크롤링 3일차 강의 (정적 크롤링 마무리와 셀레니움 입문)
- Selenium 공식 문서 — find_element / By / WebDriverWait
'그로스마케터 부트캠프' 카테고리의 다른 글
| 260602_웹 크롤링 입문(html, css, 정적 크롤링) (0) | 2026.06.09 |
|---|---|
| 260608_동적크롤링(한솥 / 뉴발란스) (0) | 2026.06.09 |
| 부트캠프 8일차_GA4 UI 익숙해지기 + UTM 개념 (0) | 2026.05.12 |
| 부트캠프5일차_유튜브광고 (0) | 2026.05.07 |
| 부트캠프4일차_그로스마케터 _구글 검색 광고(GDN / PMAX / Demand Gen) (1) | 2026.05.02 |