AI & Automation
Python 기반 데이터 수집과 자동화 파이프라인 설계
Python 기본 문법, 가상환경, Pandas 데이터 처리, 웹 크롤링, Open API 호출 실습을 자동화 파이프라인 관점으로 연결해 정리합니다.

Python을 처음 정리할 때는 변수, 자료형, 제어문, 함수 같은 문법 항목이 먼저 나온다. 하지만 백엔드와 플랫폼 관점에서 Python이 특히 유용해지는 지점은 “반복되는 데이터 작업을 작은 파이프라인으로 만들 수 있다”는 데 있다. HTTP로 외부 데이터를 가져오고, JSON이나 HTML에서 필요한 필드를 뽑고, Pandas로 정리한 뒤, 파일이나 데이터베이스 또는 API 응답으로 넘기는 흐름이 짧은 코드로 이어진다.
웹 크롤링, Open API, 데이터 분석, Jupyter Notebook 실습은 따로 떨어진 주제가 아니다. 실제 업무에서는 운영 지표를 모으거나, 외부 공공 데이터를 가져오거나, 뉴스와 문서를 정리하거나, 배치성 리포트를 만드는 흐름으로 연결된다. 이 글은 Python 기초 학습 내용을 문법 목록이 아니라 자동화 파이프라인을 구성하는 요소로 다시 묶어본 기록이다.
실행 환경은 재현 가능해야 한다
Python은 설치가 쉽지만, 프로젝트가 늘어날수록 환경이 쉽게 섞인다. 전역 Python에 모든 라이브러리를 설치하면 어느 프로젝트가 어떤 버전의 패키지를 필요로 하는지 추적하기 어려워진다. 그래서 작은 실습이라도 가상환경을 먼저 만든다.
python -m venv .venv
# Windows PowerShell
.\.venv\Scripts\Activate.ps1
# macOS / Linux
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install requests beautifulsoup4 pandas pyarrow openpyxl jupyter
python -m pip freeze > requirements.txtpip install 대신 python -m pip 형태를 쓰면 현재 활성화된 Python 실행 파일과 pip가 같은 환경을 바라보는지 확인하기 쉽다. 여러 Python 버전이 설치된 Windows 환경에서 특히 유용하다.
프로젝트 폴더는 코드와 데이터를 분리해둔다. 실습 단계에서는 단순한 구조면 충분하다.
project/
code/
collect_market_data.py
transform_prices.py
data/
raw/
processed/
requirements.txtJupyter Notebook은 탐색과 시각화에는 좋지만, 운영 자동화의 최종 형태로 두기에는 약하다. 노트북에서 실험한 코드는 재사용 가능한 함수와 스크립트로 옮기는 것이 좋다. 노트북은 “무엇을 확인했는지”를 빠르게 보는 작업대에 가깝고, 스케줄러나 CI에서 실행할 코드는 .py 파일로 관리하는 편이 안전하다.
Python 기초는 데이터 흐름을 표현하는 도구다
Python의 기본 자료형은 자동화 코드에서 그대로 데이터 모델 역할을 한다. API 응답의 JSON 객체는 dict와 list로 다루기 쉽고, 여러 단계의 처리 결과는 함수로 나눠 연결할 수 있다.
def normalize_price(value: str) -> int:
return int(value.replace(",", "").strip())
def build_record(name: str, price: str, source: str) -> dict:
return {
"name": name.strip(),
"price": normalize_price(price),
"source": source,
}동적 타이핑은 빠르게 코드를 작성하기 좋지만, 데이터 파이프라인에서는 입력 형태가 조금만 바뀌어도 오류가 늦게 드러날 수 있다. 그래서 함수 경계에는 type hint를 붙이고, 외부 입력은 명시적으로 변환한다.
from dataclasses import dataclass
@dataclass(frozen=True)
class MarketItem:
name: str
price: int
source: str
def parse_item(row: dict) -> MarketItem:
return MarketItem(
name=str(row["name"]).strip(),
price=int(row["price"]),
source=str(row.get("source", "unknown")),
)리스트, 튜플, 집합, 딕셔너리는 각각 쓰임이 다르다. 순서를 유지하며 레코드 목록을 처리할 때는 리스트가 자연스럽다. 중복 제거에는 집합이 편하고, 식별자 기준 조회에는 딕셔너리가 좋다. 불변의 좌표나 짝을 표현할 때는 튜플이 가볍다.
symbols = ["005930", "000660", "035420"]
unique_symbols = set(symbols)
symbol_to_name = {
"005930": "Samsung Electronics",
"000660": "SK hynix",
}이런 기본 자료구조를 적절히 쓰는 것만으로도 작은 배치 프로그램의 복잡도를 크게 줄일 수 있다.
HTTP 요청은 실패를 기본값으로 보고 작성한다
웹 크롤링과 Open API 호출은 모두 HTTP 요청에서 시작한다. URL은 scheme, domain, path, query string으로 구성되고, 응답은 상태 코드와 body를 가진다. 2xx는 성공, 3xx는 리다이렉트, 4xx는 클라이언트 오류, 5xx는 서버 오류다. 자동화 코드는 이 상태를 반드시 확인해야 한다.
import requests
def fetch_json(url: str, params: dict | None = None) -> dict:
response = requests.get(
url,
params=params,
headers={"User-Agent": "tykdev-data-pipeline/1.0"},
timeout=10,
)
response.raise_for_status()
return response.json()timeout을 지정하지 않으면 외부 서버가 응답하지 않을 때 프로세스가 오래 묶일 수 있다. raise_for_status()를 호출하지 않으면 404나 500 응답도 정상 body처럼 처리하다가 뒤쪽 단계에서 이상한 오류가 날 수 있다.
크롤링에서는 특히 수집 대상의 정책을 먼저 확인해야 한다. robots.txt, 서비스 약관, 요청 빈도 제한, 저작권, 개인정보 처리 여부를 봐야 한다. 브라우저에서 보이는 데이터를 가져올 수 있다고 해서 자동 수집이 항상 괜찮은 것은 아니다.
from urllib.robotparser import RobotFileParser
def can_fetch(base_url: str, target_url: str) -> bool:
parser = RobotFileParser()
parser.set_url(f"{base_url}/robots.txt")
parser.read()
return parser.can_fetch("tykdev-data-pipeline", target_url)API가 제공되는 데이터라면 HTML scraping보다 API를 우선한다. API는 구조가 명확하고, 인증과 quota 정책이 문서화되어 있으며, 필드 변경 가능성을 상대적으로 관리하기 쉽다.
HTML 파싱은 DOM 구조 변경을 견딜 수 있어야 한다
HTML scraping은 개발자 도구의 Network와 Elements 탭을 함께 보면서 시작한다. 페이지가 서버에서 HTML을 내려주는지, 브라우저에서 JavaScript로 API를 호출해 렌더링하는지 먼저 구분해야 한다. 실제 데이터가 별도 JSON API로 내려온다면 HTML을 파싱하는 것보다 그 API 요청을 재현하는 편이 안정적일 수 있다.
BeautifulSoup을 사용할 때는 CSS selector를 너무 깊게 잡지 않는다. body > div:nth-child(3) > table > tr...처럼 구조에 민감한 selector는 작은 마크업 변경에도 깨진다. 가능하면 의미 있는 class, id, table header, link attribute를 기준으로 잡는다.
from bs4 import BeautifulSoup
def parse_news(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
items = []
for link in soup.select("a.news-title"):
title = link.get_text(strip=True)
href = link.get("href")
if not title or not href:
continue
items.append({
"title": title,
"url": href,
})
return itemsHTML 파싱 단계에서는 데이터가 없을 수 있다는 사실을 정상 흐름으로 다룬다. selector가 아무것도 찾지 못했을 때 바로 실패시킬지, 빈 결과로 저장할지, 알림을 보낼지는 파이프라인의 목적에 따라 다르다. 운영 지표 수집이라면 빈 결과가 장애 신호일 수 있고, 뉴스 수집이라면 특정 시간대에는 결과가 적을 수 있다.
Pandas는 전처리와 검증에 강하다
Pandas는 표 형태 데이터를 다루기 좋다. CSV, Excel, JSON을 읽고, 결측치 처리, 자료형 변환, 필터링, 그룹화, 조인을 빠르게 수행할 수 있다. 실습에서는 Series, DataFrame, loc, iloc, groupby, merge, read_csv, to_csv 같은 기능을 확인했다.
import pandas as pd
def normalize_items(items: list[dict]) -> pd.DataFrame:
df = pd.DataFrame(items)
if df.empty:
return pd.DataFrame(columns=["name", "price", "source", "collected_at"])
df["name"] = df["name"].astype(str).str.strip()
df["price"] = (
df["price"]
.astype(str)
.str.replace(",", "", regex=False)
.astype(int)
)
df["collected_at"] = pd.Timestamp.utcnow()
return df.drop_duplicates(subset=["name", "source"])Pandas에서 중요한 것은 변환 결과를 눈으로만 확인하지 않고, 코드로 검증하는 것이다. 예를 들어 가격이 음수이면 안 되고, 필수 컬럼이 비어 있으면 안 된다.
def validate_frame(df: pd.DataFrame) -> None:
required = {"name", "price", "source"}
missing = required - set(df.columns)
if missing:
raise ValueError(f"missing columns: {sorted(missing)}")
if df["name"].isna().any():
raise ValueError("name contains null")
if (df["price"] < 0).any():
raise ValueError("price contains negative value")Excel은 빠른 수작업과 시각적 확인에 강하지만, 반복 가능한 대규모 처리와 버전 관리에는 한계가 있다. Python은 초기 환경 설정이 필요하지만, 데이터 수집과 전처리 절차가 코드로 남기 때문에 같은 작업을 재실행하고 리뷰하기 쉽다.
작은 파이프라인으로 연결한다
수집, 파싱, 정제, 검증, 저장을 함수로 나누면 흐름이 명확해진다.
from pathlib import Path
DATA_DIR = Path("data")
def run_pipeline() -> None:
html = requests.get(
"https://example.com/news",
headers={"User-Agent": "tykdev-data-pipeline/1.0"},
timeout=10,
).text
records = parse_news(html)
df = pd.DataFrame(records)
validate_news_frame(df)
output = DATA_DIR / "processed" / "news.parquet"
output.parent.mkdir(parents=True, exist_ok=True)
df.to_parquet(output, index=False)
def validate_news_frame(df: pd.DataFrame) -> None:
if "title" not in df.columns or "url" not in df.columns:
raise ValueError("news frame requires title and url")
if __name__ == "__main__":
run_pipeline()처음에는 로컬 파일로 저장해도 충분하다. 이후 스케줄링이 필요해지면 GitHub Actions, cron, Azure Functions, AWS Lambda 같은 실행 환경으로 옮길 수 있다. 저장 대상도 CSV에서 Parquet, PostgreSQL, Blob Storage, S3, Cosmos DB 등으로 바뀔 수 있다. 중요한 것은 함수 경계가 분리되어 있어 수집 소스와 저장소가 바뀌어도 전체 코드를 갈아엎지 않는 구조다.
자동화 파이프라인은 코드가 짧아도 운영 고려사항이 필요하다. 외부 API key는 환경 변수나 secret store로 분리하고, 요청 실패는 재시도 정책을 둔다. 수집 시간이 길어지면 timeout과 pagination을 나눠야 하고, 중복 저장을 막기 위해 idempotency key나 unique constraint를 둔다. 결과 파일에는 수집 시각, 소스 URL, 버전 같은 metadata를 남기면 나중에 추적하기 쉽다.
import os
API_KEY = os.environ["OPEN_DATA_API_KEY"]
params = {
"serviceKey": API_KEY,
"pageNo": 1,
"numOfRows": 100,
}Jupyter는 탐색, 스크립트는 실행 경로로 둔다
Jupyter Notebook은 데이터의 모양을 확인하고, 그래프를 그리며, 전처리 아이디어를 빠르게 실험하기 좋다. %timeit, %whos, describe(), value_counts() 같은 도구를 쓰면 데이터 특성을 빠르게 파악할 수 있다.
df.info()
df.describe()
df["source"].value_counts()하지만 노트북은 셀 실행 순서가 꼬이기 쉽고, 숨은 상태가 남을 수 있다. 그래서 반복 실행해야 하는 작업은 스크립트로 옮기고, 노트북은 분석 결과를 설명하는 문서에 가깝게 유지하는 편이 좋다. 팀 작업에서는 requirements.txt, 샘플 데이터, 실행 명령, 출력 위치를 함께 기록해야 재현성이 생긴다.
정리
Python 학습의 출발점은 문법이지만, 실제 힘은 반복 가능한 자동화 흐름을 만드는 데서 나온다. 변수와 자료형은 외부 데이터를 표현하고, 함수는 수집과 정제 단계를 나누며, requests와 BeautifulSoup은 HTTP와 HTML을 다루고, Pandas는 표 형태 데이터를 검증 가능한 구조로 바꾼다. Jupyter는 탐색에 좋고, 스크립트는 실행과 배포에 적합하다.
자동화 파이프라인을 작게라도 끝까지 만들어보면 백엔드와 플랫폼 작업에도 바로 연결된다. API 호출에는 timeout과 인증이 필요하고, 수집 데이터에는 스키마 검증이 필요하며, 저장소에는 중복과 실패 복구 전략이 필요하다. Python은 이 흐름을 빠르게 실험하고, 검증한 뒤, 운영 가능한 작은 도구로 바꾸기에 좋은 언어다.
이 글의 무단 복제, 재배포, 자동 수집을 허용하지 않습니다. 인용이 필요한 경우 출처와 원문 링크를 함께 남겨주세요.