GIL's LAB

전종목 기업 리포트 크롤링 본문

퀀트 투자/데이터 수집

전종목 기업 리포트 크롤링

GIL~ 2021. 9. 6. 17:50

본 포스팅에서는 한국경제신문에서 제공하는 한경컨센선스에 업로드된 2016년 9월 1일부터 2021년 8월 31일까지의 증권사 리포트를 크롤링하는 방법을 소개한다.

정확히는 리포트 파일이 아니고, 리포트 요약 자료를 크롤링하는 방법을 소개한다. 

 

한경컨센선스 (http://consensus.hankyung.com/)에 먼저 접속하면 아래와 같은 페이지를 볼 수 있다.

기업 리포트, 산업 리포트, 시장 리포트 등 다양한 리포트를 볼 수 있다.

본 포스팅에서는 기업 리포트에 초점을 두도록 한다. 

 

기업 REPORT 아이콘을 클릭한 뒤, 상단에 날짜를 2016년 9월 1일부터 2021년 8월 31일까지로 바꾸면 아래와 같은 화면을 보게 된다.

먼저 예시로서 지금보는 페이지를 크롤링해서 저장하도록 하자.

가져올 부분은 작성일, 제목, 적정가격, 투자의견, 작성자, 제공출처이며, 제목에서 종목코드를 추출하도록 하자.

 

위에서 보는 페이지의 주소는 http://consensus.hankyung.com/apps.analysis/analysis.list?&sdate=2016-09-01&edate=2021-08-31&report_type=CO&order_type=&now_page=1이다.

 

크롤링을 할때 url에 페이지 번호가 있는지를 유심히 봐야한다. 다행히도 맨 뒤에 now_page={페이지번호}라는 구조로 페이지번호가 들어가있으니, urllib과 BeautifulSoup을 이용하면 크롤링이 어렵지 않을 것이라 예상된다.

또한, 마우스 우클릭 버튼을 눌러 페이지 소스 보기를 클릭해보자. 그러면 아래와 같이 제목, 저자 등이 페이지 소스에 있음을 확인할 수 있다. 만약 페이지 소스에 해당정보가 없다면 셀레니움을 사용하는 것이 더 적절하다.

 

먼저 크롤링에 필요한 모듈을 불러오자.

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

 

그리고 해당 페이지를 requests.get 함수를 이용하여 연 다음에, BeautifulSoup의 입력으로 넘겨준다.

이때, headers를 입력해야 데이터가 리턴된다. 

url = "http://consensus.hankyung.com/apps.analysis/analysis.list?&sdate=2016-09-01&edate=2021-08-31&report_type=CO&order_type=&now_page=1"
html = requests.get(url, headers={'User-Agent':'Gils'}).content
soup = BeautifulSoup(html, 'lxml')

이제 하나하나 요소를 찾아보도록 하자. 

크롬의 검사 기능을 이용하면, 각 요소가 페이지 소스에 어떻게 반영되었는지 확인해보자.

먼저 테이블은 table_style01이라는 클래스를 갖는 div 요소 아래에 있음을 알 수 있다.

soup의 find를 이용하여 테이블을 먼저 찾아보도록 하자.

아래 코드는 class가 table_style01인 div 요소를 찾는코드이다. 

참고로 find 함수는 아래 조건을 만족하는 첫 번째 요소를 반환하고, find_all 함수는 모든 요소를 리스트 형태로 반환한다.

잘 찾았음을 확인했으니, 이제 저 출력값에서 <table> 요소를 찾은 것을 table에 저장하도록 하자.

table = soup.find("div", {"class":"table_style01"}).find('table')

이제 테이블에서 모든 행들을 찾아서 정리해보자.

table 요소에서 행은 tr 요소에 정리되어 있다 (물론 크롬의 검사를 이용해도 알 수 있다).

 

첫 행이 어떻게 구성되어 있는지를 먼저 살펴보자. 첫 행은 제목 행이며, 각 셀들이 th에 있음을 알 수 있다. 

제목은 우리가 직접 정의를 할 것이기 때문에, 두 번째 행을 살펴보자. 

tr은 여러 개의 td 셀로 구성되어 있으며, 각 td에서 텍스트에 해당하는 부분만 가져오면 될 것이라는 판단이 된다. 

예를 들어, 두 번째 행의 첫 번째와 두 번째 요소를 확인해보자.

아래 코드를 해석해보면, table에서 1번째 위치의 tr의 0번째 위치의 td의 텍스트와 1번째 위치의 텍스트를 반환한 것이다. 

문제는 제목이 조금 더럽게 되어 있다.

그 이유는 아래와 같이 제목이 짤려있어서 그렇다. 

다른 행에서도 같은 현상이 발생하는지를 확인해보자.

제목에서 종목코드와 종목명을 추출한 뒤, 노이즈를 제거하는 방식을 채택하기 위한 함수를 만들어, 해당 문제를 해결하자. 먼저 종목코드와 종목명을 가져오기 위해, FinanceDataReader를 사용하여 다음과 같이 가져온다.

import FinanceDataReader as fdr
stocks = fdr.StockListing('KRX') # 코스피, 코스닥, 코넥스 전체
stocks.head()

그리고 아래와 같은 함수를 작성하자. 

만약 종목명과 코드가 없는 제목이면 [None]을 리턴하는데, 이는 추후에 데이터에 추가하지 않기 위한 트릭이다.

stocks['Symbol'] = stocks['Symbol'].astype(str)
def remove_noise_and_split_title(title):
    in_code = ''
    in_name = ''
    
    for code, name in stocks[['Symbol', 'Name']].values:
        if code in title and name in title:
            in_code = code
            in_name = name
    
    # 한글, 영어, 숫자 외 노이즈 제거
    clean_title = re.sub('[^A-Za-z0-9가-힣]', ' ', title)
    
    # 기업명 코드 수정
    clean_title = clean_title.replace(in_code, ' ')
    clean_title = clean_title.replace(in_name, ' ')
    while ' ' * 2 in clean_title:
        clean_title = clean_title.replace(' ' * 2, ' ')
    
    if in_name == '': # 기업명이 없는 제목이라면, 데이터에 추가하지 않음
        return [None] 
    else:
        return [in_name, in_code, clean_title]

 

 

이제 각 행과 셀을 순회하면서 값을 저장하자.

참고로 표는 작성일, 제목, 적정가격, 투자의견, 작성자, 제공출처 순으로 되어 있으니, 6개의 셀만 순회하고 1번째 셀에서 방금 만든 함수를 사용하도록 하자. 

 

data = []
for tr in table.find_all("tr")[1:]: # 1번째 행부터 순회
    record = []
    try: # 종목코드가 없는 경우에는 오류가 발생해서 try except 구문으로 넘김
        for i, td in enumerate(tr.find_all("td")[:6]): # 6번째 셀까지 순회
            if i == 1:
                record += remove_noise_title(td.text) # remove_noise_title의 출력과 이어 붙임
            elif i == 3: # 노이즈가 껴있는 세번째 셀만 따로 처리
                record.append(td.text.replace(" ", "").replace("\r","").replace("\n",""))
            else: # 1번째 셀이 아니면:
                record.append(td.text) # 셀의 텍스트 값 그대로 입력
                
        data.append(record)
    except:
        pass

여기서 평가 의견인 3번째 셀에는 이상한 값이 많이 껴서 따로 노이즈를 처리하였다.

이제 data를 데이터프레임으로 만들어 보자.


전체 페이지에 대한 크롤링

이제 위에서 소개한 코드를 종합하여 전체 페이지를 크롤링해보자.

전체 페이지 개수는 4283개임을 확인했다. 

완성된 소스 코드는 다음과 같다.

사실 위의 코드를 이어붙인 것이라서 특별히 설명할 부분이 없다.

기본 url 구조를 정의하고, 거기에 페이지 번호를 바꿔가면서 데이터를 가져왔다는것정도...??

그리고 연결이 아예 끊어지면 15분 쉬었다가 재시도하도록 while 구문을 넣었단거 정도?

base_url = "http://consensus.hankyung.com/apps.analysis/analysis.list?&sdate=2016-09-01&edate=2021-08-31&report_type=CO&order_type=&now_page={}"
data = [] 

for page_no in range(1, 4284):
    while True:
        try:
            url = base_url.format(page_no)
            html = requests.get(url, headers={'User-Agent':'Gils'}).content
            soup = BeautifulSoup(html, 'lxml')
            print("{}/{}".format(page_no, 4284))
            break
        except:
            time.sleep(15 * 60)

    table = soup.find("div", {"class":"table_style01"}).find('table')
    for tr in table.find_all("tr")[1:]: # 1번째 행부터 순회
        record = []
        for i, td in enumerate(tr.find_all("td")[:6]): # 6번째 셀까지 순회
            if i == 1:
                record += remove_noise_and_split_title(td.text) # remove_noise_title의 출력과 이어 붙임
            elif i == 3: # 노이즈가 껴있는 세번째 셀만 따로 처리
                record.append(td.text.replace(" ", "").replace("\r","").replace("\n",""))
            else: # 1번째 셀이 아니면:
                record.append(td.text) # 셀의 텍스트 값 그대로 입력
            
        if None not in record: # 레코드에 None이 없으면
            data.append(record)

    time.sleep(1) # 연결 끊김 방지를 위해, 1초씩 재움
    
data = pd.DataFrame(data, columns = ["작성일", "종목명", "종목코드", "제목", "적정가격", "평가의견", "작성자", "작성기관"])
data.to_csv("기업리포트_요약.csv", index = False, encoding = "cp949")

아무튼 이것으로 리포트 데이터도 크롤링을 완료하였다~

완성 코드는 아래와 같다.

한경컨센선스 리포트 수집.ipynb
0.00MB

 

수집하고 싶은 금융 데이터나 실험하고 싶은 퀀트 관련 아이디어가 있으면 댓글로 남겨주세요! 
관련 포스팅을 준비하도록 하겠습니다!
Comments