블로그 글 아카이빙 패턴 (셀레니움 없이)¶
발행한 블로그 글을 로컬 마크다운으로 백업하거나, 다른 채널(티스토리/브런치)에 옮겨 쓸 때 활용하는 크롤링 패턴.
핵심 비유: 블로그 글이 "감춘 방(iframe)" 안에 있을 때, 그 방의 진짜 주소를 알면 셀레니움 없이 직접 들어갈 수 있다.
1. 왜 셀레니움이 아니라 그냥 fetch?¶
브라우저 자동화(셀레니움)는 무거워. 크롬 띄우고, 페이지 로드 기다리고, JS 실행 끝날 때까지 또 기다리고. 글 한 편 가져오는 데 10초 이상 걸림.
대부분의 블로그 글은 사실 그냥 HTML로 응답해. requests.get(URL) 한 줄이면 본문 다 담긴 HTML이 옴. 셀레니움은 마지막 수단.
| 방식 | 속도 | 의존성 | 안정성 |
|---|---|---|---|
requests + BeautifulSoup |
< 1초 | 가벼움 | 높음 (직접 HTTP) |
Selenium |
5~15초 | 크롬 필요 | 페이지 구조 변경에 약함 |
→ 먼저 requests로 시도하고, JS 렌더링 필수일 때만 셀레니움.
2. iframe 함정과 우회¶
네이버 블로그를 fetch하면 빈 껍데기만 옴. 본문이 안 보여. 왜?
<!-- https://blog.naver.com/{id}/{logNo} 응답 -->
<iframe id="mainFrame" src="/PostView.naver?blogId={id}&logNo={logNo}"></iframe>
본문이 <iframe> 안에 들어있어. 사용자 브라우저는 iframe도 자동으로 로드해서 함께 보여주지만, requests는 메인 HTML만 가져오고 끝.
우회법: iframe src에 적힌 진짜 주소를 직접 fetch.
# 원본 URL
blog_url = "https://blog.naver.com/mintnamu147/224280890904"
# iframe 안 진짜 URL
postview_url = "https://blog.naver.com/PostView.naver?blogId=mintnamu147&logNo=224280890904"
# 변환
import re
m = re.match(r'https?://blog\.naver\.com/([^/]+)/(\d+)', blog_url)
blog_id, log_no = m.group(1), m.group(2)
postview_url = f"https://blog.naver.com/PostView.naver?blogId={blog_id}&logNo={log_no}"
후자로 fetch하면 본문 다 옴. 200 OK + HTML.
3. 일반 패턴 (네이버/티스토리/브런치 다 비슷)¶
import re
import requests
from bs4 import BeautifulSoup
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36",
}
r = requests.get(postview_url, headers=headers, timeout=30)
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
# 본문 컨테이너 (네이버 SmartEditor ONE)
container = soup.select_one(".se-main-container")
# 컴포넌트 순회 (텍스트, 사진, 인용문, 구분선...)
for comp in container.select(".se-component"):
cls = " ".join(comp.get("class", []))
if "se-text" in cls:
... # 텍스트 단락
elif "se-image" in cls:
... # 이미지
elif "se-quotation" in cls:
... # 인용문
각 사이트별 본문 컨테이너 셀렉터:
| 사이트 | 셀렉터 |
|---|---|
| 네이버 블로그 (스마트에디터 ONE) | .se-main-container |
| 네이버 블로그 (구버전) | #postViewArea |
| 티스토리 | .entry-content (스킨마다 다름) |
| 브런치 | .wrap_body |
User-Agent 헤더는 반드시 넣어야 함. 안 넣으면 봇으로 인식해서 막는 사이트 많음.
4. 함정: 보이지 않는 문자¶
웹 에디터들이 빈 줄에 zero-width space ( U+200B), non-breaking space ( U+00A0) 같은 invisible 문자를 넣음. 텍스트 추출 시 이게 남으면 빈 단락이 자꾸 생김.
import re
def clean_text(s):
# zero-width / NBSP 등 제거
s = re.sub(r'[ ]+', '', s)
return s.strip()
# 사용
text = clean_text(p.get_text())
if not text:
continue # 빈 단락 skip
후처리 필터도 추가: 가끔 ## 만 남는 빈 헤딩이 만들어지면 마지막에 한 번 더 정리.
cleaned = []
for block in blocks:
stripped = block.strip()
if not stripped:
continue
m = re.match(r'^##\s*(.*)$', stripped)
if m and not clean_text(m.group(1)):
continue
cleaned.append(block)
5. <a> 링크 처리¶
본문에 사용자가 직접 박은 하이퍼링크가 있으면 마크다운 [text](href)로 변환:
def paragraph_to_md(p):
parts = []
for node in p.descendants:
if getattr(node, "name", None) == "a":
href = node.get("href", "").strip()
text = clean_text(node.get_text())
if text and href:
parts.append(f"[{text}]({href})")
if parts:
return " ".join(parts)
return clean_text(p.get_text())
6. 실전 적용 사례¶
D:\claude-lecture\260408 9 대구 국민건강보험 대구경북지역본부\260510 일기콘 668 건보공단 바이브코딩\archive_naver.py
- 네이버 블로그 발행 후 URL을 인자로 넘기면
- 헤더(일기콘 + 강의 메타) + 본문 + 이미지 + 텍스트 링크 모두 추출
archived-naver.md로 저장- 셀레니움 한 줄도 안 씀. 30초 안에 완료.
7. 다른 사이트도 같은 패턴¶
티스토리/브런치/워드프레스 등 대부분 같은 흐름:
- 정확한 본문 URL 찾기 (iframe 있으면 우회)
requests+User-Agent로 HTML 가져오기BeautifulSoup로 본문 컨테이너 셀렉터 찾기- 컴포넌트별 마크다운 변환
- invisible 문자 + 빈 헤딩 정리
<a>태그 마크다운 링크화
셀레니움이 정말 필요한 경우: 로그인 후에만 보이는 페이지, 무한 스크롤, JS로 데이터 동적 로드 (관리자 화면 등). 그게 아니면 안 씀.
8. 관련 메모리¶
~/.claude/projects/D--Sites/memory/reference_naver_blog_pipeline.md- 네이버 블로그 발행 4종 세트 (발행 + 아카이브 통합)- 셀레니움 일반 패턴 →
selenium-automation.md