[응용#5] 파이썬 크롤링 + 요약·키워드 추출: 자동 리포트 생성기

한줄 요약:
여러 웹페이지를 크롤링 → 본문 정제 → 요약 + 키워드 추출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.동작 원리 요약

  1. fetch_html: User-Agent 지정해 HTML 수집
  2. extract_main_text: 스크립트·스타일·내비 제거, 본문 후보 중 가장 긴 블록 선택
  3. sumy(TextRank): 가벼운 추출식 요약(속도 빠름)
  4. yake: 언어 지정(ko)으로 키워드 추출
  5. CSV/Markdown 출력: report.csv, report.md 생성

5.사용 방법

  1. 코드 저장: auto_report.py
  2. URL 목록을 URLS = [...]에 채우기
  3. 실행: python auto_report.py
  4. 결과 확인:
    • 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 작업 스케줄러)**로 매일 자동 실행
  • 이메일 자동 발송: smtplibreport.md/report.csv 첨부
  • DB 저장: SQLite/PostgreSQL에 누적 저장 → 대시보드로 시각화
  • 요약기 교체: 추출식(sumy) → 생성식(LLM)으로 업그레이드
    • 예: 사내/허용된 LLM API로 summary_prompt(text) 호출 후 결과 저장
    • (API 키·정책 준수 필수)

8.주의사항

항목설명
크롤링 정책각 사이트의 robots.txt·약관 준수
요청 간격너무 빠른 수집은 차단 위험 → time.sleep() 간격 두기
HTML 구조 변경셀렉터/본문 추출 로직 주기적 점검 필요
저작권요약·인용 범위 내 활용, 원문 링크 명시

9.체크리스트

  • requests/bs4/sumy/yake 설치
  • URL 목록 구성
  • 요약 문장 수/키워드 개수 조정
  • report.csv, report.md 생성 확인
  • 크롤링 정책 준수

10.요약 한 줄

한 번에 수집→정리→리포트까지!
가벼운 추출식 요약과 키워드 추출로 콘텐츠 리서치 자동화 완성 ✅


이전 강좌 👈 [응용#4] 자동 로그인 + 데이터 다운로드 매크로 만들기
다음 강좌 👉 [응용#6] 스케줄러로 매일 자동 리포트 메일 발송(윈도우/맥)

댓글 남기기

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.