是否可以使用 Beautiful Soup 以编程方式组合某些 HTML 标签的内容?

Is it possible to programmatically combine the content of certain HTML tags using Beautiful Soup?

我正在使用一个名为 Calibre 的程序将 PDF 文件转换为 EPUB 文件,但结果非常混乱且不可读。实际上,一个 EPUB 文件只是 HTML 个文件的集合,转换的结果很乱,因为 Calibre 将 PDF 文件的每一行解释为一个

元素,这会产生很多难看的行EPUB 文件中断。

由于 EPUB 实际上是 HTML 个文件的集合,因此可以使用 Beautiful Soup 对其进行解析。但是,我编写的用于查找带有 "calibre1" class (普通段落)的元素并将它们组合成单个元素(因此没有丑陋的换行符)的程序不起作用,我无法理解为什么。

Beautiful Soup 可以处理我正在尝试做的事情吗?

import os
from bs4 import BeautifulSoup

path = "C:\Users\Eunice\Desktop\eBook"

for pathname, directorynames, filenames in os.walk(path):
    # Get all HTML files in the target directory
    for file_name in filenames:
        # Open each HTML file, which is encoded using the "Latin1" encoding scheme
        with open(pathname + "\" + file_name, 'r', encoding="Latin1") as file:
            # Create a list, which we will write our new HTML tags to later
            html_elem_list: list = []
            # Create a BS4 object
            soup = BeautifulSoup(file, 'html.parser')
            # Create a list of all BS4 elements, which we will traverse in the proceeding loop
            html_elements = [x for x in soup.find_all()]

            for html_element in html_elements:
                try:
                    # Find the element with a class called "calibre1," which is how Calibre designates normal body text in a book
                    if html_element.attrs['class'][0] in 'calibre1':
                        # Combine the next element with the previous element if both elements are part of the same body text
                        if html_elem_list[-1].attrs['class'][0] in 'calibre1':
                            # Remove nonbreaking spaces from this element before adding it to our list of elements
                            html_elem_list[-1].string = html_elem_list[-1].text.replace(
                                '\n', ' ') + html_element.text
                    # This element must not be of the "calibre1" class, so add it to the list of elements without combining it with the previous element
                    else:
                        html_elem_list.append(html_element)
                # This element must not have any class, so add it to the list of elements without combining it with the previous element
                except KeyError:
                    html_elem_list.append(html_element)

            # Create a string literal, which we will eventually write to our resultant file
            str_htmlfile = ''
            # For each element in the list of HTML elements, append the string representation of that element (which will be a line of HTML code) to the string literal
            for elem in html_elem_list:
                    str_htmlfile = str_htmlfile + str(elem)
        # Create a new file with a distinct variation of the name of the original file, then write the resultant HTML code to that file
        with open(pathname + "\" + '_modified_' + file_name, 'wb') as file:
            file.write(str_htmlfile.encode('Latin1'))

这是一个输入:

<?xml version='1.0' encoding='Latin1'?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">

<body class="calibre">
<p class="calibre5" id="calibre_pb_62">Note for Tyler</p>
<p class="calibre1">In the California registry, there was</p>
<p class="calibre1">a calm breeze blowing through the room. A woman</p>
<p class="calibre1">who must have just walked in quietly beckoned for the</p>
<p class="calibre1">counterman to approach to store her slip.</p>
<p class="calibre1">642</p>
</body></html>

这是我期望发生的事情:

<?xml version='1.0' encoding='Latin1'?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">

<body class="calibre">
<p class="calibre5" id="calibre_pb_62">Note for Tyler</p>
<p class="calibre1">In the California registry, there was a calm breeze blowing through the room. A woman who must have just walked in quietly beckoned for the counterman to approach to store her slip.642</p>
</body></html>

这是实际输出:

<html lang="" xml:lang="" xmlns="http://www.w3.org/1999/xhtml">
<body class="calibre">
<p class="calibre5" id="calibre_pb_62">Note for Tyler</p>
<p class="calibre1">In the California registry, there was</p>
<p class="calibre1">a calm breeze blowing through the room. A woman</p>
<p class="calibre1">who must have just walked in quietly beckoned for the</p>
<p class="calibre1">counterman to approach to store her slip.</p>
<p class="calibre1">642</p>
</body></html><body class="calibre">
<p class="calibre5" id="calibre_pb_62">Note for Tyler</p>
<p class="calibre1">In the California registry, there was</p>
<p class="calibre1">a calm breeze blowing through the room. A woman</p>
<p class="calibre1">who must have just walked in quietly beckoned for the</p>
<p class="calibre1">counterman to approach to store her slip.</p>
<p class="calibre1">642</p>
</body><p class="calibre5" id="calibre_pb_62">Note for Tyler</p>

这可以使用 BeautifulSoup 完成,方法是使用 extract() 删除不需要的 <p> 元素,然后使用 new_tag() 创建新的 <p> 标签包含所有已删除元素的文本。例如:

html = """<?xml version='1.0' encoding='Latin1'?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">

<body class="calibre">
<p class="calibre5" id="calibre_pb_62">Note for Tyler1</p>
<p class="calibre1">In the California registry, there was</p>
<p class="calibre1">a calm breeze blowing through the room. A woman</p>
<p class="calibre1">who must have just walked in quietly beckoned for the</p>
<p class="calibre1">counterman to approach to store her slip.</p>
<p class="calibre1">642</p>

<p class="calibre5" id="calibre_pb_62">Note for Tyler2</p>
<p class="calibre1">In the California registry, there was</p>
<p class="calibre1">a calm breeze blowing through the room. A woman</p>
<p class="calibre1">who must have just walked in quietly beckoned for the</p>
<p class="calibre1">counterman to approach to store her slip.</p>
<p class="calibre1">642</p>

</body></html>"""

from bs4 import BeautifulSoup
from itertools import groupby
import re

soup = BeautifulSoup(html, "html.parser")

for level, group in groupby(soup.find_all("p", class_=re.compile(r"calibre\d")), lambda x: x["class"][0]):
    if level == "calibre1":
        calibre1 = list(group)
        p_new = soup.new_tag('p', attrs={"class" : "calibre1"})
        p_new.string = ' '.join(p.get_text(strip=True) for p in calibre1)
        calibre1[0].insert_before(p_new)

        for p in calibre1:
            p.extract()

print(soup.prettify())

会给你 HTML 作为:

<?xml version='1.0' encoding='Latin1'?>
<html lang="" xml:lang="" xmlns="http://www.w3.org/1999/xhtml">
 <body class="calibre">
  <p class="calibre5" id="calibre_pb_62">
   Note for Tyler1
  </p>
  <p class="calibre1">
   In the California registry, there was a calm breeze blowing through the room. A woman who must have just walked in quietly beckoned for the counterman to approach to store her slip. 642
  </p>
  <p class="calibre5" id="calibre_pb_62">
   Note for Tyler2
  </p>
  <p class="calibre1">
   In the California registry, there was a calm breeze blowing through the room. A woman who must have just walked in quietly beckoned for the counterman to approach to store her slip. 642
  </p>
 </body>
</html>

它的工作原理是找到 运行 个 calibre1 标签。对于每个 运行,它首先组合所有文本并在第一个标签之前插入一个新标签。然后它会删除所有旧标签。

您的 EPUB 文件中可能需要针对更复杂的场景修改逻辑,但这应该有助于您入门。

Question: programmatically combine the content of certain HTML tags

此示例使用 lxml 解析 XHTML 文件并构建新的 XHTML 树。

import io, os
from lxml import etree

XHTML = b"""<?xml version='1.0' encoding='Latin1'?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<body class="calibre">
<p class="calibre5" id="calibre_pb_62">Note for Tyler</p>
<p class="calibre1">In the California registry, there was</p>
<p class="calibre1">a calm breeze blowing through the room. A woman</p>
<p class="calibre1">who must have just walked in quietly beckoned for the</p>
<p class="calibre1">counterman to approach to store her slip.</p>
<p class="calibre1">642</p>
</body></html>"""

class Calibre2EPUB(etree.iterparse):
    def __init__(self, fh):
        """
        Initialize 'iterparse' to only generate 'start' and 'end' events
        :param fh: File Handle from the XHTML File to parse
        """
        super().__init__(fh, events=('start', 'end'))
        self.parse()

    def element(self, elem, parent=None):
        """
        Copy 'elem' with attributes and text to new Element
        :param elem: Source Element
        :param parent: Parent of the new Element
        :return: New Element
        """
        if parent is None:
            e  = etree.Element(elem.tag, nsmap={None: etree.QName(elem).namespace})
        else:
            e = etree.SubElement(parent, elem.tag)

        [e.set(key, elem.attrib[key]) for key in elem.attrib]

        if elem.text:
            e.text = elem.text

        return e

    def parse(self):
        """
        Parse all Elements, copy Elements 1:1 except <p class:'calibre1' Element
        Aggregate all <p class:'calibre1' text to one Element
        :return: None
        """
        self.calibre1 = None

        for event, elem in self:
            if event == 'start':
                if elem.tag.endswith('html'):
                    self._xhtml = self.element(elem)

                elif elem.tag.endswith('body'):
                    self.body = self.element(elem, parent=self._xhtml)

            if event == 'end':
                if elem.tag.endswith('p'):
                    _class = elem.attrib['class']
                    if not _class == 'calibre1':
                        p = self.element(elem, parent=self.body)
                    else:
                        if self.calibre1 is None:
                            self.calibre1 = self.element(elem, parent=self.body)
                        else:
                            self.calibre1.text += ' ' + elem.text

    @property
    def xhtml(self):
        """
        :return: The new Element Tree XHTML
        """
        return etree.tostring(self._xhtml, xml_declaration=True, encoding='Latin1', pretty_print=True)

Usage_

if __name__ == "__main__":
    # with open(os.path.join(pathname, file_name), 'rb', encoding="Latin1") as in_file:
    with io.BytesIO(XHTML) as in_file:
        print(Calibre2EPUB(in_file).xhtml.decode())

    #with open(os.path.join(pathname, '_modified_' + file_name), 'wb') as out_file:
    #    out_file.write(Calibre2EPUB(xml_file).xhtml)

Output:

<?xml version='1.0' encoding='Latin1'?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<body class="calibre">
<p class="calibre5" id="calibre_pb_62">Note for Tyler</p>
<p class="calibre1">In the California registry, ... (omitted for brevity)to store her slip. 642</p>
</body></html>

测试 Python:3.5