EPUB の完成度を上げる

EPUB ファイル内に存在する問題を検出して修正する

EPUB 文書には、通常の検証方法では検出できない問題が存在する場合があります。EPUB 文書が整形式 XML であると検証され、EPUB 標準に従っている限り、問題はないように見えますが、それでも電子書籍リーダーで正しく読み込めないことがあります。そのような事態が発生するのは、例えば OCR スキャンによってパラグラフの分断、誤ったページ番号付け、スペル・ミスなどが発生した場合です。それでも、2 つの手段を使えば、このような問題を目で確認して修正することができます。その手段とは、Sigil という EPUB エディター、そして SimpleXML と Enchant ライブラリーを組み合わせた PHP スクリプトです。そして正規表現を使用することが、効果的な処理をする鍵となります。

Colin Beckingham, Writer and Researcher, Freelance

Colin Beckingham はカナダのオンタリオ州東部に住むフリーランスの研究者であり、ライターであり、プログラマーでもあります。キングストンの Queen's University で学位を取得している彼は、園芸、競馬、教育、行政サービス、小売業、旅行/観光業などにも関わってきました。彼はデータベース・アプリケーションの作成者であり、数え切れないほどの新聞記事や雑誌記事、オンライン記事を執筆しており、また Linux でのオープンソース・プログラミングや VoIP、音声制御アプリケーションも研究しています。



2011年 10月 07日

EPUB フォーマットは、文書を効果的に表現する手段です。その XML 構造により、文書のコンポーネントが適切な位置にあること、そして多種多様な機器で適切に表示されることが確実になります。EPUB の概要については、「参考文献」で紹介している Liza Daly の記事を参照してください。

よく使われる頭文字語

  • GUI: Graphical User Interface
  • OCR: Optical Character Recognition
  • HTML: HyperText Markup Language
  • WYSIWYG: What You See Is What You Get
  • XML: Extensible Markup Language

EPUB フォーマットの文書には、次の 2 つのレベルで問題が発生する可能性があります。

  • XML マークアップやコンテンツが壊れるなどの基本的なレベル
  • 上記よりも微妙で、XML をチェックしても検出できないレベル

EPUB の内部破損という前者の問題については EpubCheck プロジェクト (「参考文献」にリンクを記載) を使用して対処できるので、この記事では 2 番目のタイプとして挙げた、読者にとって頭痛の種となりがちな問題について検討します。

XML が課す厳格な管理には限界があります。XML が幸いにも許容する数々の問題は、ソフトウェアの障害を引き起こすほどの深刻なものではありませんが、それでもやはり、文書をスムーズに読む妨げにはなります。これらの問題が発生する仕組みは簡単に理解することができます。出版者が OCR を使って、印刷されたページをテキスト・フォーマットに移し変えると、フォントの非互換性による問題を含め、その印刷ページのおかしなところがすべてテキスト・フォーマットに引き継がれることになるからです。営利目的の場合には、編集者が手作業で結果をレビューして完成版を作成しますが、オープンソースとして無料で配布するように意図された製品の場合、出版者はこれらの作業コストを簡単には負担することができません。そうなると、電子書籍リーダーで読み込んだ結果は悪くはないとは言え、期待するほどのものでなくなります。例えば、パラグラフの分断、空白のページ、誤ったページ番号付け、スペル・ミスなどが発生する結果となります。

開発者の視点からすると、ここで課題となるのは、EPUB の構造を使用して問題に取り組む方法です。この記事では、一部の問題には Sigil EPUB エディターを使用し、残りの多くの問題は、PHP に SimpleXML とスペル・ライブラリーを組み合わせるという手段で解決する方法を考察します。

パラグラフの分断と空白ページ

2 次的な問題の例としてパラグラフの分断を取り上げると、この問題は HTML マークアップでは以下のようになって表れます。

<p>This is where my paragraph begins, hits the end of a physical page here</p>
<div class="newpage" id="page-12"></div>
<p>and then continues from the top of the next physical page, 
     finally coming to an end here.</p>

スキャナーはページの最後まで読み込むと、そこにパラグラフ・タグが適用されるかどうかはお構いなしに、ページを構文的に完全にするためにパラグラフ・タグを挿入します。そして次のページの先頭をパラグラフ・タグから開始して、ページが新しいパラグラフで始まるようにします。この場合も、そのページが新しいパラグラフであるかどうかは考慮されません。この処理はコードを完全にするためには役立ちますが、孤立したセクションができ、途中で分断された不完全なパラグラフが作られてしまいます。電子書籍リーダーでは、ページ・マーカーは表示されず、分断された 2 つのパラグラフ・セクションは機器の同じページ上に表示されるかもしれませんが、それでもそれぞれのパラグラフ・セクションは独立したパラグラフであるかのように分離されてしまいます。

同様に、以下の空白ページについて考えてみてください。

<div class="newpage" id="page-128"></div>
<p></p>
<div class="newpage" id="page-129"></div>

上記のスニペットに示されているページ 129 は実際に存在するのでしょうか?ページを空白にすることが重要な意味を持つ場合もありますが、それ以外の場合には本来 1 ページしか必要ないのに 2 ページめくるのでは不便です。

スペル・ミスは、上記のような問題とは別の類の問題です。スペル・ミスの場合には、複雑なパターンを探すのではなく、2 つの異なる単語リストを比較します。この問題については、スクリプトを作成するという方法で別途、対処します。


Sigil

Sigil (Web サイトおよびサポート・ページについては「参考文献」を参照) は、問題のパターン・マッチング・タイプを検出して、プログラマーが問題を修正できるようにする、WYSIWYG EPUB エディターです。正規表現の概要については「正規表現」の囲み記事を、その詳細については「参考文献」を参照してください。

正規表現

正規表現は、パターン・マッチング手法によってテキストを検索置換する強力な手段となります。その構文は簡潔なので、不要な効果を避けるためには注意が必要です。

例えば、[^.]</p> という正規表現は、先行するピリオドがないパラグラフ・タグの終わりを検索します。このようなパラグラフ・タグは、問題になることもあれば、ならないこともあります。

上記の正規表現では、一連の文字が角括弧 ([]) で囲まれています。これは、括弧内のいずれか 1 文字に一致すればよいことを意味します。キャレット記号 (^) があると、これに続くいずれの文字とも一致しないことを意味し、括弧内のピリオド(.) はそれ自身が突き合わせの対象であることを表しています。角括弧の外にある残りの記号も同じくその記号自身を表します。

この有用なツールについての詳細は、「参考文献」を参照してください。

お使いの Linux リポジトリーに Sigil がない可能性もありますが、その場合には、コンパイル済みバイナリーまたはソース・ファイルとして入手することができます。Sigil の GUI では、直接 EPUB を開くために「File (ファイル)」 > 「Open (開く)」の順にクリックしてください。これによって EPUB が抽出されて、左側にコンポーネント・ファイルのディレクトリーが表示されます。右側のブラウザー・ペインでは、個々のファイルのコンテンツを電子書籍リーダーで表示した場合のプレビュー、あるいはマークアップ付きコードとして表示することができます。後者は、問題を検出して修正する際に必要不可欠な機能です。

EPUB に含まれる HTML ファイルのいずれかを選択し、ダブルクリックしてブラウザー・ウィンドウで開いてください。続いて「View (表示)」 > 「Code View (コード・ビュー)」の順にクリックすると、ファイルに隠されたコードが表示され、すべてのタグを確認できるようになるはずです。

例えば、孤立したパラグラフの塊を見つけたいとします。この場合に検索する基準は、通常のセンテンス終了文字が前にないパラグラフ終了タグ </p> となります。センテンス終了文字として最も一般的に使われるのはピリオドです。Sigil には検索機能 (「Edit (編集)」 > 「Find (検索)」) が用意されていて、通常の検索モードでは .</p> のような文字列を検索することはできますが、ピリオドが前にないパラグラフの終わりを検索するといった場合には役に立ちません。この場合には、「More (詳細)」をクリックして、正規表現検索モードを使用する必要があります。ブラウザー・ウィンドウでコードの先頭までナビゲートしてから、以下のステップに従ってください。

  1. 検索方向として「Down (下へ)」を選択します。
  2. 検索モードとして「Regular expression (正規表現)」を選択します。
  3. Find what (検索対象)」文字列として、「[^.]</p>」と入力します。
  4. Find Next (次を検索)」をクリックします。

このプロセスにより、検索している対象 (存在する場合) が検出されます。一致結果がない場合には、検索機能が有効であることを確認するためだけの一時的なファイルを作成するのも一案です。

この手法をしばらく使っていると、パラグラフはピリオド以外の文字でも、英語の正常な文法に従って終了する場合があることに気付くはずです。該当する文字には、例えば二重引用符 (")、感嘆符 (!)、疑問符 (?) があります。その他にも、完全なセンテンスにするための要件を満たす文字であれば、終了文字として使用することができます。こうした文字をセンテンス終了文字として使用しても、正規表現を使う場合に問題が生じるわけではありません。角括弧はグループを意味するため、例えば「Find what (検索対象)」を [^.?!"]</p> に変更すれば、通常どおりの検索によって、ピリオド、疑問符、感嘆符、または二重引用符で終わるパラグラフが許可され、それ以外の文字で終わるパラグラフには、問題であることを示すフラグが立てられます。

パラグラフが分断されている紛れもない証拠としては、<p> の後に小文字のアルファベット文字が続く場合も挙げられます。これを正規表現で表すと、<p>[a-z]. となります。また、数字で始まるパラグラフを検索する <p>[0-9]. も有用な正規表現です。数字で始まるパラグラフは、スキャナーがページ番号を拾う場合には有効かもしれませんが、電子書籍リーダーのコンテキストでは意味がありません。

これらの問題をどのように修正するかは、また別の話です。ページ・マーカーがパラグラフを 2 つに分割している場合には、実際のパラグラフの前または後ろにマーカーを移動して、2 つの部分を 1 つのパラグラフに結合するという方法が考えられます。その場合、ページ番号はだいたいのところで合っていますが、完全に正確にはなりません。

ページ・マーカーを検索する場合も、上記のプロセスと同じように正規表現オプションを使用します。例えば、「Find what (検索対象)」が page-[0-9]+ の場合、Sigil はリテラル文字 p、a、g、e のいずれかで始まり、その後のダッシュに 0 から 9 までの数字が 1 つ以上続く文字列を検索します。

簡単に見つけられる興味深い分断は、単語、パラグラフ、ページがどれも分断されている場合です。この分断箇所は、印刷バージョンではハイフンまたはダッシュで示されるため、コード・ビューで簡単に見分けることも、検索することもできます。

<p>This is where my paragraph begins, hits the end of a phys-</p>
<div class="newpage" id="page-12"></div>
<p>ical page and then continues from the top of the next physical page, 
     finally coming to an end here.</p>

この場合、「Find what (検索対象)」文字列を -</p> に設定して通常のグローバル検索を実行すると、たちまち該当する箇所が見つかります。


ページ番号の確認

改ページとページ番号を検索して確認する場合にも Sigil を使えますが、100 ページを超える文書となると、うんざりする作業になります。それよりも簡単な方法は、PHP を使用して文書を繰り返し処理し、ページ番号を確認する方法です。

リスト 1 に、HTML ページを見つけて、改ページをひと通り調べるためのスクリプトを記載します。このスクリプトは、最初のページの番号 (大抵の場合、ページ 1 とはなっていません) を見つけて、それに続く各ページの番号が最初のページから増分されていることを確認します。このページ番号付けのテストはかなり単純なものの、OPF ファイルを使ってコンポーネント HTML を検索して調べる際の手本になります。

リスト 1. PHP と SimpleXML による EPUB のページ・チェック
<?php
/* epub is a zipped package containing many files
  the file "content.opf" contains the pointers to the constituent files
  inside content.opf you have 

  package (root)
    -> manifest
      -> item
          which we need to filter for media-type="application/xhtml+xml"
          and to check these are real text pages, not just full page images

  these are the text chapters which need to be checked one by one
*/
$firstpage = 0;
$oldpage = 0;
// look for the text to be checked
$opf_file = "./OEBPS/content.opf";
if (!file_exists($opf_file)) {
  //cleanup();
  die("Cannot find the OPF file\n");
} else {
  echo "Found it!\n";
  $xml = simplexml_load_file($opf_file);
  // get the manifest items
  foreach ($xml->manifest->item as $mi) {
    if ($mi['media-type']=='application/xhtml+xml') {
      echo "Found ".$mi['href']."\n";
      if (substr($mi['href'],0,4) == 'part') {
          echo "Page number check in document ".$mi['href']."\n";
          echo scan_chap("./OEBPS/".$mi['href']);
      }
    }
  }
}
function scan_chap($chap) {
global $firstpage, $oldpage;
  echo "Trying to page num check section $chap \n";
  if (!file_exists($chap)) {
    echo "Cannot find the chapter $chap\n";
  } else {
    echo "Found it!\n";
    $xml = simplexml_load_file($chap);
    //$i = 0;
    foreach ($xml->body->div->div as $pagnumdiv) {
      if ($pagnumdiv["class"]=='newpage') {
          echo $pagnumdiv["id"]."\n";
          $page = (int) substr($pagnumdiv["id"],5);
          if ($firstpage == 0) {
          $firstpage = $oldpage = $page;
          } else {
          if ($page != $oldpage+1) echo "Problem at page after $oldpage\n";
          $oldpage++;
          }
      }
    }
  }
  return "Done...\n";
}
?>

上記のコードはまず、検出された最初の論理ページの番号のグローバル変数 (ループの開始時に設定されます) と、前のページで確認した番号のグローバル変数 (繰り返し処理ごとに変わります) をセットアップします。次に、OPF ファイルの名前を宣言し、そのファイルを検索します。ファイルが見つからない場合、スクリプトはエラーで終了します。該当するファイルが見つかった場合には、そのファイルを XML オブジェクトとして開き、マニフェスト・ファイルのなかで media-type 属性を使用して HTML であると示されているファイルの名前を探します。この特定の EPUB 文書の場合、一部の HTML ファイルにはページ全体を占める画像しか格納されていません。したがって、これらのファイルは無視することができます。このようなページのファイル名には leaf という文字列が含まれる一方、拡張テキストを格納しているその他のファイルには、part というラベルが付けられています。このコードは、サブ文字列を使用してこれらのファイルをフィルタリングします。

ファイルの名前がわかれば、そのファイルを専用の simpleXML オブジェクトに読み込むことができます。<div> タグを繰り返し処理して、クラス属性が newpage に設定されているタグをフィルタリングすると、ページ番号を格納する id 属性の値を見つけることができます。最初のページがページ 1 になっていないことはよくあるため、書籍から最初のページの番号が取得されるようにする必要があります。この値を最初のページを示すグローバル変数に格納すれば、その後に続くページの番号を予測できるというわけです。期待される番号でない場合には、スクリプトがエラーを生成してチェックを続けます。

このスクリプトはテキストを変更するためのものではなく、注意を向ける必要がある箇所にフラグを立てるだけのものです。


PHP、XML、および Enchant を使用したスペル・チェック

スペルの問題は、これまでの問題とは異なります。このスペルの問題では、例えば Upon という単語を、似ているけれども正しくはない TJpon や IJpon として OCR が読み取るような場合を実際に追跡したりします。OCR が単語を誤って読み取る可能性はさまざまなにありますが、読み取った結果があまりにもおかしいと、スペル・チェッカーは実際のスペルに近い候補も、役に立つ候補も提示することができません。

スペル・チェッカーは、単語を 1 つひとつ調べて標準のスペル・リストと比較し、スペルが一致しない単語を指摘すると同時に候補を提示して、ユーザーがスペルを変更できるようにします。Sigil では、EPUB パッケージに含まれる複数の文書で特定の文字列を置換することができますが、きめ細かに制御するためには、専用のライブラリーと共に PHP、Perl、Python などのスクリプト・エンジンの力が必要です。

PHP の最近のバージョンには、SimpleXML を使用してXML および HTML ファイルを徹底的に調べるためだけでなく、Enchant スペリング・マネージャー・ライブラリーを使用するためにも必要なフックが含まれています。Enchant では複数の異なる基本スペル・リストを管理できるため、例えば、英国英語と米国英語のスペルを区別するといった場合に役立ちます。

リスト 2 は、複数のマニフェスト・ファイルを個別に調べるスクリプトです。その方法はリスト 1 と同じですが、このスクリプトの場合、パラグラフ単位および単語単位で繰り返し処理して、既知のスペル・リストと照らし合わせます。HTML コンポーネント・ファイルについてもリスト 1 と同じように繰り返し処理しますが、辞書にアクセスするために必要な命令が追加されています。

リスト 2. PHP、SimpleXML、および Enchant による EPUB のスペル・チェック
<?php
  // spell check an epub
/* epub is a zipped package containing many files
  the file "content.opf" contains the pointers to the constituent files
  inside content.opf we have 

  package (root)
    -> manifest
      -> item
          which we need to filter for media-type="application/xhtml+xml"
          and to check these are real text pages, not just full page images

  these are the text chapters that need to be checked one by one

  Acknowledgment: Some of the dictionary-related code
  was copied from the PHP Enchant manual page

*/
// set up console for input
$console = fopen("php://stdin","r");
// set up enchant (from PHP manual)
$tag = 'en_CA';
$r = enchant_broker_init();
$bprovides = enchant_broker_describe($r);
echo "Current broker provides the following backend(s):\n";
print_r($bprovides);
$dicts = enchant_broker_list_dicts($r);
print_r($dicts);
if (enchant_broker_dict_exists($r,$tag)) {
    $d = enchant_broker_request_dict($r, $tag);
    $dprovides = enchant_dict_describe($d);
    echo "dictionary $tag provides:\n";
} else {
  cleanup();
  die ("Cannot set up the spell checker\n");
}
// look for the text to be checked
$opf_file = "./OEBPS/content.opf";
if (!file_exists($opf_file)) {
  cleanup();
  die("Cannot find the OPF file\n");
} else {
  echo "Found it!\n";
  $xml = simplexml_load_file($opf_file);
  foreach ($xml->manifest->item as $mi) {
    if ($mi['media-type']=='application/xhtml+xml') {
      echo "Found ".$mi['href']."\n";
      if (substr($mi['href'],0,4) == 'part') {
          echo "Need to spell check ".$mi['href']."\n";
          echo scan_chap("./OEBPS/".$mi['href']);
      }
    }
  }
}
function cleanup() {
global $d, $r;
  enchant_broker_free_dict($d);
  enchant_broker_free($r);
}
function scan_chap($chap) {
  echo "Trying to spell check section $chap \n";
  if (!file_exists($chap)) {
    echo "Cannot find the chapter $chap\n";
  } else {
    echo "Found it!\n";
    $xml = simplexml_load_file($chap);
    $i = 0;
    foreach ($xml->body->div->p as $para) {
      echo $para."\n";
      // need to spell check the contents of $para
      spell_check(trim($para));
      $i++;
      if ($i > 5) break;
    }
  }
  return "Done...\n";
}
function spell_check($para) {
global $console, $d;
  $para = str_replace("  "," ",$para);
  $para = str_replace(".","",$para);
  $para = $para." ";
  echo "Checking text : $para\n";
  $start = 0;
  while ($pos !== false) {
    $pos = strpos($para," ",$start);
    echo "Found $pos\n";
    if (!$pos) break;
    $len = $pos-$start;
    $theword = substr($para,$start,$len);
    // tidy up theword which may contain punctuation
    $punc = array(':',';',',','"','?','!');
    $theword = str_replace($punc,"",$theword);
    //
    if ((strlen($theword) > 0) and (!is_numeric($theword))) {
      if ($wordcorrect = enchant_dict_check($d, $theword)) {
          echo "$theword is OK!\n";
      } else {
          $suggs = enchant_dict_suggest($d, $theword);
          echo "Suggestions for <$theword>:\n";
          //print_r($suggs);
          $max = 5;
          foreach ($suggs as $k=>$sugg) {
            echo "$k => $sugg\n";
            if ($k > $max) break;
          }
          $inp = fgets($console,1024);
      }
    }
    $start += $len+1;
  }
}
?>

このコードでは、スペル・チェック・プロセス中にキーボードから対話情報を取得できるように、標準入力へのファイル・ポインターを宣言するところから始めます。これに続くのは、辞書との接続を確立するセクションです。tag 変数が en-CA を示していることに注意してください。つまり、ここではスペルをカナダ英語に設定しているため、スペル・チェッカーは、例えば color ではなく colour を選択し、acknowledgment ではなく acknowledgement を選択することになりますが、tag は en-US に設定されることとのほうが一般的です。辞書に接続した後、スクリプトはリスト 1 と同じように HTML テキスト・ファイルを検索しますが、今回はページ番号の <div> タグを検索する代わりに、実際のテキストを持つパラグラフを検索します。

実際のスペル・チェックを行う前に、スクリプトはパラグラフのテキストをクリーンアップして、パラグラフを管理しやすくします。つまり、ここで目標としているのは単語単位で調べることなので、長いスペースを削除し、ピリオドとカンマを削除して管理しやすくするということです。クリーンアップが完了すると、実際のスペル・チェックが開始され、パラグラフ内の数字を除く単語を順に移動しながら、各単語を辞書と比較します。辞書にその単語が含まれていなければ、スクリプトはよりふさわしい単語を置換候補として提示します。この場合、スクリプトが提示するのは最初の 5 つの候補だけです。スクリプトは、問題のある単語が見つかるごとに停止し、キーボードからのユーザー入力を待ちます。ここから後は、単語を変更するためのコードや、その単語を一度またはセッション全体で無視するためのコードなどを追加することができます。


まとめ

Sigil、そして XML とスペリング・ライブラリーを使用した PHP スクリプトは、標準 EPUB チェック・ルーチンでは検出できない問題を見つけ出して修正するのに役立つツールです。これらの 2 次的な問題が真の問題であるのか、あるいはたいしたことのない単に見掛け上不都合な問題であるのかどうかは、その文書を使用しているコンテキストと、ハードウェア・リーダーおよびそのソフトウェアがオンザフライでこれらの問題を解決する能力によって決まります。

参考文献

学ぶために

製品や技術を入手するために

  • Sigil: EPUB フォーマットで書籍を編集するために設計された、このマルチプラットフォーム対応の WYSIWYG 電子書籍エディターについて調べてください。
  • Enchant: 複数のライブラリーをベースに均一性および適合性を提供するこのラッパーを使用したスペル・チェックについて学んでください。
  • EpubCheck プロジェクト: IDPF EPUB ファイルを対象としたこの便利な検証ツールについて調べてください。EPUB でのさまざまなタイプの問題を検出できます。
  • IBM 製品の評価版: DB2、Lotus、Rational、Tivoli、および WebSphere のアプリケーション開発ツールとミドルウェア製品を体験するには、評価版をダウンロードするか、IBM SOA Sandbox のオンライン試用版を試してみてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source
ArticleID=762698
ArticleTitle=EPUB の完成度を上げる
publish-date=10072011