한줄 요약:
여러 웹페이지를 크롤링 → 본문 정제 → 요약 + 키워드 추출 → CSV/Markdown 리포트까지 자동으로 생성하는 올인원 파이프라인!
1.목표
- URL 목록에서 본문 텍스트 자동 수집
- 문장 정제/노이즈 제거(스크립트·내비 등 제외)
- 추출식 요약(설치 가벼움) + 키워드 추출
- CSV + report.md로 결과 저장
2.준비 (필요 패키지 설치)
pip install requests beautifulsoup4 lxml nltk sumy yake pandas
최초 1회 NLTK 리소스 다운로드가 필요할 수 있어요(코드에 자동 처리 포함).
3.완성 코드 (복붙해서 바로 실행)
파일명 예시:
auto_report.py
import re
import csv
import time
import pandas as pd
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse
from typing import List, Tuple
# ---- 요약 도구 (sumy: 추출식 요약) ----
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.text_rank import TextRankSummarizer
# ---- 키워드 추출 (yake) ----
import yake
# ---- NLTK 리소스 (존재 안 하면 자동 다운로드) ----
import nltk
try:
nltk.data.find("tokenizers/punkt")
except LookupError:
nltk.download("punkt")
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; AutoReportBot/1.0)"}
def fetch_html(url: str, timeout: int = 10) -> str:
res = requests.get(url, headers=HEADERS, timeout=timeout)
res.raise_for_status()
return res.text
def extract_main_text(html: str) -> str:
"""
아주 정교한 '본문 추출기'는 아니지만,
불필요한 스크립트/스타일/내비영역을 최대한 제거한 후 본문 후보를 합칩니다.
"""
soup = BeautifulSoup(html, "lxml")
# 제거 대상 태그
for t in soup(["script", "style", "noscript", "header", "footer", "nav", "form", "aside"]):
t.decompose()
# 본문 후보: article, main, section, div 등
candidates = []
for sel in ["article", "main", "section", "div"]:
for node in soup.select(sel):
text = " ".join(node.get_text(separator=" ", strip=True).split())
# 너무 짧은 블럭 제외
if len(text) > 300:
candidates.append(text)
if not candidates:
text = " ".join(soup.get_text(separator=" ", strip=True).split())
return text
# 가장 긴 블럭을 본문으로 가정
candidates.sort(key=len, reverse=True)
return candidates[0]
def clean_text(text: str) -> str:
text = re.sub(r"\s+", " ", text).strip()
return text
def summarize_text(text: str, sent_count: int = 5, language: str = "korean") -> str:
parser = PlaintextParser.from_string(text, Tokenizer(language))
summarizer = TextRankSummarizer()
sentences = summarizer(parser.document, sent_count)
return " ".join(str(s) for s in sentences)
def extract_keywords(text: str, top_k: int = 10, language: str = "ko") -> List[Tuple[str, float]]:
kw = yake.KeywordExtractor(lan=language, n=1, top=top_k, dedupLim=0.9)
result = kw.extract_keywords(text)
# score 오름차순(낮을수록 중요) -> 상식적으로 가독성 위해 정렬 반전
result.sort(key=lambda x: x[1])
return result[:top_k]
def domain_of(url: str) -> str:
return urlparse(url).netloc
def generate_markdown_report(rows: List[dict], path: str = "report.md"):
md = []
md.append("# 📝 웹 자동 리포트")
md.append("")
md.append(f"- 생성 시각: {time.strftime('%Y-%m-%d %H:%M:%S')}")
md.append(f"- 총 문서 수: {len(rows)}")
md.append("")
for i, r in enumerate(rows, 1):
md.append(f"## {i}. {r['title'] or r['url']}")
md.append(f"- URL: {r['url']}")
md.append(f"- 도메인: {r['domain']}")
md.append("")
md.append("**요약**")
md.append("")
md.append(r['summary'] or "_요약 불가_")
md.append("")
if r["keywords"]:
md.append("**키워드**")
md.append("")
md.append(", ".join([k for k, _ in r["keywords"]]))
md.append("")
md.append("---")
md.append("")
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(md))
def process_urls(urls: List[str], sent_count: int = 5) -> List[dict]:
rows = []
for url in urls:
print(f"▶ 처리 중: {url}")
try:
html = fetch_html(url)
soup = BeautifulSoup(html, "lxml")
title = soup.title.get_text(strip=True) if soup.title else ""
text = extract_main_text(html)
text = clean_text(text)
if not text or len(text) < 200:
raise ValueError("본문이 너무 짧음")
summary = summarize_text(text, sent_count=sent_count, language="korean")
keywords = extract_keywords(text, top_k=10, language="ko")
rows.append({
"url": url,
"domain": domain_of(url),
"title": title,
"summary": summary,
"keywords": keywords
})
print(" ✅ 완료")
except Exception as e:
print(f" ❌ 실패: {e}")
rows.append({
"url": url,
"domain": domain_of(url),
"title": "",
"summary": "",
"keywords": []
})
return rows
def save_csv(rows: List[dict], path: str = "report.csv"):
flat = []
for r in rows:
kws = ", ".join([k for k, _ in r["keywords"]]) if r["keywords"] else ""
flat.append({
"url": r["url"],
"domain": r["domain"],
"title": r["title"],
"summary": r["summary"],
"keywords": kws
})
pd.DataFrame(flat).to_csv(path, index=False, encoding="utf-8-sig")
if __name__ == "__main__":
# ▶ 여기에 분석할 URL들을 넣으세요
URLS = [
"https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%B4%EC%8D%AC",
"https://www.python.org/about/",
]
rows = process_urls(URLS, sent_count=5)
save_csv(rows, "report.csv")
generate_markdown_report(rows, "report.md")
print("🎉 생성 완료: report.csv, report.md")
4.동작 원리 요약
- fetch_html: User-Agent 지정해 HTML 수집
- extract_main_text: 스크립트·스타일·내비 제거, 본문 후보 중 가장 긴 블록 선택
- sumy(TextRank): 가벼운 추출식 요약(속도 빠름)
- yake: 언어 지정(
ko)으로 키워드 추출 - CSV/Markdown 출력:
report.csv,report.md생성
5.사용 방법
- 코드 저장:
auto_report.py - URL 목록을
URLS = [...]에 채우기 - 실행:
python auto_report.py - 결과 확인:
report.csv: 엑셀에서 바로 열기(UTF-8-SIG라 한글 OK)report.md: 워드프레스/노션에 붙여넣기 용이
6.커스터마이즈 팁
- 요약 분량 조절:
sent_count=5→ 3~7로 조정 - 키워드 개수:
top_k=10변경 - 언어 설정: 한국어(
language="korean"/yake lan="ko") - 본문 필터 강화: 본문 선택 로직에
data-article,.content,.post등 사이트별 셀렉터 추가
main = soup.select_one("article, main, .content, .post")
if main:
text = " ".join(main.get_text(" ", strip=True).split())
7.확장 아이디어
- **스케줄러(cron/Windows 작업 스케줄러)**로 매일 자동 실행
- 이메일 자동 발송:
smtplib로report.md/report.csv첨부 - DB 저장: SQLite/PostgreSQL에 누적 저장 → 대시보드로 시각화
- 요약기 교체: 추출식(sumy) → 생성식(LLM)으로 업그레이드
- 예: 사내/허용된 LLM API로
summary_prompt(text)호출 후 결과 저장 - (API 키·정책 준수 필수)
- 예: 사내/허용된 LLM API로
8.주의사항
| 항목 | 설명 |
|---|---|
| 크롤링 정책 | 각 사이트의 robots.txt·약관 준수 |
| 요청 간격 | 너무 빠른 수집은 차단 위험 → time.sleep() 간격 두기 |
| HTML 구조 변경 | 셀렉터/본문 추출 로직 주기적 점검 필요 |
| 저작권 | 요약·인용 범위 내 활용, 원문 링크 명시 |
9.체크리스트
requests/bs4/sumy/yake설치- URL 목록 구성
- 요약 문장 수/키워드 개수 조정
report.csv,report.md생성 확인- 크롤링 정책 준수
10.요약 한 줄
한 번에 수집→정리→리포트까지!
가벼운 추출식 요약과 키워드 추출로 콘텐츠 리서치 자동화 완성 ✅
이전 강좌 👈 [응용#4] 자동 로그인 + 데이터 다운로드 매크로 만들기
다음 강좌 👉 [응용#6] 스케줄러로 매일 자동 리포트 메일 발송(윈도우/맥)