0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.docxから番号付き見出しを抽出する

Posted at

Microsoft Wordの.docxファイルから「番号付きの見出し」を抽出したい、というニーズは多くの現場で存在します。
.docxファイルの構造はOpen XML仕様として公開されており、基本的には.docxを.zipファイルとして解凍し、word/document.xmlファイルを参照することで、本文の情報を得ることができます。
しかし、単純にdocument.xmlだけを解析しても、Word上で表示される見出し番号や階層構造を正確に再現することはできません。なぜなら、見出しの番号付与ルールやスタイル情報は、document.xml以外の複数のXMLファイルに分散して定義されているからです。

見出し番号の構造

Wordの見出し番号は以下の要素の組み合わせで決まります:

  • 段落ごとの <w:numPr>: 段落自身に番号付けが記録されている場合はこれを最優先します
  • スタイルに紐づく <w:numPr>: 段落に <w:numPr> が無い場合、適用されているスタイルにデフォルトの番号付与ルールがあればそれを利用します
  • numbering.xml: 番号付与ルール本体が定義されます。どのレベルでどんな形式(decimal, upperLetter, lowerRomanなど)の番号が使われるか、また %1.%2. のように親レベルを含むかどうかもここで指定されます
  • styles.xml: 各段落スタイル(Heading1, Heading2など)がどの番号付与ルールに紐付いているか、またアウトラインレベル(<w:outlineLvl>)が何かが記述されています

特に、見出し判定にはスタイル名が「Heading1」「Heading2」だからといって盲信するのではなく、<w:outlineLvl> の値を参照するのが確実です。例えば Heading1 スタイルは通常 <w:outlineLvl w:val="0"/> を持ち、Heading2 なら <w:outlineLvl w:val="1"/> となります。

XMLサンプル

例えば、Wordのdocument.xmlには以下のような段落(見出し)が記述されています。

<w:p>
  <w:pPr>
    <w:pStyle w:val="Heading1" />
    <w:numPr>
      <w:ilvl w:val="0" />
      <w:numId w:val="9" />
    </w:numPr>
  </w:pPr>
  <w:r>
    <w:t>見出し1だよー</w:t>
  </w:r>
</w:p>

numbering.xmlには番号付与ルールが記述されています。例えば:

<w:numbering>
  <w:abstractNum w:abstractNumId="9">
    <w:lvl w:ilvl="0">
      <w:numFmt w:val="decimal" />
      <w:lvlText w:val="%1." />
    </w:lvl>
    <w:lvl w:ilvl="1">
      <w:numFmt w:val="decimal" />
      <w:lvlText w:val="%1.%2." />
    </w:lvl>
  </w:abstractNum>
  <w:num w:numId="9">
    <w:abstractNumId w:val="9" />
  </w:num>
</w:numbering>

styles.xmlにはスタイル定義が記述されています。例えば:

<w:styles>
  <w:style w:type="paragraph" w:styleId="Heading1">
    <w:pPr>
      <w:outlineLvl w:val="0" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="9" />
      </w:numPr>
    </w:pPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading2">
    <w:pPr>
      <w:outlineLvl w:val="1" />
      <w:numPr>
        <w:ilvl w:val="1" />
        <w:numId w:val="9" />
      </w:numPr>
    </w:pPr>
  </w:style>
</w:styles>

Pythonサンプルコード

以下は、styles.xmlnumbering.xmlを横断的に参照しながら見出しの番号パターンを抽出する例です。実際にWord上と同じ番号列を再現するには、ここで取得した<w:lvlText>を元にカウンタ管理とプレースホルダ展開処理が必要になります。

import zipfile
from lxml import etree
import io

with zipfile.ZipFile("sample.docx", "r") as docx:
    doc_xml = docx.read("word/document.xml")
    styles_xml = docx.read("word/styles.xml")
    numbering_xml = docx.read("word/numbering.xml")

doc_tree = etree.parse(io.BytesIO(doc_xml))
style_tree = etree.parse(io.BytesIO(styles_xml))
num_tree = etree.parse(io.BytesIO(numbering_xml))

# スタイルIDからデフォルトのnumId/ilvlとアウトラインレベルを取得
style_numpr_map = {}
for style in style_tree.xpath('//w:style', namespaces={'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}):
    style_id = style.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}styleId')
    pPr = style.find('w:pPr', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
    if pPr is not None:
        outline_lvl = pPr.find('w:outlineLvl', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
        numPr = pPr.find('w:numPr', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
        if outline_lvl is not None:
            lvl_val = outline_lvl.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')
        else:
            lvl_val = None
        if numPr is not None:
            ilvl_elem = numPr.find('w:ilvl', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
            numId_elem = numPr.find('w:numId', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
            if numId_elem is not None:
                numId = numId_elem.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')
                ilvl = ilvl_elem.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val') if ilvl_elem is not None else None
                style_numpr_map[style_id] = {'numId': numId, 'ilvl': ilvl, 'outlineLvl': lvl_val}

# 段落ごとに見出し・番号を抽出
for p in doc_tree.xpath('//w:p', namespaces={'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}):
    pPr = p.find('w:pPr', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
    pStyle = None
    if pPr is not None:
        pStyle_elem = pPr.find('w:pStyle', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
        if pStyle_elem is not None:
            pStyle = pStyle_elem.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')
    if pStyle and pStyle in style_numpr_map:
        # スタイルからnumId/ilvl/outlineLvlを取得
        numId = style_numpr_map[pStyle]['numId']
        ilvl = style_numpr_map[pStyle]['ilvl']
        outlineLvl = style_numpr_map[pStyle]['outlineLvl']
        # numbering.xmlから番号パターンを取得
        number = None
        if numId and ilvl:
            num_elem = num_tree.find(f".//w:num[@w:numId='{numId}']", {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
            if num_elem is not None:
                absnum_elem = num_elem.find('w:abstractNumId', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
                if absnum_elem is not None:
                    absnumid = absnum_elem.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')
                    lvl_elem = num_tree.find(f".//w:abstractNum[@w:abstractNumId='{absnumid}']/w:lvl[@w:ilvl='{ilvl}']", {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
                    if lvl_elem is not None:
                        lvlText_elem = lvl_elem.find('w:lvlText', {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'})
                        if lvlText_elem is not None:
                            number = lvlText_elem.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')
        text = ''.join([t.text for t in p.xpath('.//w:t', namespaces={'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}) if t.text])
        print(f"見出し: {text} (スタイル: {pStyle}, アウトラインレベル: {outlineLvl}, 番号パターン: {number})")

まとめ

  • 番号付き見出しは document.xml, styles.xml, numbering.xml の三者を突き合わせる必要がある
  • 段落に <w:numPr> があればそれを優先し、無ければスタイルに定義された <w:numPr> を利用する
  • 見出しかどうかはスタイル名ではなく <w:outlineLvl> を見るのが正しい
  • 実際の番号列再現には %1 %2 プレースホルダを展開するカウンタロジックが必須

これらを踏まえれば、PythonからでもWord上と同じ階層付き番号を再現できます。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?