mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Smarten punctuation: Convert double dashes to em dashes. Preprocessing: Various tweaks
This commit is contained in:
commit
aec2ebb9a2
@ -353,7 +353,7 @@ class HTMLPreProcessor(object):
|
|||||||
(re.compile(r'((?<=</a>)\s*file:////?[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''),
|
(re.compile(r'((?<=</a>)\s*file:////?[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''),
|
||||||
|
|
||||||
# Center separator lines
|
# Center separator lines
|
||||||
(re.compile(u'<br>\s*(?P<break>([*#•]+\s*)+)\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group(1) + '</p>'),
|
(re.compile(u'<br>\s*(?P<break>([*#•✦]+\s*)+)\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group(1) + '</p>'),
|
||||||
|
|
||||||
# Remove page links
|
# Remove page links
|
||||||
(re.compile(r'<a name=\d+></a>', re.IGNORECASE), lambda match: ''),
|
(re.compile(r'<a name=\d+></a>', re.IGNORECASE), lambda match: ''),
|
||||||
@ -363,13 +363,11 @@ class HTMLPreProcessor(object):
|
|||||||
# Remove gray background
|
# Remove gray background
|
||||||
(re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>'),
|
(re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>'),
|
||||||
|
|
||||||
# Detect Chapters to match default XPATH in GUI
|
# Convert line breaks to paragraphs
|
||||||
(re.compile(r'<br>\s*(?P<chap>(<[ibu]>){0,2}\s*.?(Introduction|Chapter|Kapitel|Epilogue|Prologue|Book|Part|Dedication|Volume|Preface|Acknowledgments)\s*([\d\w-]+\s*){0,3}\s*(</[ibu]>){0,2})\s*(<br>\s*){1,3}\s*(?P<title>(<[ibu]>){0,2}(\s*\w+){1,4}\s*(</[ibu]>){0,2}\s*<br>)?', re.IGNORECASE), chap_head),
|
(re.compile(r'<br[^>]*>\s*'), lambda match : '</p>\n<p>'),
|
||||||
# Cover the case where every letter in a chapter title is separated by a space
|
(re.compile(r'<body[^>]*>\s*'), lambda match : '<body>\n<p>'),
|
||||||
(re.compile(r'<br>\s*(?P<chap>([A-Z]\s+){4,}\s*([\d\w-]+\s*){0,3}\s*)\s*(<br>\s*){1,3}\s*(?P<title>(<[ibu]>){0,2}(\s*\w+){1,4}\s*(</[ibu]>){0,2}\s*(<br>))?'), chap_head),
|
(re.compile(r'\s*</body>'), lambda match : '</p>\n</body>'),
|
||||||
|
|
||||||
# Have paragraphs show better
|
|
||||||
(re.compile(r'<br.*?>'), lambda match : '<p>'),
|
|
||||||
# Clean up spaces
|
# Clean up spaces
|
||||||
(re.compile(u'(?<=[\.,;\?!”"\'])[\s^ ]*(?=<)'), lambda match: ' '),
|
(re.compile(u'(?<=[\.,;\?!”"\'])[\s^ ]*(?=<)'), lambda match: ' '),
|
||||||
# Add space before and after italics
|
# Add space before and after italics
|
||||||
@ -455,9 +453,9 @@ class HTMLPreProcessor(object):
|
|||||||
# delete soft hyphens - moved here so it's executed after header/footer removal
|
# delete soft hyphens - moved here so it's executed after header/footer removal
|
||||||
if is_pdftohtml:
|
if is_pdftohtml:
|
||||||
# unwrap/delete soft hyphens
|
# unwrap/delete soft hyphens
|
||||||
end_rules.append((re.compile(u'[](\s*<p>)+\s*(?=[[a-z\d])'), lambda match: ''))
|
end_rules.append((re.compile(u'[](</p>\s*<p>\s*)+\s*(?=[[a-z\d])'), lambda match: ''))
|
||||||
# unwrap/delete soft hyphens with formatting
|
# unwrap/delete soft hyphens with formatting
|
||||||
end_rules.append((re.compile(u'[]\s*(</(i|u|b)>)+(\s*<p>)+\s*(<(i|u|b)>)+\s*(?=[[a-z\d])'), lambda match: ''))
|
end_rules.append((re.compile(u'[]\s*(</(i|u|b)>)+(</p>\s*<p>\s*)+\s*(<(i|u|b)>)+\s*(?=[[a-z\d])'), lambda match: ''))
|
||||||
|
|
||||||
# Make the more aggressive chapter marking regex optional with the preprocess option to
|
# Make the more aggressive chapter marking regex optional with the preprocess option to
|
||||||
# reduce false positives and move after header/footer removal
|
# reduce false positives and move after header/footer removal
|
||||||
@ -475,7 +473,7 @@ class HTMLPreProcessor(object):
|
|||||||
end_rules.append((re.compile(u'(?<=.{%i}[–—])\s*<p>\s*(?=[[a-z\d])' % length), lambda match: ''))
|
end_rules.append((re.compile(u'(?<=.{%i}[–—])\s*<p>\s*(?=[[a-z\d])' % length), lambda match: ''))
|
||||||
end_rules.append(
|
end_rules.append(
|
||||||
# Un wrap using punctuation
|
# Un wrap using punctuation
|
||||||
(re.compile(u'(?<=.{%i}([a-zäëïöüàèìòùáćéíóńśúâêîôûçąężı,:)\IA\u00DF]|(?<!\&\w{4});))\s*(?P<ital></(i|b|u)>)?\s*(<p.*?>\s*)+\s*(?=(<(i|b|u)>)?\s*[\w\d$(])' % length, re.UNICODE), wrap_lines),
|
(re.compile(u'(?<=.{%i}([a-zäëïöüàèìòùáćéíóńśúâêîôûçąężıãõñæøþðß,:)\IA\u00DF]|(?<!\&\w{4});))\s*(?P<ital></(i|b|u)>)?\s*(</p>\s*<p>\s*)+\s*(?=(<(i|b|u)>)?\s*[\w\d$(])' % length, re.UNICODE), wrap_lines),
|
||||||
)
|
)
|
||||||
|
|
||||||
for rule in self.PREPROCESS + start_rules:
|
for rule in self.PREPROCESS + start_rules:
|
||||||
@ -508,7 +506,15 @@ class HTMLPreProcessor(object):
|
|||||||
if is_pdftohtml and length > -1:
|
if is_pdftohtml and length > -1:
|
||||||
# Dehyphenate
|
# Dehyphenate
|
||||||
dehyphenator = Dehyphenator()
|
dehyphenator = Dehyphenator()
|
||||||
html = dehyphenator(html,'pdf', length)
|
html = dehyphenator(html,'html', length)
|
||||||
|
|
||||||
|
if is_pdftohtml:
|
||||||
|
from calibre.ebooks.conversion.utils import PreProcessor
|
||||||
|
pdf_markup = PreProcessor(self.extra_opts, None)
|
||||||
|
totalwords = 0
|
||||||
|
totalwords = pdf_markup.get_word_count(html)
|
||||||
|
if totalwords > 7000:
|
||||||
|
html = pdf_markup.markup_chapters(html, totalwords, True)
|
||||||
|
|
||||||
#dump(html, 'post-preprocess')
|
#dump(html, 'post-preprocess')
|
||||||
|
|
||||||
@ -554,5 +560,9 @@ class HTMLPreProcessor(object):
|
|||||||
html = smartyPants(html)
|
html = smartyPants(html)
|
||||||
html = html.replace(start, '<!--')
|
html = html.replace(start, '<!--')
|
||||||
html = html.replace(stop, '-->')
|
html = html.replace(stop, '-->')
|
||||||
|
# convert ellipsis to entities to prevent wrapping
|
||||||
|
html = re.sub('(?u)(?<=\w)\s?(\.\s?){2}\.', '…', html)
|
||||||
|
# convert double dashes to em-dash
|
||||||
|
html = re.sub('\s--\s', u'\u2014', html)
|
||||||
return substitute_entites(html)
|
return substitute_entites(html)
|
||||||
|
|
||||||
|
@ -6,8 +6,10 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from math import ceil
|
||||||
from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator
|
from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator
|
||||||
from calibre.utils.logging import default_log
|
from calibre.utils.logging import default_log
|
||||||
|
from calibre.utils.wordcount import get_wordcount_obj
|
||||||
|
|
||||||
class PreProcessor(object):
|
class PreProcessor(object):
|
||||||
|
|
||||||
@ -17,6 +19,9 @@ class PreProcessor(object):
|
|||||||
self.found_indents = 0
|
self.found_indents = 0
|
||||||
self.extra_opts = extra_opts
|
self.extra_opts = extra_opts
|
||||||
|
|
||||||
|
def is_pdftohtml(self, src):
|
||||||
|
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
|
||||||
|
|
||||||
def chapter_head(self, match):
|
def chapter_head(self, match):
|
||||||
chap = match.group('chap')
|
chap = match.group('chap')
|
||||||
title = match.group('title')
|
title = match.group('title')
|
||||||
@ -64,7 +69,7 @@ class PreProcessor(object):
|
|||||||
inspect. Percent is the minimum percent of line endings which should
|
inspect. Percent is the minimum percent of line endings which should
|
||||||
be marked up to return true.
|
be marked up to return true.
|
||||||
'''
|
'''
|
||||||
htm_end_ere = re.compile('</p>', re.DOTALL)
|
htm_end_ere = re.compile('</(p|div)>', re.DOTALL)
|
||||||
line_end_ere = re.compile('(\n|\r|\r\n)', re.DOTALL)
|
line_end_ere = re.compile('(\n|\r|\r\n)', re.DOTALL)
|
||||||
htm_end = htm_end_ere.findall(raw)
|
htm_end = htm_end_ere.findall(raw)
|
||||||
line_end = line_end_ere.findall(raw)
|
line_end = line_end_ere.findall(raw)
|
||||||
@ -101,12 +106,101 @@ class PreProcessor(object):
|
|||||||
with open(os.path.join(odir, name), 'wb') as f:
|
with open(os.path.join(odir, name), 'wb') as f:
|
||||||
f.write(raw.encode('utf-8'))
|
f.write(raw.encode('utf-8'))
|
||||||
|
|
||||||
|
def get_word_count(self, html):
|
||||||
|
word_count_text = re.sub(r'(?s)<head[^>]*>.*?</head>', '', html)
|
||||||
|
word_count_text = re.sub(r'<[^>]*>', '', word_count_text)
|
||||||
|
wordcount = get_wordcount_obj(word_count_text)
|
||||||
|
return wordcount.words
|
||||||
|
|
||||||
|
def markup_chapters(self, html, wordcount, blanks_between_paragraphs):
|
||||||
|
# Typical chapters are between 2000 and 7000 words, use the larger number to decide the
|
||||||
|
# minimum of chapters to search for
|
||||||
|
self.min_chapters = 1
|
||||||
|
if wordcount > 7000:
|
||||||
|
self.min_chapters = int(ceil(wordcount / 7000.))
|
||||||
|
#print "minimum chapters required are: "+str(self.min_chapters)
|
||||||
|
heading = re.compile('<h[1-3][^>]*>', re.IGNORECASE)
|
||||||
|
self.html_preprocess_sections = len(heading.findall(html))
|
||||||
|
self.log("found " + unicode(self.html_preprocess_sections) + " pre-existing headings")
|
||||||
|
|
||||||
|
# Build the Regular Expressions in pieces
|
||||||
|
init_lookahead = "(?=<(p|div))"
|
||||||
|
chapter_line_open = "<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*"
|
||||||
|
title_line_open = "<(?P<outer2>p|div)[^>]*>\s*(<(?P<inner4>font|span|[ibu])[^>]*>)?\s*(<(?P<inner5>font|span|[ibu])[^>]*>)?\s*(<(?P<inner6>font|span|[ibu])[^>]*>)?\s*"
|
||||||
|
chapter_header_open = r"(?P<chap>"
|
||||||
|
title_header_open = r"(?P<title>"
|
||||||
|
chapter_header_close = ")\s*"
|
||||||
|
title_header_close = ")"
|
||||||
|
chapter_line_close = "(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>"
|
||||||
|
title_line_close = "(</(?P=inner6)>)?\s*(</(?P=inner5)>)?\s*(</(?P=inner4)>)?\s*</(?P=outer2)>"
|
||||||
|
|
||||||
|
is_pdftohtml = self.is_pdftohtml(html)
|
||||||
|
if is_pdftohtml:
|
||||||
|
chapter_line_open = "<(?P<outer>p)[^>]*>(\s*<[ibu][^>]*>)?\s*"
|
||||||
|
chapter_line_close = "\s*(</[ibu][^>]*>\s*)?</(?P=outer)>"
|
||||||
|
title_line_open = "<(?P<outer2>p)[^>]*>\s*"
|
||||||
|
title_line_close = "\s*</(?P=outer2)>"
|
||||||
|
|
||||||
|
|
||||||
|
if blanks_between_paragraphs:
|
||||||
|
blank_lines = "(\s*<p[^>]*>\s*</p>){0,2}\s*"
|
||||||
|
else:
|
||||||
|
blank_lines = ""
|
||||||
|
opt_title_open = "("
|
||||||
|
opt_title_close = ")?"
|
||||||
|
n_lookahead_open = "\s+(?!"
|
||||||
|
n_lookahead_close = ")"
|
||||||
|
|
||||||
|
default_title = r"(<[ibu][^>]*>)?\s{0,3}([\w\'\"-]+\s{0,3}){1,5}?(</[ibu][^>]*>)?(?=<)"
|
||||||
|
|
||||||
|
chapter_types = [
|
||||||
|
[r"[^'\"]?(Introduction|Synopsis|Acknowledgements|Chapter|Kapitel|Epilogue|Volume\s|Prologue|Book\s|Part\s|Dedication|Preface)\s*([\d\w-]+\:?\s*){0,4}", True, "Searching for common Chapter Headings"],
|
||||||
|
[r"<b[^>]*>\s*(<span[^>]*>)?\s*(?!([*#•]+\s*)+)(\s*(?=[\d.\w#\-*\s]+<)([\d.\w#-*]+\s*){1,5}\s*)(?!\.)(</span>)?\s*</b>", True, "Searching for emphasized lines"], # Emphasized lines
|
||||||
|
[r"[^'\"]?(\d+(\.|:)|CHAPTER)\s*([\dA-Z\-\'\"#,]+\s*){0,7}\s*", True, "Searching for numeric chapter headings"], # Numeric Chapters
|
||||||
|
[r"([A-Z]\s+){3,}\s*([\d\w-]+\s*){0,3}\s*", True, "Searching for letter spaced headings"], # Spaced Lettering
|
||||||
|
[r"[^'\"]?(\d+\.?\s+([\d\w-]+\:?\'?-?\s?){0,5})\s*", True, "Searching for numeric chapters with titles"], # Numeric Titles
|
||||||
|
[r"[^'\"]?(\d+|CHAPTER)\s*([\dA-Z\-\'\"\?!#,]+\s*){0,7}\s*", True, "Searching for simple numeric chapter headings"], # Numeric Chapters, no dot or colon
|
||||||
|
[r"\s*[^'\"]?([A-Z#]+(\s|-){0,3}){1,5}\s*", False, "Searching for chapters with Uppercase Characters" ] # Uppercase Chapters
|
||||||
|
]
|
||||||
|
|
||||||
|
# Start with most typical chapter headings, get more aggressive until one works
|
||||||
|
for [chapter_type, lookahead_ignorecase, log_message] in chapter_types:
|
||||||
|
if self.html_preprocess_sections >= self.min_chapters:
|
||||||
|
break
|
||||||
|
full_chapter_line = chapter_line_open+chapter_header_open+chapter_type+chapter_header_close+chapter_line_close
|
||||||
|
n_lookahead = re.sub("(ou|in|cha)", "lookahead_", full_chapter_line)
|
||||||
|
self.log("Marked " + unicode(self.html_preprocess_sections) + " headings, " + log_message)
|
||||||
|
if lookahead_ignorecase:
|
||||||
|
chapter_marker = init_lookahead+full_chapter_line+blank_lines+n_lookahead_open+n_lookahead+n_lookahead_close+opt_title_open+title_line_open+title_header_open+default_title+title_header_close+title_line_close+opt_title_close
|
||||||
|
chapdetect = re.compile(r'%s' % chapter_marker, re.IGNORECASE)
|
||||||
|
else:
|
||||||
|
chapter_marker = init_lookahead+full_chapter_line+blank_lines+opt_title_open+title_line_open+title_header_open+default_title+title_header_close+title_line_close+opt_title_close+n_lookahead_open+n_lookahead+n_lookahead_close
|
||||||
|
chapdetect = re.compile(r'%s' % chapter_marker, re.UNICODE)
|
||||||
|
html = chapdetect.sub(self.chapter_head, html)
|
||||||
|
|
||||||
|
words_per_chptr = wordcount
|
||||||
|
if words_per_chptr > 0 and self.html_preprocess_sections > 0:
|
||||||
|
words_per_chptr = wordcount / self.html_preprocess_sections
|
||||||
|
self.log("Total wordcount is: "+ str(wordcount)+", Average words per section is: "+str(words_per_chptr)+", Marked up "+str(self.html_preprocess_sections)+" chapters")
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __call__(self, html):
|
def __call__(self, html):
|
||||||
self.log("********* Preprocessing HTML *********")
|
self.log("********* Preprocessing HTML *********")
|
||||||
|
|
||||||
|
# Count the words in the document to estimate how many chapters to look for and whether
|
||||||
|
# other types of processing are attempted
|
||||||
|
totalwords = 0
|
||||||
|
totalwords = self.get_word_count(html)
|
||||||
|
|
||||||
|
if totalwords < 20:
|
||||||
|
self.log("not enough text, not preprocessing")
|
||||||
|
return html
|
||||||
|
|
||||||
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
|
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
|
||||||
html = re.sub(r"\s*</p>", "</p>\n", html)
|
html = re.sub(r"\s*</(?P<tag>p|div)>", "</"+"\g<tag>"+">\n", html)
|
||||||
html = re.sub(r"\s*<p(?P<style>[^>]*)>\s*", "\n<p"+"\g<style>"+">", html)
|
html = re.sub(r"\s*<(?P<tag>p|div)(?P<style>[^>]*)>\s*", "\n<"+"\g<tag>"+"\g<style>"+">", html)
|
||||||
|
|
||||||
###### Check Markup ######
|
###### Check Markup ######
|
||||||
#
|
#
|
||||||
@ -141,12 +235,17 @@ class PreProcessor(object):
|
|||||||
self.log("replaced "+unicode(self.found_indents)+ " nbsp indents with inline styles")
|
self.log("replaced "+unicode(self.found_indents)+ " nbsp indents with inline styles")
|
||||||
# remove remaining non-breaking spaces
|
# remove remaining non-breaking spaces
|
||||||
html = re.sub(ur'\u00a0', ' ', html)
|
html = re.sub(ur'\u00a0', ' ', html)
|
||||||
|
# Get rid of various common microsoft specific tags which can cause issues later
|
||||||
# Get rid of empty <o:p> tags to simplify other processing
|
# Get rid of empty <o:p> tags to simplify other processing
|
||||||
html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html)
|
html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html)
|
||||||
|
# Delete microsoft 'smart' tags
|
||||||
|
html = re.sub('(?i)</?st1:\w+>', '', html)
|
||||||
# Get rid of empty span, bold, & italics tags
|
# Get rid of empty span, bold, & italics tags
|
||||||
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]*>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]*>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
||||||
html = re.sub(r"\s*<[ibu][^>]*>\s*(<[ibu][^>]*>\s*</[ibu]>\s*){0,2}\s*</[ibu]>", " ", html)
|
html = re.sub(r"\s*<[ibu][^>]*>\s*(<[ibu][^>]*>\s*</[ibu]>\s*){0,2}\s*</[ibu]>", " ", html)
|
||||||
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]>\s*</span>){0,2}\s*</span>\s*", " ", html)
|
||||||
|
# ADE doesn't render <br />, change to empty paragraphs
|
||||||
|
#html = re.sub('<br[^>]*>', u'<p>\u00a0</p>', html)
|
||||||
|
|
||||||
# If more than 40% of the lines are empty paragraphs and the user has enabled remove
|
# If more than 40% of the lines are empty paragraphs and the user has enabled remove
|
||||||
# paragraph spacing then delete blank lines to clean up spacing
|
# paragraph spacing then delete blank lines to clean up spacing
|
||||||
@ -168,59 +267,12 @@ class PreProcessor(object):
|
|||||||
#print "blanks between paragraphs is marked True"
|
#print "blanks between paragraphs is marked True"
|
||||||
else:
|
else:
|
||||||
blanks_between_paragraphs = False
|
blanks_between_paragraphs = False
|
||||||
|
|
||||||
#self.dump(html, 'before_chapter_markup')
|
#self.dump(html, 'before_chapter_markup')
|
||||||
# detect chapters/sections to match xpath or splitting logic
|
# detect chapters/sections to match xpath or splitting logic
|
||||||
#
|
#
|
||||||
# Build the Regular Expressions in pieces
|
|
||||||
init_lookahead = "(?=<(p|div))"
|
|
||||||
chapter_line_open = "<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*"
|
|
||||||
title_line_open = "<(?P<outer2>p|div)[^>]*>\s*(<(?P<inner4>font|span|[ibu])[^>]*>)?\s*(<(?P<inner5>font|span|[ibu])[^>]*>)?\s*(<(?P<inner6>font|span|[ibu])[^>]*>)?\s*"
|
|
||||||
chapter_header_open = r"(?P<chap>"
|
|
||||||
title_header_open = r"(?P<title>"
|
|
||||||
chapter_header_close = ")\s*"
|
|
||||||
title_header_close = ")"
|
|
||||||
chapter_line_close = "(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>"
|
|
||||||
title_line_close = "(</(?P=inner6)>)?\s*(</(?P=inner5)>)?\s*(</(?P=inner4)>)?\s*</(?P=outer2)>"
|
|
||||||
|
|
||||||
if blanks_between_paragraphs:
|
html = self.markup_chapters(html, totalwords, blanks_between_paragraphs)
|
||||||
blank_lines = "(\s*<p[^>]*>\s*</p>){0,2}\s*"
|
|
||||||
else:
|
|
||||||
blank_lines = ""
|
|
||||||
opt_title_open = "("
|
|
||||||
opt_title_close = ")?"
|
|
||||||
n_lookahead_open = "\s+(?!"
|
|
||||||
n_lookahead_close = ")"
|
|
||||||
|
|
||||||
default_title = r"\s{0,3}([\w\'\"-]+\s{0,3}){1,5}?(?=<)"
|
|
||||||
|
|
||||||
min_chapters = 10
|
|
||||||
heading = re.compile('<h[1-3][^>]*>', re.IGNORECASE)
|
|
||||||
self.html_preprocess_sections = len(heading.findall(html))
|
|
||||||
self.log("found " + unicode(self.html_preprocess_sections) + " pre-existing headings")
|
|
||||||
|
|
||||||
chapter_types = [
|
|
||||||
[r"[^'\"]?(Introduction|Synopsis|Acknowledgements|Chapter|Kapitel|Epilogue|Volume\s|Prologue|Book\s|Part\s|Dedication)\s*([\d\w-]+\:?\s*){0,4}", True, "Searching for common Chapter Headings"],
|
|
||||||
[r"[^'\"]?(\d+\.?|CHAPTER)\s*([\dA-Z\-\'\"\?\.!#,]+\s*){0,7}\s*", True, "Searching for numeric chapter headings"], # Numeric Chapters
|
|
||||||
[r"<b[^>]*>\s*(<span[^>]*>)?\s*(?!([*#•]+\s*)+)(\s*(?=[\w#\-*\s]+<)([\w#-*]+\s*){1,5}\s*)(</span>)?\s*</b>", True, "Searching for emphasized lines"], # Emphasized lines
|
|
||||||
[r"[^'\"]?(\d+\.?\s+([\d\w-]+\:?\'?-?\s?){0,5})\s*", True, "Searching for numeric chapters with titles"], # Numeric Titles
|
|
||||||
[r"\s*[^'\"]?([A-Z#]+(\s|-){0,3}){1,5}\s*", False, "Searching for chapters with Uppercase Characters" ] # Uppercase Chapters
|
|
||||||
]
|
|
||||||
|
|
||||||
# Start with most typical chapter headings, get more aggressive until one works
|
|
||||||
for [chapter_type, lookahead_ignorecase, log_message] in chapter_types:
|
|
||||||
if self.html_preprocess_sections >= min_chapters:
|
|
||||||
break
|
|
||||||
full_chapter_line = chapter_line_open+chapter_header_open+chapter_type+chapter_header_close+chapter_line_close
|
|
||||||
n_lookahead = re.sub("(ou|in|cha)", "lookahead_", full_chapter_line)
|
|
||||||
self.log("Marked " + unicode(self.html_preprocess_sections) + " headings, " + log_message)
|
|
||||||
if lookahead_ignorecase:
|
|
||||||
chapter_marker = init_lookahead+full_chapter_line+blank_lines+n_lookahead_open+n_lookahead+n_lookahead_close+opt_title_open+title_line_open+title_header_open+default_title+title_header_close+title_line_close+opt_title_close
|
|
||||||
chapdetect = re.compile(r'%s' % chapter_marker, re.IGNORECASE)
|
|
||||||
else:
|
|
||||||
chapter_marker = init_lookahead+full_chapter_line+blank_lines+opt_title_open+title_line_open+title_header_open+default_title+title_header_close+title_line_close+opt_title_close+n_lookahead_open+n_lookahead+n_lookahead_close
|
|
||||||
chapdetect = re.compile(r'%s' % chapter_marker, re.UNICODE)
|
|
||||||
|
|
||||||
html = chapdetect.sub(self.chapter_head, html)
|
|
||||||
|
|
||||||
|
|
||||||
###### Unwrap lines ######
|
###### Unwrap lines ######
|
||||||
@ -247,7 +299,7 @@ class PreProcessor(object):
|
|||||||
# Calculate Length
|
# Calculate Length
|
||||||
unwrap_factor = getattr(self.extra_opts, 'html_unwrap_factor', 0.4)
|
unwrap_factor = getattr(self.extra_opts, 'html_unwrap_factor', 0.4)
|
||||||
length = docanalysis.line_length(unwrap_factor)
|
length = docanalysis.line_length(unwrap_factor)
|
||||||
self.log("*** Median line length is " + unicode(length) + ", calculated with " + format + " format ***")
|
self.log("Median line length is " + unicode(length) + ", calculated with " + format + " format")
|
||||||
# only go through unwrapping code if the histogram shows unwrapping is required or if the user decreased the default unwrap_factor
|
# only go through unwrapping code if the histogram shows unwrapping is required or if the user decreased the default unwrap_factor
|
||||||
if hardbreaks or unwrap_factor < 0.4:
|
if hardbreaks or unwrap_factor < 0.4:
|
||||||
self.log("Unwrapping required, unwrapping Lines")
|
self.log("Unwrapping required, unwrapping Lines")
|
||||||
@ -260,7 +312,7 @@ class PreProcessor(object):
|
|||||||
self.log("Done dehyphenating")
|
self.log("Done dehyphenating")
|
||||||
# Unwrap lines using punctation and line length
|
# Unwrap lines using punctation and line length
|
||||||
#unwrap_quotes = re.compile(u"(?<=.{%i}\"')\s*</(span|p|div)>\s*(</(p|span|div)>)?\s*(?P<up2threeblanks><(p|span|div)[^>]*>\s*(<(p|span|div)[^>]*>\s*</(span|p|div)>\s*)</(span|p|div)>\s*){0,3}\s*<(span|div|p)[^>]*>\s*(<(span|div|p)[^>]*>)?\s*(?=[a-z])" % length, re.UNICODE)
|
#unwrap_quotes = re.compile(u"(?<=.{%i}\"')\s*</(span|p|div)>\s*(</(p|span|div)>)?\s*(?P<up2threeblanks><(p|span|div)[^>]*>\s*(<(p|span|div)[^>]*>\s*</(span|p|div)>\s*)</(span|p|div)>\s*){0,3}\s*<(span|div|p)[^>]*>\s*(<(span|div|p)[^>]*>)?\s*(?=[a-z])" % length, re.UNICODE)
|
||||||
unwrap = re.compile(u"(?<=.{%i}([a-zäëïöüàèìòùáćéíóńśúâêîôûçąężı,:)\IA\u00DF]|(?<!\&\w{4});))\s*</(span|p|div)>\s*(</(p|span|div)>)?\s*(?P<up2threeblanks><(p|span|div)[^>]*>\s*(<(p|span|div)[^>]*>\s*</(span|p|div)>\s*)</(span|p|div)>\s*){0,3}\s*<(span|div|p)[^>]*>\s*(<(span|div|p)[^>]*>)?\s*" % length, re.UNICODE)
|
unwrap = re.compile(u"(?<=.{%i}([a-zäëïöüàèìòùáćéíóńśúâêîôûçąężıãõñæøþðß,:)\IA\u00DF]|(?<!\&\w{4});))\s*</(span|p|div)>\s*(</(p|span|div)>)?\s*(?P<up2threeblanks><(p|span|div)[^>]*>\s*(<(p|span|div)[^>]*>\s*</(span|p|div)>\s*)</(span|p|div)>\s*){0,3}\s*<(span|div|p)[^>]*>\s*(<(span|div|p)[^>]*>)?\s*" % length, re.UNICODE)
|
||||||
html = unwrap.sub(' ', html)
|
html = unwrap.sub(' ', html)
|
||||||
#check any remaining hyphens, but only unwrap if there is a match
|
#check any remaining hyphens, but only unwrap if there is a match
|
||||||
dehyphenator = Dehyphenator()
|
dehyphenator = Dehyphenator()
|
||||||
@ -276,7 +328,7 @@ class PreProcessor(object):
|
|||||||
html = re.sub(u'\xad\s*(</span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*', '', html)
|
html = re.sub(u'\xad\s*(</span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*', '', html)
|
||||||
|
|
||||||
# If still no sections after unwrapping mark split points on lines with no punctuation
|
# If still no sections after unwrapping mark split points on lines with no punctuation
|
||||||
if self.html_preprocess_sections < 5:
|
if self.html_preprocess_sections < self.min_chapters:
|
||||||
self.log("Looking for more split points based on punctuation,"
|
self.log("Looking for more split points based on punctuation,"
|
||||||
" currently have " + unicode(self.html_preprocess_sections))
|
" currently have " + unicode(self.html_preprocess_sections))
|
||||||
chapdetect3 = re.compile(r'<(?P<styles>(p|div)[^>]*)>\s*(?P<section>(<span[^>]*>)?\s*(?!([*#•]+\s*)+)(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*(</span>)?(</[ibu]>){0,2}\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</span>)?\s*</(p|div)>)', re.IGNORECASE)
|
chapdetect3 = re.compile(r'<(?P<styles>(p|div)[^>]*)>\s*(?P<section>(<span[^>]*>)?\s*(?!([*#•]+\s*)+)(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*(</span>)?(</[ibu]>){0,2}\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</span>)?\s*</(p|div)>)', re.IGNORECASE)
|
||||||
|
85
src/calibre/utils/wordcount.py
Normal file
85
src/calibre/utils/wordcount.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
"""
|
||||||
|
Get word, character, and Asian character counts
|
||||||
|
|
||||||
|
1. Get a word count as a dictionary:
|
||||||
|
wc = get_wordcount(text)
|
||||||
|
words = wc['words'] # etc.
|
||||||
|
|
||||||
|
2. Get a word count as an object
|
||||||
|
wc = get_wordcount_obj(text)
|
||||||
|
words = wc.words # etc.
|
||||||
|
|
||||||
|
properties counted:
|
||||||
|
* characters
|
||||||
|
* chars_no_spaces
|
||||||
|
* asian_chars
|
||||||
|
* non_asian_words
|
||||||
|
* words
|
||||||
|
|
||||||
|
Sourced from:
|
||||||
|
http://ginstrom.com/scribbles/2008/05/17/counting-words-etc-in-an-html-file-with-python/
|
||||||
|
http://ginstrom.com/scribbles/2007/10/06/counting-words-characters-and-asian-characters-with-python/
|
||||||
|
"""
|
||||||
|
__version__ = 0.1
|
||||||
|
__author__ = "Ryan Ginstrom"
|
||||||
|
|
||||||
|
IDEOGRAPHIC_SPACE = 0x3000
|
||||||
|
|
||||||
|
def is_asian(char):
|
||||||
|
"""Is the character Asian?"""
|
||||||
|
|
||||||
|
# 0x3000 is ideographic space (i.e. double-byte space)
|
||||||
|
# Anything over is an Asian character
|
||||||
|
return ord(char) > IDEOGRAPHIC_SPACE
|
||||||
|
|
||||||
|
def filter_jchars(c):
|
||||||
|
"""Filters Asian characters to spaces"""
|
||||||
|
if is_asian(c):
|
||||||
|
return ' '
|
||||||
|
return c
|
||||||
|
|
||||||
|
def nonj_len(word):
|
||||||
|
u"""Returns number of non-Asian words in {word}
|
||||||
|
- 日本語AアジアンB -> 2
|
||||||
|
- hello -> 1
|
||||||
|
@param word: A word, possibly containing Asian characters
|
||||||
|
"""
|
||||||
|
# Here are the steps:
|
||||||
|
# 本spam日eggs
|
||||||
|
# -> [' ', 's', 'p', 'a', 'm', ' ', 'e', 'g', 'g', 's']
|
||||||
|
# -> ' spam eggs'
|
||||||
|
# -> ['spam', 'eggs']
|
||||||
|
# The length of which is 2!
|
||||||
|
chars = [filter_jchars(c) for c in word]
|
||||||
|
return len(u''.join(chars).split())
|
||||||
|
|
||||||
|
def get_wordcount(text):
|
||||||
|
"""Get the word/character count for text
|
||||||
|
|
||||||
|
@param text: The text of the segment
|
||||||
|
"""
|
||||||
|
|
||||||
|
characters = len(text)
|
||||||
|
chars_no_spaces = sum([not x.isspace() for x in text])
|
||||||
|
asian_chars = sum([is_asian(x) for x in text])
|
||||||
|
non_asian_words = nonj_len(text)
|
||||||
|
words = non_asian_words + asian_chars
|
||||||
|
|
||||||
|
return dict(characters=characters,
|
||||||
|
chars_no_spaces=chars_no_spaces,
|
||||||
|
asian_chars=asian_chars,
|
||||||
|
non_asian_words=non_asian_words,
|
||||||
|
words=words)
|
||||||
|
|
||||||
|
def dict2obj(dictionary):
|
||||||
|
"""Transform a dictionary into an object"""
|
||||||
|
class Obj(object):
|
||||||
|
def __init__(self, dictionary):
|
||||||
|
self.__dict__.update(dictionary)
|
||||||
|
return Obj(dictionary)
|
||||||
|
|
||||||
|
def get_wordcount_obj(text):
|
||||||
|
"""Get the wordcount as an object rather than a dictionary"""
|
||||||
|
return dict2obj(get_wordcount(text))
|
Loading…
x
Reference in New Issue
Block a user