네이버 파이낸스 - 재무제표 크롤링

네이버 파이낸스 - 재무제표 크롤링

2016 이승준 fb.com/plusjune

네이버 파이낸스 크롤링

파이썬으로 데이터 크롤링 할 때 requests 와 BeautifulSoup 를 많이 사용한다. 표(table)로 정리된 데이터의 경우 pandas의 read_html()을 활용하면 간편하게 크롤링 할 수 있는 경우가 많다. 이 글에서는 네이버 파이낸스의 페이지 구조를 파악하고, 재무제표 정보를 pandas DataFrame으로 읽어 처리하는 방법을 살펴본다. 종목별 예제로는 삼성전자(005930)을 사용한다.

페이지 구성

네이버 파이낸스에서 005930(삼성전자) 종목의 재무제표 정보 페이지의 구성을 단계별로 살펴보자. (이 구성은 크롬 혹은 파이어폭스의 개발자 도구로 살펴볼 수 있다)

1) 종목정보

아래 링크에서 종목정보를 볼 수 있다.

2) Financial Summary 영역

iframe 의 src 링크는 아래와 같이 구성되어 있다.

3) 재무제표

JavaScript가 요청하는 URL은 아래와 같다.

네이버 파이낸스에서 재무제표 데이터를 크롤링 하기 위해 알아야 할 정보는 아래 URL이 전부다.

http://companyinfo.stock.naver.com/v1/company/ajax/cF1001.aspx?cmp_cd=105560&fin_typ=0&freq_typ=Y

URL을 구성하는 파라미터의 의미는 다음과 같다.

인자 의미
cmp_cd 종목코드 005930 (종목코드)
fin_typ 재무제표 타입 0: 주재무제표, 1: GAAP개별, 2: GAAP연결, 3: IFRS별도, 4:IFRS연결
freq_typ 기간 Y:년, Q:분기

pandas.read_html 활용한 크롤링

pandas.read_html(url)은 HTML 페이지에 포함된 TABLE들을 DataFrame 의 리스트로 반환한다. 우리가 사용하는 재무제표 URL 페이지는 1개의 TABLE만을 가지고 있기 때문에 첫번째 [0] 요소가 바로 재무제표 정보를 담은 DataFrame이 된다.

pd.set_option('display.float_format', '{:,.1f}'.format)

DataFrame 을 화면에 표시할 때 보기 좋도록 하기 위한 것이다. 천 단위에 ',' 표시와 소수점 이하 1자리를 표시하도록 했다. 쓰지 않더라고 아무런 지장은 없다.

In [2]:
import pandas as pd

url_tmpl = 'http://companyinfo.stock.naver.com/v1/company/ajax/cF1001.aspx?cmp_cd=%s&fin_typ=%s&freq_typ=%s'
url = url_tmpl % ('005930', '4', 'Y') # 삼성전자, 4(IFRS 연결), Y:년 단위

dfs = pd.read_html(url)
df = dfs[0]
df = df.set_index('주요재무정보')
df.head()
df.head(10) # 10개 항목만 표시(실제 32개 항목)
Out[2]:
연간 2011/12 (IFRS연결) 2012/12 (IFRS연결) 2013/12 (IFRS연결) 2014/12 (IFRS연결) 2015/12 (IFRS연결) 2016/12(E) (IFRS연결) 2017/12(E) (IFRS연결) 2018/12(E) (IFRS연결)
주요재무정보
매출액 1650018 2011036 2286927 2062060 2006535 2004675 2208352 2331110 NaN
영업이익 156443 290493 367850 250251 264134 282505 411526 438342 NaN
세전계속사업이익 171919 299150 383643 278750 259610 296454 424088 452837 NaN
당기순이익 137590 238453 304748 233944 190601 220057 314203 335387 NaN
당기순이익(지배) 133826 231854 298212 230825 186946 217184 310029 330858 NaN
당기순이익(비지배) 3764 6599 6535 3119 3655 NaN NaN NaN NaN
자산총계 1558003 1810716 2140750 2304230 2421795 2552193 2827016 3099505 NaN
부채총계 544866 595914 640590 623348 631197 648497 691565 715024 NaN
자본총계 1013136 1214802 1500160 1680882 1790598 1900904 2136128 2385824 NaN
자본총계(지배) 970904 1170941 1444426 1621817 1728768 1838676 2074968 2319315 NaN

단, 5줄의 코드로 삼성전자 재무제표를 크롤링 했다! (의외로 이렇게 크롤링 할 수 있는 정보들이 꽤 많다)

문제점과 해결방안

표시된 DataFrame을 자세히 살펴보면 아래와 같은 문제점들이 있다. (대부분 네이버 파이낸스 페이지의 HTML TABLE 표현의 문제다)

문제점

  1. '연간'이른 컬럼명이 추가되었고, 컬럼 이름이 한 컬럼씩 오른쪽으로 밀렸다.
  2. 마지막 컬럼의 값이 NaN 값 (컬럼 이름이 밀려서 발생)
  3. 날짜에 "(IFRS연결)"와 같이 불필요한 문자열 포함하고 있다.
  4. 시계열 데이터로 처리하려면, '주요재무정보'가 컬럼이 되고 날짜가 행(row)가 되는 것이 편리하다.

각 문제에 대한 해결방안

  1. 컬럼명 '연간' 삭제
  2. 컬럼 문자열에서 날짜(년, 월)만 추출 (정규식 사용)
  3. 마지막 컬럼 삭제
  4. 컬럼과 로우를 전환 (df.T) 한다 (transpose 우리말로 전치행렬 이라고 한다)

1. 컬럼명 '연간' 삭제

In [3]:
# 리스트로 전환
cols = list(df.columns)
cols
Out[3]:
['연간',
 '\n\t\t\t\t\t\t\t\t\t2011/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2012/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2013/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2014/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2015/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2016/12(E)\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2017/12(E)\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2018/12(E)\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t']
In [4]:
cols.remove('연간')
cols
Out[4]:
['\n\t\t\t\t\t\t\t\t\t2011/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2012/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2013/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2014/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2015/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2016/12(E)\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2017/12(E)\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t2018/12(E)\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t']

2. 컬럼 문자열에서 날짜(년, 월) 추출

컬럼은 날짜 데이터이다. 그러나 불필요한 문자('\n', '\t', '(IFRS연결)' 등)이 포함되어 있다. 추후 Datetime으로 활용하기 위해 날짜에 해당하는 문자열만 추출해 보자.

'\n\t\t\t\t\t\t\t\t\t2011/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t'

위 문자열에서 '2011/12' 문자열만 추출하기 위해 아래와 같은 코드를 사용할 수 도 있겠지만,

date_str = r['date'].replace('\t', '').replace('\n', '')
    date_str = date_str.replace('(E)', '')
    date_str = date_str.replace('/', '-')
    date_str = date_str.replace(')', '')

정규식(regular expression)을 사용하는 것이 훨씬 깔끔하고 적응력이 좋아진다. 날짜 추출에 필요한 정규식을 만들어 보자.

In [5]:
import re
from datetime import datetime

s =  '\n\t\t\t\t\t\t\t\t\t2011/12\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(IFRS연결)\n\t\t\t\t\t\t\t\t'

date_str = ''
r = re.search("\d{4}/\d{2}", s)
if r:
    date_str = r.group()

date_str
Out[5]:
'2011/12'

날짜를 추출하는 내용을 함수로 만들어 컬럼 이름에 적용한다.

In [6]:
import re
from datetime import datetime

def get_date_str(s):
    date_str = ''
    r = re.search("\d{4}/\d{2}", s)
    if r:
        date_str = r.group()
        date_str = date_str.replace('/', '-')

    return date_str

이 함수를 사용하여 아래와 같이 모든 컬럼에서 날짜에 해당하는 문자열을 추출할 수 있다.

In [7]:
cols = [get_date_str(x) for x in cols]
cols
Out[7]:
['2011-12',
 '2012-12',
 '2013-12',
 '2014-12',
 '2015-12',
 '2016-12',
 '2017-12',
 '2018-12']

3. 마지막 컬럼 제외

In [8]:
df = df.ix[:, :-1]
df.head()
Out[8]:
연간 2011/12 (IFRS연결) 2012/12 (IFRS연결) 2013/12 (IFRS연결) 2014/12 (IFRS연결) 2015/12 (IFRS연결) 2016/12(E) (IFRS연결) 2017/12(E) (IFRS연결)
주요재무정보
매출액 1650018 2011036 2286927 2062060 2006535 2004675 2208352 2331110
영업이익 156443 290493 367850 250251 264134 282505 411526 438342
세전계속사업이익 171919 299150 383643 278750 259610 296454 424088 452837
당기순이익 137590 238453 304748 233944 190601 220057 314203 335387
당기순이익(지배) 133826 231854 298212 230825 186946 217184 310029 330858
In [9]:
# 컬럼이름 변경

df.columns = cols
df.head()
Out[9]:
2011-12 2012-12 2013-12 2014-12 2015-12 2016-12 2017-12 2018-12
주요재무정보
매출액 1650018 2011036 2286927 2062060 2006535 2004675 2208352 2331110
영업이익 156443 290493 367850 250251 264134 282505 411526 438342
세전계속사업이익 171919 299150 383643 278750 259610 296454 424088 452837
당기순이익 137590 238453 304748 233944 190601 220057 314203 335387
당기순이익(지배) 133826 231854 298212 230825 186946 217184 310029 330858

4. 컬럼과 로우 전환

DataFrame.T (transpose, 전치행열)를 사용하면 행, 열이 전환된 DataFrame을 구할 수 있다. 시간을 인덱스로하여 시계열 데이터로 만드는 것이 데이터 분석과 시각화에 유리하다.

In [10]:
dft = df.T

# 첫번째 컬럼 이름을 data로 변경
dft.rename(columns={'주요재무정보':'date'}, inplace=True)

# 인덱스를 날짜형식(datetime)으로 변환
dft.index = pd.to_datetime(dft.index)

# 필요한 컬럼만 출력해 본다
dft[['매출액', '당기순이익', '부채총계', 'PER(배)' , 'PBR(배)', '현금배당수익률']]
Out[10]:
주요재무정보 매출액 당기순이익 부채총계 PER(배) PBR(배) 현금배당수익률
2011-12-01 1650018 137590 544866 13.45 1.64 0.52
2012-12-01 2011036 238453 595914 11.17 1.96 0.53
2013-12-01 2286927 304748 640590 7.83 1.43 1.04
2014-12-01 2062060 233944 623348 9.78 1.23 1.51
2015-12-01 2006535 190601 631197 11.47 1.06 1.67
2016-12-01 2004675 220057 648497 13.60 1.37 1.56
2017-12-01 2208352 314203 691565 9.94 1.28 1.66
2018-12-01 2331110 335387 715024 9.31 1.15 1.77
In [11]:
# 컬럼 확인

dft.columns
Out[11]:
Index(['매출액', '영업이익', '세전계속사업이익', '당기순이익', '당기순이익(지배)', '당기순이익(비지배)', '자산총계',
       '부채총계', '자본총계', '자본총계(지배)', '자본총계(비지배)', '자본금', '영업활동현금흐름', '투자활동현금흐름',
       '재무활동현금흐름', 'CAPEX', 'FCF', '이자발생부채', '영업이익률', '순이익률', 'ROE(%)',
       'ROA(%)', '부채비율', '자본유보율', 'EPS(원)', 'PER(배)', 'BPS(원)', 'PBR(배)',
       '현금DPS(원)', '현금배당수익률', '현금배당성향(%)', '발행주식수(보통주)'],
      dtype='object', name='주요재무정보')
In [12]:
# 인덱스 확인

dft.index
Out[12]:
DatetimeIndex(['2011-12-01', '2012-12-01', '2013-12-01', '2014-12-01',
               '2015-12-01', '2016-12-01', '2017-12-01', '2018-12-01'],
              dtype='datetime64[ns]', freq=None)

정리: 함수로 만들기

재사용하기 위해 앞선 내용을 정리하여 함수로 만든다.

In [13]:
import re
from datetime import datetime
import pandas as pd
import requests
from bs4 import BeautifulSoup

'''
get_date_str(s) - 문자열 s 에서 "YYYY/MM" 문자열 추출
'''
def get_date_str(s):
    date_str = ''
    r = re.search("\d{4}/\d{2}", s)
    if r:
        date_str = r.group()
        date_str = date_str.replace('/', '-')

    return date_str

'''
* code: 종목코드
* fin_type = '0': 재무제표 종류 (0: 주재무제표, 1: GAAP개별, 2: GAAP연결, 3: IFRS별도, 4:IFRS연결)
* freq_type = 'Y': 기간 (Y:년, Q:분기)
'''
def get_finstate_naver(code, fin_type='0', freq_type='Y'):
    url_tmpl = 'http://companyinfo.stock.naver.com/v1/company/ajax/cF1001.aspx?' \
                   'cmp_cd=%s&fin_typ=%s&freq_typ=%s'

    url = url_tmpl % (code, fin_type, freq_type)
    #print(url)

    dfs = pd.read_html(url, encoding="utf-8")
    df = dfs[0]
    if df.ix[0,0].find('해당 데이터가 존재하지 않습니다') >= 0:
        return None

    df.rename(columns={'주요재무정보':'date'}, inplace=True)
    df.set_index('date', inplace=True)

    cols = list(df.columns)
    if '연간' in cols: cols.remove('연간')
    if '분기' in cols: cols.remove('분기')
    cols = [get_date_str(x) for x in cols]
    df = df.ix[:, :-1]
    df.columns = cols
    dft = df.T
    dft.index = pd.to_datetime(dft.index)

    # remove if index is NaT
    dft = dft[pd.notnull(dft.index)]
    return dft
In [14]:
# 삼성전자 (년간, IFRS연결)
df = get_finstate_naver('005930')
df[['매출액','영업이익', '당기순이익', '영업활동현금흐름', '순이익률']]
Out[14]:
date 매출액 영업이익 당기순이익 영업활동현금흐름 순이익률
2011-12-01 1650018 156443 137590 229179 8.34
2012-12-01 2011036 290493 238453 379728 11.86
2013-12-01 2286927 367850 304748 467074 13.33
2014-12-01 2062060 250251 233944 369754 11.34
2015-12-01 2006535 264134 190601 400618 9.50
2016-12-01 2004675 282505 220057 455796 10.98
2017-12-01 2208352 411526 314203 516450 14.23
2018-12-01 2331110 438342 335387 557257 14.39

활용 예

In [15]:
#df = get_finstate_naver('035720') # 셀트리온 068270 
df = get_finstate_naver('035720') # 카카오 035720
#df = get_finstate_naver('035720') # CJ E&M 130960
#df = get_finstate_naver('035720') # 메디톡스 086900

df[['ROE(%)', 'ROA(%)', '부채비율', '자본유보율', 'EPS(원)', 'PER(배)', 'BPS(원)', 'PBR(배)']]
Out[15]:
date ROE(%) ROA(%) 부채비율 자본유보율 EPS(원) PER(배) BPS(원) PBR(배)
2011-12-01 26.86 21.45 22.46 6533.61 8079 14.85 34031 3.53
2012-12-01 15.70 13.09 17.68 7376.32 5656 16.11 38158 2.39
2013-12-01 17.73 14.81 22.53 728.30 4532 18.54 13398 6.27
2014-12-01 11.41 10.04 12.37 8411.95 6116 20.21 42337 2.92
2015-12-01 3.02 2.65 23.32 8407.52 1269 91.24 42476 2.73
2016-12-01 1.87 1.49 48.06 NaN 828 94.18 48994 1.59
2017-12-01 3.43 2.53 45.85 NaN 1719 51.25 51292 1.72
2018-12-01 4.53 3.36 44.74 NaN 2375 37.09 53621 1.64
In [16]:
%matplotlib inline
import matplotlib.pyplot as plt

df[['ROE(%)', 'ROA(%)']].plot()
Out[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f33d7b0c278>

csv로 저장

DataFrmae (재무제표 데이터)를 .csv로 저장하고자 한다면,

In [17]:
df.to_csv('035720.csv')
In [18]:
# 확인
! head 035720.csv
,매출액,영업이익,세전계속사업이익,당기순이익,당기순이익(지배),당기순이익(비지배),자산총계,부채총계,자본총계,자본총계(지배),자본총계(비지배),자본금,영업활동현금흐름,투자활동현금흐름,재무활동현금흐름,CAPEX,FCF,이자발생부채,영업이익률,순이익률,ROE(%),ROA(%),부채비율,자본유보율,EPS(원),PER(배),BPS(원),PBR(배),현금DPS(원),현금배당수익률,현금배당성향(%),발행주식수(보통주)
2011-12-01,4213.0,1168.0,1274.0,1080.0,1085.0,-5.0,5587.0,1025.0,4562.0,4565.0,-3.0,68.0,1118.0,-1140.0,-140.0,428.0,690.0,6.0,27.71,25.63,26.86,21.45,22.46,6533.61,8079.0,14.85,34031.0,3.53,1607.0,1.34,19.9,13454569.0
2012-12-01,4534.0,1018.0,1009.0,766.0,763.0,4.0,6119.0,919.0,5199.0,5153.0,46.0,68.0,1233.0,-513.0,-322.0,368.0,865.0,1.0,22.44,16.9,15.7,13.09,17.68,7376.32,5656.0,16.11,38158.0,2.39,1110.0,1.22,19.65,13504829.0
2013-12-01,2108.0,659.0,604.0,614.0,614.0,0.0,2172.0,399.0,1773.0,1773.0,0.0,210.0,705.0,-583.0,12.0,133.0,572.0,,31.24,29.13,17.73,14.81,22.53,728.3,4532.0,18.54,13398.0,6.27,1133.0,1.35,24.41,13560229.0
2014-12-01,4989.0,1764.0,1688.0,1498.0,1501.0,-3.0,27680.0,3048.0,24632.0,24546.0,86.0,291.0,2203.0,1726.0,354.0,128.0,2075.0,3.0,35.37,30.03,11.41,10.04,12.37,8411.95,6116.0,20.21,42337.0,2.92,173.0,0.14,6.68,58142204.0
2015-12-01,9322.0,886.0,1095.0,788.0,757.0,31.0,31885.0,6030.0,25855.0,25524.0,331.0,301.0,1622.0,-4142.0,1972.0,761.0,861.0,2218.0,9.5,8.45,3.02,2.65,23.32,8407.52,1269.0,91.24,42476.0,2.73,167.0,0.14,13.26,60096088.0
2016-12-01,14295.0,1129.0,920.0,619.0,547.0,,51254.0,16650.0,34646.0,33125.0,,337.0,3270.0,-10317.0,10341.0,788.0,-1165.0,,7.9,4.33,1.87,1.49,48.06,,828.0,94.18,48994.0,1.59,160.0,0.2,0.19,
2017-12-01,18179.0,1949.0,1828.0,1318.0,1163.0,,52925.0,16640.0,36296.0,34704.0,,338.0,3042.0,-1292.0,-372.0,670.0,2569.0,,10.72,7.25,3.43,2.53,45.85,,1719.0,51.25,51292.0,1.72,177.0,0.2,0.1,
2018-12-01,20370.0,2632.0,2519.0,1814.0,1607.0,,55021.0,17008.0,38013.0,36279.0,,338.0,3661.0,-1409.0,-270.0,694.0,3075.0,,12.92,8.9,4.53,3.36,44.74,,2375.0,37.09,53621.0,1.64,195.0,0.22,0.08,

요약

  • 파이썬 데이터 크롤링, requests를 주로 사용하지만 TABLE에 pandas.read_html()를 사용하여 간결하게 처리할 수 있다.
  • 정규식을 사용하여 문자열을 추출하는 것이 유연성과 적응성이 뛰어나다. (replace 같은 문자열 처리함수를 사용하는 것 보다.
  • pandas.DataFrame의 컬럼, 인덱스 조작을 통해 DataFrame을 적절하게 가공할 수 있다.
  • DataFrame으로 만들어지면 파일(csv, excel, RDB)등으로 변환하기 좋고, 시각화 하기도 용이하다.

댓글

Comments powered by Disqus