KevinFire2030 / Fire2025

0 stars 0 forks source link

8장 정적 크롤링 실습하기 #20

Open KevinFire2030 opened 1 year ago

KevinFire2030 commented 1 year ago

각종 금융 웹사이트에는 주가, 재무정보 등 우리가 원하는 대부분의 주식 정보가 제공되고 있으며, 크롤링을 통해 이러한 데이터를 수집할 수 있다. 크롤링 혹은 스크래핑이란 웹사이트에서 원하는 정보를 수집하는 기술이다. 이번 장에서는 크롤링에 대한 간단한 설명과 예제를 살펴보겠다.

KevinFire2030 commented 1 year ago

8.1 GET과 POST 방식 이해하기

우리가 인터넷에 접속해 서버에 파일을 요청(Request)하면, 서버는 이에 해당하는 파일을 우리에게 보내준다(Response). 크롬과 같은 웹 브라우저는 이러한 과정을 사람이 수행하기 편하고 시각적으로 보기 편하도록 만들어진 것이며, 인터넷 주소는 서버의 주소를 기억하기 쉽게 만든 것이다. 우리가 서버에 데이터를 요청하는 형태는 다양하지만 크롤링에서는 주로 GET과 POST 방식을 사용한다.

8.1.1 GET 방식

GET 방식은 인터넷 주소를 기준으로 이에 해당하는 데이터나 파일을 요청하는 것이다. 주로 클라이언트가 요청하는 쿼리를 앰퍼샌드(&) 혹은 물음표(?) 형식으로 결합해 서버에 전달한다.

네이버 홈페이지에 접속한 후 [퀀트]를 검색하면, 주소 끝부분에 [&query=퀀트]가 추가되며 이에 해당하는 페이지의 내용을 보여준다. 즉, 해당 페이지는 GET 방식을 사용하고 있으며 입력 종류는 query, 입력값은 퀀트임을 알 수 있다.


https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&`query=퀀트`

https://search.naver.com/search.naver?sm=tab_hty.top&where=nexearch&`query=헤지펀드&oquery=퀀트`

8.1.2 POST 방식

POST 방식은 사용자가 필요한 값을 추가해서 요청하는 방법이다. GET 방식과 달리 클라이언트가 요청하는 쿼리를 body에 넣어서 전송하므로 요청 내역을 직접 볼 수 없다. 동행복권 홈페이지에 접속해 [당첨결과] 메뉴를 확인해보자.

이번엔 회차 바로가기를 변경한 후 [조회]를 클릭한다. 페이지의 내용은 선택일 기준으로 변경되었지만, 주소는 변경되지 않고 그대로 남아 있다. GET 방식에서는 입력 항목에 따라 웹페이지 주소가 변경되었지만, POST 방식을 사용해 서버에 데이터를 요청하는 해당 웹사이트는 그렇지 않은 것을 알 수 있다.

POST 방식의 데이터 요청 과정을 살펴보려면 개발자도구를 이용 해야 하며, 크롬에서는 [F12]키를 눌러 개발자도구 화면을 열 수 있다. 개발자도구 화면을 연 상태에서 다시 한번 [조회]를 클릭해보자. [Network] 탭을 클릭하면, [조회]을 클릭함과 동시에 브라우저와 서버 간의 통신 과정을 살펴볼 수 있다. 이 중 상단의 gameResult.do?method=byWin 이라는 항목이 POST 형태임을 알 수 있다.

image

해당 메뉴를 클릭하면 통신 과정을 좀 더 자세히 알 수 있다. [Payload] 탭의 [Form Data]에는 서버에 데이터를 요청하는 내역이 있다. drwNo와 dwrNoList에는 선택한 회차의 숫자가 들어가있다.

image

이처럼 POST 방식은 요청하는 데이터에 대한 쿼리가 GET 방식처럼 URL을 통해 전송되는 것이 아닌 body를 통해 전송되므로, 이에 대한 정보는 웹 브라우저를 통해 확인할 수 없으며, 개발자도구 화면을 통해 확인해야 한다.

KevinFire2030 commented 1 year ago

8.2 크롤링 예제

일반적으로 크롤링은 [HTML 정보 받기 → 태크 및 속성 찾기 → 클랜징 ] 과정을 따른다. 먼저, request 패키지의 get() 혹은 post() 함수를 이용해 데이터를 요청한 후 HTML을 정보를 가져오며, bs4 패키지의 함수들을 이용해 원하는 데이터를 찾는 과정으로 이루어진다. 기본적인 크롤링을 시작으로 GET 방식과 POST 방식으로 데이터를 받는 예제를 학습해 보겠다.

8.2.1 명언 크롤링하기

크롤링의 간단한 예제로 'Quotes to Scrape' 사이트에 있는 명언을 수집하겠다.

https://quotes.toscrape.com/

image


import requests as rq

url = 'https://quotes.toscrape.com/'
quote = rq.get(url)

print(quote)
<Response [200]>

url에 해당 주소를 입력한 후 get() 함수를 이용해 해당 페이지의 내용을 받았다. 이를 확인해보면 Response가 200, 즉 데이터가 이상 없이 받아졌음이 확인된다.

quote.content[:1000]

content를 통해 함수를 통해 받아온 내용을 확인할 수 있으며, 텍스트 형태로 이루어져있다. BeautifulSoup() 함수를 이용해 원하는 HTML 요소에 접근하기 쉬운 BeautifulSoup 객체로 변경할 수 있다.

image


from bs4 import BeautifulSoup

quote_html = BeautifulSoup(quote.content, 'html.parser')
quote_html.head()

[<meta charset="utf-8"/>,
 <title>Quotes to Scrape</title>,
 <link href="/static/bootstrap.min.css" rel="stylesheet"/>,
 <link href="/static/main.css" rel="stylesheet"/>]

BeautifulSoup() 함수 내에 HTML 정보에 해당하는 quote.content와 파싱 방법에 해당하는 html.parser를 입력하면 개발자도구 화면에서 보던 것과 비슷한 형태인 BeautifulSoup 객체로 변경되며, 이를 통해 원하는 요소의 데이터를 읽어올 수 있다.

8.2.1.1 find() 함수를 이용한 크롤링

먼저 BeautifulSoup 모듈의 find() 함수를 통해 크롤링 하는법을 알아보자. 우리는 개발자도구 화면에서 명언에 해당하는 부분이 [class가 quote인 div 태그 → class가 text인 span 태그]에 위치하고 있음을 살펴보았다. 이를 활용해 명언만을 추출하는 방법은 다음과 같다.


quote_div = quote_html.find_all('div', class_='quote')

quote_div[0]

image

findall() 함수를 이용할 경우 원하는 태그의 내용들을 찾아올 수 있다. 먼저 태그에 해당하는 'div'를 입력하고, class 이름인 'quote'를 입력한다. ** class라는 키워드는 파이썬에서 클래스를 만들 때 사용하는 키워드이므로 언더바()를 통해 중복을 피해준다. ** 조건에 만족하는 결과가 리스트 형태로 반환되므로, 첫번째 내용만 확인해보면 div class="quote"에 해당하는 내용을 찾아왔으며, 이제 여기서 [class가 text인 span 태그]에 해당하는 내용을 추가로 찾도록 하자.


quote_span[0].text

image

결과물 마지막에 .text를 입력하면 텍스트 데이터만을 출력할 수 있다. for문 중에서 리스트 내포 형태를 이용하여 명언에 해당하는 부분을 한번에 추출해보도록 하자.


quote_div = quote_html.find_all('div', class_ = 'quote')

[i.find_all('span', class_ ='text')[0].text for i in quote_div]

[print(i.find_all('span', class_ ='text')[0].text) for i in quote_div]

“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”
“It is our choices, Harry, that show what we truly are, far more than our abilities.”
“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”
“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”
“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”
“Try not to become a man of success. Rather become a man of value.”
“It is better to be hated for what you are than to be loved for what you are not.”
“I have not failed. I've just found 10,000 ways that won't work.”
“A woman is like a tea bag; you never know how strong it is until it's in hot water.”
“A day without sunshine is like, you know, night.”

find_all() 함수가 아닌 find() 함수를 사용하면 해당 태그의 첫번째 내용만을 가져온다.

8.2.1.2 select() 함수를 이용한 크롤링

위 예제에서는 간단하게 원하는 데이터를 찾았지만, 데이터가 존재하는 곳의 태그를 여러번 찾아 내려가야 할 경우 find_all() 함수를 이용하는 방법은 매우 번거롭다. select() 함수의 경우 좀더 쉬운 방법으로 원하는 데이터가 존재하는 태그를 입력할 수 있다. 위의 동일한 내용을 select() 함수를 이용해 크롤링해보도록 하자.


quote_text = quote_html.select('div.quote > span.text')

quote_text_list = [i.text for i in quote_text]

find_all() 함수를 이용한 것 보다 훨씬 간단하게 원하는 데이터를 찾을 수 있었다.

이번에는 명언을 말한 사람 역시 크롤링해보도록 하자. 해당 데이터는 [class가 quote인 div 태그] 하단의 [span 태그], 다시 하단의 [class가 author인 small 태그]에 위치하고 있다.

8.2.1.3 모든 페이지 데이터 크롤링하기

화면 하단의 [Next→] 부분을 클릭하면 URL이 https://quotes.toscrape.com/page/2/ 로 바뀌며 다음 페이지의 내용이 나타난다. 이처럼 웹페이지 하단에서 다음 페이지 혹은 이전 페이지로 넘어가게 해주는 것을 흔히 페이지네이션이라고 한다.

URL의 'page/' 뒤에 위치하는 숫자를 for문을 이용해 바꿔준다면, 모든 페이지의 데이터를 크롤링할 수 있다.


import requests as rq
from bs4 import BeautifulSoup
import time

text_list = []
author_list = []
infor_list = []

for i in range(1, 100):

    url = f'https://quotes.toscrape.com/page/{i}/'
    quote = rq.get(url)
    quote_html = BeautifulSoup(quote.content, 'html.parser')

    quote_text = quote_html.select('div.quote > span.text')
    quote_text_list = [i.text for i in quote_text]

    quote_author = quote_html.select('div.quote > span > small.author')
    quote_author_list = [i.text for i in quote_author]

    quote_link = quote_html.select('div.quote > span > a')
    qutoe_link_list = ['https://quotes.toscrape.com' + i['href'] for i in quote_link]

    if len(quote_text_list) > 0:

        text_list.extend(quote_text_list)
        author_list.extend(quote_author_list)        
        infor_list.extend(qutoe_link_list)        
        time.sleep(1)

    else:
        break

import pandas as pd

pd.DataFrame({'text': text_list, 'author': author_list, 'infor': infor_list})

8.2.2 금융 속보 크롤링

이번에는 금융 속보의 제목을 추출해보겠다. 먼저 네이버 금융에 접속한 후 [뉴스 → 실시간 속보]를 선택하며, URL은 다음과 같다.

https://finance.naver.com/news/news_list.nhn?mode=LSS2D&section_id=101&section_id2=258 이 중 뉴스의 제목에 해당하는 텍스트만 추출해보도록 하자. 개발자도구 화면을 통헤 제목에 해당하는 부분은 [dl 태그 → class가 articleSubject 인 dd 태그 → a 태그 중 title 속성]에 위치하고 있음을 확인할 수 있다.

image


import requests as rq
from bs4 import BeautifulSoup

url = 'https://finance.naver.com/news/news_list.nhn?mode=LSS2D&section_id=101&section_id2=258'
data = rq.get(url)
html = BeautifulSoup(data.content, 'html.parser')
html_select = html.select('dl > dd.articleSubject > a')

html_select[0:3]

[i['title'] for i in html_select]

캐롯, 고객 패널 프로그램 ‘보이스 캐롯’ 운영
법인세법 개정·엔테크 열풍... 외화예금 5개월 만에 증가
5월 외화예금 54억달러 늘어…5개월 만에 증가세
한컴 자회사, 방위사업청과 867억원 규모 생산계약
4만원 대로 내려간 카카오…매수 시점은 '이것'에 달렸다
'또 탈락'...MSCI 선진지수 편입이 안되는 이유 [권영훈의 증시뉴스 PICK]
美 파월 연준의장, 하원 이어 상원서 올 금리 '추가 인상' 재언급
韓, MSCI선진지수 편입 불발됐지만… ‘내년 등재’ 가능성 봤다
3개월 내 최대 변동성 보이며 급등한 비트코인, 숨고르기[코인브리핑]
HD한국조선해양, PC선 4척 2천421억원에 수주
SK텔레콤, 런던증권거래소 DR 상폐 결정…"거래량 미미"
카카오, 5만원도 깨졌다…약 8개월 만
공모주, 가격제한폭 400%로 확대…'따따블' 첫 주인공은?
미래에셋운용, TIGER 일본엔선물 ETF 순자산 600억 돌파
신한자산운용, 신한 만기투자형 제5호[채권] 펀드 출시
KB증권, 토큰증권 오너스 데이 행사 개최
하락세 계속되는 카카오…5만원도 깨졌다[특징주]
삼정KPMG “그린수소 31년까지 연평균 51.6% 성장…수전해 주목”

8.2.3 표 크롤링하기

우리가 크롤링하고자 하는 데이터가 테이블 형태로 제공될 경우, 위와 같이 복잡한 과정을 거칠 필요 없이 매우 간단하게 테이블에 해당하는 내용만 가져올 수 있다. 먼저 아래 사이트에는 각 국가별 GDP가 테이블 형태로 제공되고 있다.

https://en.wikipedia.org/wiki/List_of_countries_by_stock_market_capitalization


import pandas as pd

url = 'https://en.wikipedia.org/wiki/List_of_countries_by_stock_market_capitalization'
tbl = pd.read_html(url)

tbl[0].head()
  1. URL을 입력한다.

  2. pandas 패키지의 read_html() 함수에 URL을 입력하면, 해당 페이지에 존재하는 테이블을 가져온 후 데이터프레임 형태로 불러온다.

이처럼 테이블 형태로 존재하는 데이터는 HTML 정보를 불러온 후 태그와 속성을 찾을필요 없이 read_html() 함수를 이용해 매우 손쉽게 불러올 수 있다.

8.2.4 기업공시채널에서 오늘의 공시 불러오기

해당 페이지에서 날짜를 변경한 후 [검색]을 누르면, 페이지의 내용은 해당일의 공시로 변경되지만 URL은 변경되지 않는다. 이처럼 POST 방식은 요청하는 데이터에 대한 쿼리가 body의 형태를 통해 전송되므로, 개발자도구 화면을 통해 해당 쿼리에 대한 내용을 확인해야 한다.

개발자도구 화면을 연 상태에서 조회일자를 원하는 날짜로 선택, [검색]을 클릭한 후 [Network] 탭의 todaydisclosure.do 항목에서 [Headers]탭의 [General] 부분에는 데이터를 요청하는 서버 주소가, [Payload] 탭의 [Form Data]를 통해 서버에 데이터를 요청하는 내역을 확인할 수 있다. 여러 항목 중 selDate 부분이 우리가 선택한 일자로 설정되어 있다.

image

image


import requests as rq
from bs4 import BeautifulSoup
import pandas as pd

url = 'https://kind.krx.co.kr/disclosure/todaydisclosure.do'
payload = {
    'method': 'searchTodayDisclosureSub',
    'currentPageSize': '15',
    'pageIndex': '1',
    'orderMode': '0',
    'orderStat': 'D',
    'forward': 'todaydisclosure_sub',
    'chose': 'S',
    'todayFlag': 'N',
    'selDate': '2022-07-27'
}

data = rq.post(url, data=payload)
html = BeautifulSoup(data.content, 'html.parser')

# print(html)

html_unicode = html.prettify()
tbl = pd.read_html(html.prettify())

tbl[0].head()
  1. URL과 쿼리를 입력한다. 쿼리는 딕셔너리 형태로 입력하며, Form Data와 동일하게 입력해준다. 쿼리 중 marketType과 같이 값이 없는 항목은 입력하지 않아도 된다.

  2. POST() 함수를 통해 해당 URL에 원하는 쿼리를 요청한다.

  3. BeautifulSoup() 함수를 통해 파싱한다.

  4. prettify() 함수를 이용해 BeautifulSoup 에서 파싱한 파서 트리를 유니코드 형태로 다시 돌려준다.

  5. read_html() 함수를 통해 테이블을 읽어온다.