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
|