0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PDF・HTML・Wordの「見えない文字」は、どの抽出ライブラリで残るか? pypdf / PyMuPDF / pdftotext / BeautifulSoup / markdownify / textutil / python-docx を同じファイルで比べた

0
Posted at

何を確かめたか

PDFやWord、Webページを「AIに読ませて要約・採点させる」前処理では、ファイルからテキストを抽出します。このとき、人の目には見えない文字(白文字、極小文字、display:none など)が抽出結果に残るかどうかは、使うライブラリで変わります。

残れば、文書に埋め込まれた「隠し指示」がそのままプロンプトに入ります(間接プロンプトインジェクション。OWASP Top 10 for LLM Applications 2025 の LLM01)。残らなければ、その経路では入りません。どちらになるかは、ライブラリの仕組みを知らないと予想しにくいので、同じファイルを10通りの方法で抽出して比べました。

この記事は抽出方法ごとの挙動の違いにしぼった技術メモです。測った条件と結果の要約を先に、再現用のコードを後ろに置きます。

測った条件

項目 内容
ファイル 架空の提案書。PDF 10、HTML 17、Word(DOCX)9 の計36。見えない文字あり30、なし(対照)6
PDFの抽出 pypdf 6.11.0 / PyMuPDF 1.27.2 / poppler の pdftotext
HTMLの抽出 BeautifulSoup 4.12.3 の get_text() / markdownify / macOS textutil / Chrome の document.body.innerText
DOCXの抽出 python-docx 1.2.0 / macOS textutil / word/document.xml の全文
判定 抽出したテキストに、埋め込んだ文が含まれていれば「残る」
環境 2026-10-09、macOS、Python 3.13。空のフォルダで2回実行し、結果ファイルがバイト単位で一致

結果の要約: ライブラリ別に「何が残るか」

PDF

隠し方 pypdf PyMuPDF pdftotext
白文字、ほぼ白(灰96%)、極小文字(0.5pt)、白い四角で覆う、透明テキスト(描画モード3) 残る 残る 残る
ページ外に配置 残る 残らない 残らない
文書プロパティ(件名・キーワード)、注釈 残らない 残らない 残らない

PDFの抽出は「どこにどの文字があるか」を読む仕組みで、色・大きさ・重なり・透明度は見ていません。ページ外の文字だけは、ページ境界で切るかどうかの差が出ました。

HTML

隠し方 BeautifulSoup markdownify textutil Chrome innerText
font-size:0、白文字、画面外(left:-9999px)、opacity:0、不可視Unicodeタグ文字、ほぼ白(#fafafa)、height:0+overflow:hidden、transform:scale(0) 残る 残る 残る 残る
display:none、<style> のクラスで非表示 残る 残る 残らない 残らない
visibility:hidden 残る 残る 残る 残らない
画像の alt 属性 残らない 残る 残らない 残らない
HTMLコメント、title属性、meta description 残らない 残らない 残らない 残らない

ポイントは2つです。

  1. CSSを解釈しない抽出(BeautifulSoup、markdownify)は、display:none も拾う。 HTMLをそのままテキスト化してRAGに取り込む構成では、非表示要素も入ります。
  2. ブラウザの innerText でも、font-size:0 や opacity:0 や画面外の文字は残る。 innerText は「レイアウト上そこにある文字」を返すだけで、人に見えるかは判定しません。除外されたのは display:none と visibility:hidden だけでした。「画面で見て問題なかった」は、AIに渡る文字が安全という意味になりません。

DOCX

隠し方 python-docx textutil XML全文
白文字、ほぼ白(F5F5F5)、極小文字(1pt)、Wordの「隠し文字」属性、テーマ色「背景1(白)」 残る 残る 残る
コメント、文書プロパティ 残らない 残らない 残る

Wordの「隠し文字」属性(w:vanish)は、Word上では表示をオフにすると消えますが、python-docx の paragraph.text も textutil も本文として返しました。

対照ファイル

「濃い帯の上の白文字」「7ptの注記」「閉じたメニュー(display:none)」のように、正当な理由で隠れている要素もあります。見えない文字=悪意、ではないので、検出は「警告」にとどめ、最終判断は人が行う前提にしました。

再現用コード

抽出(10通り)

import subprocess, zipfile, re
from pypdf import PdfReader
import fitz  # PyMuPDF
from bs4 import BeautifulSoup
from markdownify import markdownify as md
from docx import Document

def pdf_pypdf(path):
    return "\n".join((p.extract_text() or "") for p in PdfReader(path).pages)

def pdf_pymupdf(path):
    with fitz.open(path) as doc:
        return "\n".join(page.get_text() for page in doc)

def pdf_pdftotext(path):
    return subprocess.run(["pdftotext", "-layout", path, "-"],
                          capture_output=True, text=True, check=True).stdout

def html_bs4(path):
    return BeautifulSoup(open(path, encoding="utf-8").read(), "html.parser").get_text("\n")

def html_markdownify(path):
    return md(open(path, encoding="utf-8").read())

def textutil_txt(path):  # macOS。HTMLとDOCXの両方に使える
    return subprocess.run(["textutil", "-convert", "txt", "-stdout", path],
                          capture_output=True, text=True, check=True).stdout

def docx_python_docx(path):
    return "\n".join(p.text for p in Document(path).paragraphs)

def docx_xml(path):
    xml = zipfile.ZipFile(path).read("word/document.xml").decode("utf-8")
    return re.sub(r"<[^>]+>", " ", xml)

Chrome の innerText は Playwright で取りました。

from playwright.sync_api import sync_playwright

def html_innertext(path):
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto(f"file://{path}")
        text = page.evaluate("document.body.innerText")
        browser.close()
        return text

テストファイルの作り方(例)

HTML は属性やスタイルを1つ変えるだけです。

<p>通常の本文です。</p>
<p style="font-size:0">ここに見えない文が入る</p>
<p style="color:#fff;background:#fff">白文字の例</p>
<p style="position:absolute;left:-9999px">画面外の例</p>

DOCX は python-docx で「隠し文字」や白文字を作れます。

from docx import Document
from docx.shared import RGBColor, Pt

doc = Document()
doc.add_paragraph("通常の本文です。")
run = doc.add_paragraph().add_run("隠し文字属性の例")
run.font.hidden = True            # Wordの「隠し文字」
run2 = doc.add_paragraph().add_run("白文字の例")
run2.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
run3 = doc.add_paragraph().add_run("極小文字の例")
run3.font.size = Pt(1)
doc.save("case.docx")

PDF は reportlab で白文字を置く例です。

from reportlab.pdfgen import canvas

c = canvas.Canvas("case.pdf")
c.drawString(72, 750, "通常の本文です。")
c.setFillColorRGB(1, 1, 1)        # 白
c.drawString(72, 700, "白文字の例")
c.save()

判定は、埋め込んだ文がそのまま抽出結果に含まれるかを in で見るだけにし、空白・改行の差で落ちないよう両方から空白類を除いてから比較しました。

実務でのポイント

  1. 自分のパイプラインが実際に使う抽出方法で測る。 同じPDFでも pypdf と pdftotext で結果が違いました。
  2. 抽出の前後で、人に見えにくい文字を機械的に洗い出す工程を入れる。 色・サイズ・透明度・位置・隠し文字属性を見る。本文抽出ライブラリは、この判定を代わりにやってくれません。
  3. 検出は警告にとどめる。 対照ファイルのように正当な理由で隠れている要素があるため、自動で捨てずに人が見る。

限界

  • 測ったのは「抽出したテキストに残るか」まで。AIがその指示に従うかは測っていません。 各AIサービスが内部でどの抽出方法を使っているかも未確認です。
  • ファイルは架空で、1つの隠し方につき1ファイル。実際の文書での発生頻度は分かりません。
  • 画像内の文字、スキャンPDF、Excel・PowerPoint、JavaScriptで後から書き換えるページは対象外です。

参考

  • OWASP Top 10 for LLM Applications 2025, LLM01: Prompt Injection
  • Nikkei Asia, 2025-07-01: 'Positive review only': Researchers hide AI prompts in papers

30の隠し方×10の抽出方法の結果全体の要約は、Zennの記事(無料)にまとめています: https://zenn.dev/kagaage/articles/7f670b3a59a94d

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?