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 CHANGED
@@ -1,3 +1,10 @@
1
+ 0.1.1 2013.5.26
2
+ fix bug: 提取目录结构时文本内容开始部分存在全角空格而无法正确提取目录结构
3
+ fix bug: 无法提取文本目录中包含“?”等标点符号的目录
4
+ fix bug: 识别以“正文”开始的标题
5
+ fix bug: 调整段落自动修复
6
+ fix bug: 修正以讲、则为目录结构
7
+
1
8
  0.1.0 2013.4.10
2
9
  refactor struct extract
3
10
 
data/ebook_tools.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{ebook_tools}
5
- s.version = '0.1.0'
5
+ s.version = '0.1.1'
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Aaron"]
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 hav_complete_sentence?(text)
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 :section if guess_section?(text)
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
- return :section if guess_digital_section?(text)
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
- @content = content
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],block)
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 =~ /\p{Han}/
94
- return true if short_text.length > 80
95
- return true if short_text.length < length * 2
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
- lengths.sort!
124
- while true
125
- line_length = lengths.pop
126
- break if line_length < 80
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ebook_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: