방학기간 동안 인턴활동을 하면서 DART 전자공시시스템에서 제공하는 연결재무상태표와 (개별)재무상태표를 크롤링하는 프로젝트를 진행하게 되었다. 프로젝트를 공식적으로 시작한 날짜는 1월 3일이었으나, 중간에 다른 업무들이 겹쳐서 실질적으로 시작한 날짜는 1월 10일 정도였다.

코드가 추가 및 수정된 부분이 꽤 되었으나, 그걸 기록해두지 않았어서 (-_-;;)
여기서부터라도 기록해두면 괜찮지 않을까 싶어 포스팅을 시작했다.

요구사항

- 웹 크롤링 동작 실패시 실패 사유에 대해 정확히 알 수 있도록 한다.
- dart.fss.or.kr 사이트에 업로드 되어 있는 기업별 사업보고서를 이용한다.
- 기업종목코드만으로 재무제표 내용을 가져올 수 있어야 한다.
- 웹 크롤링으로 가져온 재무제표 내용은 데이터베이스에 저장할 수 있는 형태여야 한다. (Excel, Json, Notepad)
- 웹 크롤링으로 가져온 재무제표 내용이 실제 재무제표 내용과 오차가 없어야 한다.
- 사업보고서를 기준으로 연결재무제표, 개별재무제표, 손익계산을 한 눈에 파악할 수 있어야 한다.
- 재무제표의 단위를 통일 시킨다.
- 웹 크롤링으로 가져오는 재무제표의 내용이 연 단위를 포함할 수 있도록 한다.
- 결과 파일의 이름을 기업명.기업종목코드.날짜 등으로 구분 가능하도록 한다.

요구 사항은 상기 내용과 같았는데, 이전에도 계속해서 변동사항이 있었으므로
아마 앞으로도 계속해서 수정되는 내용이 있지 않을까 싶다.
하기는 앞선 1월 3일부터의 분석 및 설계 보고 내용이다.

분석 및 설계

1월 3일

- Python 컴파일러 및 에디터 설치

1월 6일

- dart.fss.or.kr 사이트를 통해 재무제표를 탐색하는 과정을 배제하고,  
  재무제표를 크롤링 하는 코드만 작성함

- 크롤링 한 내용을 터미널에 출력할 때는 정상적으로 잘 출력되었으나,  
  엑셀 파일로 저장할 경우 계속해서 한글 폰트가 깨지는 인코딩 문제가 발생함.

- 때문에 엑셀 파일에 저장할 때 인코딩을 UTF-8로 강제 수정까지 해주었으나, 달라지지 않음.

1월 9일

- 엑셀 파일로 저장하는 동작은 배제하고, 터미널에 출력하는 코드를 다듬음.
- dart.fss.or.kr 사이트를 통해 재무제표를 탐색하는 과정에 대해 고찰
- dart.fss.or.kr 사이트를 이용할 때는 url의 변화가 보이지 않았으나,  
  F12 개발자도구를 이용하여 해당 페이지의 네트워크 탭을 살펴보았을 때  
  javascript ajax를 이용한 비동기통신을 주고 받고 있는 것을 확인할 수 있었음
- 이를 탐색하여보았을 때, startDate, endDate, textCrpNm 3가지 정보가 기업 검색에 사용되는 것을 파악함.
- 실제 startDate, endDate, textCrpNm을 url에 조합하여 입력해보았을 때,  
  정상적으로 해당 페이지로 이동하는 것을 확인하였음.
- 그렇게 만들어진 url을 Python으로 테스트해보았으나,  
  Python 모듈에서 textCrpNm에 들어가는 한글을 인식하지 못함.
- UTF-8 인코딩 기반 16진수로 변환하여 입력하면 정상적인 테스트 결과 값을 얻을 수 있었음.

1월 10일

- UTF-8 인코딩 기반 16진수로 변환하는 방법에 대한 해법을 찾아냄.  
  기존에 존재하는 모듈을 응용하는 방식으로, 직접 변환에 관여하는 것이 아니라  
  url구성 양식에 맞추어 자동으로 변환되는 방법을 이용함.
- 검색결과 화면을 얻어낸 상태에서, '사업보고서' 파일의  
  url구성 번호들을 얻어낼 수 있는 방법에 대한 고찰 및 분석이 성공적이었으나,  
  dart사이트에서 제공하는 API를 이용하는 방식보다 굉장히 비효율적이었음.  
  (얻어낸 검색결과 화면에 불필요한 정보들이 너무 많았고, 이를 걸러내는 추가 작업을 필요로 함)
- dart사이트에서 제공하는 API를 이용하는 방식은 html소스코드를 정제할 필요 없이  
  xml형태의 정렬된 소스만 제공해주므로, 이를 이용하면 간단하게  
  '사업보고서' 파일의 url구성 번호들을 얻어낼 수 있을 것이라 판단함.

1월 13일

- 기존 관리자도구 콘솔창을 이용해서 url을 분석 및 구성하는 방법에서,  
  dart사이트에서 제공하는 API를 이용하는 방법으로 코드 전체를 수정하는 과정을 거침.
  - 이유1 : url을 직접 분석 및 구성하는 방법에서는 사업보고서의 갯수가  
            10개가 넘어갔을 때 html소스를 분석하기 까다로웠음
  - 이유2 : dart사이트에서 제공하는 API를 이용하면 html소스를 분석할 필요 없이  
            깔끔한 xml 형태의 소스를 얻을 수 있음

- API를 통하여 사업보고서 검색결과 xml소스를 구하는 코드 작성
- 얻게 된 xml소스를 이용하여 사업보고서를 열람하는 url을 구성하는 방법에 대하여 고찰 및 분석

1월 14일

- 웹크롤링 결과를 Excel로 저장하기 위한 인코딩 테스트 후, 저장에 성공함.
- dart.fss.or.kr 사이트 재무제표와 웹크롤링 결과물 간의 오차 확인.  
  오차 없이 성공적으로 크롤링 되었음.
- 사업보고서 검색 기준에 기업명이 아닌 기업종목코드를 입력하는 것으로 코드를 수정함.

1월 15일

- 기업별로 상이한 사업보고서 양식에 대해 관찰 및 해결방안에 대해 고찰  
  대부분의 기업 사업보고서 목차 부분이 통일되어 있었으나,  
  오래된 사업보고서의 경우 목차 부분이 다르거나, 보고서 양식이 완전히 다른 경우가 있었음.
- 해결방안 마련을 위해 별도로 테스트 코드를 작성하고 테스트를 진행함.  
  (완성도를 높이기 위해서는 오랜 시간이 걸릴 것으로 예상 됨)

작성 코드

from bs4 import BeautifulSoup       # for html parser
from urllib.request import urlopen  # for html request/respone
import pandas as pd                 # for DataFrame
from html_table_parser import parser_functions as parser

API_KEY = "" # API 이용에 필요한 인증번호
COMPANY_CODE = input("기업종목코드 6자리 : ")

def searching_report() :                    # 기업종목번호 기반 전체 사업보고서 검색 함수
    SEARCH_URL = "http://dart.fss.or.kr/api/search.xml?auth=" + API_KEY + "&crp_cd=" + COMPANY_CODE
    SEARCH_URL = SEARCH_URL + "&start_dt=19990101&bsn_tp=A001&fin_rpt=Y"    #검색날짜 범위 / 사업보고서 / 최종보고서만
    XML_RESULT = BeautifulSoup(urlopen(SEARCH_URL).read(), 'html.parser')

    print(SEARCH_URL)
    
    find_list = XML_RESULT.findAll("list")  # list태그를 모두 탐색
    data = pd.DataFrame()                   # 데이터를 저장할 프레임 선언
    for t in find_list :                    # 나열번호, 기업명, 기업번호, 보고서명, 보고서번호, 제출인,  접수일자, 비고
        temp = pd.DataFrame(([[t.crp_cls.string, t.crp_nm.string, t.crp_cd.string, t.rpt_nm.string,
                t.rcp_no.string, t.flr_nm.string, t.rcp_dt.string, t.rmk.string]]),
                columns = ["crp_cls", "crp_nm", "crp_cd", "rpt_nm", "rcp_no", "flr_nm", "rcp_dt", "rmk"])
        data = pd.concat([data, temp])
    
    if len(data) < 1 :                      # 검색 결과가 없을 시.
        print("### Failed. (There's no report.)")
        return None

    del data['crp_cls']
    del data['crp_cd']                      # 나열번호, 기업번호 제거 (불필요)
    data = data.reset_index(drop=True)

    return data

def fs_table() :                            # 검색된 전체 사업보고서 기반 재무제표 추출 함수
    data = searching_report()
    document_count = 0

    for i in range(len(data)) :
        MAIN_URL = "http://dart.fss.or.kr/dsaf001/main.do?rcpNo=" + data['rcp_no'][document_count]
        print(MAIN_URL)

        page = BeautifulSoup(urlopen(MAIN_URL).read(), 'html.parser')
        body = str(page.find('head'))

        if len(body.split('연결재무제표",')) <= 1 :             # 연결재무제표 & 재무제표 탐색 시작
            if len(body.split('연 결 재 무 제 표",')) >=2 : 
                body = body.split('연 결 재 무 제 표",')[1]     # "연 결 재 무 제 표" 로 발견
                print_page_1 = '연 결 재 무 제 표'
            else :
                if len(body.split('재무제표",')) <= 1:          # 연결재무제표가 없다면 재무제표 로 탐색 시작
                    if len(body.split('재 무 제 표",')) >= 2:
                        body = body.split('재 무 제 표",')[1]   # "재 무 제 표" 로 발견
                        print_page_1 = '재 무 제 표'
                    else :
                        print("### Failed. (연결재무제표/재무제표 페이지 탐색 실패.)") 
                        return 0                               # 아무것도 발견하지 못할 시 프로그램 종료.
                else :
                    body = body.split('재무제표",')[1]          # "재무제표" 로 발견
                    print_page_1 = '재무제표'
        else :
            body = body.split('연결재무제표",')[1]               # "연결재무제표" 로 발견
            print_page_1 = '연결재무제표'

        body = body.split('cnt++')[0].split('viewDoc(')[1].split(')')[0].split(', ')
        body = [body[i][1:-1] for i in range(len(body))]       # 찾아낸 재무제표 페이지로 이동하기 위한 url구성 번호 파싱
        VIEWER_URL = "http://dart.fss.or.kr/report/viewer.do?rcpNo=" + body[0] \
                    + '&dcmNo=' + body[1] + '&eleId=' + body[2] + '&offset=' + body[3] \
                    + '&length=' + body[4] + '&dtd=dart3.xsd'
        print(VIEWER_URL)

        page = BeautifulSoup(urlopen(VIEWER_URL).read(), 'html.parser')
        if len(str(page.find('body')).split('재 무 상 태 표')) == 1 :   # 재무상태표 탐색 시작
            if len(str(page.find('body')).split('재무상태표')) <= 1 :   # 재무상태표를 찾아내지 못한다면 프로그램 종료
                print("### Failed. (재무상태표 탐색 실패.)")
                return 0
            else :
                body = str(page.find('body')).split('재무상태표')[1]    # "재무상태표" 로 발견
                print_page_2 = '재무상태표'
        else : 
            body = str(page.find('body')).split('재 무 상 태 표')[1]    # "재 무 상 태 표" 로 발견
            print_page_2 = '재 무 상 태 표'
        
        body = BeautifulSoup(body, 'html.parser')                       # 찾아낸 재무상태표를 읽어내기 위해 파싱

        print(print_page_1 + " - " + print_page_2)
        print(body.find(align='RIGHT').text)
        
        table = body.find_all('table')  # table 태그 탐색
        if len(table) <= 1 :            # 탐색 실패시 프로그램 종료
            print("### Failed. (there's no table.)")
            return 0

        p = parser.make2d(table[0])    
        table = pd.DataFrame(p[1:], columns = p[0])
        table = table.set_index(p[0][0])

        table.to_csv('C:\\Users\\admin\\Desktop\\Test_Result\\' + print_page_1 + "_" + print_page_2 + '_'
                    + str(document_count) +'.csv', encoding='cp949')
        document_count += 1

    return table


print(fs_table())          # for testing.

재무제표 탐색을 위해 기업종목코드를 입력 받는다. 초기에는 기업명을 입력 받는 형태였지만, 동명의 기업도 많이 존재하였고, 애초에 재무제표를 탐색할 정도면 기업종목코드를 알고 있을 것이란 대리님의 말씀 덕분에 기업종목코드를 기준으로 채택했다.
기업종목코드를 입력받게 되면 몇 가지 조건을 붙이게 되는데,

  1. 검색 범위를 1990년 1월 1일부터 오늘까지
  2. ‘사업보고서’만
  3. 중복되는 보고서(기재정정 등의 이유로)이 있을 경우 최신보고서만

3가지 조건을 이용해 URL을 합성하고 검색결과 화면을 만들어낸다.

검색결과 화면을 통해 만들어낸 결과 중 보고서 번호 (rcp_no)를 이용해 해당 사업보고서 페이지를 만들어내게 된다. 만들어진 사업보고서의 목차에서 연결재무제표와 재무제표를 차례대로 탐색하고, 탐색에 성공한 재무제표의 재무상태표를 크롤링하여 엑셀파일로 저장하게 된다.

일련의 과정을 거치어 결과물을 내뱉게 되지만, 아직 온전히 정리된 결과화면이 아니라 첨부할 필요는 없는 것 같다.
이번 글은 여기서 끝!

댓글남기기