Tcl을 이용한 Word 문서를 xml로 변환하기

admin의 아바타

Word로 작성된 문서를 XML로 변환하는 스크립트를 소개합니다. 여기서 소개하는 방법을 테스트 해보는것에 앞서 아래의 싸이틀 둘러 보는것이 좋을것입니다.

Export a Word Document to XML

여기서는 COM 을 통하여 Word로 부터 정보를 추출하여, Tcl로 XML 형식으로 변환합니다. 실행하기에 앞서 Word가 설치되어 있어야 하며, Tcl로부터 COM 의 엑세스를 위해서는 tcom 패키지가 필요합니다.

Word로 부터 데이타 추출

Tcl로 부터 Word를 실행및 종료

다음의 코드는 Word를 실행하여, 핸들을 변수 wordapp 변수에 설정하는 코드입니다.

package require tcom
set wordapp [::tcom::ref createobject "Word.Application"]

처음 상태에서는 Word가 화면에 보이지 않을것입니다. 보이게 하고 싶을 경우는 아래와 같이 Visible 프로퍼티를 true로 설정합니다.

$wordapp Visible 1

Word를 종료 할 경우는 Quit 메쏘드를 사용합니다.

$wordapp Quit

파일 열기

파일을 열어 보겠습니다. 아래의 코드는 커맨드 라인의 인자로 받은 파일을 열어 Document 오브젝트를 doc 변수에 대입합니다.

set doc [[$wordapp Documents] Open [lindex $argv 0]]

단락 엑세스

Document 오브젝트의 Paragraphs 프로퍼티는 문서내의 모든 단락을 가리키는 컬렉션을 리턴해줍니다. Word 가운데에서는 표제, 조목별, 아래쪽 으로 나누어 작성된 글도 다른 스타일로 주어진 단락 으로 표현 되고 있습니다.

이 컬렉션에 대해서 반복 처리를하면 간단하게 XML로의 변환을 할 수 있을것 같습니다. 또한

$doc Paragraphs

가 리턴해 주는 것은 COM의 Paragraphs 컬렉션의 핸들이지 Tcl의 리스트가 아니기 때문에, foreach가 아니라 tcom의 ::tcom::foreach 를 사용해야만 합니다.

::tcom::foreach para [$doc Paragraphs] {
        # 여기서 처리를 한다.
}

단락의 내용 추출

단락의 내용은

[[$para Range] Text]

라는 코드로 추출할수 있습니다. 여기에서 얻어진 내용의 개행 코드는 일반적인 것과 다르며, \v 로 표현되고 있습니다. 또 문자열의 마지막에는 \r 이 붙여지고 있습니다.

단락의 스타일 이름 추출

[[$para Style] NameLocal]

라는 코드로 스타일의 이름을 추출할수 있습니다. 스타일 이름은 한국어의 Word는 번호 매기기, 제목 2, 표준 같은 값입니다.

간단한 스크립트 작성 하기

지금까지의 설명을 바탕으로 다음과 같은 스크립트를 만들수 있습니다. 아래의 스크립트는 인자로 Word의 파일을 받아 표준출력으로 XML을 출력합니다.

package require tcom

set wordapp [::tcom::ref createobject "Word.Application"]
#$wordapp Visible 1
if {[catch {
        set doc [[$wordapp Documents] Open [lindex $argv 0]]
        puts <Document>
        ::tcom::foreach para [$doc Paragraphs] {
                set namelocal [[$para Style] NameLocal]
                set content [[$para Range] Text]
                regsub {\r$} $content {} content
                regsub -all {\v} $content \n content
                puts "<Paragraph style=\"$namelocal\">$content</Paragraph>"
        }
        puts </Document>
        $wordapp Quit
}]} {
        $wordapp Quit
        puts stderr $errorInfo
}

Word 문서를 위의 스크립트에 넣어 실행하면 다음의 XML을 얻을수 있습니다.

<Document>
<Document>
<Paragraph style="제목 2">제목1</Paragraph>
<Paragraph style="표준">제목1의 내용입니다.</Paragraph>
<Paragraph style="제목 2">제목2</Paragraph>
<Paragraph style="표준">제목2의 내용입니다.</Paragraph>
<Paragraph style="제목 2">제목3</Paragraph>
<Paragraph style="표준">제목3의 내용입니다.</Paragraph>
</Document>

Word 문서에 구조 주기

지금까지의 성과는 나쁘지 않은것 같습니다만, XML문서로 출력했을시 약간의 모자란 점이 있습니다. 그것은 구조가 너무 평이하다는 점인데 위의 출력 결과를 보듯이 레벨별로 출력이 되지 않는것입니다.

나중의 처리를 생각한다면, 적절한 트리 구조로 표현되는것이 바람직할것입니다.

아래의 스크립트는 Word에 내장된 스타일의 구조를 가진 XML을 생성합니다.

package require tcom

# wdStyle constants
set const(Normal) [expr -1]
set const(Heading1) [expr -2]
set const(Heading2) [expr -3]
set const(Heading3) [expr -4]
set const(Heading4) [expr -5]
set const(Heading5) [expr -6]
set const(Heading6) [expr -7]
set const(Heading7) [expr -8]
set const(Heading8) [expr -9]
set const(Heading9) [expr -10]
set const(ListBullet) [expr -49]
set const(ListNumber) [expr -50]
set const(List2) [expr -51]
set const(List3) [expr -52]
set const(List4) [expr -53]
set const(List5) [expr -54]
set const(ListBullet2) [expr -55]
set const(ListBullet3) [expr -56]
set const(ListBullet4) [expr -57]
set const(ListBullet5) [expr -58]
set const(ListNumber2) [expr -59]
set const(ListNumber3) [expr -60]
set const(ListNumber4) [expr -61]
set const(ListNumber5) [expr -62]
set const(HtmlAddress) [expr -97]
set const(HtmlPre) [expr -102]

set sourcefile [lindex $argv 0]
set targetfile [lindex $argv 1]

set f [open $targetfile w]
fconfigure $f -encoding utf-8

set wordapp [::tcom::ref createobject "Word.Application"]
#$wordapp Visible 1
if {[catch {
        set doc [[$wordapp Documents] Open $sourcefile]
        foreach key [array names const] {
                set wd([[[$doc Styles] Item $const($key)] NameLocal]) $key
        }
        puts $f "<Document>"
        set lstack [list]; # Stack for list
        set sstack [list]; # Stack for section
        ::tcom::foreach para [$doc Paragraphs] {
                set namelocal [[$para Style] NameLocal]
                set content [[$para Range] Text]
                regsub -all {\r$} $content {} content
                regsub -all {\v} $content \n content
                variable level 0
                if {[info exists wd($namelocal)]} {
                        set style $wd($namelocal)
                } else {
                        set style Normal
                }
                if {[regexp {^(ListNumber)(\d?)} $style match listtype level] ||
                [regexp {^(ListBullet)(\d?)} $style match listtype level]} {
                        if {$level == ""} {set level 1}
                        if {[lrange $lstack end end] == $listtype} {
                                while {[llength $lstack] > $level} {
                                        puts $f "</[lrange $lstack end end]>"
                                        set lstack [lreplace $lstack end end]
                                }
                        } else {
                                while {[llength $lstack] >= $level} {
                                        puts $f "</[lrange $lstack end end]>"
                                        set lstack [lreplace $lstack end end]
                                }
                        }
                        while {[llength $lstack] < $level} {
                                puts $f "<$listtype>"
                                lappend lstack $listtype
                        }
                        if {$listtype == "ListNumber"} {
                                puts $f "<Item value=\"[[[$para Range] ListFormat] ListValue]\">$content</Item>"
                        } else {
                                puts $f "<Item>$content</Item>"
                        }
                } else {
                        while {[llength $lstack] > 0} {
                                puts $f "</[lrange $lstack end end]>"
                                set lstack [lreplace $lstack end end]
                        }
                        if {[regexp {^(Heading)(\d?)} $style match heading level]} {
                                if {$level == ""} {set level 1}
                                while {[llength $sstack] >= $level} {
                                        puts $f "</[lrange $sstack end end]>"
                                        set sstack [lreplace $sstack end end]
                                }
                                while {[llength $sstack] < $level} {
                                        puts $f "<Section>"
                                        lappend sstack Section
                                }
                                puts $f "<$heading>$content</$heading>"
                        } else {
                                puts $f "<$style>$content</$style>"
                        }
                }
        }
        while {[llength $lstack] > 0} {
                puts $f "</[lrange $lstack end end]>"
                set lstack [lreplace $lstack end end]
        }
        while {[llength $sstack] > 0} {
                puts $f "</[lrange $sstack end end]>"
                set sstack [lreplace $sstack end end]
        }
        puts $f "</Document>"
        $wordapp Quit
        close $f
}]} {
        $wordapp Quit
        puts stderr $errorInfo
}

위의 스크립트로 방금전과 같은 Word 문서를 변환한 결과는 다음과 같습니다.

<Document>
<Section>
<Section>
<Heading>제목1</Heading>
<Normal>제목1의 내용입니다.</Normal>
</Section>
<Section>
<Heading>제목2</Heading>
<Normal>제목2의 내용입니다.</Normal>
</Section>
<Section>
<Heading>제목3</Heading>
<Normal>제목3의 내용입니다.</Normal>
</Section>
</Section>
</Document>

여기서는 섹션이나 조목별로 나누어 쓴 글이 계층형 트리로 표현 되었습니다.