본문 바로가기

Python

[Python] 법령 개정 알림 서비스 구축하기 (1) - 기본 설계 및 크롤링 구현

이번 시리즈에서는 법령 개정 사항을 자동으로 감지하고 이메일로 알림을 보내는 간단한 서비스를 만들어 보겠습니다. 해당 시리즈는 총 3부작으로 구성됩니다.

 

(1)  기본 설계 및 크롤링 구현
(2) 데이터베이스 연동 및 변경 감지
(3) Streamlit으로 대시보드 구현

 

1. 전체 서비스 개요

서비스는 다음과 같은 기능을 갖습니다.

  • 법령 정보 웹사이트에서 데이터 수집하기
  • 이전 데이터와 비교하여 변경점 찾기
  • 변경이 있으면 이메일로 알림 보내기
  • Streamlit으로 구독자 관리 및 변경 이력 확인하기

 

2. 필요한 패키지 설치하기

먼저 필요한 패키지를 설치합니다.

# 가상환경 생성 (선택사항이지만 권장)
python -m venv venv
source venv/bin/activate  # 윈도우: venv\Scripts\activate

# 필요한 패키지 설치
pip install requests beautifulsoup4

 

3. 기본 크롤러 만들기

`crawler.py` 파일을 만들고 아래 코드를 작성합니다.

import requests
from bs4 import BeautifulSoup
import json
import os
from datetime import datetime


class LawCrawler:
    """간단한 법령 크롤러"""

    def __init__(self):
        # 국가법령정보센터 URL (예시)
        self.base_url = "https://www.law.go.kr"
        # 헤더 설정 (웹사이트에서 차단당하지 않기 위함)
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124"
        }

    def get_recent_laws(self, limit=5):
        """최근 개정된 법령 목록 가져오기"""
        try:
            print(f"최근 법령 {limit}개 수집 시도 중...")

            # 실제 환경에서는 아래 URL을 실제 법령 사이트 URL로 변경해야 합니다
            # 예: url = f"{self.base_url}/lsSc.do?menuId=10&subMenuId=10"
            # 테스트를 위해 샘플 데이터 반환

            law_items = []
            for i in range(1, limit + 1):
                law_items.append(
                    {
                        "id": f"TEST{i:03d}",
                        "title": f"테스트 법령 {i}",
                        "crawled_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                    }
                )

            print(f"{len(law_items)}개 법령 정보 수집 완료")
            return law_items

        except Exception as e:
            print(f"법령 정보 수집 중 오류: {str(e)}")
            return []

    def get_law_detail(self, law_id):
        """특정 법령의 상세 정보 가져오기"""
        try:
            print(f"법령 상세 정보 수집 중: {law_id}")

            # 실제 환경에서는 아래 URL을 실제 법령 사이트 URL로 변경해야 합니다
            # 예: url = f"{self.base_url}/lsInfoP.do?lsId={law_id}"
            # 테스트를 위해 샘플 데이터 반환

            law_detail = {
                "id": law_id,
                "title": f"테스트 법령 {law_id}",
                "content": f"이 법은 {law_id} 관련 사항을 규정함을 목적으로 한다.\n\n제1조(목적) 이 법은...\n\n제2조(정의) 이 법에서 사용하는 용어의 뜻은...",
                "last_update": "2023-01-01",
                "crawled_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            }

            print(f"법령 '{law_detail['title']}' 상세 정보 수집 완료")
            return law_detail

        except Exception as e:
            print(f"법령 상세 정보 수집 중 오류: {str(e)}")
            return None

    def save_to_file(self, data, filename):
        """데이터를 JSON 파일로 저장"""
        # data 디렉토리가 없으면 생성
        os.makedirs("data", exist_ok=True)

        filepath = os.path.join("data", filename)
        with open(filepath, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

        print(f"데이터 저장 완료: {filepath}")


# 테스트 코드
if __name__ == "__main__":
    crawler = LawCrawler()

    # 최근 법령 목록 가져오기
    laws = crawler.get_recent_laws(limit=3)
    crawler.save_to_file(laws, "recent_laws.json")

    # 첫 번째 법령의 상세 정보 가져오기
    if laws:
        law_detail = crawler.get_law_detail(laws[0]["id"])
        if law_detail:
            crawler.save_to_file(law_detail, f"law_{laws[0]['id']}.json")

참고: 위 코드는 실제로 웹사이트를 크롤링하지 않고 테스트용 데이터를 반환합니다.
실제 서비스에서는 주석 처리된 부분을 참고하여 실제 웹사이트 크롤링 코드로 변경해야 합니다.

 

3-1. 실제 크롤링 구현 예시

실제 웹사이트에서 데이터를 가져오는 코드는 다음과 같이 구현할 수 있습니다.

def get_recent_laws(self, limit=5):
    """최근 개정된 법령 목록 가져오기"""
    try:
        # 최근 개정 법령 페이지 URL (실제 URL로 수정 필요)
        url = f"{self.base_url}/lsSc.do?menuId=10&subMenuId=10"
        response = requests.get(url, headers=self.headers)
        
        # HTML 파싱
        soup = BeautifulSoup(response.text, 'html.parser')
        
        # 법령 목록 추출 (CSS 선택자는 실제 웹사이트에 맞게 수정 필요)
        law_items = []
        law_elements = soup.select('.law_list li')[:limit]
        
        for element in law_elements:
            # 가정: 각 요소에서 제목과 링크 추출
            title = element.select_one('a').text.strip()
            link = element.select_one('a')['href']
            law_id = link.split('lsId=')[1].split('&')[0]  # URL에서 ID 추출
            
            law_items.append({
                'id': law_id,
                'title': title,
                'crawled_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            })
        
        print(f"{len(law_items)}개 법령 정보 수집 완료")
        return law_items
        
    except Exception as e:
        print(f"법령 정보 수집 중 오류: {str(e)}")
        return []

 

4. 법령 내용 비교 함수 만들기

`utils.py` 파일을 만들어 법령 내용을 비교하는 함수를 작성합니다.

import hashlib
import json
from datetime import datetime


def get_content_hash(content):
    """콘텐츠의 해시값 생성"""
    if isinstance(content, dict):
        content = json.dumps(content, sort_keys=True)
    return hashlib.md5(content.encode("utf-8")).hexdigest()


def compare_laws(old_law, new_law):
    """두 법령 데이터를 비교하여 변경 여부 확인"""
    if not old_law or not new_law:
        return {"changed": True, "reason": "이전 데이터 없음"}

    # 법령 제목 비교
    if old_law.get("title") != new_law.get("title"):
        return {
            "changed": True,
            "reason": "제목 변경",
            "details": {"old": old_law.get("title"), "new": new_law.get("title")},
        }

    # 법령 내용 비교 (해시값 이용)
    old_content_hash = get_content_hash(old_law.get("content", ""))
    new_content_hash = get_content_hash(new_law.get("content", ""))

    if old_content_hash != new_content_hash:
        return {
            "changed": True,
            "reason": "내용 변경",
            "details": {"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
        }

    # 마지막 업데이트 날짜 비교
    if old_law.get("last_update") != new_law.get("last_update"):
        return {
            "changed": True,
            "reason": "업데이트 날짜 변경",
            "details": {
                "old": old_law.get("last_update"),
                "new": new_law.get("last_update"),
            },
        }

    # 변경 없음
    return {"changed": False}
참고: `utils.py` 는 다음과 같은 작업을 수행합니다.
1. 법령 내용을 해시값으로 변환하여 효율적으로 비교
2. 법령 제목, 내용, 업데이트 날짜 등의 변경 여부 확인
3. 변경 사항이 있을 경우 상세 정보 반환

 

5. 실행 방법

이제 기본적인 크롤러와 비교 함수가 준비되었습니다. 다음 명령으로 크롤러를 테스트해 볼 수 있습니다.

python crawler.py
참고:  `crawler.py` 는 다음과 같은 작업을 수행합니다.
1. 최근 법령 3개의 목록을 수집합니다.
2. 수집한 법령 목록을 data/recent_laws.json 파일에 저장합니다.
3. 첫 번째 법령의 상세 정보를 수집하여 data/law_[ID].json 파일에 저장합니다.

 

6. 기능 요약

  1. 법령 정보를 수집하는 크롤러 클래스를 구현했습니다.
  2. 수집한 데이터를 파일로 저장하는 기능을 추가했습니다.
  3. 두 법령 데이터를 비교하는 함수를 작성했습니다.