Communityコーディング&開発github.com

wintersdragon-c/office-docx-skills(Word 変更履歴付き .docx 編集)

.docx ファイルをプログラムで編集し、編集箇所を Microsoft Word の変更履歴(トラックチェンジ)としてマークアップするエージェントスキルです。

office-docx-skills(Word 変更履歴付き .docx 編集) とは?

このスキルは、`.docx` ファイルを直接 XML 操作(lxml を使用)して編集し、すべての変更を Word の変更履歴(削除は赤色取り消し線、挿入は赤色下線、余白に作成者・日付のメタデータ)として記録します。`python-docx` の高レベル API では変更履歴がサポートされていないため、このスキルは OOXML の `w:ins` / `w:del` 要素を直接操作します。依存関係は `python-docx`(初期調査用)と `lxml`(実際の XML 編集用)で、いずれも純 Python インストール可能です。主なユースケースは、学術論文の改訂、契約書のレビュー、共同編集文書の変更追跡などで、ユーザーが Word 上で各編集を承諾/拒否できるようにする場合です。注意点として、すべての編集は単一のスクリプト内で行うか、前回の出力ファイルを読み込む必要があり、元ファイルから再読み込みすると既存の変更履歴が失われます。また、引用文献の挿入時にはセクション境界を正しく特定し、学術論文の書き換え時にはすべての引用を保持する必要があります。このスキルは、`word-default-formatting` や `word-formula-writing` といった他のスキルと組み合わせて使用することもできます。

対応✓Claude Code✓Codex CLI~Cursor
npx skills add wintersdragon-c/office-docx-skills

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

.docx を Word 変更履歴(修正モード)で編集する

概要

.docx ファイルをプログラムで編集し、変更箇所が Microsoft Word で変更履歴として表示されるようにします。削除は赤色の取り消し線、挿入は赤色の下線、そして余白に作成者/日付のメタデータが表示されます。これは、lxml を用いた OOXML の w:ins / w:del 要素の直接 XML 操作によって実現されます。python-docx の高レベル API は変更履歴をサポートしていないため、これを使用しません。

依存関係

pip install python-docx lxml
  • python-docx >= 1.2.0 — 初期調査(段落スタイル、実行書式)にのみ使用。実際の変更履歴書き込みには使用しません。
  • lxml >= 5.0 — 実際の処理を担うライブラリ。.docx ZIP 内の word/document.xml を解析・修正します。

どちらも純 Python でインストール可能で、システムレベルの依存関係はありません。

使用するタイミング

  • ユーザーが .docx を編集し、Word で変更箇所を確認したい場合(修正モード / トラックチェンジ)
  • レビュアーが個々の編集を承諾/拒否できる改訂原稿を送る必要がある場合
  • 直接テキスト置換では不十分で、変更履歴が重要な場合

使用すべきでないタイミング

  • ユーザーが変更履歴を付けずに最終テキストを置換したいだけの場合 — python-docx を直接使用
  • ファイルが .doc(旧バイナリ形式)の場合 — 非対応
  • ヘッダー/フッター/脚注を変更履歴付きで編集したい場合 — この手法は本文段落のみ対応

連携

このスキルは単独で使用することも、office-docx-skills 内の他のスキルと組み合わせて使用することもできます。

  • word-default-formatting と併用:改訂テキストにもパッケージの既定の Word 書式プロファイルを適用する場合。
  • word-formula-writing と併用:数式関連の編集を Word ネイティブ編集可能な数式と表示可能な変更履歴で表現する場合。
  • 書式と数式の両方が重要な場合は、3 つすべてのスキルを同時に使用します。

OOXML 変更履歴の構造

.docx は ZIP アーカイブで、word/document.xml を含みます。変更履歴は、w:p(段落)内の w:r(ラン)要素をラップする 2 つの要素を使用します。

<!-- 削除:元のランをラップし、テキストタグは w:delText になる -->
<w:del w:id="102" w:author="Claude" w:date="2026-04-17T10:00:00Z">
  <w:r>
    <w:rPr><!-- 元の書式を保持 --></w:rPr>
    <w:delText xml:space="preserve">古いテキスト</w:delText>
  </w:r>
</w:del>

<!-- 挿入:新しいランをクローンした書式でラップ -->
<w:ins w:id="103" w:author="Claude" w:date="2026-04-17T10:00:00Z">
  <w:r>
    <w:rPr><!-- 元のランからクローン --></w:rPr>
    <w:t xml:space="preserve">新しいテキスト</w:t>
  </w:r>
</w:ins>

重要なルール:

  • w:del と w:ins は w:p の直接子要素で、w:pPr と兄弟関係
  • それぞれ一意の w:id(整数)が必要で、ドキュメント内の既存 ID と衝突してはいけません
  • w:author と w:date は Word の余白注釈に表示されます
  • w:del 内ではテキスト要素は必ず w:delText(w:t ではない)にしなければなりません
  • w:ins 内ではテキスト要素は通常の w:t です
  • 実行プロパティ(w:rPr)は、フォント/サイズ/太字を保持するために元のランからクローンする必要があります

コア実装

再利用可能な実装は、このスキルと同階層の tracked_change_editor.py にあります。直接インポートして使用したい場合は、このヘルパーファイルを作業ディレクトリにコピーしてください。

from tracked_change_editor import TrackedChangeEditor

editor = TrackedChangeEditor('input.docx', author='あなたの名前')

for i, p in enumerate(editor.body_paras):
    if '対象テキスト' in editor._get_para_text(p):
        editor.replace_paragraph_text(i, '置換テキスト')
        break

editor.save('output.docx')

tracked_change_editor.py は以下の重要なメカニズムを処理します:

  • 新しい w:ins / w:del ノードを作成する前に、既存のリビジョン ID をスキャン
  • 段落の既定 w:rPr をクローンして段落書式を保持
  • 安全な一時ファイルに書き込んでから最終 .docx に移動

使用フロー

editor = TrackedChangeEditor('input.docx', author='あなたの名前')

# テキスト内容で目的の段落を見つける
for i, p in enumerate(editor.body_paras):
    if '対象テキスト' in editor._get_para_text(p):
        editor.replace_paragraph_text(i, '置換テキスト')
        break

editor.save('output.docx')  # 必ず新しいファイルに保存

重要な注意点 — 編集前に必ずお読みください

0. すべての編集を 1 つのスクリプトで行うか、最後に保存したファイルから連鎖させる

7 つの編集を行う必要がある場合、すべてを単一の TrackedChangeEditor インスタンスに対する単一のスクリプト実行で行い、最後に 1 回保存してください。以下のようにしないでください:

  • スクリプト A を実行 → _revised.docx に保存
  • スクリプト B を元のファイルに対して実行 → _revised.docx に保存(スクリプト A の作業を上書き)

どうしても複数のスクリプト実行に分割する必要がある場合、後続の各実行は前回の出力ファイルを読み込む必要があります:

# ラウンド 1
editor = TrackedChangeEditor('original.docx')
# ... 編集 ...
editor.save('revised.docx')

# ラウンド 2 — revised.docx を読み込む(original ではない)
editor2 = TrackedChangeEditor('revised.docx')
# ... さらに編集 ...
editor2.save('revised.docx')

なぜ重要か: 元のファイルから読み込むと、以前のラウンドのすべての変更履歴が破棄されます。これは最もよくある間違いで、エラーも出ずに作業が静かに消えてしまいます。

0b. 新しい段落の挿入:テキストだけでなくセクションを見つける

新しい段落(例:参考文献エントリ)を挿入するとき、ドキュメント全体からテキスト一致を検索してはいけません。本文中の引用には参考文献エントリと同じ著者名が含まれます。if 'Acharya' in text のような単純な検索は、本文段落に最初に一致し、参考文献をセクション 2 の途中に挿入してしまいます。

正しい方法 — まずセクション境界を見つける:

# 参考文献セクションの開始位置を見つける
refs_start_idx = None
for i, p in enumerate(editor.body_paras):
    t = editor._get_para_text(p)
    if t.strip() == 'References':
        refs_start_idx = i
        break

# 参考文献セクション内のみを検索
for i in range(refs_start_idx + 1, len(editor.body_paras)):
    t = editor._get_para_text(editor.body_paras[i])
    if t.startswith('Acharya') and 'Johnson' in t:
        # この段落の後に挿入
        break

なぜ重要か: 著者名は文中引用と参考文献エントリの両方に現れます。セクションを考慮した検索をしないと、新しい参考文献段落が本文に挿入されてしまい、ユーザーが Word で開くまで気づきにくい破損が発生します。

0c. 学術論文の段落書き換え:すべての引用を保持する

段落のテキストを置換するときは、元の段落にあるすべての (著者 年) 形式の引用を保持しなければなりません。学術論文は本文中の引用と参考文献リストが一致している必要があります。引用を削除すると、参考文献エントリが孤立したり、論旨が弱まったりします。

正しい方法 — 書き換え前に抽出:

import re
old_text = editor._get_para_text(editor.body_paras[idx])
old_cites = re.findall(r'\([^)]*\d{4}[a-z]?\)', old_text)
# old_cites のすべてのアイテムが new_text に含まれるように新しいテキストを書く
# 書き換え後、確認:
for c in old_cites:
    assert c in new_text, f'引用が失われました: {c}'

なぜ重要か: (Malmendier and Nagel 2011; Choi, Gao, and Jiang 2020) を削除して書き換えた導入部は、論文の引用の整合性を静かに壊します。参考文献リストにはこれらのエントリが残っていますが、本文中にそれらを指すものは何もありません。査読者や編集者はこれを指摘します。

1. 既存の変更履歴を含む段落

段落に以前の編集による w:ins/w:del がすでに含まれている場合、_collect_direct_runs は直接の子 w:r のみを取得します。既存の w:ins/w:del ラッパー内のランはスキップされます。これにより変更履歴のネストを防げますが、置換は「承諾済み」のテキスト部分のみをカバーします。過去の変更が多く含まれる段落の場合は、Word ですべての変更を承諾してから再編集してください。

2. 常に新しいファイルに保存する

元のファイルを上書きしないでください。_revised.docx として保存し、ユーザーに Word で確認させてから手動で置き換えてもらいます。

3. 目次は自動更新されない

python-docx / lxml は Word の目次を更新できません。編集後、ユーザーは Word で開き、目次を右クリック → フィールド更新を行う必要があります。

4. リビジョン ID の一意性

_find_max_id() メソッドは、既存のすべての w:ins、w:del、w:rPrChange、w:pPrChange、w:sectPrChange 要素をスキャンします。新しい ID は最大値からインクリメントされます。ID が衝突すると、Word はリビジョン履歴を破損する可能性があります。

5. 書式の保持

_get_default_rpr は段落内の最初のランから w:rPr をクローンします。これは段落全体が均一な書式の場合に機能します。段落内で書式が混在している場合(例:太字の「キーワード:」とそれに続く通常テキスト)、段落全体を置換するのではなく、各ランごとに外科的に置換してください。

6. 往復の忠実性

lxml による保存では、XML の見た目に軽微な違いが生じることがあります(引用符スタイル " と '、改行コード \r\n と \n など)。内容と書式は保持されます。ZIP 圧縮レベルの違いによりファイルサイズが変わる場合がありますが、問題ありません。

検証チェックリスト

保存後、Word で開いて以下を確認してください:

  • 削除テキストが赤色の取り消し線で表示される
  • 挿入テキストが赤色の下線で表示される
  • 作成者名が余白の注釈に表示される
  • 周辺の段落は元の書式を保持している
  • 重複またはネストしたリビジョンマークがない
  • 選択した作成者の挿入数を期待値と一致させる — 検証スクリプトを実行して、作成者名を持つ w:ins をカウントし、それぞれの最初の 100 文字を表示します。カウントが期待値より少ない場合、以前の編集が失われています(注意点 0 を参照)。
  • 本文中に参考文献エントリがない — References 見出しより前の段落をスキャンし、完全な書誌エントリ(著者、年、ジャーナル、巻、ページ)のように見える段落がないか確認します。見つかった場合、参考文献が誤った場所に挿入されています(注意点 0b を参照)。
# 保存後検証スニペット
target_author = 'Codex'
with zipfile.ZipFile('output.docx') as z:
    xml = z.read('word/document.xml')
root = etree.fromstring(xml)
nsmap = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
author_ins = [e for e in root.findall('.//w:ins', nsmap)
              if e.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}author') == target_author]
print(f'{target_author} の挿入数: {len(author_ins)}')
for i, ins in enumerate(author_ins):
    texts = ''.join(t.text for t in ins.findall('.//w:t', nsmap) if t.text)
    print(f'  [{i+1}] {texts[:100]}')

関連スキル