이미지 로딩 중...

기술 문서 크롤링 전략 및 실습 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 20. · 3 Views

기술 문서 크롤링 전략 및 실습 완벽 가이드

개발자라면 반드시 알아야 할 기술 문서 크롤링 방법을 처음부터 끝까지 배웁니다. GitHub README부터 공식 문서까지, BeautifulSoup과 Selenium을 활용하여 자동으로 수집하는 실전 기법을 초급자도 쉽게 따라할 수 있도록 설명합니다.


목차

  1. 크롤링 대상 선정
  2. BeautifulSoup과 Selenium 활용
  3. Markdown과 HTML 파싱 기법
  4. API 문서 자동 수집 방법
  5. 저작권 및 라이선스 확인
  6. 수집 데이터 저장 구조 설계

1. 크롤링 대상 선정

시작하며

여러분이 AI 학습 데이터를 수집하거나, 여러 라이브러리의 문서를 한 곳에 모아야 하는 상황을 겪어본 적 있나요? 일일이 웹사이트를 방문해서 복사-붙여넣기를 반복하다 보면 시간도 오래 걸리고, 실수도 많이 발생합니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 LLM 파인튜닝을 위한 데이터 수집이나, 여러 오픈소스 프로젝트의 문서를 통합해야 할 때 수작업은 비효율적입니다.

문서의 업데이트를 추적하기도 어렵고, 데이터의 일관성을 유지하기도 힘듭니다. 바로 이럴 때 필요한 것이 체계적인 크롤링 대상 선정입니다.

어떤 소스에서 데이터를 수집할지 전략적으로 결정하면, 고품질의 데이터를 효율적으로 모을 수 있습니다.

개요

간단히 말해서, 크롤링 대상 선정은 웹에서 수집할 데이터의 출처를 전략적으로 결정하는 과정입니다. 마치 도서관에서 책을 고를 때 어떤 책이 가장 신뢰할 만하고 최신 정보를 담고 있는지 파악하는 것과 같습니다.

크롤링 대상을 잘못 선정하면 낮은 품질의 데이터를 수집하게 되거나, 저작권 문제에 부딪힐 수 있습니다. 예를 들어, 개인 블로그보다는 GitHub 공식 README나 공식 문서 사이트가 더 신뢰할 수 있고, 법적으로도 안전한 경우가 많습니다.

기존에는 무작정 구글 검색 결과를 크롤링했다면, 이제는 GitHub API, 공식 문서 사이트, Read the Docs 같은 구조화된 소스를 우선적으로 선정할 수 있습니다. 크롤링 대상 선정의 핵심 특징은 세 가지입니다: 첫째, 데이터의 신뢰성과 최신성, 둘째, 구조화된 포맷 여부(Markdown, HTML), 셋째, 법적 허용 여부입니다.

이러한 특징들을 고려하면 나중에 데이터를 처리하고 활용하기가 훨씬 수월해집니다.

코드 예제

# 주석: 크롤링 대상 URL 목록을 정의합니다
target_sources = {
    'github_readme': [
        'https://github.com/tensorflow/tensorflow/blob/master/README.md',
        'https://github.com/pytorch/pytorch/blob/main/README.md'
    ],
    'official_docs': [
        'https://docs.python.org/3/',
        'https://pytorch.org/docs/stable/index.html'
    ]
}

# 주석: 각 소스의 우선순위와 데이터 품질을 평가합니다
def evaluate_source(url, source_type):
    priority = {'github_readme': 1, 'official_docs': 2}
    return {
        'url': url,
        'type': source_type,
        'priority': priority.get(source_type, 3),
        'structured': source_type in ['github_readme', 'official_docs']
    }

# 주석: 모든 소스를 평가하고 우선순위대로 정렬합니다
evaluated_sources = []
for source_type, urls in target_sources.items():
    for url in urls:
        evaluated_sources.append(evaluate_source(url, source_type))

# 주석: 우선순위가 높은 순서대로 정렬하여 크롤링 계획을 수립합니다
evaluated_sources.sort(key=lambda x: x['priority'])
print("크롤링 대상 목록:", evaluated_sources)

설명

이것이 하는 일: 이 코드는 크롤링할 웹사이트 목록을 체계적으로 관리하고, 각 소스의 신뢰도와 우선순위를 평가하여 효율적인 크롤링 계획을 수립합니다. 첫 번째로, target_sources 딕셔너리에서 크롤링 대상을 카테고리별로 분류합니다.

'github_readme'는 오픈소스 프로젝트의 공식 설명 문서를, 'official_docs'는 각 기술의 공식 문서 사이트를 의미합니다. 이렇게 분류하는 이유는 나중에 데이터 품질을 추적하고, 소스별로 다른 파싱 전략을 적용하기 위함입니다.

그 다음으로, evaluate_source 함수가 실행되면서 각 URL의 특성을 분석합니다. 함수 내부에서는 소스 타입에 따라 우선순위 점수를 부여하고, 데이터가 구조화되어 있는지 판단합니다.

GitHub README는 보통 Markdown 형식으로 잘 정리되어 있어서 파싱이 쉽고, 공식 문서는 HTML이지만 일관된 구조를 가지고 있어 자동화하기 좋습니다. 마지막으로, 모든 평가 결과를 리스트에 모아 우선순위 순으로 정렬합니다.

이렇게 하면 가장 중요하고 품질 좋은 소스부터 크롤링을 시작할 수 있어서, 시간이 제한적일 때도 핵심 데이터를 먼저 확보할 수 있습니다. 여러분이 이 코드를 사용하면 수백 개의 문서 소스를 체계적으로 관리할 수 있습니다.

우선순위가 명확하니까 어디서부터 시작해야 할지 고민할 필요가 없고, 나중에 팀원들과 협업할 때도 "왜 이 소스를 선택했는지" 설명하기 쉬워집니다.

실전 팁

💡 GitHub의 경우 raw.githubusercontent.com을 사용하면 README를 HTML 없이 순수 Markdown으로 받을 수 있어서 파싱이 훨씬 간단합니다.

💡 공식 문서 사이트는 sitemap.xml 파일을 먼저 확인하세요. 전체 문서 구조를 한눈에 파악할 수 있고, 놓치는 페이지 없이 수집할 수 있습니다.

💡 크롤링 전에 robots.txt를 반드시 확인하세요. 크롤링이 금지된 경로를 수집하면 법적 문제가 발생할 수 있습니다.

💡 API가 제공되는 사이트는 웹 크롤링 대신 API를 사용하는 것이 더 안정적입니다. GitHub API는 README뿐만 아니라 메타데이터까지 깔끔하게 제공합니다.

💡 크롤링 대상 목록은 JSON이나 YAML 파일로 관리하면 나중에 수정하기 편하고, 버전 관리도 쉽습니다.


2. BeautifulSoup과 Selenium 활용

시작하며

여러분이 웹페이지에서 데이터를 추출하려고 할 때, 어떤 도구를 사용해야 할지 고민해본 적 있나요? 단순한 HTML 페이지도 있지만, JavaScript로 동적으로 내용이 로드되는 복잡한 페이지도 많습니다.

이런 문제는 실제 크롤링 프로젝트에서 가장 자주 마주치는 난관입니다. 정적인 HTML만 있는 페이지를 크롤링하다가, 갑자기 JavaScript로 렌더링되는 페이지를 만나면 기존 방법으로는 데이터를 가져올 수 없습니다.

빈 페이지만 받아오거나, 로딩 중 화면만 캡처되는 경우가 생깁니다. 바로 이럴 때 필요한 것이 BeautifulSoup과 Selenium의 적절한 조합입니다.

각 도구의 장점을 이해하고 상황에 맞게 선택하면, 거의 모든 웹페이지에서 원하는 데이터를 추출할 수 있습니다.

개요

간단히 말해서, BeautifulSoup은 HTML/XML 문서를 쉽게 파싱하는 라이브러리이고, Selenium은 실제 브라우저를 제어하여 JavaScript로 렌더링되는 페이지도 크롤링할 수 있는 도구입니다. 마치 BeautifulSoup은 종이책을 읽는 것이고, Selenium은 전자책 앱을 직접 조작하는 것과 같습니다.

BeautifulSoup은 속도가 빠르고 리소스를 적게 사용하기 때문에 대량의 정적 페이지를 크롤링할 때 매우 효율적입니다. 예를 들어, GitHub README나 Read the Docs 같은 서버 사이드 렌더링 페이지를 수집할 때는 BeautifulSoup만으로 충분합니다.

기존에는 모든 페이지를 Selenium으로 크롤링했다면, 이제는 정적 페이지는 BeautifulSoup으로, 동적 페이지만 Selenium으로 처리할 수 있습니다. 이렇게 하면 크롤링 속도가 10배 이상 빨라지고, 서버 리소스도 크게 절약됩니다.

핵심 특징은 세 가지입니다: 첫째, BeautifulSoup은 CSS 선택자와 태그 기반 탐색이 직관적이고, 둘째, Selenium은 버튼 클릭이나 스크롤 같은 사용자 상호작용을 시뮬레이션할 수 있고, 셋째, 두 도구를 함께 사용하면 Selenium으로 페이지를 렌더링한 후 BeautifulSoup으로 파싱하여 장점을 결합할 수 있습니다.

코드 예제

from bs4 import BeautifulSoup
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

# 주석: 정적 페이지 크롤링 - BeautifulSoup 사용
def crawl_static_page(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    # 주석: CSS 선택자로 원하는 요소를 추출합니다
    title = soup.select_one('h1').text
    content = soup.select('.documentation-content')
    return {'title': title, 'content': [c.text for c in content]}

# 주석: 동적 페이지 크롤링 - Selenium 사용
def crawl_dynamic_page(url):
    driver = webdriver.Chrome()
    driver.get(url)
    # 주석: JavaScript 렌더링 완료를 기다립니다
    WebDriverWait(driver, 10).until(
        lambda d: d.find_element(By.CLASS_NAME, 'content-loaded')
    )
    # 주석: 렌더링된 HTML을 BeautifulSoup으로 파싱합니다
    soup = BeautifulSoup(driver.page_source, 'html.parser')
    data = soup.select('.api-documentation')
    driver.quit()
    return [item.text for item in data]

# 주석: 페이지 타입에 따라 적절한 크롤링 방법을 선택합니다
def smart_crawl(url, is_dynamic=False):
    return crawl_dynamic_page(url) if is_dynamic else crawl_static_page(url)

설명

이것이 하는 일: 이 코드는 웹페이지의 특성에 따라 가장 효율적인 크롤링 방법을 자동으로 선택하여 데이터를 추출합니다. 첫 번째로, crawl_static_page 함수는 requests 라이브러리로 HTML을 다운로드한 후 BeautifulSoup으로 파싱합니다.

여기서 select_one()은 CSS 선택자로 첫 번째 일치하는 요소를 찾고, select()는 모든 일치하는 요소를 리스트로 반환합니다. 이 방법은 서버가 완성된 HTML을 보내주는 전통적인 웹사이트에서 완벽하게 작동하며, 속도도 매우 빠릅니다.

그 다음으로, crawl_dynamic_page 함수가 실행되면서 실제 Chrome 브라우저를 띄우고 페이지를 방문합니다. WebDriverWait는 특정 요소가 나타날 때까지 최대 10초를 기다리는데, 이것이 중요한 이유는 JavaScript가 데이터를 로드하는 데 시간이 걸리기 때문입니다.

'content-loaded' 클래스가 나타나면 렌더링이 완료된 것으로 판단하고, 그때의 HTML을 가져와서 BeautifulSoup으로 파싱합니다. 마지막으로, smart_crawl 함수가 is_dynamic 파라미터를 확인하여 적절한 크롤링 방법을 선택합니다.

이 함수를 사용하면 페이지마다 다른 크롤링 로직을 작성할 필요 없이, 하나의 인터페이스로 모든 페이지를 처리할 수 있습니다. 여러분이 이 코드를 사용하면 크롤링 성능을 극적으로 개선할 수 있습니다.

정적 페이지 100개를 크롤링할 때 Selenium만 쓰면 10분 걸리지만, BeautifulSoup을 쓰면 1분이면 충분합니다. 또한 서버 메모리도 훨씬 적게 사용해서, 동시에 여러 크롤링 작업을 실행하기에도 유리합니다.

실전 팁

💡 페이지가 동적인지 확인하려면 브라우저에서 "소스 보기"와 "개발자 도구의 Elements"를 비교하세요. 내용이 다르면 JavaScript 렌더링이 필요합니다.

💡 Selenium은 헤드리스 모드(headless mode)로 실행하면 브라우저 창이 뜨지 않아 서버 환경에서 안정적으로 작동합니다.

💡 BeautifulSoup의 find_all() 대신 select()를 사용하면 CSS 선택자 문법이 더 직관적이고 강력합니다.

💡 Selenium으로 크롤링 후 driver.quit()를 꼭 호출하세요. 그렇지 않으면 Chrome 프로세스가 계속 남아 메모리 누수가 발생합니다.

💡 대량 크롤링 시 User-Agent 헤더를 설정하고 요청 간 딜레이(time.sleep)를 넣어서 서버에 무리를 주지 않도록 주의하세요.


3. Markdown과 HTML 파싱 기법

시작하며

여러분이 GitHub에서 받아온 README 파일이나 웹사이트의 HTML 문서를 처리할 때, 제목과 본문을 구분하거나 코드 블록만 추출하고 싶었던 적 있나요? 그냥 텍스트로 저장하면 구조 정보가 사라져서 나중에 활용하기 어렵습니다.

이런 문제는 실제 문서 데이터 파이프라인에서 매우 중요한 단계입니다. Markdown과 HTML은 각각 다른 형식으로 구조화되어 있는데, 이를 제대로 파싱하지 않으면 LLM 학습 데이터로 사용할 때 품질이 떨어지고, 검색 시스템을 만들 때도 제목-본문 관계를 파악할 수 없습니다.

바로 이럴 때 필요한 것이 체계적인 Markdown/HTML 파싱 기법입니다. 문서의 계층 구조를 유지하면서 필요한 정보만 정확하게 추출할 수 있습니다.

개요

간단히 말해서, Markdown과 HTML 파싱은 구조화된 문서에서 의미 있는 요소(제목, 본문, 코드, 링크 등)를 추출하고 원하는 형식으로 변환하는 기술입니다. 마치 신문 기사에서 제목, 부제목, 본문, 사진 설명을 자동으로 분류하는 것과 같습니다.

이 기법이 필요한 이유는 원본 문서의 구조를 보존해야 나중에 다양한 용도로 활용할 수 있기 때문입니다. 예를 들어, API 문서를 크롤링할 때 함수 이름, 파라미터, 설명을 따로 저장하면 나중에 자동 완성 기능이나 문서 검색 시스템을 만들기 훨씬 쉽습니다.

기존에는 정규표현식으로 문서를 파싱했다면, 이제는 markdown 라이브러리와 BeautifulSoup의 계층 탐색 기능을 사용하여 더 정확하고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 핵심 특징은 세 가지입니다: 첫째, Markdown은 markdown 라이브러리로 HTML로 변환한 후 파싱하면 편리하고, 둘째, HTML은 태그의 계층 구조를 따라 부모-자식 관계를 탐색할 수 있으며, 셋째, 코드 블록과 일반 텍스트를 구분하여 처리하면 데이터 품질이 크게 향상됩니다.

코드 예제

import markdown
from bs4 import BeautifulSoup
import re

# 주석: Markdown을 HTML로 변환하고 구조화된 데이터로 파싱합니다
def parse_markdown(md_content):
    html = markdown.markdown(md_content, extensions=['fenced_code', 'tables'])
    soup = BeautifulSoup(html, 'html.parser')

    # 주석: 문서의 섹션별로 데이터를 구조화합니다
    sections = []
    current_section = None

    for element in soup.find_all(['h1', 'h2', 'h3', 'p', 'pre', 'code']):
        # 주석: 제목 태그를 만나면 새로운 섹션을 시작합니다
        if element.name in ['h1', 'h2', 'h3']:
            if current_section:
                sections.append(current_section)
            current_section = {
                'title': element.text,
                'level': int(element.name[1]),
                'content': [],
                'code_blocks': []
            }
        # 주석: 코드 블록과 일반 텍스트를 구분하여 저장합니다
        elif element.name in ['pre', 'code']:
            if current_section:
                current_section['code_blocks'].append(element.text)
        elif element.name == 'p':
            if current_section:
                current_section['content'].append(element.text)

    if current_section:
        sections.append(current_section)

    return sections

# 주석: HTML 문서에서 특정 패턴의 요소를 추출합니다
def parse_html_docs(html_content):
    soup = BeautifulSoup(html_content, 'html.parser')

    # 주석: API 문서에서 함수 정보를 추출하는 예시입니다
    api_functions = []
    for func_div in soup.select('.function-doc'):
        api_functions.append({
            'name': func_div.select_one('.function-name').text,
            'params': [p.text for p in func_div.select('.param')],
            'description': func_div.select_one('.description').text
        })

    return api_functions

설명

이것이 하는 일: 이 코드는 Markdown과 HTML 문서에서 구조적 정보를 유지하면서 제목, 본문, 코드 블록을 계층적으로 추출하여 재사용 가능한 형태로 변환합니다. 첫 번째로, parse_markdown 함수는 markdown 라이브러리로 Markdown을 HTML로 변환합니다.

'fenced_code'와 'tables' 확장을 활성화하면 코드 블록과 표를 더 정확하게 파싱할 수 있습니다. 변환된 HTML을 BeautifulSoup으로 읽으면 Markdown의 구조를 HTML 태그로 쉽게 탐색할 수 있습니다.

그 다음으로, 반복문이 모든 제목과 내용 요소를 순회하면서 문서를 섹션 단위로 나눕니다. h1, h2, h3 태그를 만나면 새로운 섹션을 시작하고, 그 아래의 텍스트와 코드는 해당 섹션에 속하도록 분류합니다.

이렇게 하면 "Introduction 섹션에는 이런 내용과 코드가 있다"는 식으로 문맥 정보를 유지할 수 있습니다. 세 번째로, 코드 블록(pre, code 태그)과 일반 텍스트(p 태그)를 별도의 리스트에 저장합니다.

이것이 중요한 이유는 LLM 학습 데이터를 만들 때 코드와 설명을 쌍으로 묶거나, 코드만 따로 추출하여 코드 검색 인덱스를 만들 때 유용하기 때문입니다. 마지막으로, parse_html_docs 함수는 특정 패턴을 가진 HTML 문서(예: API 문서)에서 구조화된 정보를 추출합니다.

CSS 선택자를 사용하여 함수 이름, 파라미터, 설명을 각각 찾아내고, 딕셔너리 형태로 깔끔하게 정리합니다. 여러분이 이 코드를 사용하면 수천 개의 문서를 자동으로 구조화할 수 있습니다.

원본 문서의 계층 구조가 보존되기 때문에 나중에 "3단계 제목만 추출"하거나 "코드 예제만 모아서 튜토리얼 만들기" 같은 작업이 매우 쉬워집니다. 또한 데이터베이스에 저장할 때도 JSON 형태로 바로 저장할 수 있어서 편리합니다.

실전 팁

💡 Markdown 파싱 시 python-markdown의 TOC(Table of Contents) 확장을 사용하면 문서의 전체 목차를 자동으로 생성할 수 있습니다.

💡 HTML에서 불필요한 스타일과 스크립트 태그는 soup.find_all(['script', 'style'])로 먼저 제거하세요. 파싱 속도가 빨라지고 결과도 깔끔해집니다.

💡 코드 블록의 언어 정보를 보존하려면 <code class="language-python"> 같은 클래스 속성을 확인하세요. 나중에 언어별로 필터링할 때 유용합니다.

💡 링크와 이미지 URL을 추출할 때는 상대 경로를 절대 경로로 변환해야 합니다. urljoin()을 사용하면 자동으로 처리됩니다.

💡 대용량 문서를 파싱할 때는 BeautifulSoup의 'lxml' 파서가 'html.parser'보다 3-5배 빠르니 성능이 중요하면 lxml을 사용하세요.


4. API 문서 자동 수집 방법

시작하며

여러분이 여러 라이브러리의 API 레퍼런스를 통합 검색 시스템에 넣거나, LLM에게 최신 API 정보를 학습시키고 싶을 때, 수백 페이지의 문서를 일일이 복사하는 건 현실적으로 불가능하죠? API 문서는 구조가 복잡하고 페이지가 많아서 수작업은 생각조차 할 수 없습니다.

이런 문제는 특히 머신러닝 프로젝트나 개발자 도구를 만들 때 심각한 병목이 됩니다. PyTorch, TensorFlow, HuggingFace Transformers 같은 라이브러리는 API가 수천 개이고, 버전마다 내용이 달라지기 때문에 최신 정보를 유지하는 것도 큰 도전입니다.

바로 이럴 때 필요한 것이 API 문서 자동 수집 시스템입니다. 문서의 규칙적인 구조를 파악하고, 모든 API 항목을 자동으로 순회하며 수집하는 방법을 알면, 몇 시간 만에 전체 문서를 데이터베이스에 넣을 수 있습니다.

개요

간단히 말해서, API 문서 자동 수집은 웹사이트의 API 레퍼런스 페이지를 체계적으로 탐색하면서 함수명, 파라미터, 반환값, 예제 코드 등을 구조화된 데이터로 추출하는 프로세스입니다. 마치 백과사전의 모든 항목을 자동으로 읽고 데이터베이스로 만드는 것과 같습니다.

이 방법이 필요한 이유는 API 문서가 보통 일관된 패턴을 따르기 때문에 자동화하기 좋고, 한 번 시스템을 구축하면 정기적으로 업데이트된 내용을 자동으로 수집할 수 있기 때문입니다. 예를 들어, PyTorch 문서는 모든 함수 페이지가 같은 HTML 구조를 가지고 있어서, 하나의 파싱 로직으로 수천 개의 API를 처리할 수 있습니다.

기존에는 문서 사이트를 방문해서 필요한 부분만 복사했다면, 이제는 sitemap을 분석하고 URL 패턴을 파악하여 모든 API 페이지를 자동으로 크롤링할 수 있습니다. 심지어 문서가 업데이트되면 변경된 부분만 다시 수집하는 것도 가능합니다.

핵심 특징은 세 가지입니다: 첫째, sitemap.xml이나 인덱스 페이지를 분석하여 모든 API 페이지 URL을 찾고, 둘째, 각 페이지에서 함수 시그니처와 설명을 정규화된 구조로 추출하며, 셋째, 예제 코드와 매개변수 설명을 별도로 저장하여 나중에 검색하기 쉽게 만듭니다.

코드 예제

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import json

# 주석: sitemap에서 모든 API 문서 URL을 추출합니다
def get_api_urls_from_sitemap(sitemap_url):
    response = requests.get(sitemap_url)
    soup = BeautifulSoup(response.content, 'xml')
    # 주석: API 문서 URL만 필터링합니다 (보통 /api/ 또는 /reference/ 경로)
    urls = [loc.text for loc in soup.find_all('loc')
            if '/api/' in loc.text or '/reference/' in loc.text]
    return urls

# 주석: 개별 API 페이지에서 구조화된 정보를 추출합니다
def parse_api_page(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')

    # 주석: 일반적인 API 문서 구조를 파싱합니다
    api_data = {
        'url': url,
        'function_name': soup.select_one('.function-name, h1').text.strip(),
        'signature': soup.select_one('.signature, code.python').text.strip(),
        'description': soup.select_one('.description, .doc-content p').text.strip(),
        'parameters': [],
        'examples': []
    }

    # 주석: 파라미터 정보를 추출합니다
    for param in soup.select('.parameter'):
        api_data['parameters'].append({
            'name': param.select_one('.param-name').text,
            'type': param.select_one('.param-type').text,
            'description': param.select_one('.param-desc').text
        })

    # 주석: 코드 예제를 추출합니다
    for example in soup.select('.example code, pre.highlight'):
        api_data['examples'].append(example.text.strip())

    return api_data

# 주석: 전체 API 문서를 수집하고 JSON으로 저장합니다
def collect_all_api_docs(sitemap_url, output_file):
    urls = get_api_urls_from_sitemap(sitemap_url)
    all_apis = []

    for url in urls[:10]:  # 예시로 처음 10개만 수집
        try:
            api_data = parse_api_page(url)
            all_apis.append(api_data)
            print(f"수집 완료: {api_data['function_name']}")
        except Exception as e:
            print(f"에러 발생 ({url}): {e}")

    # 주석: 수집한 데이터를 JSON 파일로 저장합니다
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(all_apis, f, ensure_ascii=False, indent=2)

    return all_apis

설명

이것이 하는 일: 이 코드는 API 문서 사이트의 sitemap을 분석하여 모든 API 페이지를 찾고, 각 페이지에서 함수 정보를 구조화된 형태로 추출하여 JSON 파일로 저장하는 완전한 수집 파이프라인을 구현합니다. 첫 번째로, get_api_urls_from_sitemap 함수는 sitemap.xml 파일을 파싱하여 모든 URL 목록을 가져온 후, '/api/'나 '/reference/' 같은 키워드로 필터링합니다.

대부분의 문서 사이트는 API 페이지를 특정 경로 아래에 모아두기 때문에 이 방법이 효과적입니다. sitemap이 없는 사이트라면 메인 인덱스 페이지의 링크를 크롤링하는 방식으로 대체할 수 있습니다.

그 다음으로, parse_api_page 함수가 개별 API 페이지를 방문하여 핵심 정보를 추출합니다. 함수명, 시그니처(함수 선언부), 설명을 각각 CSS 선택자로 찾아냅니다.

여기서 중요한 점은 '.function-name, h1'처럼 여러 선택자를 쉼표로 연결하면, 사이트마다 다른 구조에도 대응할 수 있다는 것입니다. 첫 번째 선택자로 못 찾으면 두 번째를 시도합니다.

세 번째로, 파라미터 정보와 예제 코드를 별도로 수집합니다. 파라미터는 이름, 타입, 설명을 각각 추출하여 딕셔너리 리스트로 만들고, 예제 코드는 텍스트 그대로 저장합니다.

이렇게 구조화하면 나중에 "특정 타입의 파라미터를 받는 함수 찾기" 같은 복잡한 검색도 가능해집니다. 마지막으로, collect_all_api_docs 함수가 모든 URL에 대해 파싱을 실행하고 결과를 JSON 파일로 저장합니다.

try-except 블록으로 감싸서 일부 페이지에서 에러가 나더라도 전체 프로세스가 멈추지 않도록 했습니다. 실전에서는 에러 로그를 파일로 남겨서 나중에 수동으로 확인할 수 있게 하면 좋습니다.

여러분이 이 코드를 사용하면 수천 개의 API 문서를 몇 시간 만에 구조화된 데이터베이스로 만들 수 있습니다. 한 번 수집한 데이터는 검색 엔진, RAG 시스템, 자동 완성 기능 등 다양한 용도로 재사용할 수 있고, 정기적으로 다시 크롤링하면 최신 버전의 API 정보를 항상 유지할 수 있습니다.

실전 팁

💡 sitemap이 너무 크면 sitemap_index.xml을 먼저 확인하세요. 보통 언어별, 버전별로 sitemap이 분할되어 있습니다.

💡 API 페이지의 HTML 구조가 일관되지 않으면, 2-3개 샘플 페이지를 먼저 분석하여 공통 패턴을 파악한 후 파싱 로직을 작성하세요.

💡 대량 크롤링 시에는 requests 대신 aiohttp와 asyncio를 사용하면 10배 이상 빠른 병렬 수집이 가능합니다.

💡 문서 버전 정보를 URL이나 페이지에서 추출하여 함께 저장하면, 여러 버전의 API를 비교하거나 버전별 검색을 제공할 수 있습니다.

💡 수집한 데이터에 타임스탬프를 추가하고, 이전 수집 데이터와 diff를 비교하여 변경된 API만 업데이트하면 효율적입니다.


5. 저작권 및 라이선스 확인

시작하며

여러분이 열심히 크롤링한 데이터를 상업적 프로젝트나 공개 데이터셋으로 사용하려고 할 때, 법적 문제를 걱정해본 적 있나요? 인터넷의 모든 콘텐츠가 자유롭게 사용 가능한 것은 아니며, 저작권 침해는 심각한 법적 결과를 초래할 수 있습니다.

이런 문제는 실제 데이터 수집 프로젝트에서 가장 중요하면서도 자주 간과되는 부분입니다. 특히 크롤링한 데이터를 LLM 학습에 사용하거나, 재배포하거나, 상업적 서비스에 활용할 때는 반드시 라이선스를 확인해야 합니다.

나중에 문제가 생기면 프로젝트 전체를 중단해야 할 수도 있습니다. 바로 이럴 때 필요한 것이 체계적인 저작권 및 라이선스 확인 프로세스입니다.

크롤링 전에 사전 검토를 하고, 수집한 데이터마다 라이선스 정보를 기록하면 법적 리스크를 최소화할 수 있습니다.

개요

간단히 말해서, 저작권 및 라이선스 확인은 크롤링 대상 웹사이트의 이용 약관, robots.txt, 라이선스 고지를 검토하여 데이터 수집과 사용이 법적으로 허용되는지 판단하는 과정입니다. 마치 도서관에서 책을 빌릴 때 대출 규칙을 확인하는 것과 같습니다.

이 과정이 필요한 이유는 저작권법과 각 사이트의 이용 약관이 데이터 수집을 제한할 수 있기 때문입니다. 예를 들어, 일부 사이트는 robots.txt에서 크롤링을 명시적으로 금지하고, 어떤 사이트는 CC-BY-NC 라이선스로 비상업적 사용만 허용합니다.

GitHub의 오픈소스 프로젝트도 MIT, Apache, GPL 등 라이선스에 따라 사용 조건이 다릅니다. 기존에는 크롤링부터 하고 나중에 문제가 생기면 대응했다면, 이제는 사전에 robots.txt를 확인하고 라이선스 정보를 자동으로 추출하여 데이터와 함께 저장할 수 있습니다.

이렇게 하면 법적 안전성을 확보하면서도 작업을 진행할 수 있습니다. 핵심 특징은 세 가지입니다: 첫째, robots.txt를 파싱하여 크롤링 가능한 경로와 금지된 경로를 확인하고, 둘째, 페이지나 저장소에서 라이선스 정보를 자동으로 추출하여 메타데이터로 기록하며, 셋째, CC(Creative Commons), MIT, Apache 같은 주요 라이선스의 사용 조건을 데이터베이스에 함께 저장하여 나중에 필터링할 수 있게 합니다.

코드 예제

import requests
from urllib.parse import urljoin, urlparse
from urllib.robotparser import RobotFileParser
import re

# 주석: robots.txt를 파싱하여 크롤링 허용 여부를 확인합니다
def check_robots_txt(url, user_agent='*'):
    parsed = urlparse(url)
    robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"

    rp = RobotFileParser()
    rp.set_url(robots_url)
    try:
        rp.read()
        can_fetch = rp.can_fetch(user_agent, url)
        crawl_delay = rp.crawl_delay(user_agent)
        return {
            'allowed': can_fetch,
            'crawl_delay': crawl_delay,
            'robots_url': robots_url
        }
    except:
        # 주석: robots.txt가 없으면 크롤링 가능한 것으로 간주
        return {'allowed': True, 'crawl_delay': None}

# 주석: GitHub 저장소의 라이선스를 확인합니다
def get_github_license(repo_url):
    # 주석: GitHub API를 사용하면 라이선스 정보를 쉽게 가져올 수 있습니다
    api_url = repo_url.replace('github.com', 'api.github.com/repos')
    response = requests.get(api_url)
    if response.status_code == 200:
        data = response.json()
        license_info = data.get('license', {})
        return {
            'license_name': license_info.get('name', 'Unknown'),
            'license_key': license_info.get('spdx_id', 'Unknown'),
            'license_url': license_info.get('url', '')
        }
    return None

# 주석: 웹페이지에서 라이선스 정보를 추출합니다
def extract_license_from_page(html_content):
    # 주석: 일반적인 라이선스 표현을 찾습니다
    license_patterns = {
        'MIT': r'MIT License',
        'Apache-2.0': r'Apache License.*2\.0',
        'GPL-3.0': r'GNU General Public License.*v3',
        'CC-BY-4.0': r'Creative Commons.*Attribution 4\.0',
        'CC-BY-NC-4.0': r'Creative Commons.*Attribution-NonCommercial 4\.0'
    }

    found_licenses = []
    for license_key, pattern in license_patterns.items():
        if re.search(pattern, html_content, re.IGNORECASE):
            found_licenses.append(license_key)

    return found_licenses if found_licenses else ['Unknown']

# 주석: 수집한 데이터와 함께 저장할 라이선스 메타데이터를 생성합니다
def create_data_with_license(url, content):
    robots_check = check_robots_txt(url)

    if not robots_check['allowed']:
        raise ValueError(f"크롤링 금지: {url}")

    # 주석: GitHub인 경우 API로 라이선스 확인
    license_info = None
    if 'github.com' in url:
        license_info = get_github_license(url)

    return {
        'url': url,
        'content': content,
        'license': license_info or extract_license_from_page(content),
        'robots_allowed': robots_check['allowed'],
        'crawl_delay': robots_check['crawl_delay']
    }

설명

이것이 하는 일: 이 코드는 크롤링 전에 법적 허용 여부를 자동으로 확인하고, 수집한 데이터와 함께 라이선스 정보를 메타데이터로 저장하여 법적 리스크를 최소화하는 안전한 크롤링 시스템을 구현합니다. 첫 번째로, check_robots_txt 함수는 Python의 RobotFileParser를 사용하여 사이트의 robots.txt를 자동으로 파싱합니다.

can_fetch() 메서드는 특정 URL을 크롤링해도 되는지 판단하고, crawl_delay는 요청 간 대기 시간을 알려줍니다. 예를 들어 crawl_delay가 5초면, 다음 요청까지 5초를 기다려야 서버에 무리를 주지 않습니다.

그 다음으로, get_github_license 함수가 GitHub API를 호출하여 저장소의 라이선스를 정확하게 확인합니다. GitHub는 저장소마다 LICENSE 파일을 분석하여 SPDX 표준 라이선스 식별자를 제공하기 때문에, 이 정보가 가장 신뢰할 수 있습니다.

MIT, Apache-2.0, GPL-3.0 같은 표준 이름으로 받을 수 있어서 나중에 라이선스별로 필터링하기도 쉽습니다. 세 번째로, extract_license_from_page 함수는 일반 웹페이지의 HTML에서 라이선스 문구를 정규표현식으로 찾습니다.

대부분의 문서 사이트는 푸터나 사이드바에 라이선스 고지를 표시하는데, 이를 자동으로 감지하여 기록합니다. CC(Creative Commons) 라이선스의 경우 BY(저작자 표시), NC(비상업적 사용), SA(동일조건변경허락) 같은 조건을 구분하여 저장하는 것이 중요합니다.

마지막으로, create_data_with_license 함수가 모든 확인 과정을 통합하여 데이터와 메타데이터를 함께 저장합니다. robots.txt에서 크롤링이 금지된 경우 예외를 발생시켜 작업을 중단하고, 허용된 경우에만 라이선스 정보를 포함한 완전한 데이터 객체를 반환합니다.

여러분이 이 코드를 사용하면 법적 문제 없이 안전하게 데이터를 수집할 수 있습니다. 나중에 수집한 데이터를 사용할 때도 "MIT 라이선스 데이터만 상업적 사용", "CC-BY 데이터는 저작자 표시 필수" 같은 조건을 자동으로 적용할 수 있어서, 대규모 데이터셋을 관리할 때 매우 유용합니다.

실전 팁

💡 robots.txt를 한 번 읽어서 캐시에 저장하세요. 같은 사이트의 여러 페이지를 크롤링할 때마다 매번 확인하면 비효율적입니다.

💡 GitHub API는 인증 없이도 시간당 60회 요청이 가능하지만, Personal Access Token을 사용하면 5000회로 늘어나니 대량 수집 시 토큰을 사용하세요.

💡 라이선스 정보를 찾을 수 없는 경우 "All Rights Reserved"로 간주하고 사용을 자제하는 것이 안전합니다.

💡 Creative Commons 라이선스의 NC(NonCommercial) 조건은 생각보다 엄격합니다. 무료 서비스라도 광고가 있으면 상업적 사용으로 간주될 수 있으니 주의하세요.

💡 수집한 데이터를 공개 데이터셋으로 배포할 때는 원본 라이선스를 명시하고, 가능하면 원본 출처 링크도 함께 제공하세요.


6. 수집 데이터 저장 구조 설계

시작하며

여러분이 수천 개의 문서를 크롤링한 후, 이 데이터를 어떻게 저장하고 관리해야 할지 막막했던 적 있나요? 그냥 텍스트 파일로 저장하면 나중에 검색하기 어렵고, 데이터베이스 구조를 잘못 설계하면 쿼리 성능이 느려져서 실용성이 떨어집니다.

이런 문제는 실제 데이터 파이프라인 구축에서 가장 많은 시간이 걸리는 부분입니다. 저장 구조가 잘못되면 나중에 데이터를 활용할 때마다 복잡한 전처리가 필요하고, 확장성도 떨어져서 결국 처음부터 다시 설계해야 하는 상황이 생깁니다.

바로 이럴 때 필요한 것이 체계적인 데이터 저장 구조 설계입니다. 데이터의 용도와 접근 패턴을 고려하여 적절한 스키마와 인덱스를 설계하면, 빠른 검색과 효율적인 업데이트가 가능한 시스템을 만들 수 있습니다.

개요

간단히 말해서, 수집 데이터 저장 구조 설계는 크롤링한 문서를 효율적으로 저장하고 검색할 수 있도록 데이터베이스 스키마, 파일 구조, 인덱싱 전략을 계획하는 과정입니다. 마치 도서관에서 책을 어떤 분류 체계로 배치할지 결정하는 것과 같습니다.

이 과정이 필요한 이유는 저장 구조가 이후의 모든 작업(검색, 분석, LLM 학습)의 성능과 편의성을 결정하기 때문입니다. 예를 들어, RAG(Retrieval-Augmented Generation) 시스템을 만들 때는 벡터 임베딩과 원본 텍스트를 함께 저장해야 하고, 버전 관리가 필요하면 타임스탬프와 변경 이력도 기록해야 합니다.

기존에는 단순한 키-값 저장소나 파일 시스템에 저장했다면, 이제는 PostgreSQL 같은 관계형 DB와 Elasticsearch 같은 검색 엔진, 그리고 벡터 DB를 조합하여 각 데이터 타입에 최적화된 저장 방식을 선택할 수 있습니다. 핵심 특징은 세 가지입니다: 첫째, 원본 데이터와 메타데이터를 분리하여 저장하고, 둘째, 검색 패턴에 맞는 인덱스를 미리 생성하며, 셋째, 데이터의 증분 업데이트를 지원하는 버전 관리 구조를 설계합니다.

이렇게 하면 데이터가 수백만 건으로 늘어나도 안정적으로 관리할 수 있습니다.

코드 예제

import json
from datetime import datetime
import hashlib

# 주석: 수집한 문서의 저장 구조를 정의합니다
class DocumentStorage:
    def __init__(self):
        self.documents = {}

    # 주석: 문서를 구조화된 형태로 저장합니다
    def save_document(self, url, content, metadata):
        # 주석: URL의 해시를 고유 ID로 사용합니다
        doc_id = hashlib.md5(url.encode()).hexdigest()

        # 주석: 문서 데이터를 계층적 구조로 구성합니다
        document = {
            'id': doc_id,
            'url': url,
            'collected_at': datetime.now().isoformat(),
            'content': {
                'raw_text': content.get('raw_text', ''),
                'sections': content.get('sections', []),
                'code_blocks': content.get('code_blocks', [])
            },
            'metadata': {
                'title': metadata.get('title', ''),
                'source_type': metadata.get('source_type', ''),
                'license': metadata.get('license', 'Unknown'),
                'tags': metadata.get('tags', [])
            },
            'version': self._get_version(doc_id)
        }

        # 주석: 버전 관리를 위해 이전 데이터를 히스토리에 저장합니다
        if doc_id in self.documents:
            if 'history' not in self.documents[doc_id]:
                self.documents[doc_id]['history'] = []
            self.documents[doc_id]['history'].append(
                self.documents[doc_id].copy()
            )

        self.documents[doc_id] = document
        return doc_id

    # 주석: 버전 번호를 자동으로 증가시킵니다
    def _get_version(self, doc_id):
        if doc_id in self.documents:
            return self.documents[doc_id].get('version', 0) + 1
        return 1

    # 주석: JSON 파일로 저장합니다 (실제로는 DB를 사용)
    def export_to_json(self, filepath):
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(list(self.documents.values()), f,
                     ensure_ascii=False, indent=2)

# 주석: PostgreSQL 테이블 스키마 예시 (SQL)
"""
CREATE TABLE documents (
    id VARCHAR(32) PRIMARY KEY,
    url TEXT UNIQUE NOT NULL,
    collected_at TIMESTAMP DEFAULT NOW(),
    title TEXT,
    source_type VARCHAR(50),
    license VARCHAR(50),
    raw_text TEXT,
    version INTEGER DEFAULT 1
);

CREATE TABLE document_sections (
    id SERIAL PRIMARY KEY,
    document_id VARCHAR(32) REFERENCES documents(id),
    section_title TEXT,
    section_level INTEGER,
    section_content TEXT,
    section_order INTEGER
);

CREATE TABLE document_code_blocks (
    id SERIAL PRIMARY KEY,
    document_id VARCHAR(32) REFERENCES documents(id),
    language VARCHAR(20),
    code TEXT,
    block_order INTEGER
);

-- 주석: 빠른 검색을 위한 인덱스 생성
CREATE INDEX idx_documents_source_type ON documents(source_type);
CREATE INDEX idx_documents_license ON documents(license);
CREATE INDEX idx_sections_document_id ON document_sections(document_id);
"""

설명

이것이 하는 일: 이 코드는 크롤링한 문서를 계층적이고 확장 가능한 구조로 저장하며, 버전 관리와 효율적인 검색을 지원하는 완전한 데이터 저장 시스템의 설계를 보여줍니다. 첫 번째로, DocumentStorage 클래스의 save_document 메서드는 문서를 여러 계층으로 구분하여 저장합니다.

content는 실제 텍스트 데이터를, metadata는 문서에 대한 설명 정보를 담습니다. 이렇게 분리하는 이유는 메타데이터만 검색할 때 큰 텍스트 데이터를 로드하지 않아도 되어 성능이 향상되기 때문입니다.

그 다음으로, URL의 MD5 해시를 문서의 고유 ID로 사용합니다. 이렇게 하면 같은 URL을 다시 크롤링할 때 중복을 자동으로 감지할 수 있고, 업데이트된 내용을 버전 관리할 수 있습니다.

_get_version 메서드는 같은 문서가 업데이트될 때마다 버전 번호를 1씩 증가시킵니다. 세 번째로, 버전 관리 시스템이 이전 데이터를 history 리스트에 보존합니다.

이렇게 하면 문서가 언제 어떻게 변경되었는지 추적할 수 있고, 필요하면 이전 버전으로 롤백할 수도 있습니다. 특히 API 문서처럼 자주 업데이트되는 콘텐츠를 다룰 때 매우 유용합니다.

네 번째로, 주석으로 제공된 PostgreSQL 스키마는 관계형 데이터베이스 설계를 보여줍니다. documents 테이블은 메타데이터를, document_sections와 document_code_blocks는 콘텐츠의 세부 요소를 담습니다.

이렇게 정규화하면 "Python 코드 블록이 있는 모든 문서", "3단계 섹션만 추출" 같은 복잡한 쿼리를 효율적으로 실행할 수 있습니다. 마지막으로, CREATE INDEX 문으로 자주 사용하는 컬럼에 인덱스를 생성합니다.

source_type과 license로 필터링하는 쿼리는 인덱스가 있으면 수백 배 빠르게 실행되어, 수백만 건의 문서에서도 실시간 검색이 가능해집니다. 여러분이 이 구조를 사용하면 데이터가 아무리 많아져도 안정적으로 관리할 수 있습니다.

검색 속도가 빠르고, 버전 관리가 자동이며, 나중에 새로운 메타데이터 필드를 추가하거나 구조를 확장하기도 쉽습니다. 또한 이 구조는 RAG 시스템, 문서 검색 엔진, LLM 파인튜닝 데이터 준비 등 다양한 용도로 바로 활용할 수 있습니다.

실전 팁

💡 텍스트 검색이 중요하면 PostgreSQL의 Full-Text Search나 Elasticsearch를 추가로 사용하세요. 키워드 검색 성능이 극적으로 향상됩니다.

💡 코드 블록에는 language 컬럼을 꼭 추가하세요. 나중에 "Python 예제만 모아서 학습 데이터 만들기" 같은 작업이 매우 쉬워집니다.

💡 collected_at 타임스탬프로 "최근 1주일 내 수집된 문서"를 빠르게 필터링할 수 있어서, 증분 업데이트 전략을 구현할 때 유용합니다.

💡 대용량 텍스트는 DB에 직접 저장하지 말고 S3 같은 객체 스토리지에 저장한 후 URL만 DB에 기록하는 방식도 고려하세요.

💡 벡터 임베딩을 저장할 계획이라면 pgvector(PostgreSQL 확장) 또는 Pinecone, Weaviate 같은 전문 벡터 DB를 사용하면 유사도 검색이 훨씬 빠릅니다.


#Python#WebCrawling#BeautifulSoup#Selenium#DataCollection#AI,LLM,머신러닝,파인튜닝,NLP

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.