ebook_tools 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +7 -0
- data/ebook_tools.gemspec +1 -1
- data/lib/header_detect.rb +21 -14
- data/lib/txt_book.rb +72 -2
- data/lib/utils.rb +28 -13
- metadata +1 -1
    
        data/CHANGELOG
    CHANGED
    
    
    
        data/ebook_tools.gemspec
    CHANGED
    
    
    
        data/lib/header_detect.rb
    CHANGED
    
    | @@ -10,7 +10,7 @@ | |
| 10 10 | 
             
            #   根据不同的类型,对结构信息的提取采用不同的处理手段。
         | 
| 11 11 | 
             
            #
         | 
| 12 12 | 
             
            # 有效的标题信息应该符合以下规则:
         | 
| 13 | 
            -
            #  1. 标题应该不包含完整的句子(应该不包含句子分隔符,例如“。" | 
| 13 | 
            +
            #  1. 标题应该不包含完整的句子(应该不包含句子分隔符,例如“。")
         | 
| 14 14 | 
             
            #  2. 应该包含结构信息表述,具体如下:
         | 
| 15 15 | 
             
            #    文本描述:
         | 
| 16 16 | 
             
            #     卷:  以"第xxx卷"开始
         | 
| @@ -54,34 +54,39 @@ module HeaderDetect | |
| 54 54 | 
             
                text =~ /[\.。!\?!?]/
         | 
| 55 55 | 
             
              end
         | 
| 56 56 |  | 
| 57 | 
            +
              def valid_title?(text)
         | 
| 58 | 
            +
                text = text.gsub(/^\d+(\.\d)*\s/,'')
         | 
| 59 | 
            +
                text =~ /[\.。]/
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
             | 
| 57 62 | 
             
              def guess_volume?(text,options={})
         | 
| 58 | 
            -
                return false if  | 
| 63 | 
            +
                return false if valid_title?(text)
         | 
| 59 64 | 
             
                return true if (text =~ /^第.{1,3}卷/ || text =~ /^卷\s*[\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹIi]/)
         | 
| 60 65 | 
             
                text = text.downcase
         | 
| 61 66 | 
             
                return true if text =~ /^volume\s*[\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹIi]/
         | 
| 62 67 | 
             
              end
         | 
| 63 68 |  | 
| 64 69 | 
             
              def guess_part?(text,options={})
         | 
| 65 | 
            -
                return false if  | 
| 70 | 
            +
                return false if valid_title?(text)
         | 
| 66 71 | 
             
                return true if text =~ /^第.{1,3}[部篇]/
         | 
| 67 72 | 
             
                text = text.downcase
         | 
| 68 73 | 
             
                return true if text =~ /^part\s*[\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹIi]/
         | 
| 69 74 | 
             
              end
         | 
| 70 75 |  | 
| 71 76 | 
             
              def guess_chapter?(text)
         | 
| 72 | 
            -
                return false if  | 
| 73 | 
            -
                return true if text =~ /^第.{1,4}[ | 
| 77 | 
            +
                return false if valid_title?(text)
         | 
| 78 | 
            +
                return true if text =~ /^第.{1,4}[章回则讲]/
         | 
| 74 79 | 
             
                text = text.downcase
         | 
| 75 80 | 
             
                return true if text =~ /^chapter\s*[\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹIi]/
         | 
| 76 81 | 
             
              end
         | 
| 77 82 |  | 
| 78 83 | 
             
              def guess_section?(text)
         | 
| 79 | 
            -
                return false if  | 
| 84 | 
            +
                return false if valid_title?(text)
         | 
| 80 85 | 
             
                return true if text =~ /^第.{1,3}[节]/
         | 
| 81 86 | 
             
              end
         | 
| 82 87 |  | 
| 83 88 | 
             
              def guess_preface?(text)
         | 
| 84 | 
            -
                return false if  | 
| 89 | 
            +
                return false if valid_title?(text)
         | 
| 85 90 | 
             
                return true if text =~ /^前\s*言$/
         | 
| 86 91 | 
             
                return true if text =~ /^序\s*言$/
         | 
| 87 92 | 
             
                return true if text =~ /^序$/
         | 
| @@ -94,7 +99,7 @@ module HeaderDetect | |
| 94 99 | 
             
              end
         | 
| 95 100 |  | 
| 96 101 | 
             
              def guess_index?(text)
         | 
| 97 | 
            -
                return false if  | 
| 102 | 
            +
                return false if valid_title?(text)
         | 
| 98 103 | 
             
                return true if text =~ /^索\s*引$/
         | 
| 99 104 | 
             
                return true if text =~ /^索\s*引\s*[\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹIi]/
         | 
| 100 105 | 
             
                text = text.downcase
         | 
| @@ -103,7 +108,7 @@ module HeaderDetect | |
| 103 108 | 
             
              end
         | 
| 104 109 |  | 
| 105 110 | 
             
              def guess_appendix?(text)
         | 
| 106 | 
            -
                return false if  | 
| 111 | 
            +
                return false if valid_title?(text)
         | 
| 107 112 | 
             
                return true if text =~ /^附\s*录$/
         | 
| 108 113 | 
             
                return true if text =~ /^附\s*录\s*[\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹIiA-Za-z]/
         | 
| 109 114 | 
             
                text = text.downcase
         | 
| @@ -112,7 +117,7 @@ module HeaderDetect | |
| 112 117 | 
             
              end
         | 
| 113 118 |  | 
| 114 119 | 
             
              def guess_glossary?(text)
         | 
| 115 | 
            -
                return false if  | 
| 120 | 
            +
                return false if valid_title?(text)
         | 
| 116 121 | 
             
                return true if text =~ /^术\s*语$/
         | 
| 117 122 | 
             
                return true if text =~ /^术\s*语\s*[\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹIi]/
         | 
| 118 123 | 
             
                text = text.downcase
         | 
| @@ -121,7 +126,7 @@ module HeaderDetect | |
| 121 126 | 
             
              end
         | 
| 122 127 |  | 
| 123 128 | 
             
              def guess_digital_section?(text)
         | 
| 124 | 
            -
                return false if  | 
| 129 | 
            +
                return false if valid_title?(text)
         | 
| 125 130 | 
             
                matcher = text.match(/^(\d+\.)+[\d]\s*(.*)/)
         | 
| 126 131 | 
             
                if matcher
         | 
| 127 132 | 
             
                  return false if matcher[2].length == 0
         | 
| @@ -131,7 +136,7 @@ module HeaderDetect | |
| 131 136 | 
             
              end
         | 
| 132 137 |  | 
| 133 138 | 
             
              def guess_digital_header?(text)
         | 
| 134 | 
            -
                return false if  | 
| 139 | 
            +
                return false if valid_title?(text)
         | 
| 135 140 | 
             
                matcher = text.match(/(^\d+(\.\d)*\s)(.*)/)
         | 
| 136 141 | 
             
                if matcher
         | 
| 137 142 | 
             
                  return false if matcher[3].length == 0
         | 
| @@ -150,12 +155,14 @@ module HeaderDetect | |
| 150 155 | 
             
                return :volume if guess_volume?(text)
         | 
| 151 156 | 
             
                return :part if guess_part?(text)
         | 
| 152 157 | 
             
                return :chapter if guess_chapter?(text)
         | 
| 153 | 
            -
                return : | 
| 158 | 
            +
                return :sect1 if guess_section?(text)
         | 
| 154 159 | 
             
                return :preface if guess_preface?(text)
         | 
| 155 160 | 
             
                return :appendix if guess_appendix?(text)
         | 
| 156 161 | 
             
                return :index if guess_index?(text)
         | 
| 157 162 | 
             
                return :glossary if guess_glossary?(text)
         | 
| 158 | 
            -
                 | 
| 163 | 
            +
                if type = guess_digital_section?(text)
         | 
| 164 | 
            +
                  return type
         | 
| 165 | 
            +
                end
         | 
| 159 166 | 
             
              end
         | 
| 160 167 |  | 
| 161 168 | 
             
            end
         | 
    
        data/lib/txt_book.rb
    CHANGED
    
    | @@ -12,6 +12,36 @@ require 'cgi' | |
| 12 12 | 
             
            #   5. 文档需要包含结构信息(例如: 卷、篇、部分、章(回)节或者有连续的序号)
         | 
| 13 13 | 
             
            #   6. 每个结构信息都应该独立成行。
         | 
| 14 14 | 
             
            #
         | 
| 15 | 
            +
            #
         | 
| 16 | 
            +
            #== 文本书籍现状
         | 
| 17 | 
            +
            #  目前来说,文本书籍的目录结构情况并非如想像的完整,主要存在以下几方面问题:
         | 
| 18 | 
            +
            #   1. 目录结构问题
         | 
| 19 | 
            +
            #      文本文件中包含的目录情况主要有以下几种:
         | 
| 20 | 
            +
            #         * 不包含目录结构。 典型的如诗歌、散文类电子书
         | 
| 21 | 
            +
            #         * 包含目录结构,同时列出目录。 典型的如在文件开头部分列出了电子书的目录
         | 
| 22 | 
            +
            #         * 目录结构信息是以节、讲、则组成的。
         | 
| 23 | 
            +
            #         * 目录信息被特殊的信息包裹。例如: "第一节: 第一章xxxxxxx",">>>>"等
         | 
| 24 | 
            +
            #         * 目录信息本身就有误。有些书本身就是不完整的书,目录信息不完整。
         | 
| 25 | 
            +
            #         * 信息层级结构错位,没有按照卷(篇)、章(回)、节的顺序来组织,或因为部分信息被不正确的关联到其他内容后面,导致无法识别。
         | 
| 26 | 
            +
            #
         | 
| 27 | 
            +
            #   2. 内容问题
         | 
| 28 | 
            +
            #       内容问题主要来自两个问题:
         | 
| 29 | 
            +
            #         * 页眉、页脚问题。很多从PDF转换过来的书都包含了页眉页脚
         | 
| 30 | 
            +
            #         * 断句问题。 很多PDF转换过来的电子书都有断句问题。
         | 
| 31 | 
            +
            #
         | 
| 32 | 
            +
            #== 解决办法
         | 
| 33 | 
            +
            #=== 问题1: 不包含目录结构
         | 
| 34 | 
            +
            #     这类书籍没有办法进行处理
         | 
| 35 | 
            +
            #
         | 
| 36 | 
            +
            #=== 问题2: 包含目录结构,同时列出目录
         | 
| 37 | 
            +
            #     这类书籍先要检测列出的目录并将该内容从文件内容中剥离,防止重复提取。
         | 
| 38 | 
            +
            #     有些列出的目录并不是完全符合目录结构信息,在这里只能进行猜测。猜测规则:
         | 
| 39 | 
            +
            #       1. 假设列出的目录总行数不会超过50行
         | 
| 40 | 
            +
            #       2. 只要在50行内连续出现60%以上章节的信息即作为目录块
         | 
| 41 | 
            +
            #
         | 
| 42 | 
            +
            #=== 问题3: 目录结构信息是以节等组成
         | 
| 43 | 
            +
            #     将节、讲、则作为标题的构成部分
         | 
| 44 | 
            +
            # 
         | 
| 15 45 | 
             
            class TxtBook
         | 
| 16 46 | 
             
              include HeaderDetect
         | 
| 17 47 | 
             
              attr_reader :title,:author,:publisher,:pubdate,:isbn,:content
         | 
| @@ -34,7 +64,8 @@ class TxtBook | |
| 34 64 | 
             
                unless Utils.detect_utf8(content)
         | 
| 35 65 | 
             
                  content = Utils.to_utf8(content) 
         | 
| 36 66 | 
             
                end
         | 
| 37 | 
            -
             | 
| 67 | 
            +
             | 
| 68 | 
            +
                @content = preprocess_content(content)
         | 
| 38 69 | 
             
              end
         | 
| 39 70 |  | 
| 40 71 | 
             
              def struct_content
         | 
| @@ -72,6 +103,11 @@ class TxtBook | |
| 72 103 | 
             
              end
         | 
| 73 104 |  | 
| 74 105 | 
             
              private
         | 
| 106 | 
            +
              def preprocess_content(content)
         | 
| 107 | 
            +
                paras = extract_paras(content)
         | 
| 108 | 
            +
                paras.join("\n")
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
             | 
| 75 111 | 
             
              def extract_book_struct(content,options={})
         | 
| 76 112 | 
             
                paras = extract_paras(content)
         | 
| 77 113 | 
             
                # 检查书类型(text,digital,hybrid)
         | 
| @@ -301,7 +337,7 @@ EOS | |
| 301 337 | 
             
                toc.each do |item|
         | 
| 302 338 | 
             
                  children = ""
         | 
| 303 339 | 
             
                  if item[:children].any?
         | 
| 304 | 
            -
                    children = gen_toc(item[:children] | 
| 340 | 
            +
                    children = gen_toc(item[:children],&block)
         | 
| 305 341 | 
             
                  end
         | 
| 306 342 | 
             
                  doc_toc << block.call(item,children)
         | 
| 307 343 | 
             
                end
         | 
| @@ -369,8 +405,42 @@ EOS | |
| 369 405 | 
             
                return paras if content.blank?
         | 
| 370 406 | 
             
                content.each_line do |line|
         | 
| 371 407 | 
             
                  text = Utils.clean_text(line)
         | 
| 408 | 
            +
                  text = clean_title(text)
         | 
| 372 409 | 
             
                  paras << text if text.length > 0
         | 
| 373 410 | 
             
                end
         | 
| 374 411 | 
             
                paras
         | 
| 375 412 | 
             
              end
         | 
| 413 | 
            +
             | 
| 414 | 
            +
              def clean_title(text)
         | 
| 415 | 
            +
                if text =~ /^正文.*/
         | 
| 416 | 
            +
                  temp_text = text.sub(/^正文/,'')
         | 
| 417 | 
            +
                  temp_text = Utils.clean_text(temp_text)
         | 
| 418 | 
            +
                  if guess_header?(temp_text)
         | 
| 419 | 
            +
                    text = temp_text
         | 
| 420 | 
            +
                  end
         | 
| 421 | 
            +
                end
         | 
| 422 | 
            +
                text
         | 
| 423 | 
            +
              end
         | 
| 424 | 
            +
             | 
| 425 | 
            +
              # 清除文本内容中的目录信息
         | 
| 426 | 
            +
              def clean_toc(paras)
         | 
| 427 | 
            +
                start_point = nil
         | 
| 428 | 
            +
                cur_point = nil
         | 
| 429 | 
            +
                paras.each_with_index do |para, index|
         | 
| 430 | 
            +
                  if guess_header?(para)
         | 
| 431 | 
            +
                    if start_point.nil?
         | 
| 432 | 
            +
                      start_point = index
         | 
| 433 | 
            +
                    end
         | 
| 434 | 
            +
                    cur_point = index        
         | 
| 435 | 
            +
                  else
         | 
| 436 | 
            +
                    if start_point && cur_point && (cur_point - start_point) > 0
         | 
| 437 | 
            +
                    end
         | 
| 438 | 
            +
                  end
         | 
| 439 | 
            +
                end
         | 
| 440 | 
            +
             | 
| 441 | 
            +
                if start_point && (cur_point - start_point) > 0
         | 
| 442 | 
            +
                  paras = paras[0...start_point] + paras[index..-1]
         | 
| 443 | 
            +
                end
         | 
| 444 | 
            +
                paras       
         | 
| 445 | 
            +
              end
         | 
| 376 446 | 
             
            end
         | 
    
        data/lib/utils.rb
    CHANGED
    
    | @@ -17,13 +17,16 @@ end | |
| 17 17 |  | 
| 18 18 | 
             
            module Utils
         | 
| 19 19 | 
             
              extend self
         | 
| 20 | 
            -
             | 
| 20 | 
            +
              
         | 
| 21 21 | 
             
              # fixed_page_break
         | 
| 22 22 | 
             
              #  修复文本中的异常中断
         | 
| 23 23 | 
             
              # parameters:
         | 
| 24 24 | 
             
              #   +page_text+   文本内容
         | 
| 25 25 | 
             
              def fixed_page_break(page_text,options={})
         | 
| 26 26 | 
             
                length = options[:length] || guess_content_line_length(page_text)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                return page_text if (length > 80 || length <=0)  #每行超过80个文字的默认为不需要修复
         | 
| 29 | 
            +
             | 
| 27 30 | 
             
                page_lines = text_to_array(page_text)
         | 
| 28 31 |  | 
| 29 32 | 
             
                lines = []
         | 
| @@ -49,6 +52,8 @@ module Utils | |
| 49 52 | 
             
                break_lines = []
         | 
| 50 53 | 
             
                lines = text_to_array(text)
         | 
| 51 54 | 
             
                length = options[:length] || guess_content_line_length(text)
         | 
| 55 | 
            +
                return break_lines if length <= 0
         | 
| 56 | 
            +
             | 
| 52 57 | 
             
                lines.each do |line|
         | 
| 53 58 | 
             
                  if line.length > 0
         | 
| 54 59 | 
             
                    unless line_closed?(line,length)
         | 
| @@ -84,19 +89,16 @@ module Utils | |
| 84 89 | 
             
              # line_closed?
         | 
| 85 90 | 
             
              #   判断是否为一行的结束。如何算一行结束?
         | 
| 86 91 | 
             
              #    * 以句子结束符结尾的
         | 
| 92 | 
            +
              #    * 猜测是一种标题
         | 
| 87 93 | 
             
              #    * 非结束符结束,但长度小于猜测的行长度的
         | 
| 88 94 | 
             
              # parameters:
         | 
| 89 95 | 
             
              #   +text+  一行的文本内容
         | 
| 90 96 | 
             
              def line_closed?(text,length=60)
         | 
| 91 97 | 
             
                return true if end_mark?(text)
         | 
| 92 98 | 
             
                short_text = text.gsub(/[\.\-—. ]/,'')
         | 
| 93 | 
            -
                if short_text  | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
                else
         | 
| 97 | 
            -
                  return true if short_text.length > 80
         | 
| 98 | 
            -
                  return true if short_text.length < length
         | 
| 99 | 
            -
                end
         | 
| 99 | 
            +
                return true if short_text.length > 80
         | 
| 100 | 
            +
                return true if HeaderDetect.guess_header?(short_text)
         | 
| 101 | 
            +
                return true if short_text.length < length
         | 
| 100 102 | 
             
                false
         | 
| 101 103 | 
             
              end
         | 
| 102 104 |  | 
| @@ -113,6 +115,12 @@ module Utils | |
| 113 115 | 
             
                end
         | 
| 114 116 | 
             
              end
         | 
| 115 117 |  | 
| 118 | 
            +
              # 猜测内容长度,用于修复PDF导出时出现断句的问题
         | 
| 119 | 
            +
              #  PDF导出文本中的断句特点:
         | 
| 120 | 
            +
              #    * 文本长度小于80
         | 
| 121 | 
            +
              #    * 相同长度的句子一定高于某个比例
         | 
| 122 | 
            +
              # 返回值:
         | 
| 123 | 
            +
              #   如果识别长度则返回识别的长度,否则返回0
         | 
| 116 124 | 
             
              def guess_content_line_length(content)
         | 
| 117 125 | 
             
                line_length = 0    
         | 
| 118 126 | 
             
                return line_length if content.blank?
         | 
| @@ -120,11 +128,16 @@ module Utils | |
| 120 128 | 
             
                content.each_line{|line|
         | 
| 121 129 | 
             
                  lengths << line.length
         | 
| 122 130 | 
             
                }
         | 
| 123 | 
            -
             | 
| 124 | 
            -
                 | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 131 | 
            +
             | 
| 132 | 
            +
                grouped = lengths.group_by{|i| i}
         | 
| 133 | 
            +
                sorted = grouped.map{|k,v| [k,v.count]}.sort_by{|i| i[1]}.reverse
         | 
| 134 | 
            +
                sorted.each do |length, count|
         | 
| 135 | 
            +
                  if ((count.to_f / lengths.count.to_f) > 0.1) && length < 80
         | 
| 136 | 
            +
                    line_length = (length * 0.8).to_i
         | 
| 137 | 
            +
                    break
         | 
| 138 | 
            +
                  end
         | 
| 127 139 | 
             
                end
         | 
| 140 | 
            +
                
         | 
| 128 141 | 
             
                return line_length
         | 
| 129 142 | 
             
              end
         | 
| 130 143 |  | 
| @@ -133,7 +146,9 @@ module Utils | |
| 133 146 | 
             
              def clean_text(text)
         | 
| 134 147 | 
             
                return text if text.nil?
         | 
| 135 148 | 
             
                text = text.strip
         | 
| 136 | 
            -
                text.gsub("\n",'')
         | 
| 149 | 
            +
                text = text.gsub("\n",'')
         | 
| 150 | 
            +
                #去除全角空格
         | 
| 151 | 
            +
                text.gsub(/^ */,'')
         | 
| 137 152 | 
             
              end
         | 
| 138 153 |  | 
| 139 154 | 
             
              # escape_html
         |