ebook_tools 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|