全体の流れ

この手順は、次の 11 段を順に進みます。各段で「何を決めるか」と「決めた結果」を、1 つの例に当てはめて示します。例は、Word の文書を社内の表記ルールの語リストと照合し、直す候補の一覧を作る作業です。

  1. 着想
  2. 課題の定義
  3. 分解
  4. データの形
  5. アルゴリズム
  6. Copilot に書かせる指示
  7. 実行環境の選択
  8. 実行
  9. テストと検証
  10. 運用と自動化
  11. 保守

例の文書と語リストはすべて架空です。コードは Python 3.12.10 と python-docx 1.2.0 で手元で実行し、結果をそのまま載せています(2026-09-12)。

1. 着想 ── 繰り返しの作業を見つける

着想は、毎週くり返している手作業の中から、規則で判定できる部分を見つけることから始まります。例では、提案書や Web 記事の原稿を、表記ルールの語リスト(使わない語・言い換える語・表記ゆれ)と目で照合しています。件数が多いと見落としが出て、時間もかかります。

自動化に向くかは、次の 4 点で見ます。

観点問い例での答え
頻度同じ作業をくり返すか毎週、複数の文書で行う
規則判定の規則を書き出せるか語リストに載った語が文に含まれるか
入力入力が電子データで手に入るかWord(.docx)のファイル
確かめ方結果を人が確かめられるか候補の一覧を見て、直すかどうかを人が決める

例では 4 点とも当てはまるので、「照合は Python に任せ、人は判断だけする」形にします。その語が文脈の上で本当に不適切かどうかの判断は、機械に任せません。

1 本の文書を読んで直す作業は、Word の Copilot でもできます(実践編の Word のページ)。ここでは、同じ語リストで多くの文書を毎回同じ基準で見て、結果を表として残すために Python を使います。

2. 課題の定義 ── 入力・出力・合格の条件を書く

作り始める前に、何を入れて何を出すか、どうなれば合格か、何をしないかを 1 枚に書きます。ここがあいまいなまま Copilot に頼むと、読み直しと手直しが増えます。

項目決めたこと
入力フォルダーの中の .docx ファイル。語リスト style_terms.csv(列: term, category, action)
出力CSV ファイル(列: file, where, term, action, sentence)と、ファイルごとの件数の表示
合格の条件語リストの語を含む文を、段落と表のセルの両方から漏れなく出す。全角と半角の違い(例: No.1 と No.1)でも見つける
許すこと直す必要のない箇所が候補に混じること(人が一覧で外す)
範囲外語の意味や文脈の判断、文書の自動修正、ヘッダー・フッター・テキストボックスの中の文字(第 2 版で扱う)

「漏れなく出す」を合格の条件にし、「余分が混じる」を許すことにしたのは、見落としの方が後で困るからです。どちらを優先するかは、課題ごとに先に決めます。

3. 分解 ── 小さな処理に分けて名前を付ける

課題を、1 つずつ確かめられる小さな処理に分け、それぞれに関数の名前を付けます。名前を付けると、Copilot への指示と試験の単位がそろいます。

処理関数名入力 → 出力
1語リストを読むload_termsCSV のパス → Term の一覧
2文書から文字を順に取り出すiter_blocks文書 → (場所, 文字)の並び
3文に分けるsplit_sentences文字 → 文の一覧
4表記の違いをそろえるnormalize文字 → そろえた文字
5語を探すfind_hits文と語の一覧 → Hit の一覧
6フォルダーをまとめて調べるcheck_folderフォルダー → Hit の一覧
7結果を書き出すwrite_csvHit の一覧 → CSV

分け方の目安は、「その処理だけを、少ない入力で確かめられるか」です。4 の normalize と 5 の find_hits は文書を開かずに試験できるので、9 章の「テストと検証」で中心になります。

4. データの形 ── 受け渡す値の形を先に決める

処理の間で受け渡す値の形を決めます。例では、語リストの 1 行を Term、見つかった 1 件を Hit という形にしました。Python の dataclass を使うと、項目の名前と数が固定され、書き間違いに気づきやすくなります。Python の文書によると、frozen を真にすると、作った後の項目への代入は例外になります。

項目
Termterm, category, action, key(照合用にそろえた term)No.1 / 順位の表現 / 調査の出典・時点・範囲を併記する / no.1
Hitfile, where, term, action, sentencedoc_a.docx / paragraph 6 / No.1 / … / 利用者満足度 No.1(参考 1)。

文書から取り出す文字は、「場所」と組にします(paragraph 5、table 1 row 2 col 2 など)。一覧を見た人が、元の文書のどこを直せばよいかをすぐに探せるようにするためです。

python-docx の文書によると、Document.paragraphs は文書の段落を文書の順に返します。Document.tables は文書の最上位にある表だけを返し、表のセルの中に入れ子になった表は含みません。例ではこの 2 つを使うので、入れ子の表は範囲外になります(入れ子の表の読み方は Word × Python のページ)。

5. アルゴリズム ── 処理の順番と境目の扱いを決める

  1. 語リストを読み、各語を照合用にそろえた形(key)に変える
  2. フォルダーの .docx を名前の順に開く。Word が文書を開いている間に作る「~$」で始まるファイルは飛ばす
  3. 段落を順に、続けて表のセルを順に取り出す
  4. 文末の記号(。!?!?)で文に分ける
  5. 文をそろえた形に変え、各語の key が含まれるかを見る
  6. 見つかったものを Hit として並べ、CSV に書く

表記の違いをそろえるには、Unicode の正規化 NFKC を使います。Python の unicodedata の文書では、NFKC は互換等価に基づく正規形の 1 つとして説明されています。全角の英数字「No.1」は NFKC で「No.1」になります(9 章の試験で確かめています)。英字の大文字と小文字の違いは casefold でそろえます。

文に分ける正規表現 (?<=[。!?!?]) は、文末の記号の直後で区切る「後読み」です。Python の re の文書では、後読みの中の型は固定の長さの文字列にしか一致してはならないとされています。1 文字の記号の集合なので、この条件を満たします。

境目の扱い: 「No.10」は「No.1」を含むので候補に出ます。これは 2 章で許すことにした「余分」に当たるので、そのまま出し、人が一覧で外します。語の前後の文字を条件にすると余分は減りますが、見落としを生む変更になりうるので、合格の条件と照らしてから入れます。語が大幅に増えたときは、処理の時間を測ってから方法を見直します。

6. Copilot に書かせる指示 ── 決めたことをそのまま渡す

ここまでに決めた入力・出力・合格の条件・関数の分け方を、そのまま指示に書きます。Microsoft 365 の Copilot のチャットでも、GitHub Copilot でも、書き方は同じです。

Copilot に書かせる指示の例

Python 3.12 と python-docx で、フォルダーの中の .docx を社内の表記ルールと照合するスクリプトを書いてください。 入力: フォルダーのパスと、語リストの CSV(UTF-8 BOM つき、列は term, category, action)。 出力: CSV(UTF-8 BOM つき、列は file, where, term, action, sentence)と、ファイルごとの件数の表示。 処理: 段落と表のセルから文字を取り出し、文末の記号(。!?!?)で文に分け、NFKC と casefold でそろえてから、語を含む文を探す。~$ で始まるファイルは飛ばす。 関数: load_terms, iter_blocks, split_sentences, normalize, find_hits, check_folder, write_csv に分け、値の受け渡しには dataclass(Term, Hit)を使う。 制約: 元の文書は読むだけで書き換えない。外部と通信しない。標準ライブラリと python-docx 以外を使わない。 あわせて、normalize と find_hits の unittest を書いてください(全角の No.1 が No.1 に一致すること、書き換えた文では見つからないこと)。

受け取ったコードは、次の順に確かめます。① 使っている関数と引数が公式の文書にあるか(python-docx の API の文書と照らす)。② 入力のファイルを書き換える処理が無いか。③ 少ない見本で動かし、結果を手で数えた数と比べる(9 章)。この確認を終えるまで、業務の文書には使いません。指示の型は 指示からコードへ のページで詳しく扱います。

7. 実行環境の選択 ── どこで動かすかを決める

候補向く場面この例で使えるか
手元の PC の仮想環境(venv)文書が手元か同期したフォルダーにある。まず 1 人で試す使える(今回はこれ)
Python in ExcelExcel のブックの中のデータを分析する使えない。コードは Microsoft のクラウドの中で動き、PC のファイルにもネットワークにも触れない
Azure Functions決まった時刻に動かす、チームで共有する使える(10 章)。文書は Microsoft Graph で SharePoint から取る

Python in Excel について、Microsoft の文書は、Python のコードが利用者のコンピューター・デバイス・アカウントにアクセスできないこと、ネットワークにもアクセスできないことを明記しています。Word のファイルを読むこの例には向きません(比べ方は 実行環境の選び方)。

手元では、プロジェクトごとに仮想環境を作り、そこにライブラリを入れます。Python の文書によると、仮想環境は venv モジュールを実行して作り、PowerShell では Scripts\Activate.ps1 で有効にします。実行ポリシーの設定が要る場合があることも書かれています。入れたライブラリの版は pip freeze で requirements.txt に書き出します。

PowerShell で、プロジェクトのフォルダーから実行する

PowerShell
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install python-docx==1.2.0
python -m pip freeze > requirements.txt

8. 実行 ── 完成したプログラムと結果

完成したプログラムです。見本の 3 本の文書(doc_a.docx、doc_b.docx、e2e_doc_c.docx)と語リストで実行した結果を、下の[実行結果]に載せています。

check_terms.py

Python手元で動く
"""Check .docx files in a folder against a style list and write the hits to a CSV file.

usage: python check_terms.py DOCS_FOLDER TERMS_CSV OUT_CSV
"""
import csv
import re
import sys
import unicodedata
from collections import Counter
from dataclasses import dataclass
from pathlib import Path

from docx import Document

SENTENCE_END = re.compile(r"(?<=[。!?!?])")


@dataclass(frozen=True)
class Term:
    term: str
    category: str
    action: str
    key: str


@dataclass(frozen=True)
class Hit:
    file: str
    where: str
    term: str
    action: str
    sentence: str


def normalize(text):
    # NFKC turns full-width letters and digits into their usual forms; casefold ignores case
    return unicodedata.normalize("NFKC", text).casefold()


def load_terms(path):
    with open(path, encoding="utf-8-sig", newline="") as f:
        return [Term(r["term"], r["category"], r["action"], normalize(r["term"]))
                for r in csv.DictReader(f) if r["term"].strip()]


def iter_blocks(doc):
    for i, p in enumerate(doc.paragraphs, 1):
        yield "paragraph %d" % i, p.text
    for ti, table in enumerate(doc.tables, 1):
        for ri, row in enumerate(table.rows, 1):
            for ci, cell in enumerate(row.cells, 1):
                yield "table %d row %d col %d" % (ti, ri, ci), cell.text


def split_sentences(text):
    return [s.strip() for s in SENTENCE_END.split(text) if s.strip()]


def find_hits(name, where, sentence, terms):
    key = normalize(sentence)
    return [Hit(name, where, t.term, t.action, sentence) for t in terms if t.key in key]


def check_folder(folder, terms):
    hits = []
    for path in sorted(Path(folder).glob("*.docx")):
        if path.name.startswith("~$"):
            continue  # lock files that Word leaves while a document is open
        for where, text in iter_blocks(Document(path)):
            for sentence in split_sentences(text):
                hits += find_hits(path.name, where, sentence, terms)
    return hits


def write_csv(hits, out):
    with open(out, "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["file", "where", "term", "action", "sentence"])
        w.writerows([h.file, h.where, h.term, h.action, h.sentence] for h in hits)


def main(folder, terms_csv, out_csv):
    hits = check_folder(folder, load_terms(terms_csv))
    write_csv(hits, out_csv)
    print("hits:", len(hits))
    for name, n in sorted(Counter(h.file for h in hits).items()):
        print(" ", name, n)
    for h in hits[:8]:
        print("  %s | %s | %s -> %s" % (h.file, h.where, h.term, h.action))


if __name__ == "__main__":
    main(*(sys.argv[1:] or ["samples", "samples/style_terms.csv", "out_hits.csv"]))
実行結果(2026-09-12)
hits: 11
  doc_a.docx 7
  e2e_doc_c.docx 4
  doc_a.docx | paragraph 5 | 最高 -> 根拠を確認する
  doc_a.docx | paragraph 5 | 業界初 -> 根拠を確認する
  doc_a.docx | paragraph 5 | 絶対 -> 言い換える
  doc_a.docx | paragraph 6 | No.1 -> 調査の出典・時点・範囲を併記する
  doc_a.docx | paragraph 7 | 下さい -> 「ください」に統一
  doc_a.docx | paragraph 7 | 問合せ -> 「問い合わせ」に統一
  doc_a.docx | table 1 row 2 col 2 | 完全無料 -> 条件を併記する
  e2e_doc_c.docx | paragraph 2 | No.1 -> 調査の出典・時点・範囲を併記する

結果の読み方: 候補は全部で 11 件です。doc_a.docx の 7 件は、手で数えた数と一致します(9 章)。doc_b.docx は候補が 0 件なので、ファイルごとの件数に出ていません。e2e_doc_c.docx の 4 件には、全角の「No.1」、許すことにした「No.10」、表のセルの中の「唯一」、表記ゆれの「出来る」が含まれます。

9. テストと検証 ── 動いたことと正しいことを分けて確かめる

エラーなく動いたことと、正しいこと(出すべきものを出し、出すべきでないものは出さない)は、別に確かめます。確かめ方は 3 つです。

  1. 単体の試験: normalize・split_sentences・find_hits を、文書を開かずに小さな入力で試す
  2. 答えの分かっている見本での試験: 手で数えた件数とプログラムの件数を比べる。例では doc_a.docx を手で数えて 7 件だった
  3. 境目の試験: 全角の文字、表のセルの中の語、余分として出る「No.10」を、わざと入れた見本(e2e_doc_c.docx)で試す

Python の unittest では、unittest.TestCase を継承したクラスの中に、名前が test で始まるメソッドとして試験を書きます(Python の文書による)。

test_check_terms.py(check_terms.py と同じフォルダーに置く)

Python手元で動く
"""Tests for check_terms.py (run: python test_check_terms.py)."""
import sys
import unittest

from check_terms import Term, check_folder, find_hits, load_terms, normalize, split_sentences

TERMS = [Term("No.1", "順位の表現", "出典を併記する", normalize("No.1")),
         Term("下さい", "表記ゆれ", "「ください」に統一", normalize("下さい"))]


class CheckTermsTest(unittest.TestCase):
    def test_full_width_matches(self):
        hits = find_hits("a.docx", "paragraph 1", "満足度No.1です。", TERMS)
        self.assertEqual([h.term for h in hits], ["No.1"])

    def test_rewritten_sentence_has_no_hit(self):
        self.assertEqual(find_hits("a.docx", "paragraph 1", "ご連絡ください。", TERMS), [])

    def test_sentences_are_split_after_the_mark(self):
        self.assertEqual(split_sentences("一文目。二文目!三文目"), ["一文目。", "二文目!", "三文目"])

    def test_known_false_positive_stays_visible(self):
        # "No.10" contains "No.1". It is reported on purpose; a person removes it from the list.
        self.assertEqual(len(find_hits("a.docx", "paragraph 1", "No.10 を参照。", TERMS)), 1)

    def test_known_answer_on_sample_document(self):
        hits = check_folder("samples", load_terms("samples/style_terms.csv"))
        self.assertEqual(sum(1 for h in hits if h.file == "doc_a.docx"), 7)

    def test_table_cell_is_checked(self):
        hits = check_folder("samples", load_terms("samples/style_terms.csv"))
        self.assertTrue(any(h.file == "e2e_doc_c.docx" and h.where.startswith("table") and h.term == "唯一"
                            for h in hits))


if __name__ == "__main__":
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(CheckTermsTest)
    result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
    sys.exit(0 if result.wasSuccessful() else 1)
実行結果(2026-09-12)
test_full_width_matches (__main__.CheckTermsTest.test_full_width_matches) ... ok
test_known_answer_on_sample_document (__main__.CheckTermsTest.test_known_answer_on_sample_document) ... ok
test_known_false_positive_stays_visible (__main__.CheckTermsTest.test_known_false_positive_stays_visible) ... ok
test_rewritten_sentence_has_no_hit (__main__.CheckTermsTest.test_rewritten_sentence_has_no_hit) ... ok
test_sentences_are_split_after_the_mark (__main__.CheckTermsTest.test_sentences_are_split_after_the_mark) ... ok
test_table_cell_is_checked (__main__.CheckTermsTest.test_table_cell_is_checked) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.114s

OK

試験は、語リストやプログラムを変えたとき、ライブラリの版を上げたときに、毎回流します。

残る限界: ヘッダー・フッター・テキストボックスの文字は読んでいません(範囲外)。python-docx の文書によると、変更履歴(変更の追跡)の挿入・削除の中にある段落は Document.paragraphs に現れません。点検は、変更を反映した後の文書で行います。

10. 運用と自動化 ── 決まった時刻に動かし、記録を残す

手元で試して結果が安定したら、決まった時刻に動かします。手元の Windows で動かす場合は、タスク スケジューラに登録します。schtasks create の文書によると、週単位の予定には /sc weekly が必要で、曜日は /d で指定できます。開始時刻 /st は 24 時間制の HH:mm で書き、/tr には実行するプログラムやコマンドを書きます。

毎週月曜 8:00 に run.cmd(仮想環境の python で check_terms.py を呼ぶ)を動かす

PowerShell
schtasks /create /sc weekly /d MON /st 08:00 /tn "TermCheck" /tr "C:\tools\term-check\run.cmd"

チームで共有する場合や、文書が SharePoint にある場合は、Azure Functions のタイマーで動かし、文書は Microsoft Graph で取得します。Azure Functions の文書によると、スケジュールは NCRONTAB 式({second} {minute} {hour} {day} {month} {day-of-week})で書き、既定の時間帯は協定世界時(UTC)です。文書の例では「0 30 9 * * 1-5」が平日の 9:30 に当たります。

function_app.py(タイマーで起動する骨組み)

Pythonサービスとして動かす
"""Run the term check at 9:30 every weekday (Azure Functions, Python v2 model, timer trigger)."""
import logging
import os

import azure.functions as func

app = func.FunctionApp()


# {second} {minute} {hour} {day} {month} {day-of-week}. The default time zone is UTC;
# the Azure Functions docs describe the WEBSITE_TIME_ZONE app setting for other time zones.
@app.timer_trigger(schedule="0 30 9 * * 1-5", arg_name="timer", run_on_startup=False)
def weekday_term_check(timer: func.TimerRequest) -> None:
    if timer.past_due:
        logging.warning("The timer is past due")
    folder = os.environ.get("TERM_CHECK_FOLDER", "")
    logging.info("term check started (folder setting: %s)", folder or "not set")
    # Fetch the changed documents with Microsoft Graph and call check_folder() here
    # (see "Check only documents that changed" in the workflow recipes).

実行のたびに、いつ、何本の文書を、何件の候補で終えたかを記録に残します。Python の logging の文書にある logging.basicConfig(filename=..., level=logging.INFO) の形で、ファイルに書けます。記録には文書の本文や個人情報を書かず、ファイル名と件数だけにします。結果の知らせ方は Teams × Python のページを参照してください。

11. 保守 ── 変わるものを見張り、試験を流し直す

変わるもの起きること対応
語リスト語の追加・削除で件数が変わる語リストを版管理(SharePoint の版の履歴など)し、変えたら 9 章の試験を流す
python-docx の版読み取りの動きが変わることがあるrequirements.txt で版を固定し、上げるときは試験を流してから
文書の書き方入れ子の表やテキストボックスが増える範囲外の箇所を一覧にし、第 2 版で扱う
担当者作った人が異動する課題の定義(2 章)・試験・手順を、プログラムと同じ場所に置く

このセクションの自動収集は、python-docx の GitHub のリリースを毎日見ています。使い方が変わる変更が出たら、Word × Python のページにノウハウとして追加されます。

見直しの日を決めておきます(例: 半年ごと)。見直しでは、候補の一覧のうち人が外したものの割合を見て、語リストと規則を直します。