- Published on
feedparser + BeautifulSoup4로 RSS 크롤링 파이프라인 만들기
- Authors

- Name
- Hyo814
feedparser + BeautifulSoup4로 RSS 크롤링 파이프라인 만들기
PyNews 프로젝트의 핵심은 여러 RSS 소스에서 백엔드 관련 글만 골라 모으는 것이었습니다. 수집 자체는 feedparser 몇 줄이면 되지만, 실제로 쓸 만한 뉴스레터를 만들려면 필터링, HTML 정제, 중복 제거, 자동 태깅까지 필요했습니다.
이 글에서는 그 파이프라인 각 단계를 정리합니다.
1. RSS 소스 목록 — 설정을 코드에 선언
소스가 자주 바뀌지는 않아서 DB 테이블로 빼지 않고, 관리 명령 파일 상단에 dict 리스트로 두었습니다.
RSS_FEEDS = [
# 영어 소스
{
"url": "https://blog.python.org/feeds/posts/default?alt=rss",
"source": "Python Blog",
"tags": ["Python"],
},
{
"url": "https://www.djangoproject.com/rss/weblog/",
"source": "Django Blog",
"tags": ["Django"],
},
{
"url": "https://realpython.com/atom.xml",
"source": "Real Python",
"tags": ["Python"],
},
{
"url": "https://planetpython.org/rss20.xml",
"source": "Planet Python",
"tags": ["Python"],
},
# 한국어 소스
{
"url": "https://news.hada.io/rss/news",
"source": "GeekNews",
"tags": ["Python"],
},
{
"url": "https://meetup.nhncloud.com/rss",
"source": "NHN Cloud Meetup",
"tags": ["Python"],
},
{
"url": "https://techblog.woowahan.com/feed/",
"source": "우아한형제들 기술블로그",
"tags": ["Python"],
},
{
"url": "https://engineering.linecorp.com/ko/feed/",
"source": "LINE Engineering",
"tags": ["Python"],
},
{
"url": "https://toss.tech/rss.xml",
"source": "Toss Tech",
"tags": ["Toss Tech"],
},
]
각 소스에 기본 태그를 미리 붙여두는 것이 포인트입니다. 뒤에 나올 자동 태깅은 본문 기반이라 실패할 수 있는데, 기본 태그가 있으면 최소한의 분류는 보장됩니다.
2. 한국어 소스는 "파이썬 관련" 필터링이 필수
Python 공식 블로그나 Real Python은 들어오는 글이 전부 파이썬 관련이지만, GeekNews나 토스/우아한형제들 기술블로그는 프론트엔드, 디자인, iOS 같은 다양한 주제가 섞여 있습니다. 그대로 수집하면 뉴스레터의 성격이 흐려집니다.
그래서 한국어 소스에는 키워드 필터를 먼저 태웁니다.
PYTHON_KEYWORDS = [
"python", "파이썬", "django", "fastapi", "flask",
"celery", "sqlalchemy", "pydantic", "uvicorn", "gunicorn",
"pip", "poetry", "pytest", "asyncio", "백엔드", "backend",
]
def _is_python_related(title: str, summary: str) -> bool:
haystack = f"{title} {summary}".lower()
return any(kw in haystack for kw in PYTHON_KEYWORDS)
제목 + 요약을 lowercase로 합쳐서 키워드 하나라도 걸리면 통과시킵니다. 정확도는 완벽하지 않지만 노이즈의 90% 이상을 걸러줍니다. 영어 소스는 이 단계를 스킵합니다.
3. HTML 정제 — BeautifulSoup4로 노이즈 제거
feedparser가 뽑아주는 summary 필드는 HTML 원본을 그대로 담고 있어서, 그대로 저장하면 뉴스레터에 광고/CTA 텍스트가 같이 들어갑니다. 특히 Real Python 같은 소스는 "Subscribe to our newsletter", "Share on Twitter" 같은 문구가 매 글마다 붙어 있습니다.
NOISE_PATTERNS = [
"Improve Your Python",
"Python Tricks",
"Get a short & sweet Python Trick",
"Click here to learn more",
">> Click here",
"delivered to your inbox",
"Subscribe to our newsletter",
"Sign up for",
"Join our mailing list",
"Share on Twitter",
"Share on Facebook",
"Tweet this",
"Read the full article",
"Continue reading on",
]
def _clean_html(html: str) -> str:
soup = BeautifulSoup(html, "html.parser")
# 스크립트/스타일/네비게이션 제거
for tag in soup(["script", "style", "nav", "aside"]):
tag.decompose()
# 노이즈 패턴이 포함된 p 태그 제거
for p in soup.find_all("p"):
text = p.get_text()
if any(pattern in text for pattern in NOISE_PATTERNS):
p.decompose()
return str(soup)
핵심은 두 단계:
- 태그 단위 제거:
script,style,nav,aside는 통째로 날립니다. - 문단 단위 제거: 노이즈 패턴이 텍스트에 포함된
p태그만 제거합니다. 코드 블록(<pre>,<code>)이나 본문 HTML 구조는 유지해서, 나중에 마크다운 렌더링이 깨지지 않도록 했습니다.
4. 자동 태깅 — 본문 키워드로 카테고리 부여
수집 시점에 태그를 자동으로 붙여두면, 나중에 프론트에서 "Django만 보기" 같은 필터를 걸기 쉽습니다.
keyword_tags = {
"Django": ["django", "drf", "django rest"],
"FastAPI": ["fastapi", "starlette"],
"Flask": ["flask"],
"Database": ["postgres", "mysql", "sqlite", "database", "sql ", "orm",
"sqlalchemy", "데이터베이스", "migration"],
"DevOps": ["docker", "kubernetes", "k8s", "ci/cd", "deploy", "aws",
"배포", "인프라"],
"Testing": ["pytest", "unittest", "testing", "tdd", "테스트",
"test-driven", "test driven"],
"AI/ML": ["machine learning", "llm", "openai", "langchain",
"머신러닝", "딥러닝", "gpt", "transformer", "neural"],
"Async": ["asyncio", "async/await", "비동기", "concurrency",
"uvicorn", "aiohttp"],
}
def _auto_tag(title: str, content: str) -> list[str]:
haystack = f"{title} {content}".lower()
matched = []
for tag, keywords in keyword_tags.items():
if any(kw in haystack for kw in keywords):
matched.append(tag)
return matched
설계 포인트 몇 가지:
- 중복 매칭 허용: 한 글이 "Django + Testing" 태그를 동시에 가질 수 있습니다. 실제로 "Django 앱 pytest 테스트" 같은 글은 두 태그 모두 해당됩니다.
"sql "뒤 공백: 그냥"sql"을 넣으면postgresql이나mysql같은 단어가 모두 걸립니다. 공백을 넣어 독립 단어일 때만 매칭되게 했습니다. 작지만 중요한 디테일입니다.- 한영 키워드 혼용: 영어 소스와 한국어 소스를 동일한 로직으로 처리하기 위해 양쪽 키워드를 한 리스트에 섞어 넣었습니다.
5. 중복 방지 — source_url + slug
같은 소스를 주 1회 돈다고 해도, 같은 글이 RSS에 며칠간 남아 있으면 중복 수집됩니다. 두 단계로 막았습니다.
# 1. source_url로 먼저 체크 (RSS 원본 URL)
if Post.objects.filter(source_url=entry.link).exists():
continue
# 2. slug로 2차 체크 (제목 해시 기반)
slug = slugify(entry.title)[:200]
if Post.objects.filter(slug=slug).exists():
slug = f"{slug}-{uuid.uuid4().hex[:6]}"
- 1차: 원본 URL이 같으면 같은 글이라고 간주합니다. 가장 확실한 기준입니다.
- 2차: URL이 다른데 제목이 같은 케이스(예: 한 글이 여러 소스에 배포됨)는
slug로 잡되, 완전히 스킵하지 않고 해시 꼬리를 붙여서 저장합니다. 일부러 다른 소스의 글도 모으고 싶을 때를 위한 타협점입니다.
6. handle() — 전체 파이프라인 엔트리
management command의 handle() 메서드는 의외로 단순합니다. 각 단계를 _process_feed()로 위임하고, 총 카운트만 집계합니다.
def handle(self, *args, **options):
days = options["days"]
dry_run = options["dry_run"]
cutoff = datetime.now() - timedelta(days=days)
total_created = 0
self.stdout.write(f"\n크롤링 시작 (최근 {days}일 이내 글 수집)\n")
for feed_config in RSS_FEEDS:
created = self._process_feed(feed_config, cutoff, dry_run)
total_created += created
self.stdout.write(
self.style.SUCCESS(f"\n완료! 총 {total_created}개 새 글 저장\n")
)
두 가지 옵션이 쓸모 있었습니다:
--days 7: 최근 N일 이내 글만 수집. 매주 월요일에 도는 크론이라 7로 두면 딱 맞습니다.--dry-run: DB에 저장하지 않고 수집 결과만 출력. 소스 추가/필터 수정 시 먼저--dry-run으로 확인한 뒤 실제 실행합니다.
7. 파이프라인 전체 흐름 요약
RSS URL
↓ feedparser.parse()
엔트리 리스트 (title, link, summary, published)
↓ cutoff 날짜 필터
최근 N일 이내 글만
↓ (한국어 소스만) PYTHON_KEYWORDS 필터
파이썬 관련 글만
↓ BeautifulSoup4 정제
노이즈 제거된 HTML
↓ 중복 체크 (source_url, slug)
새 글만
↓ 자동 태깅
Post 객체 저장 (기본 태그 + 자동 태그)
각 단계가 독립적이라 나중에 특정 단계만 교체(예: BeautifulSoup → readability-lxml)하거나 소스별로 다른 필터를 끼우기 쉽습니다.
정리
RSS 크롤링은 feedparser 호출만으로는 부족하고, "의미 있는 뉴스레터 콘텐츠"로 만들기 위한 정제 단계가 더 많았습니다. 특히 한국어 소스의 파이썬 관련 필터, HTML 내 CTA 제거, 키워드 기반 자동 태깅 세 가지는 도메인별로 직접 튜닝해야 하는 부분이었습니다.
다음 글에서는 이 crawl_news를 포함한 Django management command 4종(crawl_news, send_newsletter, seed_editorial, backfill_urls)을 어떻게 역할별로 나눠 설계했는지 정리하겠습니다.