폭풍같안던 한기영 강사님의 데이터 분석 파트가 끝나고...
미니프로젝트 D-day 카운트와 함께 박두진 강사님께서 오셨다.
전직 국어쌤으로서 괜히 친근감이 드는 성함이다. 청록파
박두진 강사님은 뭐랄까,
기본을 탄탄하게 쌓아 주시고, 잡담이 없으신 편이다.
처음에는 너무 달리신다... 싶었는데,
오히려 그래서 집중도 잘 되고 시간이 녹게 만드는 마성(?)을 지니셨다.
오늘부터 이틀간 배울 것은 데이터 수집!
분석할 데이터를 가져 오는 아주 중요한 과정이다.
다만 오늘의 내용은 그 전에 알아야 할 과정이랄까?
데이터가 클라이언트와 서버 간에 어떻게 전달되는지,
그 원리를 통해서 동적 페이지에서 크롤링을 어떻게 해 오는지,
파이썬에서 클래스란 무엇인지 등을 배웠다.
웹 크롤링 이론
- 클라이언트 - 서버 구조
크롤링은 웹 상의 데이터를 수집하는 작업을 말한다.
따라서 웹 서버와 나의 디바이스 간의 정보 전달이 어떨게 되는지 이해할 필요가 있다.
기본적으로 우리가 웹 페이지로부터 정보를 전달받는 방법은 다음 단계를 따른다.
1. 우리가 쓰는 브라우저 어플리케이션(구글이나 사파리 같은 것)이 각 서버에 정보를 요청(request)한다.
- 이때 요청하는 정보의 내용이 URL로 표시된다. 자세한 것은 후술.
2. 서버의 어플리케이션 WAS(Web Application Server)가 요청받은 정보를 DB로부터 응답(response)한다.
- 이때 형식은 일반적으로 html 또는 JSON.
3. 클라이언트 브라우저가 받은 html이나 JSON파일을 파싱(Parsing)하여 화면으로 출력한다.
따라서 우리는 데이터를 크롤링하기 위해
① 정보를 요청할 URL을 생성해서
② WAS에 정보를 요청하고
③ 응답받은 데이터를 파싱하는
3단계를 거치게 된다.
- URL(Uniform Resource Locator)
URL은 흔히들 생각하는 인터넷 주소를 말한다.
URL의 구성요소는 일반적으로 다음과 같다.
명칭 | 역할 | 예시 |
Protocol | 데이터 통신 규약 | http://, https:// |
Sub Domain | 도메인 명의 확장자 역할 | www, news, tool, dev |
Primary Domain | 임의로 정하는 도메인 명칭. DNS에 매핑된 바에 따라 서버 IP를 호출한다. |
naver, tistory |
Top-level Domain | 도메인의 종류/목적/국가 등을 정의 | .com, .net, .kr등 |
Port | 구현되는 어플리케이션을 구분함, 생략 가능 | http://는 80, https://는 443 |
Path | 서버컴퓨터에 저장된 파일의 위치(directory) | /main/, ./manage/ |
Page | 사용자가 요청한 파일 | top100.nhn, newpost |
Query | 사용자가 파일에서 어떤 데이터를 요청했는지에 대한 정보 | '?' 뒤에 쓰인 부분 |
특히 중요한 부분은 Query이다.
서버에서 우리의 요청을 처리할 때, WAS나 DBS등에서 '어떤' 데이터를 가져올 지 구분하는 부분이기 때문.
- HTTP 요청 방식
우리가 가장 흔히 접할 프로토콜인 http(s)://에 데이터를 요청하는 방식에는 두 가지가 있다.
get 방식 | 구현이 간편하다. URL 자체에 쿼리가 포함되므로 쿼리 노출의 위험이 있다. 데이터 전송량이 비교적 낮다. |
post 방식 | 구현이 복잡하다. 쿼이가 URL 바깥의 body에 저장되므로 쿼리가 노출되지 않는다. 데이터 전송량이 비교적 높다. |
이는 해당 웹페이지의 개발자 도구(F12 누르면 나온다.)에서 확인 가능하다.
- HTTP 상태 코드
데이터 전송이 상태에 대한 코드.
용어 정리
- 쿠키 : 데이터 송수신 과정이 클라이언트의 브라우저에 문자열로 저장된 것 ex) 로그인 정보, 본 상품 정보
- 세션 : 클라이언트와 서버의 연결 정보. 이게 끊어지면 '세션이 만료되었습니다.'
- 캐시 : 클라이언트와 서버 양측의 RAM에 저장되는 데이터. 쿠키와 다른 점은 트래픽을 발생시키지 않는다는 것.
- 웹페이지의 구분
클라이언트 | 서버 | |
동적 페이지 | 추가적 정보를 URL 변경 없이 같은 URL로 재요청 | 처음엔 html을 송신, 재요청에 대해서는JSON을 송신 |
정적 페이지 | 추가적 정보를 표시하려면 새로운 URL로 재요청 | 처음에도 재요청에도 html을 송신 |
그러므로 위에서 말한 세 단계
① URL 생성
② WAS에 정보 요청
③ 데이터 파싱
에서 ③번 과정에 차이가 생긴다. 수신한 데이터가 JSON과 html로 차이가 나기 때문.
웹 크롤링 실전(동적 페이지)
실습용으로는 네이버 증권 KOSPI 주가기록을 사용한다. 삼전아 아프지마
위 화면처럼 F12로 불러온 개발자 도구에서 Network 탭에서 오가는 데이터 정보를 클릭하면 URL이 나온다.
우리는 동적 페이지에서 크롤링을 할 것이기에 JSON이 목표 확장자다.
Fetch/XHR 항목에는 JSON만 표기되기에 쉽게 찾을 수 있다.
확인해야 할 것은 세 가지로,
URL, Reqeust Method, Status Code
상태코드가 200번대로 정상인 것을 확인하고, 요청방식을 기억한 뒤 URL을 복사하면 된다.
코드 구현 자체는 간단한데, requests 패키지를 사용하면 되기 때문이다. 오오 파이썬이시여
import requests
import pandas as pd
def stock():
# 1. URL 선언
url = 'https://m.stock.naver.com/api/index/KOSPI/price?pageSize=10&page=2'
# 2. request(URL) : 요청 방식대로 요청해 response 변수에 저장
response = requests.get(url) # 이때 받은 response는 response type 객체
# 3. JSON(str) > list or dict > DataFrame : 파싱을 통해 데이터프레임화
data = response.json()
return pd.DataFrame(data)
기본적인 함수의 형태는 위와 같다. #1은 그냥 URL을 붙여넣은 것이니 그 다음을 보자.
response = requests.get(url) # 해당 페이지의 요청방식이 GET이었기 때문에 .get사용
response # status 코드 200인 것 확인
# 출력 : <Response [200]>
type(response)
# 출력 : requests.models.Response
response.text[:40] # json 형태의 str들이 출력되는 것 확인 가능
# 출력 : '[{"localTradedAt":"2024-09-12","closePri'
requests.get()을 통해 requests 클래스에 속한 response 객체의 형태로 데이터가 호출되었다.
동적 페이지의 호출(더보기) 정보를 떼 왔기 때문에 JSON 형태인 것이 보인다.
data = response.json()
df = pd.DataFrame(data)[['localTradedAt', 'closePrice']]
response의 .json() 메서드는 이름과 다르게 json str을 파이썬 내장 자료형인 리스트나 튜플로 바꾼다.
그렇게만 되면 pd.DataFrame()으로 데이터프레임화할 수 있지.
위 코드에서는 'localTradedAt', 'closePrice'만 떼서 컬럼화했다.
※ JSON이란
보통 서버에서 클라이언트로 데이터를 보낼 때, 일정한 패턴을 지닌 문자열을 생성해 사용자가 이를 해석하도록 만든 양식이다. 프로그래밍 언어에 관계 없이 유연하게 쓰일 수 있어 웹 브라우저 분야에서 각광받는다.
하지만 이 때도 주의할 점이 있다.
# 1. URL
url = 'https://m.stock.naver.com/front-api/marketIndex/prices?category=exchange&reutersCode=FX_USDKRW&page=2'
# 2. request(URL) > response(JSON(str))
response = requests.get(url)
# 3. JSON(str) > list or dict > DataFrame
data = response.json()
df = pd.DataFrame(data)
df.head()
위와 거의 동일해 보이는 이 코드는 미국 증시 관련 자료를 가져온 것이다. 아래는 이 코드의 실행 결과.
이렇게 된 이유는 아마 JSON파일 자체의 구조가 문제겠지.
파싱한 JSON 파일을 열어 보자.
딕셔너리 안의 키 'result'의 밸류들이 또 딕셔너리 형태로 실제 값들을 구성하고 있다.
['result']만 따로 지정해서 데이터프레임으로 써먹어야겠다.
df = pd.DataFrame(data['result'])
df.head()
response는 딕셔너리가 아니라 response 객체인고로,
파싱이 끝난 data에서 result를 찾아야 한다.
어차피 result 외의 정보는 필요하지 않아서 result만 따로 따 온 모습.
다만 저 안에 'fluctuationsType'열이 또 딕셔너리 형태다. 3중 딕셔너리 뭐야
이걸 해결하려고 삽질 좀 하다가, 발견한 새로운 함수를 써 본다.
# 'fluctuationsType' 컬럼 json포맷 기반 정규화
df_drop = pd.json_normalize(df['fluctuationsType'])
# 정규화로 빼낸 데이터프레임 기존 프레임과 병합
df_concat = pd.concat([df, df_drop], axis=1)
# 'fluctuationsType' 컬럼 삭제
df_concat.drop('fluctuationsType', axis=1, inplace=True)
# 확인
df_concat.head()
우리 제이슨 너무 효자다.
깔끔하게 잘 나왔다. 묵은 체증이 내려간다.
주식 데이터이니만큼 본격적인 분석을 하려면 localTradeAt 컬럼을 손봐야 할 듯.
클래스(class)에 대하여
파이썬 설명서에는 이런 말이 있다.
'클래스는 사용자 정의 데이터 타입이다.'
이 의미를 알아보자.
클래스란 여러 변수와 함수를 모아 둔 것으로,
변수와 함수가 모여서 클래스
클래스가 모여서 모듈
모듈이 모여서 패키지
요런 느낌이다.
그래서 클래스는 (파이썬의 함수가 그러하듯) 객체처럼 선언한다.
클래스의 사용은 크게 3단계로 구성되는데,
① 클래스 생성
class Account:
# 변수 선언
balance = 0
# 함수 선언
# class에서 인스턴스형 함수를 선언할 때는 첫 인자로 반드시 self가 필요
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
이때 클래스명은 반드시 PascalCase로 작성된다.
형태소 어두마다 대문자를 쓴다는 뜻이다.
이는 snake_case로 쓰이는 함수와 구분하기 위함인데,
이후에 이 클래스에 해당하는 실제 객체를 만들 때
acc1 = Account()
이런 식으로 함수 호출할 때와 문법적으로 같기 때문.
② 객체 생성
acc1 = Account()
acc2 = Account()
클래스는 그 자체로 동작하는 실체가 아니다.
비유하자면 설계도 같은 것이다.
실제로 제작되지는 않았지만 실제 객체가 가진 모든 변수, 함수 등을 정의해 놓았기 때문이다.
따라서 그 변수와 함수를 실현하려면 반드시 해당 클래스에 속한 객체를 생성해줘야 한다.
설계도에 따라 실제로 제품을 만들면 자원이 들듯,
객체를 생성하면 메모리가 사용된다.
후술할 __init__ 함수가 이 공정에서의 메모리 낭비를 막기 위해 시행되는 것.
③ 메서드 호출
# 두 객체 acc1과 acc2의 변수 값 초기화
acc1.balance = 3000
acc2.balance = 0
# 변수값 할당 결과 출력
print(f'acc1 잔고: {acc1.balance}')
print(f'acc1 잔고: {acc2.balance}')
#결과
acc1 잔고: 3000
acc1 잔고: 0
# 메서드 호출(코드 실행)
acc1.withdraw(2000)
acc2.deposit(5000)
# 코드 실행 결과 출력
print(f'acc1 잔고: {acc1.balance}')
print(f'acc2 잔고: {acc2.balance}')
# 결과
acc1 잔고: 1000
acc2 잔고: 5000
이렇게 객체.메서드()의 형식으로 메서드를 실행할 수 있다.
...어? 이 방식 익숙한데??
# 리스트의 경우
l = []
l.append('python')
print(l)
['python']
리스트도, 딕셔너리도, 데이터프레임도 이런 식으로 메서드를 불러 온다.
리스트는 .sort(), .reverse(), .append() 따위를 불러 온다.
딕셔너리는 .values(), .items(), .get() 따위를 불러 온다.
데이터프레임은 .drop(), .value_counts(), .head() 따위를 불러 온다.
심지어 iterable하지 않은 str도 .upper(), .lower()등을 사용한다.
이제야 첫 줄을 알 것 같다.
'클래스는 사용자 정의 자료형이다.'
문자 그대로, 클래스는 사용자가 만든, 메서드와 변수를 가진 자료형인 것이다.
※ 참고 : special method
말 그대로 특별한 기능읗 하는 메서드이다.
그 중 하나인 '__init__'에 대해 알아보자.
dir() 함수를 이용해 각 자료형(클래스) 들의 메서드를 살펴 보면 빠지지 않고
'__init__'이라는 메서드가 나온다.
이 메서드는 쉽게 말해, 클래스가 정의되는 단계에서 변수로 인한 오류를 검출하는 용도로 쓰인다.
다음 코드를 보자.
class Account:
def deposit(self, amount):
self.balance += amount
이 코드는 'balance'가 정의되지 않았지만,
클래스 선언 단계에서는 코드가 실행되지 않기에
객체를 생성할 때까지(= 메모리가 사용될 때까지) 오류가 검출되지 않는다.
그러나 당연히 이후에 메서드 호출(코드 실행) 단계에서는 오류가 나고,
공연히 메모리만 낭비한 효과를 낳는다.
이것을 막아 주는 것이 '__init__' 메서드.
이것을 정의하는 것만으로(= 클래스의 메서드로 선언하는 것만으로)
클래스가 내부적으로 가진 변수 오류를 지적해 준다.
(물론 메서드의 실행 내용으로 변수 할당이 들어가야 한다.)
따라서 거의 대부분의 클래스 선언에는 맨 윗줄이 이렇게 처리된다.
class Account:
def __init__(self, balance): # __init__ 메서드 선언
self.balance = balance
def deposit(self, amount):
self.balance += amount