저번 포스팅까지 작성된 코드에서는 연결 재무상태표에 대한 정보만을 다루었다. 이번에는 (개별) 재무상태표의 정보까지 다루기 위해 연결 재무제표와 (개별) 재무제표를 구별할 수 있는 분기점을 설계 및 보완하고, 기업별로 들쭉날쭉한 사업보고서 양식을 커버할 수 있는 방법에 대해 고안해보았다.

기업별로 서로 다른 사업보고서 양식에 대해서는 사실상 모든 경우를 커버할 수는 없으므로, 어느 정도 마감 기준선을 정하기 위해 차장님께 질문을 드렸으나, 차장님께서 바쁘신 관계로 (ㅠㅠ..) 나중을 기약하기로 했다…

분석 및 설계

  • 목차가 [재무제표 등”,] 으로 탐색되는 오래된 사업보고서를 관리하기 위한 추가 분기점 코드와 해당 분기에 맞춰 동작을 달리하는 함수 추가 작성.

  • 분기점 코드를 통하여 [재무제표 등”,] 페이지로 성공적으로 이동하는 모습은 확인하였으나, 재무상태표를 표기하는 방식이 기업별로 상이한 것을 파악

  • 기업별로 상이한 재무상태표 표기방법에 대해 여러가지 예외 양식을 가진 사업보고서들에 대한 해결방안 고찰

  • 테스트를 진행하며 적정 기준선을 정하는 중

작성 코드

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 # for parsing

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

###########################################################################################################
# 기업종목번호 기반 전체 사업보고서 검색 함수                                                                #
# Input : NULL       /       Output : DataFrame                                                           #
###########################################################################################################
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')

    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 :                      # 검색 결과가 없을 시.
        return None

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

    return data

###########################################################################################################
# 검색된 전체 사업보고서 기반 재무제표 페이지 이동 함수                                                       #
# Input : NULL       /       Output : NULL                                                                #
###########################################################################################################
def fs_page() :
    data = searching_report()
    if len(data) < 1 :
        print("!!!! 실패 : 사업보고서 검색 결과 없음.")
        return 0

    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]
        link_flag = 1
        indi_flag = 1

        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]     # "연 결 재 무 제 표" 로 발견
                link_page_1 = '연결재무제표'
            else :                                             # 실패시 경고 문구
                print("!!!! 실패 : " + data['rpt_nm'][document_count] + " / '연결재무제표' 페이지 탐색 실패함.")
                link_flag = 0         #연결 재무제표 없음
        else :
            body = body.split('연결재무제표",')[1]              # "연결재무제표" 로 발견
            link_page_1 = '연결재무제표'
        
        indi_body = body        
        if len(indi_body.split('재무제표",')) <= 1:             # 개별재무제표 탐색 시작
            if len(indi_body.split('재 무 제 표",')) >= 2:
                indi_body = indi_body.split('재 무 제 표",')[1] # "재 무 제 표" 로 발견
                indi_page_1 = '재무제표'
            else :                                             # 실패시 경고 문구
                print("!!!! 실패 : " + data['rpt_nm'][document_count] + " / '재무제표' 페이지 탐색 실패함.")
                indi_flag = 0           #개별 재무제표 없음
        else :
            indi_body = indi_body.split('재무제표",')[1]        # "재무제표" 로 발견
            indi_page_1 = '재무제표'

### 오래전에 작성된 사업보고서들이 상기 영역에서 실패 후 접근했을 때 동작을 성공할 수 있는 영역
        if(link_flag == 0 and indi_flag == 0) :                       # 만약 오래된 사업보고서임이 판별 된 경우
            old_body = body
            if len(old_body.split('재무제표 등",')) <= 1 :             # 오래된 사업보고서 재무제표 탐색 시작
                if len(old_body.split('재 무 제 표 등",')) >=2 : 
                    old_body = old_body.split('재 무 제 표 등",')[1]   # "재 무 제 표 등" 으로 발견
                    old_page_1 = '재무제표 등'
                else :                                                # 실패시 경고 문구
                    print("!!!! 실패 : " + data['rpt_nm'][document_count] + " / '재무제표 등' 페이지 탐색 실패함.")
                    continue                                     ###### 다음 사업보고서로 강제 이동
            else :
                old_body = old_body.split('재무제표 등",')[1]          # "재무제표 등" 으로 발견
                old_page_1 = '재무제표 등'
        
            old_table(old_body, data, document_count, old_page_1)     # 오래된 사업보고서용 함수 호출
            document_count += 1
            continue                                             ###### 다음 사업보고서로 강제 이동

        if(link_flag == 1) : fs_table(body, data, document_count, link_page_1)      # 연결 재무제표가 있으면
        if(indi_flag == 1) : fs_table(indi_body, data, document_count, indi_page_1) # 개별 재무제표가 있으면
        document_count += 1

    return

###########################################################################################################
# 목차를 통해 이동한 페이지에서 표 값들을 추출하는 함수                                                       #
# Input : string, DataFrame, int, string       /       Output : NULL                                      #
###########################################################################################################
def fs_table(body, data, document_count, 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'

    table_attribute = BeautifulSoup(urlopen(VIEWER_URL).read(), 'html.parser')
    table_attribute = table_attribute.find('table') # 가장 먼저 등장하는 table태그 기준 가져오기
    pt = parser.make2d(table_attribute)             # list로 변환저장 - 과정에서 다른 불필요한 태그들 자동 제거
    table_attribute = pd.DataFrame(pt)              # DataFrame으로 변환하여 다시저장
    
    table_attribute = table_attribute.applymap(lambda x: x.replace('\xa0','').replace('\xa9','')) #인코딩을 위해 공백을 의미하는 특수문자열 제거
    table_attribute.to_csv('C:\\Users\\admin\\Desktop\\Test_Result\\' + data['crp_nm'][0] + "(" + COMPANY_CODE
                + ") " + data['rpt_nm'][document_count] +'.csv', encoding='cp949', header=False, index=False, mode='a')
                #header를 통해 column명 표기X, index를 통해 row명 표기X, mode w로 파일 덮어쓰기식 생성

    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("!!!! 실패 : " + data['rpt_nm'][document_count] + " / " + page_1 + " / " + "재무상태표 탐색 실패.")
            return
        else :
            body = str(page.find('body')).split('재무상태표')[1]    # "재무상태표" 로 발견
            page_2 = '재무상태표'
    else : 
        body = str(page.find('body')).split('재 무 상 태 표')[1]    # "재 무 상 태 표" 로 발견
        page_2 = '재무상태표'
    
    body = BeautifulSoup(body, 'html.parser')                       # 찾아낸 재무상태표를 읽어내기 위해 파싱
    print("탐색 성공 : " + data['rpt_nm'][document_count] + " / " + page_1 + " / " + page_2)
    table = body.find_all('table')  # table 태그 탐색
    if len(table) <= 1 :            # 탐색 실패시 프로그램 종료
        print("!!!! 실패 : " + data['rpt_nm'][document_count] + " / " + page_1 + " / " + page_2 + " - 재무상태표 파싱 실패.")
        return

    p = parser.make2d(table[0])    
    table = pd.DataFrame(p[1:], columns = p[0])
    table.to_csv('C:\\Users\\admin\\Desktop\\Test_Result\\' + data['crp_nm'][0] + "(" + COMPANY_CODE
                + ") " + data['rpt_nm'][document_count] +'.csv', encoding='cp949', index=False, mode='a')
                #index를 통해 row명 표기X, mode a로 파일 이어쓰기식 열기
    return

###########################################################################################################
# 오래된 사업보고서의 목차를 통해 이동한 페이지에서 표 값들을 추출하는 함수                                     #
# Input : string, DataFrame, int, string       /       Output : NULL                                      #
###########################################################################################################
def old_table(body, data, document_count, 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'

    # table_attribute = BeautifulSoup(urlopen(VIEWER_URL).read(), 'html.parser')
    # table_attribute = table_attribute.find('table') # 가장 먼저 등장하는 table태그 기준 가져오기
    # pt = parser.make2d(table_attribute)             # list로 변환저장 - 과정에서 다른 불필요한 태그들 자동 제거
    # table_attribute = pd.DataFrame(pt)              # DataFrame으로 변환하여 다시저장
    
    # table_attribute = table_attribute.applymap(lambda x: x.replace('\xa0','').replace('\xa9','')) #인코딩을 위해 공백을 의미하는 특수문자열 제거
    # table_attribute.to_csv('C:\\Users\\admin\\Desktop\\Test_Result\\' + data['crp_nm'][0] + "(" + COMPANY_CODE
    #             + ") " + data['rpt_nm'][document_count] +'.csv', encoding='cp949', header=False, index=False, mode='a')
    #             #header를 통해 column명 표기X, index를 통해 row명 표기X, mode w로 파일 덮어쓰기식 생성
    link_flag = 1
    page = BeautifulSoup(urlopen(VIEWER_URL).read(), 'html.parser')
    if len(str(page.find('body')).split('연 결 재 무 상 태 표')) == 1 :   # 연결재무상태표
        if len(str(page.find('body')).split('연결 재무상태표')) <= 1 :   # 재무상태표를 찾아내지 못한다면 프로그램 종료
            if len(str(page.find('body')).split('연결재무상태표')) <= 1 :
                print("!!!! 실패 : " + data['rpt_nm'][document_count] + " / " + page_1 + " / " + "연결재무상태표 탐색 실패.")
                link_flag = 0
            else :
                body = str(page.find('body')).split('연결재무상태표')[1]    # "재무상태표" 로 발견
                page_2 = '연결재무상태표'
        else :
            body = str(page.find('body')).split('연결 재무상태표')[1]    # "재무상태표" 로 발견
            page_2 = '연결재무상태표'
    else : 
        body = str(page.find('body')).split('연 결 재 무 상 태 표')[1]    # "재 무 상 태 표" 로 발견
        page_2 = '연결재무상태표'
    
    # if (link_flag == 1) :
    #     indi_body = body

    #재무상태표(대차대조표)
    #대차대조표
    #대 차 대 조 표
    #재무상태표(

    body = BeautifulSoup(body, 'html.parser')                       # 찾아낸 재무상태표를 읽어내기 위해 파싱
    table = body.find_all('table')  # table 태그 탐색
    print(table)
    return
    if len(table) <= 1 :            # 탐색 실패시 프로그램 종료
        print("!!!! 실패 : " + data['rpt_nm'][document_count] + " / " + page_1 + " / " + page_2 + " - 재무상태표 파싱 실패.")
        return

    print("탐색 성공 : " + data['rpt_nm'][document_count] + " / " + page_1 + " / " + page_2)
    p = parser.make2d(table[0])    
    table = pd.DataFrame(p[1:], columns = p[0])
    table.to_csv('C:\\Users\\admin\\Desktop\\Test_Result\\' + data['crp_nm'][0] + "(" + COMPANY_CODE
                + ") " + data['rpt_nm'][document_count] +'.csv', encoding='cp949', index=False, mode='a')
                #index를 통해 row명 표기X, mode a로 파일 이어쓰기식 열기
    return


###########################################################################################################
# 프로그램 전체 실행을 통제하는 Main                                                                        #
# Input : string       /       Output : .csv file                                                         #
###########################################################################################################
print(fs_page())          # for testing.

크게 달라진 부분은 fs_page() 함수의 내부에서 ‘연결재무제표’를 기준으로 html소스를 자르고, 잘라낸 소스에서 ‘재무제표’를 기준으로 한 차례 더 자르는 동작을 통해 연결 재무상태표와 (개별) 재무상태표를 각각 구할 수 있도록 구현하였다는 것이다. 원리 자체는 [Python 재무제표 크롤링 #2]에서 작성한 코드와 완전히 동일하다.

old_table() 함수는 기존 fs_table() 함수가 전달 받은 인자에 따라 연결/개별을 구별해내었으나, 오래된 사업보고서들의 경우 양식이 워낙 다양하기 때문에 fs_table() 함수의 규격에 맞추기가 어려워 새로 작성 중인 함수였다. 그러나 각카오, 헬지, 샘숭 등 몇 안되는 수의 기업만 검색해봐도 규격이 가지각색들이라 함수 작성 도중 ‘이건 도저히 안되겠다…’ 라는 생각이 들어 주석처리 후 개발을 멈추었다.

차장님! 탐색 기준이 절실합니다 ㅠㅠ..

결과화면

기업종목코드 6자리 : ******
탐색 성공 : 사업보고서 (2018.12) / 연결재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2018.12) / 재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2017.12) / 연결재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2017.12) / 재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2016.12) / 연결재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2016.12) / 재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2015.12) / 연결재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2015.12) / 재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2014.12) / 연결재무제표 / 재무상태표
탐색 성공 : 사업보고서 (2014.12) / 재무제표 / 재무상태표
!!!! 실패 : [기재정정]사업보고서 (2013.12) / '연결재무제표' 페이지 탐색 실패함.
!!!! 실패 : [기재정정]사업보고서 (2013.12) / '재무제표' 페이지 탐색 실패함.
[<table class="nb" width="600">
<colgroup>
<col width="600"/>
</colgroup>
<tbody>
<tr>
<td align="CENTER" height="21" width="600">
<p style="font-size:12pt;font-weight:bold;"></p></td></tr></tbody></table>]
!!!! 실패 : 사업보고서 (2012.12) / '연결재무제표' 페이지 탐색 실패함.
!!!! 실패 : 사업보고서 (2012.12) / '재무제표' 페이지 탐색 실패함.
[<table class="nb" width="600">
<colgroup>
<col width="600"/>
</colgroup>
<tbody>
<tr>
<td align="CENTER" height="21" width="600">
<p style="font-size:12pt;font-weight:bold;"></p></td></tr></tbody></table>]
None

2014년도 사업보고서까지는 정상적으로 출력되다가, 2013년 사업보고서부터 왜 실패가 되는지, 실패한 영역은 어딘지에 대해 보여주고 있다.

sample1

위 그림의 엑셀 파일 저장은 [Python 재무제표 크롤링 #2] 설계 및 구현 고도화 때와 같다.

sample2

엑셀 파일 내부의 최상단 모습 또한 이전과 완전히 동일한 모습. 속성 정보 이후 곧바로 재무상태표 값들을 보여준다.

sample3

달라진 부분. 연결 재무상태표가 끝나자마자 곧이어 (개별) 재무상태표의 속성 정보가 시작된다.

연결/개별 재무제표를 나누는 방법에 대해서는 엑셀 시트를 기준으로 나눠서 저장할 수도 있었지만, 해당 부분은 시트를 추가하거나 넘기는 함수만 추가로 입력하면 해결되는 부분이라 테스트 하는 입장에선 굳이 불필요하다 생각되어 곧바로 아래에 추가하였다.
잘 저장 된다는 것만 테스트 되었으면 된거지!

고찰 및 후기

find 함수의 범위를 명확하게 깨닫고, 신나게 활용한 날이었다.

캡처

대략 위 그림과 같은 느낌이었는데, 전체 소스에서 연결재무제표에 대한 find를 통해 소스를 줄여 연결재무제표 정보를 얻어내고, 연결재무제표 소스에서 개별재무제표에 대한 find를 한 번 더해서 개별재무제표 정보만 얻어낸 것.

아쉬운 점으로는 인턴 면접 때 ‘잘 짜여진 기획 속에서 일해보고 싶다.’ 라는 말을 드렸었는데, 근래에 그러한 만족감을 얻기 힘들었다. 차장님께 제한 사항, 수정 사항 등으로 몇 차례 질문 메일을 드렸으나 답변을 받기가 어려웠다. 물론 연초에 회사가 워낙 바쁘기 때문에 어쩔 수 없는 걸 알지만서도 :P …

곧 기회가 오겠지! 이번 포스팅도 여기서 끝!

댓글남기기