改进 EPUB

查找并更正 EPUB 文件中的问题

在 EPUB 文档中,一些问题无法使用常规的验证方法检测。只要文档被证实是格式良好的 XML 并遵守 EPUB 标准,那么它可能看起来是正确的,但却无法在电子阅读器中正确读取。示例包括损坏的段落、不规范的页码和由 OCR 扫描导致的拼写错误。然而,您可以使用两种方法查看并更正错误:使用 EPUB 编辑器 Sigil,以及结合使用 PHP 与 SimpleXML 和 Enchant 库。正则表达式是有效处理的关键。

Colin Beckingham, 作家兼研究人员, Freelance

Colin Beckingham 居住在加拿大安大略省,是一位自由研究人员、作家和程序员。他拥有金斯顿皇后大学和温莎大学的学位,对园艺、赛马、教育、公共服务、零售和旅游/观光领域都有涉猎。他是数据库应用程序的作者,也是大量报纸、杂志和在线文章的撰稿人,他的研究兴趣包括 Linux 上的开源编程以及语音控制应用程序。您可通过 colbec@start.ca 与 Colin 联系。



2011 年 10 月 24 日

EPUB 格式是一种呈现文档的有效方法。它的 XML 结构可确保文档组件位于相应的位置,并且能在各种设备上合理地显示。关于 EPUB 的介绍,请参阅 参考资料 中 Liza Daly 编写的文章。

常用缩略语

  • GUI:图形用户界面
  • OCR:光学字符识别
  • HTML:超文本标记语言
  • WYSIWYG:所见即所得
  • XML:可扩展标记语言

这些文档在两种级别上会发生故障:

  • 在基本级别,XML 标记或内容是损坏的
  • 在更加细微的级别上,检查 XML 无法检测的

对于前一种问题,内部的 EPUB 是损坏的,您可以使用 EpubCheck 项目(参阅 参考资料 相应的链接)。本文剩余内容将探讨第二种类型的问题,这些可能是读者非常厌烦的问题。

XML 执行的严格控制也无能为力。XML 允许许多问题存在,这些问题尽管不足以导致软件故障,但会影响流畅的阅读。很容易了解这些问题是如何发生的,如果发布者在打印页面上使用 OCR 将它转换为文本格式,那么打印页面的所有异常情况都会出现,包括字体不兼容所导致的错误。在商业条件下,编辑人员会手动检查结果,以生成一个改进的版本。但是由于该产品是专门设计用于免费、开源发行版本的,发行者承担不起这些成本。所以,您得到的电子阅读器最终版本虽然不错,但还有改进的空间。示例包括破坏的段段、空白页、奇怪的页码以及拼写错误。

从开发人员的角度讲,挑战是在于如何使用 EPUB 的结构来解决这些问题。本文探讨如何使用 Sigil EPUB 编辑器解决其中一些问题,以及如何结合使用 PHP 与 SimpleXML 和拼写库来解决其他的一些问题。

破坏的段落和空白页

我们将破坏的段落作为一个次要问题示例。在 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>

扫描器读到了页末,会添加一个段落标记,而不管它是否用于确定页面句法的完整性,然后从下一页的开头开始,确定从一个新的段落开始,同样不管它是否适合。孤立的段落会使代码保持完整,但会导致不完整的段落。在电子阅读器上,用户可能看到,在同一个设备页面上的两节没有显示页面标记,而是像两个独立的段落一样分开的。

类似地,考虑空白页:

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

上述片段中的页面 129 是否真的存在?可能有必要将它保持空白,否则,在仅需要一个页面时必须生成两个页面,这样很不方便。

拼写错误是另一种不同的问题,您需要对比两个不同的单词列表,而不是查找复杂的模式。可以使用脚本方法独立处理此问题。


Sigil

Sigil(参阅 参考资料 获取网站和支持页面)是一种 WYSIWYG EPUB 编辑器,它可找到模式匹配类型的错误,并且允许程序员更正它们。请参阅 正则表达式边栏 快速了解正则表达式,参阅 参考资料 获取更多的详细信息。

正则表达式

正则表达式提供了使用模式匹配技术进行文本搜索和替换的强大方式。它的语法简洁,所以您需要小心使用以避免导致不良的影响。

正则表达式的一个示例是 [^.]</p>,它搜索前面没有句点的段落结尾标记。这可以是一个问题,也可能不是一个问题。

在此正则表达式中,方括号 ([]) 中包含仅会使用的一组字符,脱字符 (^) 表示不是任何以下字符,这组字符中的句点 (.) 代表它自己,括号外的剩余字符也是如此。

参阅 参考资料 了解关于这个实用工具更深入的讨论。

可能无法从 Linux® 存储库获得 Sigil,但是可以通过一个预编译的库或源文件的形式提供。在 GUI 中,单击 File > Open 直接打开您的 EPUB。提取 EPUB 并在左侧显示一个组件文件目录,在右侧显示一个浏览器窗格,在此窗格中可以显示各个文件的内容,正如您在电子阅读器或样式代码中所看到的那样。后者是在查找和更正问题过程中使用的一项重要功能。

选择 EPUB 所包含的一个 HTML 文件,双击它在浏览器中打开。然后单击 View > Code View 显示文件的隐藏代码。现在可以看见所有的标记。

假设您希望找到孤立的段落块。您搜索的标准是前面没有正常的句子结束字符的段落结束标记 </p> 。这些最常用的句子结束字符是句点。Sigil 提供了一种搜索功能 (Edit > Find),在正常的搜索模式下允许您查找如 .</p> 字符串,但它对查找前面没有句点的段落结束标记没有帮助。对此,您需要正则表达式搜索模式,当您单击 More 时就会看到它。导航到浏览器窗口中的代码顶部,执行以下步骤:

  1. 选择 Down 作为方向。
  2. 选择 Regular expression 作为搜索模式。
  3. 键入 [^.]</p> 作为您的 Find what 字符串。
  4. 单击 Find Next

此过程应该会找到您所要搜索的内容(如果存在)。如果没有找到任何结果,您可能要临时创建一个结果来检查搜索功能是否有效。

使用此技术一段时间后,您很快就会发现段落可使用句点以外的字符合理地结束。您将发现双引号 (")、感叹号 (!)、问号 (?) 以及可能其他一些字符都能满足完整句子的要求。实现这一点对于正则表达式而言并不是问题。因为方括号表示一个组,所以如果您将 Find what 更改为 [^.?!"]</p>,搜索通常会接受在段末具有句点、问号、感叹号或双引号的任何内容,将其他内容标记为错误。

破坏段落的另一个指示可能是,段落以 <p> 开头,后跟一个小写字母符号。这个版本的表达式可以是 <p>[a-z]。 另一个是 <p>[0-9],它查找以数字开始的段落。这个符号可能在扫描仪挑选一个在电子阅读器上下文中可能不再相关的页码时有效。

您决定如何修复其中一个错误,这是另一个事项。如果一个页面标记分离为两部分,您可以将该标记移动到真实段落之前或之后,将两部分重新合并在一起形成一个完整的段落。得到的页码是近似的,但不是很准确。

搜索页面标记是一个类似过程。再次使用正则表达式选项,如果 Find whatpage-[0-9]+,编辑器会搜索任何以文字字符 p、a、g、edash 开头,并且后跟至少一个或者多个从 0 到 9 中的数字字符的段落。

您很容易发现的一种有趣的分隔符,它把一个单词、段落和页面同时分开。打印版本用连字符或破折号表示分隔符,这很容易在代码视图中看到和搜索:

<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 页的页码,这通常不为 “第 1 页”,验证每个后续页面是否从第 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 页,因为首页通常不是显示为第 1 页,在此值存储在全局第 1 页变量之后,您可以继续预测下一页的页码。如果它不是想要的页码,脚本生成一个错误并继续检查。

此脚本不会尝试更改文本。它仅标记它认为可能需要您注意的内容。


使用 PHP、XML 和 Enchant 进行拼写检查

拼写是一个不同的问题。对于此问题,您真正会关心 Upon 等事件,OCR 将它读取为 TJponIJpon,虽然他们很近似但却是错误的。它可能以一些备选词语的形式提供,拼写例程会觉得它很陌生,以至于它提出的建议为不相似或没有帮助。

拼写例程逐个检查单词,将它们与标准的已知列表对比,指出不匹配的单词,提供建议并允许您进行更改。Sigil 可在 EPUB 包中的多个文档之间替换特定的字符串,但您需要结合使用脚本引擎(比如 PHP、Perl、Python 等)和专家库来实现更细化的控制。

较新版的 PHP 现在都包含了必要的挂钩,这些挂钩是在使用 SimpleXML 深入研究 XML 和 HTML 文件时或在使用 Enchant 拼写管理器库时必要的部件。Enchant 能够管理多个不同的基础拼写列表。举例而言,它有助于区分英国英语与美国英语的拼写。

清单 2 中的脚本使用 清单 1 中相同的方法分别检查每个描述文件,这一次是逐段地仔细检查,依据已知拼写列表逐个单词地检查。它使用的是与 清单 1 中迭代 HTML 组件文件的相同方法,并添加需要的指令来访问字典。

清单 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,它在本例中把加拿大英语设置为首选项。结果是,检查器选择 colour 而不是 color,选择 acknowledgement 而不是 acknowledgment,等等。一种更标准的标记设置是 en-US。连接字典后,它对 HTML 文本文件执行一些搜索,像 清单 1 中一样,但这次没有查找页码 <div> 标记,它查找具有真实文本的段落。

在执行实际的拼写检查之前,该脚本清理段落文本,删除长空格、句点和逗号,以便段落更易于管理,因为我们的目标是逐个词检查。然后,实际的拼写检查首先在段落中逐个单词地进行,忽略数字,将单词与字典对比。如果字典不包含该单词,脚本建议并提供可作为更好的替代词语的单词。在本例中,脚本仅提供前五个替代单词。脚本在每个有问题的单词处都停下来,等待用户从键盘输入。在这时,您可以添加代码来执行更改、忽略一次、忽略整个会话,等等。


结束语

Sigil 和 PHP 脚本与 XML 和拼写库相结合,是在查找和修复使用常规 EPUB 检查例程无法检测的错误时非常有用的工具。这些次要的错误是真实的错误还是只是装饰性的问题,取决于文档的上下文,以及硬件阅读器和自身软件快速解决这些问题的能力。

参考资料

学习

  • 使用 EPUB 构建数字图书(Liza Daly,developerWorks,于 2008 年 11 月发布,2011 年 1 月更新):阅读关于 EPUB 的介绍以及一系列的 EPUB 资源。
  • 了解您的正则表达式,(Michael Stutz,developerWorks,2007 年 6 月):查阅这篇关于 UNIX® 系统上正则表达式的介绍。查找可帮助您了解如何为各种程序和语言构建正则表达式的可用工具和技术。
  • 本作者的更多文章(Colin Beckingham,developerWorks,2009 年 3 月至今):阅读关于 XML、语音识别、XHTML、PHP、SMIL 和其他技术的文章。
  • XML 新手入门获取您学习 XML 所需的资源。
  • developerWorks 的 XML 专区:查找您提升 XML 领域的技能(包括 DTD、模式和 XSLT)所需的资源。访问 XML 技术库 ,获得大量的技术文章和技巧、教程、标准和 IBM 红皮书。
  • IBM XML 认证:了解如何成为 XML 和相关技术方面的 IBM 认证开发人员。
  • developerWorks 技术活动网络广播:随时关注这些会议中涉及的技术。
  • Twitter 上的 developerWorks:立即加入,了解 developerWorks 上的活动信息。
  • developerWorks 播客:聆听面向软件开发人员的有趣访谈和讨论。
  • developerWorks 演示中心:观看演示,包括面向初学者的产品安装和设置演示,以及为经验丰富的开发人员提供的高级功能。

获得产品和技术

  • Sigil:了解这个多平台 WYSIWYG 电子书编辑器,它专为编辑 EPUB 格式的图书而设计。
  • Enchant:了解如何使用此包装器执行拼写检查,它在多个库上提供了统一性和一致性。
  • EpubCheck 项目:使用这个有用的工具验证 IDPF EPUB 文件。它可检测 EPUB 中许多类型的错误。
  • IBM 产品评估版本:下载或 在线试用 IBM SOA Sandbox,开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source
ArticleID=767365
ArticleTitle=改进 EPUB
publish-date=10242011