1 워드 관련 팩키지1

워드 파일을 전용으로 개발된 officer 팩키지와 워드 파일에서 표를 추출하는 docxtractr 팩키지도 있는 반면 텍스트 마이닝이나 자연어 처리 분야에서 다양한 텍스트 데이터를 읽어들이기 위해 개발된 textreadr, readtext 팩키지도 있다.

2 officer

officer 팩키지를 사용해서 워드 파일에 담긴 내용을 이해하고 필요한 정보를 추출해 보자. 먼저, officer 팩키지 docx_summary() 함수를 활용하여

library(tidyverse)
library(officer)
finstmt_docx <- officer::read_docx("data/word_files/word_20190102000008_01211649.docx")
finstmt_content <- docx_summary(finstmt_docx) %>% as_tibble()

finstmt_content
# A tibble: 1,104 x 11
   doc_index content_type style_name text  level num_id row_id is_header cell_id
       <int> <chr>        <chr>      <chr> <dbl>  <int>  <int> <lgl>       <dbl>
 1         1 paragraph    <NA>       "목차"…    NA     NA     NA NA             NA
 2         2 paragraph    <NA>       ""       NA     NA     NA NA             NA
 3         3 paragraph    <NA>       "에이오…    NA     NA     NA NA             NA
 4         4 paragraph    Body Text  ""       NA     NA     NA NA             NA
 5         5 paragraph    <NA>       "재 무…    NA     NA     NA NA             NA
 6         6 paragraph    Heading 1  "감사보…    NA     NA     NA NA             NA
 7         7 paragraph    Body Text  ""       NA     NA     NA NA             NA
 8         8 paragraph    Body Text  ""       NA     NA     NA NA             NA
 9         9 paragraph    Body Text  ""       NA     NA     NA NA             NA
10        10 paragraph    Body Text  ""       NA     NA     NA NA             NA
# … with 1,094 more rows, and 2 more variables: col_span <dbl>, row_span <dbl>

문서에서 표와 문단이 각각 차지하는 비율을 각각 계산해 보자. 이를 통해 문단이 얼마를 구성하고 있고 표는 몇개가 있는지 등등을 파악할 수 있다.

# tapply(finstmt_content$doc_index, finstmt_content$content_type, 
#        function(x) length(unique(x)))

finstmt_content %>% 
  count(doc_index, content_type) %>% 
  select(-n) %>%
  distinct_all() %>% 
  count(content_type)
# A tibble: 2 x 2
  content_type     n
  <chr>        <int>
1 paragraph      343
2 table cell      20

3 문서에 포함된 표

문서에 포함된 표에 대한 기본 통계량을 계산하기 위해 다음과 같이 구성을 일부 변경한다. docxtractr 팩키지에 내장된 docx_describe_tbls() 함수가 있지만 화면에 쭉 뿌리고 없어져 재사용을 위해서 docx_describe_stat_tbls() 함수를 별도 제작해서 복잡도가 높은 표를 문서내에서 검출해 낸다.

library(docxtractr)

finstmt_docx_tbl <- docxtractr::read_docx("data/word_files/word_20190102000008_01211649.docx")

docx_describe_stat_tbls <- function(docx) {
  
  ns <- docx$ns
  tbls <- docx$tbls

  res_tbl <- list()
  
  for (i in 1:length(tbls)) {
    
    tbl <- tbls[[i]]
    
    cells <- xml2::xml_find_all(tbl, "./w:tr/w:tc", ns=ns)
    rows <- xml2::xml_find_all(tbl, "./w:tr", ns=ns)
  
    cell_count_by_row <- purrr::map_int(rows, ~{ length(xml2::xml_find_all(.x, "./w:tc", ns)) })
    row_counts <- paste0(unique(cell_count_by_row), collapse=", ")
    max_cell_count <- max(cell_count_by_row)
    
    # simplistic test for whether table is uniform rows x cells == cell count
    uniform_test <- NULL
    if ((max_cell_count * length(rows)) == length(cells)) {
      # cat("  uniform    : likely!\n")
      uniform_test <- TRUE
    } else {
      # cat(sprintf(
      #   "  uniform    : unlikely => found differing cell counts (%s) across some rows\n",
      #   row_counts))
      uniform_test <- FALSE
    }
    
    one_tbl <- tibble(tbl_no = i,
                      total_cell = length(cells),
                      total_row  = length(rows),
                      is_uniform = uniform_test)
    
    res_tbl[[i]] <- one_tbl
  }
  res_df <- map_df(res_tbl, rbind)
  return(res_df)
}


tbl_info_df <- docx_describe_stat_tbls(finstmt_docx_tbl)

tbl_info_df %>% 
  arrange(desc(total_cell))
# A tibble: 22 x 4
   tbl_no total_cell total_row is_uniform
    <int>      <int>     <int> <lgl>     
 1      1        148        30 FALSE     
 2      4        108        22 FALSE     
 3     21         92         7 FALSE     
 4      2         78        16 FALSE     
 5     22         76        17 FALSE     
 6     16         32         8 FALSE     
 7      3         28         7 TRUE      
 8     13         25         4 FALSE     
 9     11         23         6 FALSE     
10     12         18         6 TRUE      
# … with 12 more rows

상기 작업을 통해서 1, 4, 21 번 표가 복잡도가 높은 표로 확인이 된다.

4 표 분류

금감원 DART에서 제공되는 재무제표에서 몇가지 재무제표가 표 중에서 특히 중요하다.

  • 재무상태표
  • 손익계산서
  • 자본변동표
  • 현금흐름표
  • 기타

문서에 포함된 수많은 표를 다음 5가지 범주로 구분하는 분류기를 만든 후에 해당 표를 상기 5가지 유형의 표로 분기한다.

4.1 표에 포함된 텍스트

먼저 표에 포함된 텍스트를 추출하는 스크립트를 작성한다. 표를 특정(1)하여 docx_extract_tbl() 함수를 사용해서 데이터프레임으로 변환시킨다. 그리고 무조건 첫번째 칼럼을 특정하여 한글만 추출하여 텍스트를 구성한다.

bs_df <- docx_extract_tbl(finstmt_docx_tbl, 1, header=TRUE)

bs_df %>% 
  janitor::clean_names() %>% 
  pull(var = 1) %>% 
  str_trim() %>% 
  str_remove_all(" ") %>% 
  str_extract(pattern = "[가-힣]+") %>% 
  str_c(collapse = " ")
[1] "자산 유동자산 당좌자산 현금및현금성자산 선급비용 당기법인세자산 비유동자산 투자자산 매도가능증권 투자채권 자산총계 부채 유동부채 미지급금 미지급비용 단기차입금 비유동부채 장기차입금 전환사채 상환할증금 사채할인발행차금 부채총계 자본 자본금 보통주자본금 결손금 미처리결손금 자본총계 부채및자본총계"

다음 단계로 함수로 만들어 표 번호를 입력하게 되면 표에 포함된 텍스트가 자동으로 추출되도록 작업한다.

extract_text_from_tbl <- function(docx, tbl_no) {
  tbl_df <- docx_extract_tbl(docx, tbl_no, header=TRUE) %>% 
  janitor::clean_names()

  coa_txt <- tbl_df %>% 
    pull(var = 1) %>% 
    str_trim() %>% 
    str_remove_all(" ") %>% 
    str_extract(pattern = "[가-힣]+") %>% 
    str_c(collapse = " ")
  
  return(coa_txt)
}

extract_text_from_tbl(finstmt_docx_tbl, 3)
[1] "전기초 당기순이익 전기말 당기초 당기순이익 당기말"

마지막으로 모든 표에서 텍스트를 추출한다. 결과를 데이터프레임으로 만들어 후속 작업에 활용한다.

tbl_text <- map_chr(1:docx_tbl_count(finstmt_docx_tbl), extract_text_from_tbl, docx = finstmt_docx_tbl)

tbl_text_df <- tibble(tbl_no = 1:docx_tbl_count(finstmt_docx_tbl),
       tbl_text = tbl_text)

tbl_text_df
# A tibble: 22 x 2
   tbl_no tbl_text                                                              
    <int> <chr>                                                                 
 1      1 자산 유동자산 당좌자산 현금및현금성자산 선급비용 당기법인세자산 비유동자산 투자자산 매도가능증권 투자채권 자산총계 부채 유동부…
 2      2 매출액 매출원가 매출총이익 판매비와관리비 지급수수료 세금과공과 영업손실 영업외수익 이자수익 잡이익 영업외비용 이자비용 법인세…
 3      3 전기초 당기순이익 전기말 당기초 당기순이익 당기말                     
 4      4 영업활동으로인한현금흐름 당기순이익 현금의유출이없는비용등의가산 이자비용 현금의유입이없는수익등의차감 영업활동으로인한자산부채의변동…
 5      5 구분 미래에셋대우증권 국민은행 계                                     
 6      6 피투자회사명 에이오엔인베스트먼트                                     
 7      7 피투자회사명 에이오엔인베스트먼트                                     
 8      8 피투자회사명 랜드마크타워                                             
 9      9 피투자회사명 랜드마크타워                                             
10     10 차입처 에이오엔비지엔                                                 
# … with 12 more rows

4.2 표 분류기

정규표현식을 사용해서 각종 재무제표를 앞서 정의한 5가지 유형으로 분류한다.

tbl_text_df <- tbl_text_df %>% 
  mutate(classify = case_when(str_detect(tbl_text, "자산총계") ~ "재무상태표",
                              str_detect(tbl_text, "매출액") ~ "손익계산서",
                              str_detect(tbl_text, "전기초") ~ "자본변동표",
                              str_detect(tbl_text, "현금흐름") ~ "현금흐름표",
                              TRUE ~ "기타"
                              )) %>% 
  select(tbl_no, classify, tbl_text)

tbl_text_df %>% 
  mutate(tbl_text = str_sub(tbl_text, start = 1L, end = 30L))
# A tibble: 22 x 3
   tbl_no classify   tbl_text                                                  
    <int> <chr>      <chr>                                                     
 1      1 재무상태표 자산 유동자산 당좌자산 현금및현금성자산 선급비용 당기법   
 2      2 손익계산서 매출액 매출원가 매출총이익 판매비와관리비 지급수수료 세   
 3      3 자본변동표 전기초 당기순이익 전기말 당기초 당기순이익 당기말         
 4      4 현금흐름표 영업활동으로인한현금흐름 당기순이익 현금의유출이없는비용등
 5      5 기타       구분 미래에셋대우증권 국민은행 계                         
 6      6 기타       피투자회사명 에이오엔인베스트먼트                         
 7      7 기타       피투자회사명 에이오엔인베스트먼트                         
 8      8 기타       피투자회사명 랜드마크타워                                 
 9      9 기타       피투자회사명 랜드마크타워                                 
10     10 기타       차입처 에이오엔비지엔                                     
# … with 12 more rows

4.3 재무제표 확인

먼저 재무상태표를 추출하여 내용을 꺼내서 확인해본다.

bs_tbl_no <- tbl_text_df %>% 
  filter(classify == "재무상태표") %>% 
  pull(tbl_no)

bs_df <- docx_extract_tbl(finstmt_docx_tbl, bs_tbl_no, header=TRUE)

bs_df %>% DT::datatable()

두번째로 손익계산서를 꺼내서 내용을 확인해본다.

is_tbl_no <- tbl_text_df %>% 
  filter(classify == "손익계산서") %>% 
  pull(tbl_no)

is_df <- docx_extract_tbl(finstmt_docx_tbl, is_tbl_no, header=TRUE)

is_df %>% DT::datatable()

마지막으로 현금흐름표를 추출해서 관련 내용을 확인해보자

cf_tbl_no <- tbl_text_df %>% 
  filter(classify == "현금흐름표") %>% 
  pull(tbl_no)

cf_df <- docx_extract_tbl(finstmt_docx_tbl, cf_tbl_no, header=TRUE)

cf_df %>% DT::datatable()

5 깔끔히 정리된 재무제표

5.1 워드 템플릿

가장 먼저 워드 템플릿을 생성한다. R마크다운을 사용해서 워드 템플릿을 만들어도 관계없다. 먼저 IEEE에서 워드 템플릿을 다운로드 받아 이를 기본 서식으로 사용한다.

word_template <- officer::read_docx("data/word_template.docx")
word_template
rdocx document with 2 element(s)

* styles:
                 Normal               heading 1               heading 2 
            "paragraph"             "paragraph"             "paragraph" 
              heading 3               heading 4               heading 5 
            "paragraph"             "paragraph"             "paragraph" 
              heading 6               heading 7               heading 8 
            "paragraph"             "paragraph"             "paragraph" 
              heading 9  Default Paragraph Font            Normal Table 
            "paragraph"             "character"                 "table" 
                No List                Abstract                 Authors 
            "numbering"             "paragraph"             "paragraph" 
             MemberType                   Title           footnote text 
            "character"             "paragraph"             "paragraph" 
             References              IndexTerms      footnote reference 
            "paragraph"             "paragraph"             "character" 
                 footer                    Text          Figure Caption 
            "paragraph"             "paragraph"             "paragraph" 
            Table Title          Reference Head                  header 
            "paragraph"             "paragraph"             "paragraph" 
               Equation               Hyperlink       FollowedHyperlink 
            "paragraph"             "character"             "character" 
       Body Text Indent Default Paragraph Font1               abs-title 
            "paragraph"             "paragraph"             "paragraph" 
              body-text    table-figure-caption                footnote 
            "paragraph"             "paragraph"             "paragraph" 
       subsection-title               Body Text               본문 Char 
            "paragraph"             "paragraph"             "character" 
            bullet list                sponsors          paper subtitle 
            "paragraph"             "paragraph"             "paragraph" 
               equation          figure caption          table col head 
            "paragraph"             "paragraph"             "paragraph" 
      table col subhead              table copy          table footnote 
            "paragraph"             "paragraph"             "paragraph" 
             table head 
            "paragraph" 

* Content at cursor location:
  level num_id text style_name content_type
1    NA     NA              NA    paragraph

5.2 기본 내용 채워넣기

재무제표에 큰 제목으로 넣고 나서 재무상태표, 손익계산서, 현금흐름표를 순차적으로 채워넣고자 틀을 잡아둔다.

finstmt_word <- word_template %>% 
  body_add_par(value = "특정회사 재무제표", style = "heading 1") %>% 
  body_add_par(value = "재무상태표", style = "heading 2") %>% 
  body_add_par(value = "손익계산서", style = "heading 2") %>% 
  body_add_par(value = "현금흐름표", style = "heading 2")

finstmt_word
rdocx document with 6 element(s)

* styles:
                 Normal               heading 1               heading 2 
            "paragraph"             "paragraph"             "paragraph" 
              heading 3               heading 4               heading 5 
            "paragraph"             "paragraph"             "paragraph" 
              heading 6               heading 7               heading 8 
            "paragraph"             "paragraph"             "paragraph" 
              heading 9  Default Paragraph Font            Normal Table 
            "paragraph"             "character"                 "table" 
                No List                Abstract                 Authors 
            "numbering"             "paragraph"             "paragraph" 
             MemberType                   Title           footnote text 
            "character"             "paragraph"             "paragraph" 
             References              IndexTerms      footnote reference 
            "paragraph"             "paragraph"             "character" 
                 footer                    Text          Figure Caption 
            "paragraph"             "paragraph"             "paragraph" 
            Table Title          Reference Head                  header 
            "paragraph"             "paragraph"             "paragraph" 
               Equation               Hyperlink       FollowedHyperlink 
            "paragraph"             "character"             "character" 
       Body Text Indent Default Paragraph Font1               abs-title 
            "paragraph"             "paragraph"             "paragraph" 
              body-text    table-figure-caption                footnote 
            "paragraph"             "paragraph"             "paragraph" 
       subsection-title               Body Text               본문 Char 
            "paragraph"             "paragraph"             "character" 
            bullet list                sponsors          paper subtitle 
            "paragraph"             "paragraph"             "paragraph" 
               equation          figure caption          table col head 
            "paragraph"             "paragraph"             "paragraph" 
      table col subhead              table copy          table footnote 
            "paragraph"             "paragraph"             "paragraph" 
             table head 
            "paragraph" 

* Content at cursor location:
  level num_id       text style_name content_type
1    NA     NA 현금흐름표  heading 2    paragraph

5.3 표 채워넣기

이제 주요 재무제표를 데이터프레임으로 만들었는데 이를 워드 파일에 끼워넣어 재무제표 워드 파일을 목적에 맞춰 깔끔하게 다시 제작한다. cursor_reach() 함수로 문서 내 커서의 위치를 특정하고 난 후 body_add_table() 함수로 데이터프레임을 표로 변환시켜 워드 문서에 포함시킨다.

finstmt_word <- finstmt_word %>% 
  cursor_reach(keyword = "재무상태표") %>% 
  body_add_table(bs_df, first_column = TRUE) %>% 
  body_add_break() %>% 
  cursor_reach(keyword = "손익계산서") %>% 
  body_add_table(is_df, first_column = TRUE) %>% 
  body_add_break() %>% 
  cursor_reach(keyword = "현금흐름표") %>% 
  body_add_table(cf_df, first_column = TRUE) %>% 
  body_add_break()
  
finstmt_word %>% 
  docx_show_chunk()

5.4 워드 파일로 저장

마지막으로 작업한 결과물을 워드파일로 저장한다.

print(finstmt_word, "data/finstmt_word.docx")

5.5 작업결과 확인

내보낸 워드 파일을 PDF 파일로 다른 이름 저장시키고 나서 이를 R마크다운 문서로 가져온다. 앞서 정의한 3종 재무제표가 원하는 위치에 잘 정리된 것이 확인된다.

knitr::include_graphics("data/finstmt_word.pdf")
 

데이터 과학자 이광춘 저작

kwangchun.lee.7@gmail.com