TITLE

openpyxl の IllegalCharacterError ― PLC 文字列の「見えない汚れ」が帳票を全滅させる仕組みと、サニタイズという改札

投稿日:2026.08.20

CATEGORY

  • 製造業DX
openpyxl の IllegalCharacterError ― PLC 文字列の「見えない汚れ」が帳票を全滅させる仕組みと、サニタイズという改札

PLC から読み取った文字列をそのまま Excel に書き込んだら、書きかけの帳票が一枚も保存されずに終わったことがあります。原因は、大量の行のうちのたった1セルに混じっていた制御文字でした。openpyxl は、値として書き込めないと判定した制御文字を正規表現でチェックしていて、それに引っかかった瞬間に IllegalCharacterError を送出し、処理をそこで止めます。ループの途中で例外が上がるので、それより前に書いた行も save() まで到達せず、結局ファイルは何も残りません。1セルの汚れが、帳票全体を道連れにします。

なぜ止まるのか ― openpyxl の禁止文字チェック

openpyxl は、セルに文字列を代入した瞬間に、その文字列を正規表現でチェックしています。対象は \x00\x08\x0b\x0c\x0e\x1f の範囲の制御文字で、改行(\x0a)・タブ(\x09)・復帰(\x0d)はこの範囲に含まれず許可されています。1文字でもこの範囲に一致すると IllegalCharacterError が送出され、それ以降のコードは実行されません。

重要なのはタイミングです。このチェックは wb.save() を呼んだ瞬間ではなく、cell.value = 文字列 の代入そのものに組み込まれています。大量行を1行ずつ書き込むループの途中で1セルが引っかかると、それより後の行が書かれないのはもちろん、まだ一度も save() に到達していないため、それまで書いた分もファイルとしては存在しません。例外を握りつぶさない限り、処理全体がその場で止まります。

1セルの汚れが帳票全体を止める仕組みを示す図解。8行分の書込みループのうち5行は書けていたが1行で制御文字によるIllegalCharacterErrorが発生し、書けていた行も未到達の行もすべてsave()に届かず、結果としてファイルが0件になることを収束線で示した概念図

この仕組みは pandas 経由でも変わりません。DataFrame.to_excel(..., engine="openpyxl") は内部で openpyxl のワークシートオブジェクトに同じように cell.value を代入しているため、同じ検査・同じ例外を通ります。「pandas を使っているから大丈夫」という切り分けは成立しません。

制御文字はどこから来るのか ― PLC 由来 ASCII の"途中"に残るケース

その制御文字は、どこから紛れ込むのでしょうか。多くの現場では、PLC から読み取る ASCII 文字列は固定長で、末尾を \x00 で埋めて送られてきます。だから多くの実装は rstrip('\x00') を書込み前に通し、末尾のパディングを落としてから使っています。

ところが、この対策は文字列の"末尾"しか見ていません。私たちが実際に踏んだのは、文字列の途中に \x00 などの制御文字が残ってしまうケースでした。末尾ではなく中ほどに制御文字が挟まっていると、rstrip はそこを素通りします。見た目には空白か何も無いように見えても、その1文字が openpyxl の検査に引っかかり、帳票全体を止めます。

製造ラインわきで、本来は検査記録が印字されているはずの品質記録用紙(帳票)が白紙のまま積まれている様子。openpyxlのIllegalCharacterErrorにより帳票が一枚も作られなかった現場の状況を表す挿絵

私たちの製造ライン向け品質履歴システムでこの事故を実際に経験し、書込み前に必ず通すサニタイズ共通関数を導入しました。

書込み前に通す改札 ― サニタイズ共通関数と周辺の同族罠

止め方は、openpyxl 自身の検査ロジックをそのまま流用することでした。

import re

_ILLEGAL_CHARS_RE = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]')

def strip_illegal_excel_chars(value):
    """openpyxl が拒否する制御文字を除去してから書き込む"""
    if value is None:
        return value
    return _ILLEGAL_CHARS_RE.sub('', str(value))

この関数を、PLC や CSV から読んだ文字列をどこかのセルに書き込む直前に必ず通します。ポイントは、書込み箇所ごとに try/except を積み増すのではなく、Excel への書込み口を1本に絞り、そこを改札にすることです。私たちは、個々の書込み処理に例外処理を足していく方法ではなく、この共通関数を通さない限り値がセルに渡らない設計を選びました。書込み処理が増えるたびに同じ判断をやり直さずに済むからです。

同じ PLC 連携の現場では、文字列まわりでもう少し気をつけたい罠が3つあります。

1つ目は CSV 出力です。BOM 付き UTF-8(utf-8-sig)で固定します。BOM 無しや Shift-JIS で書き出すと、読み込み側のヘッダ照合に失敗し、エラーにもならず空リストが黙って返ってくることがあります。

2つ目は PLC への送信文字列です。ASCII のみに保ちます。日本語を混ぜると UnicodeEncodeError になります。

3つ目は号機番号や管理番号の比較です。0埋めで正規化してから比較します。生の文字列同士を比較すると、先頭の0の有無だけでミスマッチが起きます。

まとめ

あなたの手元のスクリプトで、書込み箇所ごとに個別の try/except で凌いでいないか、まず確認できます。次に新しい書込み処理を追加したときに同じ事故が起きるかどうかは、その1点で決まります。

openpyxl の IllegalCharacterError は、ライブラリの不具合ではなく、書き込む値を検査してから受け付けるという、意図された仕組みです。1セルの汚れが帳票全体を道連れにする仕組みを知っていれば、rstrip の盲点も、書込み前の改札も、次に踏むはずだった事故を先に潰す判断材料になります。

出典:

※ この記事は AI を使用しています。Leadeas が自社開発した AI エージェント基盤で下書きを作成し、人間のレビューを経て公開しています。AI ネイティブ開発会社として自社の技術をそのまま実演する目的で、この手法を用いています。(詳しくは AI 利用ポリシー)

AI

AI導入やシステム開発の ご相談を承っています。

お気軽にお問い合わせください