#coding=utf-8 import time from xml.sax.saxutils import escape as xmlescaper #функция, экранируюущая невалидные для xml символы import md5 import re #на текущий момент BeautifulSoup плохо обрабатывает   import BeautifulSoup as BS #отключаем в BeautifulSoup - недопускание вложенности одинаковых тегов #(она закрывает тег, перед открытием такого-же) BS.BeautifulSoup.NESTABLE_TAGS['strong'] = [] BS.BeautifulSoup.NESTABLE_TAGS['b'] = [] BS.BeautifulSoup.NESTABLE_TAGS['i'] = [] BS.BeautifulSoup.NESTABLE_TAGS['em'] = [] BS.BeautifulSoup.NESTABLE_TAGS['var'] = [] BS.BeautifulSoup.NESTABLE_TAGS['cite'] = [] BS.BeautifulSoup.NESTABLE_TAGS['p'] = [] BS.BeautifulSoup.NESTABLE_TAGS['pre'] = [] import codecs import re import urllib import os.path import base64 import os import shutil from PIL import Image import cStringIO import fb_utils #this code used for get control chars #import unicodedata #all_chars = [unichr(i) for i in xrange(0x10000)] #all_chars = [unichr(i) for i in xrange(0x110000)] #control_chars = u''.join(c for c in all_chars if (unicodedata.category(c) == 'Cc' ) and c not in (unichr(13), unichr(10), unichr(9))) #unichr(13), unichr(10), unichr(9) is \n, \r, \t #control_chars_int = [ord(cc) for cc in control_chars] BAD_CHARS = ''.join([unichr(c) for c in [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159]]) BAD_CHARS_replace = re.compile('[%s]' % re.escape(BAD_CHARS)) def bad_chars_proc(s): #use for all string, what adding to fb2 #s.replace(u' ', u' ') - for fix beatefualsoup bag: заменяет то, что получается в процессе конвертации из   на пробел return BAD_CHARS_replace.sub('', s.replace(u' ', u' ')) class params_(object): ''' параметры, передаваемые в парсер ''' def __init__(self): self.file_out = None #имя выходного файла (можно с путями) self.source_files = [] #имена выходных файлов (можно с путями) self.descr = None #объкт дескрипшена (см. fb_utils) для формирования заголовка fb2 self.skip_images = None #не обрабатывать картинки self.skip_tables = None #не обрабатывать таблицы self.skip_pre = None #не обрабатывать таблицы class binary(object): ''' служебный объект, для работы со стораджем fb2 (там где картинки хранятся ''' def __init__(self): self.f = os.tmpfile() self.ids = [] def get(self): self.f.seek(0) return self.f def add(self, type, id, data): if id not in self.ids: #если такого объекта еще нет в сторадже, сохраняем его туда self.f.write('' % (type, id)) self.f.write(base64.encodestring(data)) self.f.write('\n') self.ids.append(id) def get_image( src): ''' src - путь к файлу с картинкой. возвращает {'type':'png' или 'jpeg', 'data':данные}, None - в случае неудачи. понимает gif, jpeg, png. gif - перекодирует в png ''' try: im = Image.open(src) except IOError: return None if im.format == 'GIF': f = cStringIO.StringIO() im.save(f, "PNG") return {'data':f.getvalue(), 'type':'png'} elif im.format == 'PNG': try: data = open(src, 'rb').read() except IOError: return None else: return {'data':data, 'type':'png'} elif im.format == 'JPEG': try: data = open(src, 'rb').read() except IOError: return None else: return {'data':data, 'type':'jpeg'} return None class fb2_(object): ''' собственно - здесь формируется костяк fb2 ''' def __init__(self, f_name): ''' f_name - имя выходного файла ''' self.f_out = file(f_name, 'wb') self.soup = BS.BeautifulStoneSoup(selfClosingTags=['image']) body = BS.Tag(self.soup, 'body') self.soup.append(body) #делаем склет для fb2 (вставляем тег боди, чтоб к нему все приклеивать) self.binary = binary() #подключаем бинарные данные self.description = '' def get_rez(self): ''' собираем все вместе и записываем в файл. ''' head = """ """ foot = """\n""" self.f_out.write(head) #служебные теги self.f_out.write(self.description.encode('UTF-8')) #подключаем дискрипшн self.f_out.write( self.soup.renderContents(encoding='UTF-8', prettyPrint = False) ) # рендерим в XML soup -структуру shutil.copyfileobj(self.binary.get(), self.f_out) #добавляем в файл секцию binary self.f_out.write(foot) #служебные теги def make_description(self, descr): ''' формирование секции description для fb2 она формируется в текстовом виде, что улучшить читабельность исходника ''' #fill title-info self.description += '\n' self.description += '\n' self.description += '%s\n' % descr.genre for author_d in descr.authors: authors = '' authors += '%s' % xmlescaper(author_d['first']) authors += '%s' % xmlescaper(author_d['middle']) authors += '%s' % xmlescaper(author_d['last']) authors += '\n' self.description += authors self.description += '%s\n' % xmlescaper(descr.title) self.description += '\n' self.description += '%s\n' % xmlescaper(descr.lang) self.description += '\n' #fill document-info self.description += '\n' self.description += '\n' if descr.program_info: self.description += '%s\n' % descr.program_info self.description += '%s\n' % (time.strftime('%Y-%m-%d'), time.strftime('%Y-%m-%d')) if descr.urls: self.description += '%s\n' % ' '.join([ xmlescaper(bad_chars_proc(unicode(x))) for x in descr.urls ]) self.description += '%s\n' % descr.id self.description += '%s\n' % descr.version self.description += '\n' self.description += '\n' class html2fb2(object): ''' парсер html ''' def __init__(self, fb2, in_file, skip_images = False, skip_tables = False, skip_pre = True): self.in_file = in_file self.skip_images = skip_images self.skip_tables = skip_tables self.skip_pre = skip_pre self.fb2 = fb2 self.fb2s = fb2.soup # soup структура fb2 #читаем файлс html-кой data = codecs.open(self.in_file, 'r', 'utf-8').read() self.soup = BS.BeautifulSoup(data, selfClosingTags=[], convertEntities="html") #парсим html в soup структуру #регулярка для разделение строки на переносы self.split_lines = re.compile(r"\r\n|\r|\n", re.MULTILINE) def detect_descr(self): ''' пытаемся вытащить title ''' try: #ищем его в title = bad_chars_proc( u''.join( self.soup.html.title.findAll(text = True) ) ) #try take title from <title> except AttributeError: try: #пробуем взять его из первого попавшегося h1, h2, ... title = bad_chars_proc( ''.join(self.soup.html.body.find(name = re.compile(r'h\d')).findAll(text = True)) )#try to found h tags for title except AttributeError: title = '' return title def process(self): soup = self.fb2s #создаем секцию big_section = BS.Tag(soup, 'section') #пытаемся подобрать для секции загловок try: title_text = bad_chars_proc( ''.join(self.soup.html.title.findAll(text = True)) ).strip() #try take title from <title> except AttributeError: pass else: if title_text: #если получилось, пишем заголовок в секцию title = BS.Tag(soup, 'title') p = BS.Tag(soup, 'p') p.append( BS.NavigableString( xmlescaper(title_text) ) ) title.append(p) big_section.append(title) #если есть текст тег body - начинаем обработку с него body_data = self.soup.body if body_data: rez = self.proc_tag(body_data) #если нет, то скорее всего это не html-ка, а текстовый файл, и начинаем обоаботку с самого начала else: rez = self.proc_tag(self.soup) #если надо, оборачиваем полученый результат в тег p и присоединяем к секции, во время оборачивания, стрипаем теги br #оборачиваем необренутые в p теги, стрипаем br p_rez = self.break_tags('p', rez, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True, bad_tags = ['br']) #оборачиваем необренутые в section теги, стрипаем sect sects = self.break_tags('section', p_rez, ('strong', 'emphasis', 'code', 'sup', 'sub', 'p', 'table', 'title'), image_outline = True, image_inline = True, string = True, bad_tags = ['sect']) for sec in sects: big_section.append(sec) #присоединяем секцию к fb2 self.fb2s.body.append(big_section) def break_tags(self, this_tag, rez, good_tags, image_outline = False, image_inline = True, string = True, bad_tags = []): ''' оборачивает массив rez тегом this_tag возвращает массив. Если в массиве rez объекты из good_tags или image_outline, image_inline, string (если они включены), то они обораиваются. если нет, то таг this_tag прерывается, в массив результата добавляется объект, который нельзя обернуть, а потом продолжаетсмя оборачивание если тег, на котором прерывается, находится в bad_tags - на нем прерывание происходит, но он не добавляется в возвращаемый массив ''' soup = self.fb2s coll = [] #возвращаемый массив this = BS.Tag(soup, this_tag) for r in rez: if string and isinstance(r, BS.NavigableString): this.append(r) elif isinstance(r, BS.Tag) and (r.name in good_tags): this.append(r) elif image_outline and ( isinstance(r, BS.Tag) and (r.name == 'image') and not r.inline): this.append(r) elif image_inline and ( isinstance(r, BS.Tag) and (r.name == 'image') and r.inline): this.append(r) else: if this.contents: coll.append(this) this = BS.Tag(soup, this_tag) if r.name not in bad_tags: coll.append(r) if this.contents: coll.append(this) return coll def check_tags(self, rez, good_tags, string = False): ''' проверяет, какие soup теги содержаться в массиве rez возвращает True - если в нем содержаться только теги из набора good_tags или строки (если они разрешены в string) ''' for r in rez: if isinstance(r, BS.NavigableString): if (not string) and str(r).strip(): return False elif isinstance(r, BS.Tag) and (r.name not in good_tags): return False return True def proc_tab_tags(self, tag, in_pre = False): ''' обработка тегов таблиц ''' soup = self.fb2s coll = [] #возвращаемый массив if tag.name == 'table': rez = self.proc_tag(tag, in_pre) #если внутри содержится что-то кроме tr, th, td - вынимаем контент из th, td и добавляем к результату #из tr - вынимать ничего не надо - он не ничего не содержит (см. код обработки tr) #вместо самих же тегов: на всякий случай ставим пробелы (чтоб не скливались буковки if not self.check_tags(rez, ('td', 'th', 'tr'), string = False): for r in rez: if isinstance(r, BS.Tag): if r.name == 'tr': coll.append(BS.NavigableString(' ')) elif r.name in ('td', 'th'): coll.append(BS.NavigableString(' ')) for sub_r in r.contents: coll.append(sub_r) else: coll.append(r) else: coll.append(r) else: #пробегаем по массиву, если встречается tr - создаем этот тег #если встречаем td или th - добавляем им в созданный tr (если таковой существует) table = BS.Tag(soup, 'table') tr = None for r in rez: if isinstance(r, BS.Tag): if r.name == 'tr': if tr: table.append(tr) tr = r elif r.name in ('th', 'td'): if tr: tr.append(r) if tr: table.append(tr) if table.contents: coll.append(table) elif tag.name == 'tr': rez = self.proc_tag(tag, in_pre) #если внутри встретилось что-то кроме тегов td, th - добавляем это что-то к результату if rez: if not self.check_tags(rez, ('td', 'th'), string = False): coll += rez else: #иначе, добавляем tr, td к результату tr = BS.Tag(soup, 'tr') coll.append(tr) coll += rez #надо заметить, что tr, td, th - располагаются не вложенно, а линейно в массиве. # это будет учтено при обработке table elif (not self.skip_tables) and (tag.name in ('td','th')): #обрабатываем внутренности таблицы rez = self.proc_tag(tag, in_pre) #проверям, что внутренние теги - те которые допустимы. if not self.check_tags(rez, ('strong', 'emphasis', 'code', 'sup', 'sub', 'image'), string = True): coll += rez #если внутри все слишком сложно - нафиг такую внутри. else: if tag.name == 'td': this = BS.Tag(soup, 'td') else: this = BS.Tag(soup, 'th') #обрабатываем спаны rowspan = tag.get('rowspan', None) colspan = tag.get('colspan', None) if rowspan != None: this['rowspan'] = rowspan if colspan != None: this['colspan'] = colspan for r in rez: this.append(r) coll.append(this) return coll def proc_tag(self, parent_tag, in_pre = False): ''' рекурсивная обработка тегов возвращает массив тегов in_pre - если нужна специальная обработка строк, в pre ''' soup = self.fb2s coll = [] #возвращаемый массив for tag in parent_tag.contents: #бежим по дочерним тегам if tag.__class__ == BS.NavigableString: #если строка (не коммент, не cdata а именно строка) s = bad_chars_proc(unicode(tag))#обработка левых символов if in_pre: #если включена обработка переносов, ставим всесто каждого переноса br s_l = self.split_lines.split(s) for c in s_l[:-1]: text = BS.NavigableString(xmlescaper(c)) #создаем строку и эскейпим ее coll.append(text) br = BS.Tag(soup, 'br') coll.append(br) c = s_l[-1] text = BS.NavigableString(xmlescaper(c)) coll.append(text) else: text = BS.NavigableString(xmlescaper(s)) #создаем строку и эскейпим ее coll.append(text) elif isinstance(tag, BS.Tag): #если тег if tag.name in ('script', 'form', 'style'): #теги, обработка внутри которых не производится pass elif tag.name in ('b', 'strong'): #жирный rez = self.proc_tag(tag, in_pre) coll += self.break_tags('strong', rez, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True) elif tag.name in ('i', 'cite', 'em', 'var'): #наклонный rez = self.proc_tag(tag, in_pre) coll += self.break_tags('emphasis', rez, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True) elif tag.name == 'sup': #верхний индекс rez = self.proc_tag(tag, in_pre) coll += self.break_tags('sup', rez, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True) elif tag.name == 'sub': #нижний индекс rez = self.proc_tag(tag, in_pre) coll += self.break_tags('sub', rez, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True) elif tag.name == 'pre': #преформатированный текст if self.skip_pre: #если не обрабатываем pre, включаем спец-обработчик переносов строки для всех дочерних тегов rez = self.proc_tag(tag, in_pre = True) coll += rez else: rez = self.proc_tag(tag, in_pre) coll += self.break_tags('code', rez, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True) elif tag.name == 'p': #параграф rez = self.proc_tag(tag, in_pre) #оборачиваем те теги, который можно обернуть, при этом стрипаем теги br coll += self.break_tags('p', rez, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True, bad_tags = ['br']) elif tag.name in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'): #если заголовки - оформляем их жирным выделяем в отдельный параграф #ставим воспомагетальный тег - начало секции. Потом на обработке секции его надо обработать и удалить sect = BS.Tag(soup, 'sect') coll.append(sect) #обрабатываем то, что внутри title rez_title = self.proc_tag(tag, in_pre) #добавляем p p_title = self.break_tags('p', rez_title, ('strong', 'emphasis', 'code', 'sup', 'sub'), image_outline = False, image_inline = True, string = True, bad_tags = ['br']) #добавляем title title_tags = self.break_tags('title', p_title, ('strong', 'emphasis', 'code', 'sup', 'sub', 'p'),image_outline = False, image_inline = True, string = True) #проверям, не была-ли title рассечена чем-нибудь (например таблица выдавилась или не inline картинка) #в таком случае тоже надо добавить новую секцию #и добавляем все в возвращаемый массив if title_tags: coll.append(title_tags[0]) for title_tag in title_tags[1:]: #если еще где-то, кроме как в первом элементе встретился title, значит он был где-то рассечен if isinstance(title_tag, BS.Tag) and (title_tag.name == 'title'): sect = BS.Tag(soup, 'sect') #добавдяем секцию coll.append(sect) coll.append(title_tag) else: coll.append(title_tag) elif tag.name == 'br': # если br - приходится временно ввести дополнительный тег br (потом его надо обязательно удалить) # он не входит не в список разрешенных тегов и поэтому будет выталкиваться, пока его не удалят # удаление происходит на уровне тега p rez = self.proc_tag(tag, in_pre) br = BS.Tag(soup, 'br') coll.append(br) #если обрабатываем таблицы, теги ее обработки собраны в отдельной функции #теги таблиц, собраны в отдельной функции elif (not self.skip_tables) and (tag.name in ('table', 'tr', 'td', 'th')): coll += self.proc_tab_tags(tag, in_pre) #если таблицы не обрабатываем - на всякий случай ставим пробелы, чтоб буковки не склеивались elif (self.skip_tables) and (tag.name in ('table', 'tr', 'td', 'th')): coll.append(BS.NavigableString(' ')) coll += self.proc_tag(tag, in_pre) coll.append(BS.NavigableString(' ')) elif tag.name == 'img': #обрабатываем картинки if not self.skip_images: src = tag.get('src', None) #если у картинки есть выравнивание, значит она не inline inline = not ( tag.get('align', '').lower() in ('left', 'right') ) if src: img_path = os.path.join( os.path.dirname(self.in_file), urllib.unquote(src) ) img = get_image( img_path ) if img: id = 'i' + md5.new(img_path).hexdigest()[:10] #даем картинке новое, заведомо валидное имя self.fb2.binary.add('image/%s' % img['type'], id, img['data']) #добавляем в сторадж tag = BS.Tag(soup, 'image', [('xlink:href', "#%s"%id)] ) tag.inline = inline #добавляем к тегу свойство inline coll.append(tag) else: coll += self.proc_tag(tag, in_pre) return coll def htmls2fb2(params): ''' собственно запускаемая функция обрабатывает все html файлы переданные в params в fb2 файл ''' fb2 = fb2_(params.file_out) #создаем fb2 файл descr = params.descr titles = [] for in_file in params.source_files: #обрабатываем отдельно каждый html файл h2f = html2fb2(fb2, in_file, params.skip_images, params.skip_tables, params.skip_pre) h2f.process() if descr.selfdetect: #если нужно самим определить дискрипшн - пытаемся определить title titles.append( h2f.detect_descr() ) if descr.selfdetect: #склеиваем титлы в один descr.title = ' ||| '.join( [ t.strip() for t in titles if t.strip() ] ) descr.authors = [descr.def_author] fb2.make_description(descr) #формируем дискрипшн у fb2 fb2.get_rez() # завершем формирование fb2 return descr if __name__ == '__main__': params = params_() params.skip_images = False params.skip_tables = False #params.source_files = ['html/test.html', 'html/mail.htm'] #params.source_files = [ 'html/html.html'] #params.source_files = [ 'html/nosov.html'] params.source_files = [ 'html/test.html'] params.file_out = 'out.fb2' params.descr = fb_utils.description() #params.descr.authors = [{'first': u'петер', 'middle': u'Михайлович', 'last': u'Размазня'}, {'first': 'Галина', 'middle':'Николаевна', 'last':'Борщь'}] #params.descr.selfdetect = False htmls2fb2(params)