데이터 수집
1) 학습개요 요약
- 데이터 분석의 출발점은 목적 적합성을 갖춘 데이터 확보입니다.
- 단순 수집을 넘어, 정확성·완전성·일관성 등 품질 요건을 만족하도록 구조와 절차를 설계해야 합니다.
- 정형/반정형/비정형 유형을 구분하고, 파일·API·웹 스크래핑 등 상황에 맞는 수집 전략을 선택합니다.
2) 학습목표 매핑
- 목표 1: 왜 수집이 필요한가 → 가치·의사결정·경쟁력, 수집 정의 및 품질의 중요성.
- 목표 2: 데이터 유형 구분 → 정형/반정형/비정형의 구조·특성·한계.
- 목표 3: 다양한 소스 수집 코드 → 파일(pandas), API(requests/GraphQL/WebSocket), 스크래핑(requests+BeautifulSoup/Selenium) 예시.
3) 데이터 수집의 이해
3.1 왜 필요한가
- 의사결정 품질은 데이터 품질과 직결됩니다(개인·기업·국가 경쟁력에 영향).
- “수집”은 모으기가 아니라 분석 목적에 맞게 준비(형식·주기·정확성 보장)하는 과정입니다.
3.2 데이터 품질 핵심(ISO 8000 관점)
- 정확성: 사실과 일치
- 완전성: 결측(누락) 없음
- 일관성: 포맷/규칙의 균일성
- 유효성: 정의된 규칙/범위에 부합
- 적시성: 최신성 및 갱신 주기 충족
- 상호운용성: 시스템 간 호환
3.3 수집 시 흔한 어려움
- 양: 충분한 표본/커버리지 확보
- 다양성: 출처·스키마·포맷의 이질성
- 정확성/일관성: 중복, 포맷 불일치, 인코딩/로캘 문제
4) 데이터의 유형
4.1 정형(Structured)
- 특징: 고정 스키마, 행·열 테이블. RDBMS에 적합, 질의·집계가 용이.
- 한계: 예측하지 못한 구조/속성의 유연성 낮음.
4.2 비정형(Unstructured)
- 특징: 사전 스키마 없음(문서, 이미지, 오디오/비디오, 자유 텍스트 등).
- 의의: 생산되는 데이터의 80%가 비정형 데이터.
- 맥락 정보가 풍부하나, 저장 및 관리가 어렵고, 처리 난이도 높음(NLP, CV 등 필요).
4.3 반정형(Semi-structured)
- 특징: 키-값/태그/계층 구조(JSON, XML, 로그, 이메일 헤더 등).
- 장점: 유연한 스키마 진화, NoSQL/데이터 레이크에 적합.
- 주의: 분석 전 스키마 추론·정제 필요.
5) 데이터 수집 방법
5.1 파일 기반 수집(일회성/배치에 적합)
- 대표 포맷: CSV, Excel, JSON, XML, HTML, TXT, LOG.
- 권장 절차: 인코딩 확인 → 스키마 정의/캐스팅 → 결측/이상치 처리 → 검증/샘플링.
# CSV/Excel/JSON 빠른 로드 예시 (Python + pandas)
import pandas as pd
df_csv = pd.read_csv("data.csv") # 기본: 콤마 구분, UTF-8 가정
df_xls = pd.read_excel("report.xlsx", sheet_name=0) # 시트 지정
df_json = pd.read_json("data.json", lines=False) # JSON Lines면 lines=True
# 타입 캐스팅 & 기본 정제
df = df_csv.rename(columns=lambda c: c.strip())
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.dropna(subset=["id"]) # 필수키 결측 제거
5.2 API 기반 수집(자동화/최신성/접근 제어)
- REST API(HTTP 요청/응답, JSON/XML), GraphQL(필요한 필드만 질의), WebSocket(양방향 실시간).
- 핵심 체크: 인증(키/토큰), Rate limit, 페이징(cursor/offset), 재시도(백오프), 스키마 버전.
# REST API 예시 (requests)
import requests
BASE = "https://api.example.com/v1/items"
params = {"limit": 100, "page": 1}
headers = {"Authorization": "Bearer "}
items = []
while True:
r = requests.get(BASE, params=params, headers=headers, timeout=10)
r.raise_for_status()
data = r.json()
items.extend(data["results"])
if not data.get("next"): break
params["page"] += 1
# GraphQL 예시: 필요한 필드만 선택
import requests
url = "https://api.example.com/graphql"
query = """
query($after: String){
products(first: 50, after: $after){
pageInfo{ hasNextPage endCursor }
nodes{ id name price currency }
}
}
"""
variables = {"after": None}
res = requests.post(url, json={"query": query, "variables": variables}).json()
고빈도·지연 민감한 스트림은 WebSocket을 고려(예: 시세/알림).
5.3 웹 스크래핑(페이지에서 직접 추출)
- 정적 페이지: requests + BeautifulSoup(lxml)로 빠르고 가벼움.
- 동적 페이지(JS 렌더링): Selenium/Playwright로 렌더 후 추출.
- 주의: 서비스 약관/robots.txt, 로그인/쿠키, 차단 회피(합법 범위), 구조 변화 내성.
# 정적 페이지 스크래핑 예시
import requests
from bs4 import BeautifulSoup
html = requests.get("https://example.com/list", timeout=10).text
soup = BeautifulSoup(html, "lxml")
rows = []
for card in soup.select(".item-card"):
rows.append({
"title": card.select_one(".title").get_text(strip=True),
"price": card.select_one(".price").get_text(strip=True),
"url": card.select_one("a")["href"],
})
# 동적 페이지 스크래핑 예시 (Selenium)
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://example.com/dynamic")
items = []
for el in driver.find_elements(By.CSS_SELECTOR, ".row"):
items.append(el.text)
driver.quit()
6) 적용 가이드라인(체크리스트)
6.1 수집 전략 선택 의사결정 순서
- 목적 정의: 어떤 의사결정/모델/리포트를 위해 무엇이 필요한가.
- 데이터 위치: 파일? API? 웹? 복수 결합?
- 신선도/빈도: 배치(일/주) vs 실시간/근실시간.
- 품질·보안: 인증, 민감정보, 로그 추적성, 원천 무결성.
- 예산/복잡도: 유지관리 가능성(스키마 변화, 페이지 구조 변경).
6.2 품질 확보 절차(최소 권장)
- 스키마 계약(필드명/타입/널 허용/코드체계) 문서화
- 검증 규칙(유효 범위, 정규식, 참조 무결성) 적용
- 중복/결측 처리: 키 기준 중복 제거, 비즈니스 규칙에 따른 보간/삭제
- 로깅/메타데이터: 수집 시각, 소스, 해시(무결성) 기록
- 재현성: 동일 입력→동일 결과(버전 고정, 컨테이너/워크플로우 정의)
7) 파일·API·스크래핑 비교
- 파일: 단순·저비용·배치 적합. 단, 최신성/동기화는 별도 관리 필요.
- API(REST/GraphQL/WebSocket): 공식 채널·신뢰 높음·자동화 용이. 단, Rate limit/권한/비용 고려.
- 스크래핑: API 미제공 시 대안. 구조 변경·차단·법적 이슈 리스크. 캐시/백오프/모니터링 필수.
8) 미니 실습 과제
- CSV+JSON 병합: 두 소스 키 조인 후, 날짜 캐스팅·결측 처리·유효성 검증 보고서 생성.
- REST API 수집 파이프라인: 페이징·재시도(지수 백오프)·에러 로깅·증분 수집 구현.
- 스크래핑 견고화: CSS 셀렉터 변경에 대비해 XPATH/백업 셀렉터와 구조 검출 알림 추가.
9) 핵심 정리
- 데이터 수집은 목적 적합 + 품질 보장의 전략적 과정.
- 정형/반정형/비정형의 구조적 차이를 이해하고, 이에 맞는 파일·API·스크래핑 방식을 선택.
- 품질 요건(정확성·완전성·일관성·유효성·적시성·상호운용성)을 충족하도록 스키마·검증·로깅을 체계화.
10) 약어 해설
- API(Application Programming Interface): 애플리케이션 간 상호작용 규약
- HTTP(Hypertext Transfer Protocol): 웹 통신 프로토콜
- JSON(JavaScript Object Notation): 경량 데이터 교환 형식
- XML(eXtensible Markup Language): 확장 가능한 마크업 언어
- NLP(Natural Language Processing): 자연어 처리
- RDBMS(Relational Database Management System): 관계형 DBMS
오픈소스 기반 데이터 분석 4강 — 데이터 수집
Data-Analysis-with-Open-Source/오픈소스_데이터_분석_4강.ipynb at main · mors119/Data-Analysis-with-Open-Source
Data Analysis with Open Source. Contribute to mors119/Data-Analysis-with-Open-Source development by creating an account on GitHub.
github.com
4-1 CSV 파일 읽기
# --- pandas로 CSV 읽기: 가장 실무적인 기본형 ---
# 역할: CSV를 DataFrame으로 읽어서 표 형태로 다루기
# 주요 옵션:
# - sep: 구분자(기본 ','); 탭은 '\t'
# - header: 헤더 행 번호 또는 None
# - names: 헤더가 없을 때 컬럼명 리스트 지정
# - index_col: 인덱스 컬럼 지정(정수/이름/리스트)
# - usecols: 필요한 컬럼만 로드(메모리 절약)
# - dtype: 컬럼별 타입 강제
# - parse_dates: 날짜 컬럼 파싱(True/리스트/딕셔너리)
# - encoding: 'utf-8', 'cp949'(윈도 CSV 자주)
# - na_values: 결측치로 처리할 값 목록
# - nrows/skiprows: 일부만 읽기(샘플링/스킵)
import pandas as pd
df = pd.read_csv(
'data.csv',
sep=',',
header=0,
index_col=None,
encoding='utf-8',
skiprows=None,
nrows=None,
# names=['날짜', '체중', '골격근량', '체지방량'], # header가 없으면 사용
# usecols=['날짜', '체중'], # 필요한 열만
# dtype={'체중':'float64'},
# parse_dates=['날짜'], # 날짜 파싱
)
print(df)
# 대안 1: 표준 csv 모듈(속도↓, 의존성↓)
# [import csv]
# import csv
# with open('data.csv', encoding='utf-8') as f:
# reader = csv.DictReader(f)
# rows = list(reader)
# 대안 2: pyarrow/csv(대용량·고성능)
# [import pyarrow]
# import pyarrow.csv as pv
# import pyarrow.parquet as pq
# table = pv.read_csv('data.csv')
# pq.write_table(table, 'data.parquet') # 컬럼형 포맷으로 변환
# 대안 3: numpy(수치형 배열 위주)
# [import numpy]
# import numpy as np
# arr = np.genfromtxt('data.csv', delimiter=',', names=True, dtype=None, encoding='utf-8')
4-2 JSON 파일 읽기
# --- 표준 json + pandas.read_json 비교 ---
# 역할: (1) 파일로부터 JSON 로드, (2) DataFrame으로 변환
import json
import pandas as pd
# 1) 파일에서 JSON 객체 로드(dict/list 등)
with open('data.json', mode='r', encoding='utf-8') as f:
data = json.load(f) # 역할: JSON 문자열 → 파이썬 객체
print(data) # 구조 확인(중첩 여부 확인)
# 2) DataFrame으로 읽기
# orient: 'records'가 가장 직관적(레코드 리스트)
# lines=True면 JSON Lines 포맷(한 줄당 한 객체)
df = pd.read_json(
'data.json',
orient='records',
lines=False,
encoding='utf-8',
)
print(df)
# 중첩 구조 평탄화(칼럼 펼치기)
# [import pandas]
# from pandas import json_normalize
# flat = pd.json_normalize(data, record_path=['매출데이터']) # '매출데이터' 배열을 표로
# print(flat)
# 대안: HTTP로 JSON 받기
# [import requests]
# import requests
# resp = requests.get('https://example.com/api.json', timeout=10)
# resp.raise_for_status()
# data = resp.json()
# df = pd.json_normalize(data)
4-3 텍스트 파일 읽기 및 데이터 추출(정규식 마스킹)
# --- 로그 텍스트에서 주민등록번호 마스킹 ---
# 역할: 민감정보(패턴)를 찾아 일부만 남기고 가리기
# 주의: 실제 서비스는 더 강력한 PII(개인정보) 마스킹 정책 필요
import re
# 파일 읽기
with open('callcenter20250301.log', 'r', encoding='utf-8') as f:
content = f.read()
# 주민등록번호 패턴(예: 6자리-7자리). 실제론 유효성 검증까지 고려 권장
pattern = re.compile(r'(\d{6})-(\d{7})')
# 그룹1(앞6자리)만 남기고 뒤 7자리는 별표로 대체
masked_content = pattern.sub(r'\1-*******', content)
# 결과 저장(확장자 일관성 권장: .log 또는 .txt 중 택1)
with open('callcenter20250301_masked.log', mode='w', encoding='utf-8') as f:
f.write(masked_content)
print("주민등록번호 마스킹 완료. 'callcenter20250301_masked.log' 로 저장되었습니다.")
# 옵션/대안:
# - re.IGNORECASE, re.MULTILINE 등 플래그 사용: re.compile(pat, re.M|re.I)
# - 함수 기반 치환으로 부분 마스킹 강도 조절:
# def mask(m): return f"{m.group(1)}-{'*'*len(m.group(2))}"
# masked = pattern.sub(mask, content)
# - 대용량 파일은 스트리밍 처리 권장(라인 단위 읽기)
# - [import pathlib] Path로 경로 조작, [import io]로 버퍼링 제어
4-4 Open-Meteo 무료 날씨 API로 현재 온도 조회
# --- requests로 공공 API 호출 ---
# 역할: 위경도 좌표의 현재 기온을 JSON으로 받아 출력
# 주의: URL은 엔드포인트만 두고, 쿼리는 params에 분리하여 가독성과 안전성 확보
# 참고 옵션:
# - timeout: 네트워크 지연 대비
# - response.raise_for_status(): HTTP 에러 처리
# - timezone: 'Asia/Seoul' 지정 시 현지 시간대 응답
# [import requests]
import requests
BASE_URL = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": 37.58638333,
"longitude": 127.0203333,
"current": "temperature_2m", # 현재 기온 필드만 요청
"timezone": "Asia/Seoul",
}
try:
resp = requests.get(BASE_URL, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
temp = data['current']['temperature_2m']
unit = data['current_units']['temperature_2m']
print("API 응답:", data)
print(f"서울시 종로구의 현재 온도는 : {temp}{unit} 입니다.")
except requests.exceptions.RequestException as e:
print(f"API 호출 실패: {e}")
except (KeyError, TypeError) as e:
print(f"응답 스키마 변경/누락 가능성: {e}")
# 대안:
# - [import httpx] (비동기/타임아웃/재시도 설정 편리)
# import httpx
# with httpx.Client(timeout=10) as client:
# r = client.get(BASE_URL, params=params)
# r.raise_for_status()
# data = r.json()
# - pandas로 바로 normalize
# [import pandas]
# import pandas as pd
# from pandas import json_normalize
# df = json_normalize(data)
4-5 Selenium + lxml로 웹 스크래핑(동적 페이지)
# --- Selenium(headless Chrome)으로 동적 페이지 렌더링 + lxml로 파싱 ---
# 역할: 자바스크립트 렌더링이 필요한 페이지를 브라우저로 열어 HTML을 얻고, XPath로 요소 추출
# Colab/서버 환경에서 headless 옵션 필수적일 때가 많음
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.common.by import By
# [import webdriver_manager]
from webdriver_manager.chrome import ChromeDriverManager
# [import lxml]
from lxml import html
import time
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless') # 창 없이 실행
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage') # 공유 메모리 부족 보호
chrome_options.add_argument('--window-size=1920x1080')
# 환경에 따라 바이너리 위치 지정이 필요할 수 있음
# chrome_options.binary_location = "/usr/bin/google-chrome-stable"
# 드라이버 실행(로컬 크롬 버전에 맞는 드라이버 자동 설치)
driver = webdriver.Chrome(
service=ChromeService(ChromeDriverManager().install()),
options=chrome_options
)
url = 'https://curiousweek.tistory.com/357'
driver.get(url)
# 명시적 대기 권장(렌더 완료까지)
# [import selenium] WebDriverWait/EC 사용 예:
# from selenium.webdriver.support.ui import WebDriverWait
# from selenium.webdriver.support import expected_conditions as EC
# WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, 'title')))
time.sleep(2) # 단순 대기(학습용)
# HTML 파싱 후 제목 추출
page_source = driver.page_source
tree = html.fromstring(page_source)
title_text = tree.xpath('//title/text()')
print(title_text)
driver.quit()
# 대안:
# - 정적 페이지: requests + BeautifulSoup가 더 간단/빠름
# [import requests, bs4]
# import requests
# from bs4 import BeautifulSoup
# r = requests.get(url, timeout=10); r.raise_for_status()
# soup = BeautifulSoup(r.text, 'html.parser')
# print(soup.title.get_text(strip=True))
# - Playwright(안정적·강력): [import playwright]
# - parsel(강력 XPath/CSS 선택자): [import parsel]
4-6 공공데이터 API(경기데이터드림 예시) 호출
# --- 공개 API 호출: 페이지네이션/에러 처리 포함 예시 ---
# 역할: 경기도 지역화폐 가맹점 현황(예시 엔드포인트) 호출 후 JSON 확인
# 주의:
# - 실제 서비스키(KEY)는 노출 금지. 환경변수/비밀관리 사용 권장.
# - 응답 스키마(키 이름)는 기관별로 상이 → print로 구조 먼저 확인 후 파싱
# [import requests, os]
import requests
import os
url = 'https://openapi.gg.go.kr/RegionMnyFacltStus'
api_key = os.getenv('GG_API_KEY', 'c79d') # 데모용 기본값(실습 시 자신의 키로 대체)
params = {
'KEY': api_key, # 인증키
'Type': 'json', # 응답 타입
'pIndex': 1, # 페이지 번호(1부터)
'pSize': 10 # 페이지 크기
# 추가 검색 파라미터는 기관 문서 참조(예: SIGUN_NM=수원시)
}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
payload = resp.json()
print(payload) # 구조 확인이 먼저
# --- 필요 데이터만 DataFrame으로 변환 (가변 스키마 고려) ---
# 기관별 JSON 구조가 달라서 KeyError 방지용으로 방어적 접근
# [import pandas]
import pandas as pd
root_key = 'RegionMnyFacltStus'
if isinstance(payload, dict) and root_key in payload:
# 보통 [0]에 'head', [1]에 'row'가 들어있는 패턴이 있음(기관별 상이)
blocks = payload[root_key]
rows = None
for blk in blocks:
if 'row' in blk:
rows = blk['row']
break
if rows:
df = pd.DataFrame(rows)
print(df.head())
else:
print('row 블록을 찾지 못했습니다. 응답 구조를 확인하세요.')
else:
print('예상한 루트 키가 없습니다. 응답 구조를 확인하세요.')
# 페이지네이션(다건 수집) 예시
# for page in range(1, 6):
# params['pIndex'] = page
# r = requests.get(url, params=params, timeout=10)
# r.raise_for_status()
# data = r.json()
# # 누적 로직 작성(append/concat) → 대량이면 리스트로 모아 마지막에 concat 권장
# 대안:
# - httpx(Client, 재시도/타임아웃 관리 용이) [import httpx]
# - pydantic으로 스키마 검증 [import pydantic]
# - 요청 서명/헤더 필요한 API는 requests.get(url, headers=...)로 확장
'Python > Data analysis' 카테고리의 다른 글
| 데이터 전처리 1 (0) | 2025.10.10 |
|---|---|
| 데이터 저장 (0) | 2025.10.07 |
| 언패킹, 예외처리, 함수형 프로그래밍 (0) | 2025.10.03 |
| 시퀀스 슬라이싱과 컴프리헨션, 문자열 형식 지정, 컨텍스트 관리 (1) | 2025.10.03 |
| 데이터 분석과 오픈소스 (0) | 2025.10.03 |