KT AIVLE School/[TIL] AIVLE School 당일 복습

[TIL] [KT AIVLE School] KT 에이블스쿨 6기(DX 트랙) 5주차 1일. 데이터 수집(1) - 웹 크롤링 이론, python class

guoyee94 2024. 10. 3. 00:28

 

 

폭풍같안던 한기영 강사님데이터 분석 파트가 끝나고...

 

미니프로젝트 D-day 카운트와 함께 박두진 강사님께서 오셨다.

 

전직 국어쌤으로서 괜히 친근감이 드는 성함이다. 청록파

 

박두진 강사님은 뭐랄까,

기본을 탄탄하게 쌓아 주시고, 잡담이 없으신 편이다.

 

처음에는 너무 달리신다... 싶었는데,

오히려 그래서 집중도 잘 되고 시간이 녹게 만드는 마성(?)을 지니셨다.

 

코드 위에다 손글씨를 쓰시며 작동 과정을 추적해주셨다. 뽀짝하다.

 

오늘부터 이틀간 배울 것은 데이터 수집!

 

분석할 데이터를 가져 오는 아주 중요한 과정이다.

 

다만 오늘의 내용은 그 전에 알아야 할 과정이랄까?

 

데이터가 클라이언트와 서버 간에 어떻게 전달되는지,

그 원리를 통해서 동적 페이지에서 크롤링을 어떻게 해 오는지,

파이썬에서 클래스란 무엇인지 등을 배웠다.

 

 

 

 

 

 

 

 


 

 

웹 크롤링 이론

 

  • 클라이언트 - 서버 구조

 

클라이언트의 브라우저와 서버의 WAS가 인터넷을 매개로 요청(request)과 응답(response)를 주고받는다.

 

크롤링은 웹 상의 데이터를 수집하는 작업을 말한다.

 

따라서 웹 서버와 나의 디바이스 간의 정보 전달이 어떨게 되는지 이해할 필요가 있다.

 

기본적으로 우리가 웹 페이지로부터 정보를 전달받는 방법은 다음 단계를 따른다.

 

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의 일반적 구조.

 

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 누르면 나온다.)에서 확인 가능하다.

 

네트워크 탭에서 캐시를 보면 Request Method가 나온다.

 

 


  • HTTP 상태 코드

데이터 전송이 상태에 대한 코드.

200이 제대로 된 것, 404는 유명하고, 401이나 403도 종종 본다고 한다.

 

용어 정리
 - 쿠키 : 데이터 송수신 과정이 클라이언트의 브라우저에 문자열로 저장된 것 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