주가와 더불어 재무제표와 가치지표 역시 투자에 있어 핵심이 되는 데이터입니다. 이번에는 가치지표를 계산하기 위한 재무제표를 데이터를 크롤링해보겠습니다.
재무제표 데이터는 여러 웹사이트에서 구할 수 있으며, 국내 데이터 제공업체인 FnGuide에서 운영하는 Company Guide 웹사이트에서 손쉽게 구할 수 있습니다.
재무제표 다운로드
사이트를 접속해보면, URL이 아래와 같은 걸 알 수 있습니다. 여기서 A005930 뒤에있는 부분은 제거해도 되는 부분입니다.
이를 제거한 주소로 접속을 해준 뒤, A뒤의 6자리 티커만 변경한다면 해당 종목의 재무제표 페이지로 이동하게 됩니다.
http://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A005930
사이트를 살펴보면, 우리가 원하는 재무제표 항목들은 모두 테이블 형태로 제공되고 있으므로 pandas 패키지의 read_html() 함수를 이용해 쉽게 추출할 수 있습니다. displayed_only=False를 설정하면, 웹 페이지에 있는 모든 HTML 테이블을 가져오도록 설정합니다. 페이지를 살펴보면, [+] 버튼을 눌러야만 표시되는 항목도 있으므로 설정을 해줍니다. 먼저 삼성전자 종목의 페이지 내용을 불러와보겠습니다.
import pandas as pd
url = 'http://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A005930'
data = pd.read_html(url, displayed_only=False)
[item.head(3) for item in data]
[ IFRS(연결) 2021/12 2022/12 2023/12 2024/06 전년동기 전년동기(%)
0 매출액 2796048.0 3022314.0 2589355.0 1459839.0 1237509.0 18.0
1 매출원가 1664113.0 1900418.0 1803886.0 901984.0 876543.0 2.9
2 매출총이익 1131935.0 1121896.0 785469.0 557855.0 360966.0 54.5,
IFRS(연결) 2023/09 2023/12 2024/03 2024/06 전년동기 전년동기(%)
0 매출액 674047.0 677799.0 719156.0 740683.0 600055.0 23.4
1 매출원가 466187.0 461156.0 458863.0 443120.0 416472.0 6.4
2 매출총이익 207859.0 216644.0 260293.0 297563.0 183583.0 62.1,
IFRS(연결) 2021/12 2022/12 2023/12 2024/06
0 자산 4266212.0 4484245.0 4559060.0 4857577.0
1 유동자산계산에 참여한 계정 펼치기 2181632.0 2184706.0 1959366.0 2178581.0
2 재고자산 413844.0 521879.0 516259.0 555666.0,
IFRS(연결) 2023/09 2023/12 2024/03 2024/06
0 자산 4544664.0 4559060.0 4708998.0 4857577.0
1 유동자산계산에 참여한 계정 펼치기 2064386.0 1959366.0 2085443.0 2178581.0
2 재고자산 552560.0 516259.0 533477.0 555666.0,
IFRS(연결) 2021/12 2022/12 2023/12 2024/06
0 영업활동으로인한현금흐름 651054.0 621813.0 441374.0 287617.0
1 당기순손익 399075.0 556541.0 154871.0 165961.0
2 법인세비용차감전계속사업이익 NaN NaN NaN NaN,
IFRS(연결) 2023/09 2023/12 2024/03 2024/06
0 영업활동으로인한현금흐름 97305.0 199452.0 118663.0 168954.0
1 당기순손익 58442.0 63448.0 67547.0 98413.0
2 법인세비용차감전계속사업이익 NaN NaN NaN NaN]
크롤링을 통해 총 6개의 테이블이 들어오게 되며, 그 내용은 아래와 같습니다.
순서 | 내용 |
0 | 포괄손익계산서(연간) |
1 | 포괄손익계산서(분기) |
2 | 재무상태표(연간) |
3 | 재무상태표(분기) |
4 | 현금흐름표(연간) |
5 | 현금흐름표(분기) |
먼저 연간 기준 포괄손익계산서, 재무상태표, 현금흐름표의 열 이름을 살펴보겠습니다.
print(data[0].columns.tolist(), '\n',
data[2].columns.tolist(), '\n',
data[4].columns.tolist()
)
['IFRS(연결)', '2021/12', '2022/12', '2023/12', '2024/06', '전년동기', '전년동기(%)']
['IFRS(연결)', '2021/12', '2022/12', '2023/12', '2024/06']
['IFRS(연결)', '2021/12', '2022/12', '2023/12', '2024/06']
포괄손익계산서 테이블에는 '전년동기', '전년동기(%)' 열이 있으며, 이는 필요하지 않은 내용이므로 삭제해주어야합니다.
- 포괄손익계산서 중 '전년동기'라는 글자가 들어간 열을 제외한 데이터를 선택합니다.
- concat() 함수를 이용해 포괄손익계산서, 재무상태표, 현금흐름표 세개 테이블을 하나로 묶습니다.
- rename() 메서드를 통해 첫번째 열 이름 'IFRS(연결)'을 '계정'으로 변경합니다.
data_fs_y = pd.concat(
[data[0].iloc[:, ~data[0].columns.str.contains('전년동기')], data[2], data[4]])
data_fs_y = data_fs_y.rename(columns={data_fs_y.columns[0]: "계정"})
data_fs_y.head()
결산마감 이전에 해당 페이지를 크롤링 할 경우 연간 재무제표 데이터에 분기 재무제표 데이터가 들어오기도 하므로, 연간 재무제표에 해당하는 열 만을 선택해야 합니다. 각 종목 별 결산월은 해당 페이지의 상단에서 확인할 수 있습니다.
이제 해당 데이터를 크롤링 해보도록 하겠습니다.
- get() 함수를 통해 페이지의 데이터를 불러온 후, content 부분을 BeautifulSoup 객체로 만듭니다.
- 결산월 항목운 [corp_group1 클래스의 div 태그 하부의 h2 태그]에 존재하므로, select() 함수를 이용해 추출합니다.
- fiscal_data 중 첫번째는 종목코드에 해당하고, 두번째가 결산 데이터에 해당하므로 해당 부분을 선택해 텍스트만 추출합니다.
- 'n월 결산' 형태로 텍스트가 구성되어 있으므로, 정규 표현식을 이용해 숫자에 해당하는 부분만 추출합니다.
import requests as rq
from bs4 import BeautifulSoup
import re
page_data = rq.get(url)
page_data_html = BeautifulSoup(page_data.content, 'html.parser')
fiscal_data = page_data_html.select('div.corp_group1 > h2') # [<h2>005930</h2>, <h2>12월 결산</h2>]
fiscal_data_text = fiscal_data[1].text # 12월 결산
fiscal_data_text = re.findall('[0-9]+', fiscal_data_text) # ['12']
이를 통해 결산월에 해당하는 부분만이 선택됩니다. 이를 이용해 연간 재무제표에 해당하는 열만 선택해보도록 해보겠습니다. 열 이름이 '계정', 그리고 재무제표의 월이 결산월과 같은 부분만 선택합니다.
data_fs_y = data_fs_y.loc[:, (data_fs_y.columns == '계정') |
(data_fs_y.columns.str[-2:].isin(fiscal_data_text))]
data_fs_y.head()
이제 추가적으로 클렌징해야 하는 사항은 다음과 같습니다. 먼저 재무제표 값 중에서 모든 연도의 데이터가 NaN인 항목이 있습니다. 이는 재무제표 계정은 있으나 해당 종목들은 데이터가 없는 것들이므로 삭제해도 됩니다.
data_fs_y[data_fs_y.loc[:, ~data_fs_y.columns.isin(['계정'])].isna().all(
axis=1)].head()
또한 동일한 계정명이 여러번 반복됩니다. 이러한 계정은 대부분 중요하지 않은 것들이므로, 하나만 남겨두도록 합니다.
data_fs_y['계정'].value_counts(ascending=False).head()
이 외에도 클렌징이 필요한 내용들을 함수로 구성하면 다음과 같습니다.
함수설명
- 입력값으로는 데이터프레임, 티커, 공시구분(연간/분기)가 필요합니다.
- 먼저 연도의 데이터가 NaN인 항목은 제외합니다.
- 계정명이 중복되는 경우 drop_duplicates() 함수를 이용해 첫번째에 위치하는 데이터만 남깁니다.
- melt() 함수를 이용해 열로 긴 데이터를 행으로 긴 데이터로 변경한다.
- 계정값이 없는 항목은 제외한다.
- [계산에 참여한 계정 펼치기]라는 글자는 페이지의 [+]에 해당하는 부분이므로 replace() 메서드를 통해 제거한다.
- to_datetime() 메서드를 통해 기준일을 'yyyy-mm' 형태로 바꾼 후, MonthEnd()를 통해 월말에 해당하는 일을 붙인다.
- '종목코드' 열에는 티커를 입력한다.
- '공시구분' 열에는 연간 혹은 분기에 해당하는 값을 입력한다.
def clean_fs(df, ticker, frequency):
df = df[~df.loc[:, ~df.columns.isin(['계정'])].isna().all(axis=1)]
df = df.drop_duplicates(['계정'], keep='first')
df = pd.melt(df, id_vars='계정', var_name='기준일', value_name='값')
df = df[~pd.isnull(df['값'])]
df['계정'] = df['계정'].replace({'계산에 참여한 계정 펼치기': ''}, regex=True)
df['기준일'] = pd.to_datetime(df['기준일'],
format='%Y-%m') + pd.tseries.offsets.MonthEnd()
df['종목코드'] = ticker
df['공시구분'] = frequency
return df
연간 재무제표 항목에 위 함수를 적용하면 다음과 같은 결과를 확인할 수 있습니다.
data_fs_y_clean = clean_fs(data_fs_y, '005930', 'y')
data_fs_y_clean.head()
클렌징 처리가 된 데이터가 세로로 긴 형태로 변경되습니다. 이제 분기 재무제표도 클렌징 처리를 해보도록 해보겠습니다. 분기 데이터는 결산월에 해당하는 부분을 선택할 필요가 없으며, 이를 제외하고는 모든 과정이 연간 재무제표를 항목과 동일합니다.
# 분기 데이터
data_fs_q = pd.concat(
[data[1].iloc[:, ~data[1].columns.str.contains('전년동기')], data[3], data[5]])
data_fs_q = data_fs_q.rename(columns={data_fs_q.columns[0]: "계정"})
data_fs_q_clean = clean_fs(data_fs_q, '005930', 'q')
data_fs_q_clean.head()
concat() 함수를 통해 연간 데이터와 분기 데이터 두 테이블을 하나로 합쳐줍니다.
data_fs_bind = pd.concat([data_fs_y_clean, data_fs_q_clean])
전종목 재무제표 크롤링
위 과정을 응용해 모든 종목의 재무제표를 크롤링한 후 DB에 저장하는 과정을 살펴보도록 하겠습니다. 먼저 SQL에서 재무제표가 저장될 테이블(kor_fs)을 만들어줍니다.
use stock_db;
create table kor_fs
(
계정 varchar(30),
기준일 date,
값 float,
종목코드 varchar(6),
공시구분 varchar(1),
primary key(계정, 기준일, 종목코드, 공시구분)
)
이제 파이썬에서 아래 코드를 실행하면 for문을 통해 전종목 재무제표가 DB에 저장됩니다.
- DB에 연결합니다.
- 기준일이 최대, 즉 최근일 기준 보통주에 해당하는 티커 리스트(ticker_list)만 불러옵니다.
- DB에 저장할 쿼리(query)를 입력합니다.
- 오류 발생시 저장할 리스트(error_list)를 만듭니다.
- for문을 통해 전종목 재무제표를 다운로드 받으며, 진행상황을 알기위해 tqdm() 함수를 이용합니다.
- URL 생성, 데이터 다운로드 및 데이터 클렌징 및 DB에 저장은 위와 동일하며, try except문을 통해 오류가 발생시 티커를 출력 후 error_list에 저장합니다.
- 무한 크롤링을 방지하기 위해 한 번의 루프가 끝날 때마다 타임슬립을 적용합니다.
- 모든 작업이 끝나면 DB와의 연결을 종료합니다.
# 패키지 불러오기
import pymysql
from sqlalchemy import create_engine
import pandas as pd
import requests as rq
from bs4 import BeautifulSoup
import re
from tqdm import tqdm
import time
# DB 연결
engine = create_engine('mysql+pymysql://root:1234@127.0.0.1:3306/stock_db')
con = pymysql.connect(user='root',
passwd='1234',
host='127.0.0.1',
db='stock_db',
charset='utf8')
mycursor = con.cursor()
# 티커리스트 불러오기
ticker_list = pd.read_sql("""
select * from kor_ticker
where 기준일 = (select max(기준일) from kor_ticker)
and 종목구분 = '보통주';
""", con=engine)
# DB 저장 쿼리
query = """
insert into kor_fs (계정, 기준일, 값, 종목코드, 공시구분)
values (%s,%s,%s,%s,%s) as new
on duplicate key update
값=new.값
"""
# 오류 발생시 저장할 리스트 생성
error_list = []
# 재무제표 클렌징 함수
def clean_fs(df, ticker, frequency):
df = df[~df.loc[:, ~df.columns.isin(['계정'])].isna().all(axis=1)]
df = df.drop_duplicates(['계정'], keep='first')
df = pd.melt(df, id_vars='계정', var_name='기준일', value_name='값')
df = df[~pd.isnull(df['값'])]
df['계정'] = df['계정'].replace({'계산에 참여한 계정 펼치기': ''}, regex=True)
df['기준일'] = pd.to_datetime(df['기준일'],
format='%Y-%m') + pd.tseries.offsets.MonthEnd()
df['종목코드'] = ticker
df['공시구분'] = frequency
return df
# for loop
for i in tqdm(range(0, len(ticker_list))):
# 티커 선택
ticker = ticker_list['종목코드'][i]
# 오류 발생 시 이를 무시하고 다음 루프로 진행
try:
# url 생성
url = f'http://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A{ticker}'
# 데이터 받아오기
data = pd.read_html(url, displayed_only=False)
# 연간 데이터
data_fs_y = pd.concat([
data[0].iloc[:, ~data[0].columns.str.contains('전년동기')], data[2],
data[4]
])
data_fs_y = data_fs_y.rename(columns={data_fs_y.columns[0]: "계정"})
# 결산년 찾기
page_data = rq.get(url)
page_data_html = BeautifulSoup(page_data.content, 'html.parser')
fiscal_data = page_data_html.select('div.corp_group1 > h2')
fiscal_data_text = fiscal_data[1].text
fiscal_data_text = re.findall('[0-9]+', fiscal_data_text)
# 결산년에 해당하는 계정만 남기기
data_fs_y = data_fs_y.loc[:, (data_fs_y.columns == '계정') | (
data_fs_y.columns.str[-2:].isin(fiscal_data_text))]
# 클렌징
data_fs_y_clean = clean_fs(data_fs_y, ticker, 'y')
# 분기 데이터
data_fs_q = pd.concat([
data[1].iloc[:, ~data[1].columns.str.contains('전년동기')], data[3],
data[5]
])
data_fs_q = data_fs_q.rename(columns={data_fs_q.columns[0]: "계정"})
data_fs_q_clean = clean_fs(data_fs_q, ticker, 'q')
# 두개 합치기
data_fs_bind = pd.concat([data_fs_y_clean, data_fs_q_clean])
# 재무제표 데이터를 DB에 저장
args = data_fs_bind.values.tolist()
mycursor.executemany(query, args)
con.commit()
except:
# 오류 발생시 해당 종목명을 저장하고 다음 루프로 이동
print(ticker)
error_list.append(ticker)
# 타임슬립 적용
time.sleep(2)
# DB 연결 종료
engine.dispose()
con.close()
작업이 끝난 후 SQL의 kor_fs 테이블을 확인해보면 전 종목의 재무제표가 저장되어 있습니다. 시간이 지나 위 코드를 다시 실행하면 upsert 형식을 통해 수정된 재무제표는 update를, 새로 입력된 재무제표는 insert를 합니다.
참고자료
https://github.com/hyunyulhenry/quant_py/blob/main/data_korea.ipynb
'데이터분석 > 크롤링' 카테고리의 다른 글
파이썬 퀀트투자(6): 네이버금융 수정주가 크롤링, 적재 (4) | 2024.10.12 |
---|---|
파이썬 퀀트투자(5): WICS 기준 섹터정보 크롤링, 적재 (2) | 2024.10.12 |
파이썬 퀀트투자(3): 한국거래소 개별종목 지표 크롤링 (3) | 2024.10.09 |
파이썬 퀀트투자(2): 한국거래소 업종 분류 현황 크롤링 (1) | 2024.10.09 |
파이썬 퀀트투자(1): 최근 영업일 기준 데이터 크롤링 (7) | 2024.10.09 |
댓글