RSS 피드 404 오류 자동 감지로 죽은 피드 한 번에 정리하는 Python 스크립트

RSS 피드를 수십 개씩 관리하다 보면, 어느 순간 절반이 응답조차 하지 않는 상태가 됩니다. 사이트가 닫혔거나, URL이 바뀌었거나, 아니면 그냥 서버가 내려간 경우입니다. 문제는 이걸 하나씩 눈으로 확인하기 전까지는 알 수 없다는 점입니다. RSS 피드 404 검증 작업을 자동화하지 않으면, 죽은 피드가 계속 목록에 남아 수집 오류를 반복하게 됩니다.

왜 RSS 피드 404 검증이 필요한가

RSS 피드 404 검증

RSS 기반 콘텐츠 수집 파이프라인을 운영할 때 가장 조용하게 쌓이는 문제가 바로 사망 피드입니다. 피드 자체는 목록에 살아있지만, 요청을 보낼 때마다 404나 410, 또는 연결 타임아웃이 발생하는 상태입니다.

이 상태가 쌓이면 두 가지 문제가 생깁니다.

  • 수집 스크립트 실행 시간이 늘어납니다. 응답 없는 URL에 대기 시간이 걸리기 때문입니다.
  • 오류 로그가 실제 문제를 가립니다. 진짜 오류와 죽은 피드 오류가 섞여서 어디서 터진 건지 파악이 어려워집니다.

수동으로 확인하는 방법은 피드가 10개 이하일 때만 현실적입니다. 50개, 100개가 넘어가면 자동화가 필수입니다.

피드 URL → HTTP 요청 → 상태 코드 분류 → 정상/경고/사망 구분

RSS 피드 404 검증 Python 스크립트 구조

기본 원리는 단순합니다. 피드 URL 목록에 HTTP GET 요청을 보내고, 응답 코드에 따라 상태를 분류하면 됩니다. 핵심은 타임아웃 처리와 리디렉션 추적을 함께 구현하는 것입니다.

아래는 기본 구조입니다.

import requests
import csv
from datetime import datetime

FEEDS = [
    "https://example.com/feed",
    "https://another-blog.com/rss",
    # 나머지 URL 목록
]

RESULTS = []

for url in FEEDS:
    try:
        response = requests.get(url, timeout=10, allow_redirects=True)
        status = response.status_code
        final_url = response.url
    except requests.exceptions.ConnectionError:
        status = "CONNECTION_ERROR"
        final_url = url
    except requests.exceptions.Timeout:
        status = "TIMEOUT"
        final_url = url

    RESULTS.append({
        "url": url,
        "status": status,
        "final_url": final_url,
        "checked_at": datetime.now().isoformat()
    })

# CSV로 저장
with open("feed_check_result.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["url", "status", "final_url", "checked_at"])
    writer.writeheader()
    writer.writerows(RESULTS)

print(f"검증 완료: {len(RESULTS)}개 처리")

이 스크립트 하나로 피드 목록 전체를 순회하고, 결과를 CSV로 저장합니다. 실행 후 feed_check_result.csv 파일을 열면 각 URL의 상태 코드가 한 줄씩 기록되어 있습니다.

상태 코드별 분류 기준

모든 응답이 같은 의미를 갖지는 않습니다. 상태 코드에 따라 대응 방법이 달라집니다.

상태 코드 의미 권장 처리
200 정상 응답 유지
301 / 302 리디렉션 (URL 변경) 최종 URL로 교체
404 페이지 없음 (피드 삭제) 목록에서 제거
410 영구 삭제 즉시 제거
403 접근 차단 (User-Agent 문제일 수 있음) 헤더 추가 후 재시도
TIMEOUT 응답 없음 3회 이상 반복 시 제거 검토
CONNECTION_ERROR DNS 오류 또는 서버 다운 사망 피드로 분류

403 오류는 주의가 필요합니다. 서버가 봇 요청을 차단하는 경우가 많아서, User-Agent 헤더를 추가하면 정상 응답을 받는 경우도 있습니다. 아래처럼 헤더를 추가하면 됩니다.

headers = {"User-Agent": "Mozilla/5.0 (compatible; FeedChecker/1.0)"}
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True)

상태 코드별 결과가 정리된 CSV 출력 예시

사망 피드 자동 감지 기능 추가

단순 상태 코드 확인에서 한 단계 더 나아가면, 결과에서 죽은 피드만 자동으로 골라내는 기능을 추가할 수 있습니다.

DEAD_CODES = [404, 410, "CONNECTION_ERROR", "TIMEOUT"]

dead_feeds = [r for r in RESULTS if r["status"] in DEAD_CODES]

# 사망 피드만 별도 저장
with open("dead_feeds.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["url", "status", "final_url", "checked_at"])
    writer.writeheader()
    writer.writerows(dead_feeds)

print(f"사망 피드: {len(dead_feeds)}개")

이 코드를 추가하면 전체 결과 CSV와 사망 피드만 담긴 dead_feeds.csv 두 파일이 생성됩니다. 사망 피드 목록을 별도로 관리하면, 원본 피드 목록에서 한 번에 제거하는 작업도 수월해집니다.

실무 적용 시 고려할 것들

피드가 많을수록 순차 요청은 시간이 걸립니다. 100개 기준으로 타임아웃을 10초로 설정하면 최대 16분이 소요될 수 있습니다. 병렬 처리가 필요하다면 concurrent.futures.ThreadPoolExecutor를 활용하면 됩니다. 동시 요청 수는 서버 부하를 고려해 5~10개 수준으로 제한하는 것이 일반적입니다.

주기적으로 실행하려면 cron(리눅스/맥) 또는 Windows 작업 스케줄러에 등록하면 됩니다. 주 1회 실행 정도면 대부분의 관리 요구를 충족합니다.

Python requests 라이브러리 공식 문서에서 타임아웃, 세션 관리, 리트라이 처리 방법을 더 자세히 확인할 수 있습니다. → requests 공식 문서

cron 또는 작업 스케줄러에 등록한 자동 실행 설정

자주 묻는 질문

Q. XML 파싱 오류가 나는 피드는 어떻게 처리하나요?

HTTP 상태 코드가 200이더라도 응답 본문이 유효한 RSS/Atom XML이 아닐 수 있습니다. feedparser 라이브러리를 함께 사용하면 파싱 성공 여부까지 검증할 수 있습니다. 파싱에 실패하면 별도 경고로 분류하는 것이 좋습니다.

Q. 피드 URL이 OPML 파일로 관리되고 있으면 어떻게 읽어오나요?

OPML은 XML 형식이므로 Python 표준 라이브러리의 xml.etree.ElementTree로 파싱할 수 있습니다. xmlUrl 속성 값을 추출해서 피드 목록을 구성하면 됩니다.

Q. 결과를 이메일로 받을 수 있나요?

Python smtplib를 사용하거나, SendGrid 같은 외부 메일 API를 연동하면 가능합니다. 사망 피드 수가 일정 기준을 넘었을 때만 알림을 보내는 조건부 발송도 구현할 수 있습니다.


RSS 피드 404 검증 자체는 어렵지 않습니다. 코드 구조도 단순한 편입니다. 다만 타임아웃 처리와 403 예외를 빠뜨리면 결과가 부정확해지는 경우가 있으니, 그 부분만 챙기면 충분히 실용적으로 동작합니다.

썸네일: Lightsaber Collection on Unsplash

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤