01Word と PowerPoint をまとめて点検し、1 冊の Excel に出す確認済

フォルダーの .docx と .pptx を同じ語リストで点検し、候補の一覧と文書ごとの件数を 1 冊の Excel に書き出す。一覧にはオートフィルターを付け、確認する人が語や文書で絞り込めるようにする。

考え方と手順
  1. 拡張子ごとに文字を取り出す関数を分け(docx_texts と pptx_texts)、辞書 READERS で選ぶ
  2. Word は段落と表のセル、PowerPoint は文字枠のある図形と発表者ノートを読む
  3. NFKC と casefold でそろえてから語を探す(全角と半角の違いを吸収する)
  4. openpyxl で hits と summary の 2 枚のシートを作り、hits の見出し行からオートフィルターを掛ける
Python手元で動く
"""Check every .docx and .pptx in a folder against a style list and write one Excel workbook for review."""
import csv
import unicodedata
from collections import Counter
from pathlib import Path

from docx import Document
from openpyxl import Workbook
from pptx import Presentation

FOLDER = Path("samples")


def normalize(text):
    return unicodedata.normalize("NFKC", text).casefold()


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


def docx_texts(path):
    doc = Document(path)
    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 r%d c%d" % (ti, ri, ci), cell.text


def pptx_texts(path):
    for n, slide in enumerate(Presentation(path).slides, 1):
        for shape in slide.shapes:
            if shape.has_text_frame:
                yield "slide %d %s" % (n, shape.name), shape.text_frame.text
        if slide.has_notes_slide:
            yield "slide %d notes" % n, slide.notes_slide.notes_text_frame.text


READERS = {".docx": docx_texts, ".pptx": pptx_texts}


def check(folder, terms):
    rows = []
    for path in sorted(folder.iterdir()):
        reader = READERS.get(path.suffix.lower())
        if reader is None or path.name.startswith("~$"):
            continue
        for where, text in reader(path):
            key = normalize(text)
            rows += [[path.name, where, term, action, text.strip()] for term, action, k in terms if k in key]
    return rows


def write_book(rows, out="out_style_check.xlsx"):
    wb = Workbook()
    ws = wb.active
    ws.title = "hits"
    ws.append(["file", "where", "term", "action", "text"])
    for r in rows:
        ws.append(r)
    ws.auto_filter.ref = "A1:E%d" % (len(rows) + 1)
    summary = wb.create_sheet("summary")
    summary.append(["file", "hits"])
    for name, n in sorted(Counter(r[0] for r in rows).items()):
        summary.append([name, n])
    wb.save(out)


def main():
    rows = check(FOLDER, load_terms(FOLDER / "style_terms.csv"))
    write_book(rows)
    print("hits:", len(rows))
    for name, n in sorted(Counter(r[0] for r in rows).items()):
        print(" ", name, n)


if __name__ == "__main__":
    main()
実行結果(2026-09-12)
hits: 11
  deck_a.pptx 4
  doc_a.docx 7
Copilot に書かせる指示の例

Python で、フォルダーの .docx(段落と表のセル)と .pptx(文字枠と発表者ノート)を style_terms.csv の語と照合し、結果を openpyxl で 1 冊の Excel(hits シートにオートフィルター、summary シートに文書ごとの件数)に書き出すコードを書いてください。元のファイルは読むだけにしてください。

注意

PowerPoint の表・グループ化した図形の中の文字は、この例では読んでいない(読み方は PowerPoint × Python のページ)。openpyxl の文書によると、openpyxl は絞り込みの設定を書くだけで、実際の絞り込みは Excel などのアプリで行う。候補は下準備であり、直すかどうかは担当者が文脈を見て決める。

利用条件
python-docx・python-pptx・openpyxl を手元の仮想環境に入れて使う。
必要なもの
python-docx 1.2.0, python-pptx 1.0.2, openpyxl 3.1.5
試験
手元で実行して確認(2026-09-12、Python 3.12.10 (venv) / Windows 11、python-docx 1.2.0、python-pptx 1.0.2、openpyxl 3.1.5)
出典
openpyxl「Using filters and sorts」
python-pptx「Working with text」
python-docx「Document objects (API)」
確認日
2026-09-12(第 1 版)
出典の該当箇所
It’s possible to filter single range of values in a worksheet by adding an autofilter.
Filters and sorts can only be configured by openpyxl but will need to be applied in applications like Excel.
All the text in a shape is contained in its text frame.
The Paragraph instances in the document, in document order.

02依頼台帳から要対応の依頼を拾い、担当者ごとのシートに分ける確認済

依頼台帳から、差戻しの依頼と、受付・確認中のまま日数が過ぎた依頼を拾い、担当者ごとのシートに分けた新しいブックを作る。担当者は自分のシートだけを見ればよくなる。

考え方と手順
  1. 基準日(AS_OF)と日数の上限(LIMIT_DAYS)を先頭の定数にまとめる。基準日を固定すると結果を再現できる
  2. 依頼日を日付に変え、基準日からの経過日数の列を足す
  3. 差戻し、または受付・確認中で上限を超えた行に印を付けて絞り込む
  4. pandas.ExcelWriter の with の中で、担当者ごとに to_excel でシートを書く
Python手元で動く
"""List returned and overdue requests from the ledger, one sheet per owner, in a new workbook."""
import pandas as pd

AS_OF = pd.Timestamp("2026-09-12")  # fixed so the result can be reproduced; use pd.Timestamp.today() in daily use
LIMIT_DAYS = 10

ledger = pd.read_excel("samples/request_ledger.xlsx", sheet_name="依頼台帳")
ledger["依頼日"] = pd.to_datetime(ledger["依頼日"])
ledger["経過日数"] = (AS_OF - ledger["依頼日"]).dt.days
is_open = ledger["状態"].isin(["受付", "確認中"])
flag = (ledger["状態"] == "差戻し") | (is_open & (ledger["経過日数"] > LIMIT_DAYS))
cols = ["依頼ID", "文書名", "種別", "状態", "依頼日", "経過日数", "担当者"]
todo = ledger.loc[flag, cols].sort_values(["担当者", "経過日数"], ascending=[True, False])

with pd.ExcelWriter("out_followup.xlsx") as writer:
    for owner, rows in todo.groupby("担当者"):
        rows.drop(columns="担当者").to_excel(writer, sheet_name=str(owner), index=False)
print("rows to follow up:", len(todo))
print(todo.groupby("担当者").size().to_string())
実行結果(2026-09-12)
rows to follow up: 26
担当者
担当A    10
担当B     7
担当C     3
担当D     6
Copilot に書かせる指示の例

pandas で request_ledger.xlsx(シート 依頼台帳)から、状態が 差戻し の行と、受付・確認中で依頼日から基準日までの日数が上限を超えた行を抜き出し、担当者ごとのシートに分けて新しいブックに書き出すコードを書いてください。基準日と上限は定数にしてください。

注意

シート名には Excel の制約(使えない文字など)がある。担当者名にそうした文字が入る台帳では、名前を置き換えてから書く。元の台帳は読むだけで、結果は新しいブックに出す。

利用条件
pandas と openpyxl を手元の仮想環境に入れて使う。
必要なもの
pandas 3.0.5, openpyxl 3.1.5
試験
手元で実行して確認(2026-09-12、Python 3.12.10 (venv) / Windows 11、pandas 3.0.5、openpyxl 3.1.5)
出典
pandas「pandas.ExcelWriter」
pandas「pandas.read_excel」
確認日
2026-09-12(第 1 版)
出典の該当箇所
Class for writing DataFrame objects into excel sheets.

03依頼メールと台帳を突き合わせ、食い違いを出す確認済

受信トレイのメールの件名から依頼 ID を拾い、依頼ごとの件数と最後の受信日時を出して台帳と突き合わせる。完了後もメールが届いている依頼と、受付・確認中なのにメールが無い依頼を一覧にする。

考え方と手順
  1. $select で件名と受信日時だけを取り、@odata.nextLink をたどって全ページを読む
  2. 件名から依頼 ID を正規表現で拾い、依頼ごとに件数と最後の受信日時をまとめる(mail_by_request)
  3. 台帳の CSV を読み、状態と完了日に照らして食い違いを判定する(compare)
  4. 集計と判定は関数に分けてあるので、見本の JSON と台帳で試験してからテナントで動かす
Python要サインイン(Microsoft Entra)
"""Match request mail with the ledger: mail after completion, and open requests without any mail."""
import asyncio
import csv
import os
import re
from collections import defaultdict

from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.users.item.mail_folders.item.messages.messages_request_builder import (
    MessagesRequestBuilder,
)

SCOPES = ["Mail.ReadBasic"]  # subject and dates only; no message bodies
REQUEST_ID = re.compile(r"R-\d{4}-\d{3}")


def mail_by_request(messages):
    """messages: [(subject, received ISO string)] -> {request_id: (count, latest)}"""
    seen = defaultdict(list)
    for subject, received in messages:
        m = REQUEST_ID.search(subject or "")
        if m:
            seen[m.group(0)].append(received)
    return {rid: (len(v), max(v)) for rid, v in seen.items()}


def compare(ledger_rows, mail):
    out = []
    for r in ledger_rows:
        rid, state, done = r["依頼ID"], r["状態"], r.get("完了日") or ""
        count, latest = mail.get(rid, (0, ""))
        if state == "完了" and done and latest[:10] > done:
            out.append((rid, "mail after completion", count, latest))
        if count == 0 and state in ("受付", "確認中"):
            out.append((rid, "open request without mail", 0, ""))
    return out


async def fetch_messages(client):
    builder = client.me.mail_folders.by_mail_folder_id("inbox").messages
    query = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(
        select=["subject", "receivedDateTime"], top=100)
    page = await builder.get(request_configuration=RequestConfiguration(query_parameters=query))
    rows = []
    while page:
        rows += [(m.subject, m.received_date_time.isoformat() if m.received_date_time else "") for m in page.value or []]
        if not page.odata_next_link:
            break
        page = await builder.with_url(page.odata_next_link).get()
    return rows


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    mail = mail_by_request(await fetch_messages(client))
    with open(os.environ.get("LEDGER_CSV", "ledger.csv"), encoding="utf-8", newline="") as f:
        findings = compare(list(csv.DictReader(f)), mail)
    for row in findings:
        print(*row, sep=" | ")
    print("findings:", len(findings))


if __name__ == "__main__":
    asyncio.run(main())
実行結果(2026-09-12)
request IDs in mail: 17 / findings: 16
R-2026-002 | open request without mail | 0 | 
R-2026-004 | mail after completion | 1 | 2026-09-05T16:00:00Z
R-2026-006 | open request without mail | 0 | 
R-2026-007 | open request without mail | 0 | 
R-2026-008 | open request without mail | 0 | 
Copilot に書かせる指示の例

msgraph-sdk で受信トレイのメールを件名と受信日時だけ($select)で全ページ読み、件名の R-2026-001 の形の依頼 ID ごとに件数と最後の受信日時を出し、台帳 CSV と突き合わせて「完了後のメール」と「メールの無い未完了の依頼」を出すコードを書いてください。集計と判定は関数に分け、権限は Mail.ReadBasic にしてください。

注意

件名の書き方がそろっていないと依頼 ID を拾えない。本文は読まない設計なので、件名に ID が無いメールは数えない。個人のメールボックスを読むため、使う前に組織の規程と、アプリの登録・同意を確かめる。

利用条件
Microsoft Graph(v1.0)。委任のアクセス許可 Mail.ReadBasic。Microsoft Entra へのアプリの登録が要る。
必要なもの
msgraph-sdk 1.62.0, azure-identity 1.25.3
権限
Mail.ReadBasic(委任)
試験
サインインの手前まで実行して確認(Microsoft 365 には接続していない)・集計・判定の部分を見本データで実行して確認(2026-09-12、Python 3.12.10 (venv) / Windows 11、msgraph-sdk 1.62.0、azure-identity 1.25.3)
出典
Microsoft Learn「Paging Microsoft Graph data in your app」
Microsoft Learn「List messages」
確認日
2026-09-12(第 1 版)
出典の該当箇所
When there's at least one more page of data available, Microsoft Graph returns an @odata.nextLink property in the response that contains a URL to the next page of results.
To improve the operation response time, use $select to specify the exact properties you need; see example 1 below.

04ライブラリの列の値と台帳を突き合わせる確認済

ドキュメントライブラリの各ファイルの列(DocId・Status)を Microsoft Graph で読み、台帳の状態と突き合わせる。DocId が空のもの、台帳に無いもの、状態が食い違うものを CSV に出す。

考え方と手順
  1. リストの項目を expand=fields(select=...) で読み、要る列だけを取る
  2. @odata.nextLink をたどって全ページを読み、行を辞書にそろえる
  3. 台帳の CSV から「依頼 ID → 状態」の辞書を作る
  4. 食い違いの判定は mismatches 関数にまとめ、見本の JSON で試験してから使う
Python要サインイン(Microsoft Entra)
"""Compare a document library's Status column with the request ledger and list mismatches (report only)."""
import asyncio
import csv
import os

from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.sites.item.lists.item.items.items_request_builder import ItemsRequestBuilder

SCOPES = ["Sites.Read.All"]
COLUMNS = ["FileLeafRef", "DocId", "Status"]


def mismatches(library_rows, ledger):
    """library_rows: [{FileLeafRef, DocId, Status, webUrl}]; ledger: {request ID: status}"""
    out = []
    for r in library_rows:
        doc_id, name = r.get("DocId"), r.get("FileLeafRef")
        if not doc_id:
            out.append((name, "", "DocId is empty", r.get("webUrl")))
        elif doc_id not in ledger:
            out.append((name, doc_id, "not in the ledger", r.get("webUrl")))
        elif (r.get("Status") or "") != ledger[doc_id]:
            out.append((name, doc_id, "library %s / ledger %s" % (r.get("Status"), ledger[doc_id]), r.get("webUrl")))
    return out


async def fetch_rows(client):
    items_rb = client.sites.by_site_id(os.environ["SITE_ID"]).lists.by_list_id(os.environ["LIST_ID"]).items
    query = ItemsRequestBuilder.ItemsRequestBuilderGetQueryParameters(
        expand=["fields(select=%s)" % ",".join(COLUMNS)])
    page = await items_rb.get(request_configuration=RequestConfiguration(query_parameters=query))
    rows = []
    while page:
        for it in page.value or []:
            f = it.fields.additional_data if it.fields else {}
            rows.append({c: f.get(c) for c in COLUMNS} | {"webUrl": it.web_url})
        if not page.odata_next_link:
            break
        page = await items_rb.with_url(page.odata_next_link).get()
    return rows


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    rows = await fetch_rows(client)
    with open(os.environ.get("LEDGER_CSV", "ledger.csv"), encoding="utf-8", newline="") as f:
        ledger = {r["依頼ID"]: r["状態"] for r in csv.DictReader(f)}
    found = mismatches(rows, ledger)
    with open("out_library_vs_ledger.csv", "w", encoding="utf-8-sig", newline="") as f:
        csv.writer(f).writerows([("file", "doc_id", "problem", "webUrl")] + found)
    print("library rows:", len(rows), "/ mismatches:", len(found))


if __name__ == "__main__":
    asyncio.run(main())
実行結果(2026-09-12)
library rows: 15 / mismatches: 12
文書02.pdf | R-2026-002 | library None / ledger 受付
文書03.pdf | R-2026-003 | library 確認中 / ledger 差戻し
文書05.pdf |  | DocId is empty
文書06.pdf | R-2026-006 | library None / ledger 確認中
文書07.pdf | R-2026-007 | library None / ledger 確認中
Copilot に書かせる指示の例

msgraph-sdk で SharePoint のドキュメントライブラリ(リスト)の項目を expand=fields(select=FileLeafRef,DocId,Status) で全ページ読み、台帳 CSV の状態と突き合わせて、DocId が空・台帳に無い・状態が違う、の 3 種類を CSV に出すコードを書いてください。書き込みはせず、権限は読み取りだけにしてください。

注意

列の内部名はライブラリごとに違う(表示名と異なることがある)。先にライブラリの列の一覧で内部名を確かめる。この例は報告だけで、列の値を直さない。

利用条件
Microsoft Graph(v1.0)。委任のアクセス許可 Sites.Read.All(サイトを限定するなら Sites.Selected。SharePoint / OneDrive × Python のページ)。
必要なもの
msgraph-sdk 1.62.0, azure-identity 1.25.3
権限
Sites.Read.All(委任)
試験
サインインの手前まで実行して確認(Microsoft 365 には接続していない)・集計・判定の部分を見本データで実行して確認(2026-09-12、Python 3.12.10 (venv) / Windows 11、msgraph-sdk 1.62.0、azure-identity 1.25.3)
出典
Microsoft Learn「List items」
確認日
2026-09-12(第 1 版)
出典の該当箇所
GET /sites/{site-id}/lists/{list-id}/items?expand=fields

05前回から変わった Word 文書だけを点検する確認済

ドライブの差分クエリ(delta)で前回から変わった .docx だけを取り出し、中身を取得して表記の点検をかける。次の回のために deltaLink をファイルに残し、毎回すべての文書を読み直さない。

考え方と手順
  1. 保存した deltaLink があればそこから、無ければ root/delta から読み始める
  2. 削除されたものとフォルダーを除き、名前が .docx の項目だけを残す
  3. 各項目の content を取得し、python-docx に BytesIO で渡して段落を点検する
  4. 最後のページの deltaLink を保存し、次の回はそこから続ける
Python要サインイン(Microsoft Entra)
"""Check only the .docx files that changed since the last run (drive delta + python-docx); report only."""
import asyncio
import csv
import io
import json
import os
import unicodedata
from pathlib import Path

from azure.identity import DeviceCodeCredential
from docx import Document
from msgraph import GraphServiceClient

SCOPES = ["Files.Read.All"]
STATE = Path(os.environ.get("DELTA_STATE", "out_delta_state.json"))


def normalize(text):
    return unicodedata.normalize("NFKC", text).casefold()


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


def check_docx(data, terms):
    """data: the bytes of a .docx file -> [(where, term, action)]"""
    doc = Document(io.BytesIO(data))
    hits = []
    for i, p in enumerate(doc.paragraphs, 1):
        key = normalize(p.text)
        hits += [("paragraph %d" % i, term, action) for term, action, k in terms if k in key]
    return hits


async def changed_docx(builder, drive_id):
    first = "https://graph.microsoft.com/v1.0/drives/%s/root/delta" % drive_id
    link = json.loads(STATE.read_text(encoding="utf-8"))["deltaLink"] if STATE.exists() else first
    page, items = await builder.with_url(link).get(), []
    while True:
        items += [i for i in page.value or []
                  if i.deleted is None and i.file is not None and (i.name or "").lower().endswith(".docx")]
        if not page.odata_next_link:
            return items, page.odata_delta_link
        page = await builder.with_url(page.odata_next_link).get()


async def main():
    drive_id = os.environ["DRIVE_ID"]
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    drive = client.drives.by_drive_id(drive_id)
    items, delta_link = await changed_docx(drive.items.by_drive_item_id("root").delta, drive_id)
    terms = load_terms(os.environ.get("TERMS_CSV", "style_terms.csv"))
    for item in items:
        data = await drive.items.by_drive_item_id(item.id).content.get()
        for where, term, action in check_docx(data, terms):
            print(item.name, where, term, action, sep=" | ")
    STATE.write_text(json.dumps({"deltaLink": delta_link}), encoding="utf-8")
    print("changed .docx files:", len(items))


if __name__ == "__main__":
    asyncio.run(main())
実行結果(2026-09-12)
hits in doc_a.docx paragraphs: 6
paragraph 5 | 最高 | 根拠を確認する
paragraph 5 | 業界初 | 根拠を確認する
paragraph 5 | 絶対 | 言い換える
Copilot に書かせる指示の例

msgraph-sdk でドライブの delta を読み、変わった .docx だけの中身を取得して、python-docx(BytesIO で開く)の段落を style_terms.csv の語と照合するコードを書いてください。最後の deltaLink をファイルに保存し、次の回はそこから読むようにしてください。権限は Files.Read.All の読み取りだけにしてください。

注意

保存した deltaLink が使えなくなったときは、最初から読み直す処理が要る(SharePoint / OneDrive × Python のページの差分クエリのノウハウ)。この例は段落だけを点検し、表のセルは読まない。点検の結果は報告だけで、文書を書き換えない。

利用条件
Microsoft Graph(v1.0)。委任のアクセス許可 Files.Read.All。python-docx を手元の仮想環境に入れて使う。
必要なもの
msgraph-sdk 1.62.0, azure-identity 1.25.3, python-docx 1.2.0
権限
Files.Read.All(委任)
試験
サインインの手前まで実行して確認(Microsoft 365 には接続していない)・集計・判定の部分を見本データで実行して確認(2026-09-12、Python 3.12.10 (venv) / Windows 11、msgraph-sdk 1.62.0、azure-identity 1.25.3、python-docx 1.2.0)
出典
Microsoft Learn「driveItem: delta」
python-docx「Document objects (API)」
確認日
2026-09-12(第 1 版)
出典の該当箇所
The service starts enumerating the drive's hierarchy, returning pages of items and either an @odata.nextLink or an @odata.deltaLink, as described below.
to a .docx file (a string) or a file-like object.