콘텐츠로 이동

블로그 글 아카이빙 패턴 (셀레니움 없이)

발행한 블로그 글을 로컬 마크다운으로 백업하거나, 다른 채널(티스토리/브런치)에 옮겨 쓸 때 활용하는 크롤링 패턴.

핵심 비유: 블로그 글이 "감춘 방(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초 안에 완료.
python archive_naver.py "https://blog.naver.com/mintnamu147/224280890904"

7. 다른 사이트도 같은 패턴

티스토리/브런치/워드프레스 등 대부분 같은 흐름:

  1. 정확한 본문 URL 찾기 (iframe 있으면 우회)
  2. requests + User-Agent 로 HTML 가져오기
  3. BeautifulSoup 로 본문 컨테이너 셀렉터 찾기
  4. 컴포넌트별 마크다운 변환
  5. invisible 문자 + 빈 헤딩 정리
  6. <a> 태그 마크다운 링크화

셀레니움이 정말 필요한 경우: 로그인 후에만 보이는 페이지, 무한 스크롤, JS로 데이터 동적 로드 (관리자 화면 등). 그게 아니면 안 씀.


8. 관련 메모리

  • ~/.claude/projects/D--Sites/memory/reference_naver_blog_pipeline.md - 네이버 블로그 발행 4종 세트 (발행 + 아카이브 통합)
  • 셀레니움 일반 패턴 → selenium-automation.md