折り返し済みの日本語原稿をpandocでEPUBとDOCXにしたら、文の途中に空白が5つ入った

折り返し済みの日本語原稿をpandocでEPUBとDOCXにしたら、文の途中に空白が5つ入った

まと めたものです。変換したEPUBのXHTMLを開くと、「まとめた」の真ん中に半角の空白が1つ入っていました。原稿に空白は書いていません。あるのは、60桁で折り返した改行だけです。

Pandocは段落の中の改行を空白に置き換えます。英語ならそれで正しい動きです。日本語では単語が割れます。

ここで使った原稿は、本文が10行ほどの合成見本です。実際に販売した商品の原稿ではありません。

試した理由

ターミナルの出力やメールの本文をコピーすると、行末の改行を引きずったまま届くことがあります。AIの返答を端末で受け、そのまま原稿ファイルへ貼る人もいるはずです。そういう原稿をPandocに渡して電子書籍や配布用DOCXを作ると、何が起きるのか。手元に失敗例はありません。先に壊れ方を見ておきたくて試しました。

前に書いたEPUBの検査記事は、目次や参照といった構造を見る回でした。今回は本文の字面です。構造が正しくても、文字の間に空白が入っていれば読者は気づきます。

折り返し済みの見本原稿を作る

全角を2桁として数え、60桁で折り返したmanuscript.mdを使いました。折り返しで句点が行頭に来る行も残してあります。実際のコピー原稿にもよくある形だからです。

pandoc --version | head -1
python3 --version
pandoc 3.1.11.1
Python 3.13.5
manuscript.mdの全文
cat > manuscript.md <<'EOF'
# 副業メモ術入門

## はじめに

この小冊子は、仕事の合間に副業のネタを残すためのメモ術をまと
めたものです。道具はテキストエディタが1つあれば足ります。

## 毎日5分のルール

夜に5分だけ机へ座り、その日に気づいた困りごとを1行ずつ書き出
します。続けるコツは、きれいに書こうとしないことです。

書いた行は週末にまとめて読み返し、`todo.md` に移すものと捨て
るものを分けます。移すか迷うものは、一度寝かせてから決めます
。

## 見積もりの目安

1件の作業にかかる時間は、実際に測った値だけを書きます。測っ
ていない項目は空欄のままにして構いません。
EOF

本文の段落は4つで、折り返しは合計5か所あります。

数えるスクリプトが先に壊れていた

HTML、DOCX、EPUBの3形式へ変換して、日本語どうしの間にある空白と改行を数えます。最初に書いたスクリプトは、ファイル全体からタグを消して正規表現を当てるだけでした。結果は次のとおりです。

default.html: 11 gaps
    '術 入'
    '門\nは'
    'に\nこ'
default.docx: 6 gaps
    '術 入'
    'と め'
    '出 し'
default.epub: 9 gaps
    '術 入'
    'に\nこ'
    'と め'

折り返しは5か所のはずなのに、数字が合いません。原因は2つで、どちらも私の側にありました。

  • 最初の見出しを副業メモ術 入門と、自分で半角空白を入れて書いていた
  • HTMLでは、タグを消すと</h2>と<p>の間の改行が日本語どうしの間に残った

見出しは空白をなくし、検査は<p>の中身だけを見るように直しました。DOCXは段落ごとにw:tを連結します。直したスクリプトは次のとおりです。

cjk_gap.pyの全文
#!/usr/bin/env python3
"""日本語どうしの間に入った空白・改行を数える。使い方: cjk_gap.py FILE..."""
import re, sys, zipfile
from xml.etree import ElementTree as ET

CJK = r"[\u3040-\u30ff\u3400-\u9fff\uff00-\uffef\u3000-\u303f]"
GAP = re.compile(f"({CJK})([ \n]+)({CJK})")
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"

def html_paragraphs(text):
    for m in re.finditer(r"<p>(.*?)</p>", text, re.S):
        yield re.sub(r"<[^>]+>", "", m.group(1))

def paragraphs(path):
    if path.endswith(".docx"):
        root = ET.fromstring(zipfile.ZipFile(path).read("word/document.xml"))
        for p in root.iter(W + "p"):
            yield "".join(t.text or "" for t in p.iter(W + "t"))
    elif path.endswith(".epub"):
        z = zipfile.ZipFile(path)
        for n in sorted(z.namelist()):
            if n.endswith(".xhtml") and "nav" not in n:
                yield from html_paragraphs(z.read(n).decode("utf-8"))
    else:
        yield from html_paragraphs(open(path, encoding="utf-8").read())

for path in sys.argv[1:]:
    hits = [m.group(0) for para in paragraphs(path) for m in GAP.finditer(para)]
    print(f"{path}: {len(hits)} gaps")
    for h in hits[:3]:
        print("   ", repr(h))

デフォルトの設定で変換すると5か所に空白が入る

Pandocの標準エラー出力には、日本語の翻訳ファイルがないという警告が出ます。変換結果には関係がないので、ここでは2>/dev/nullで捨てています。

pandoc manuscript.md -f markdown -o default.html -s --metadata title="副業メモ術" 2>/dev/null
pandoc manuscript.md -f markdown -o default.docx 2>/dev/null
pandoc manuscript.md -f markdown -o default.epub --metadata title="副業メモ術" --metadata lang=ja 2>/dev/null
python3 cjk_gap.py default.html default.docx default.epub
default.html: 5 gaps
    'と\nめ'
    '出\nし'
    'て\nる'
default.docx: 5 gaps
    'と め'
    '出 し'
    'て る'
default.epub: 5 gaps
    'と め'
    '出 し'
    'て る'

折り返しの数と一致しました。DOCXの中身を見ると、空白は独立した文字として入っています。

...メモ術をまと</w:t></w:r><w:r><w:t xml:space="preserve"> </w:t></w:r><w:r><w:t xml:space="preserve">めたものです。...

EPUBでは、段落の末尾に半角空白付きの句点という形でも出ました。

<p>書いた行は週末にまとめて読み返し、<code>todo.md</code> に移すものと捨て るものを分けます。移すか迷うものは、一度寝かせてから決めます 。</p>

決めます 。の空白は、句点が行頭に来た折り返しから生まれています。目で読んでも気づきにくい位置です。

east_asian_line_breaksを付けると0件になった

Pandocのマニュアルにはeast_asian_line_breaksという拡張があります。段落内の改行を、東アジアの全角文字どうしに挟まれたときだけ無視する設定です。読み込み側に+east_asian_line_breaksを足して、同じ変換をやり直しました。

pandoc manuscript.md -f markdown+east_asian_line_breaks -o eal.html -s --metadata title="副業メモ術" 2>/dev/null
pandoc manuscript.md -f markdown+east_asian_line_breaks -o eal.docx 2>/dev/null
pandoc manuscript.md -f markdown+east_asian_line_breaks -o eal.epub --metadata title="副業メモ術" --metadata lang=ja 2>/dev/null
python3 cjk_gap.py eal.html eal.docx eal.epub
eal.html: 0 gaps
eal.docx: 0 gaps
eal.epub: 0 gaps
設定HTMLDOCXEPUB
デフォルト555
+east_asian_line_breaks000

読み込み形式をmarkdownにしたときの結果です。commonmarkやgfmで同じ拡張が効くかは試していません。

拡張を付けても残った空白

0件になって安心しかけましたが、スクリプトが数えるのは全角どうしの間だけです。そこで、半角が絡む改行を別の短い原稿で試しました。

printf '# 検査\n\n手元のメモは`todo.md`\nに移します。AIの\nClaude\nCodeに頼みます。\n' > edge.md
pandoc edge.md -f markdown+east_asian_line_breaks -t html
<h1 id="検査">検査</h1>
<p>手元のメモは<code>todo.md</code> に移します。AIの Claude
Codeに頼みます。</p>

インラインコードの直後の改行と、英字が隣にある改行は、拡張を付けても空白か改行のまま残ります。「全角どうしに挟まれたときだけ」という説明どおりの動きです。ただ、今回の検査スクリプトは日本語どうしの空白しか数えません。この2種類は、0件の表示では見逃します。

確認できていないこと

  • Kindle、Apple Books、Wordで開いたときの見え方は見ていません。XHTMLとdocumentの中身に空白が入っているところまでが確認範囲です
  • HTMLの改行をブラウザがどう描画するかは、ブラウザごとに差があるので確認していません
  • 検査した原稿は4段落の見本です。数千行の原稿で見落としが出るかは試していません

個人的には、納品前の変換コマンドに+east_asian_line_breaksを最初から書いておくのが一番安いと考えています。ただし、半角の英字やコードに接した折り返しは拡張が助けてくれません。そこは原稿側の改行を先に外すか、目で確認するしかなさそうです。

出典

この記事をシェア