BeautifulSoup输出格式化

BeautifulSoup 提供了多种输出格式化的方法,可以将解析后的HTML以美观、易读的形式输出,或者提取纯净的文本内容。本章将详细介绍 prettify()get_text() 等格式化方法的使用。

核心功能:BeautifulSoup 可以美化HTML输出、提取纯文本、处理编码问题,并支持多种输出格式。

示例HTML文档

为了演示各种输出格式化方法,我们使用以下HTML文档作为示例:

from bs4 import BeautifulSoup

html_doc = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>格式化示例页面</title>
    <style>
        .highlight { color: red; }
        .hidden { display: none; }
    </style>
    <script>
        console.log("JavaScript代码");
    </script>
</head>
<body>
    <!-- 这是一个注释 -->
    <div id="main-content" class="container">
        <h1>BeautifulSoup<span class="highlight">输出格式化</span></h1>
        <p>这是一个<strong>演示</strong>页面。</p>
        <div class="article">
            <h2>第一章:HTML解析</h2>
            <p>BeautifulSoup可以解析<em>HTML</em>和<em>XML</em>文档。</p>
            <ul>
                <li>列表项1</li>
                <li>列表项2</li>
                <li>列表项3</li>
            </ul>
            <div class="hidden">这个内容不应该显示</div>
        </div>
        <pre><code># Python代码示例
print("Hello, BeautifulSoup!")
        </code></pre>
        <footer>
            <p>版权所有 &copy; 2024</p>
            <p>联系方式: <a href="mailto:contact@example.com">contact@example.com</a></p>
        </footer>
    </div>
</body>
</html>
"""

soup = BeautifulSoup(html_doc, 'lxml')

1. prettify() - 美化HTML输出

prettify() 方法可以将HTML文档格式化为标准的缩进格式,使其更易读。

1.1 基本使用

# 美化整个文档
pretty_html = soup.prettify()
print("=== 美化后的HTML(前200个字符)===")
print(pretty_html[:200])

# 只美化特定元素
div_content = soup.find('div', id='main-content')
if div_content:
    pretty_div = div_content.prettify()
    print("\n=== div#main-content的美化输出(前150个字符)===")
    print(pretty_div[:150])

1.2 指定编码

# 指定输出编码
pretty_utf8 = soup.prettify(encoding='utf-8')
print(f"UTF-8编码的美化HTML类型: {type(pretty_utf8)}")
print(f"字节长度: {len(pretty_utf8)}")

# 转换为字符串
if isinstance(pretty_utf8, bytes):
    pretty_str = pretty_utf8.decode('utf-8')
    print(f"\n字符串长度: {len(pretty_str)}")
    print(f"前100个字符:\n{pretty_str[:100]}")

1.3 指定缩进宽度

# 默认缩进(使用空格)
default_pretty = soup.prettify()
print("默认缩进(前5行):")
for i, line in enumerate(default_pretty.split('\n')[:5], 1):
    print(f"{i}: {repr(line)}")

# 指定缩进宽度为2
pretty_2space = soup.prettify(indent_width=2)
print("\n2空格缩进(前5行):")
for i, line in enumerate(pretty_2space.split('\n')[:5], 1):
    print(f"{i}: {repr(line)}")

# 使用制表符缩进
pretty_tab = soup.prettify(indent_width='\t')
print("\n制表符缩进(前5行):")
for i, line in enumerate(pretty_tab.split('\n')[:5], 1):
    print(f"{i}: {repr(line)}")

2. get_text() - 提取文本内容

get_text() 方法用于提取HTML中的纯文本内容,忽略所有标签。

2.1 基本使用

# 提取整个文档的文本
full_text = soup.get_text()
print("=== 整个文档的文本 ===")
print(full_text[:200] + "...")

# 提取特定元素的文本
div_text = soup.find('div', id='main-content').get_text()
print("\n=== div#main-content的文本 ===")
print(div_text[:150] + "...")

# 提取标题文本
h1_text = soup.h1.get_text()
print(f"\n标题文本: {h1_text}")

# 提取列表项文本
list_items = soup.find_all('li')
print("\n列表项文本:")
for i, li in enumerate(list_items, 1):
    print(f"{i}. {li.get_text()}")

2.2 参数详解

# separator参数:指定文本分隔符
text_with_separator = soup.get_text(separator=' | ')
print("使用分隔符的文本(前200字符):")
print(text_with_separator[:200])

# strip参数:去除文本两端的空白字符
stripped_text = soup.get_text(strip=True)
print("\n去除空白字符的文本(前200字符):")
print(stripped_text[:200])

# 组合使用参数
clean_text = soup.get_text(separator=' ', strip=True)
print("\n清理后的文本(前200字符):")
print(clean_text[:200])

2.3 递归与非递归提取

# 递归提取所有后代文本(默认)
recursive_text = soup.find('div', class_='article').get_text()
print("递归提取article文本:")
print(recursive_text[:150])

# 非递归提取(仅直接子节点文本)
# 注意:BeautifulSoup的get_text()没有直接的非递归参数
# 可以通过遍历实现
def get_text_non_recursive(element):
    """非递归提取元素的文本"""
    texts = []
    for child in element.children:
        if isinstance(child, str):
            texts.append(child.strip())
        elif hasattr(child, 'name') and child.name in ['br', 'p', 'div']:
            # 某些标签强制换行
            texts.append('\n')
    return ' '.join(texts)

# 对比结果
article_div = soup.find('div', class_='article')
print("\n=== 对比递归和非递归提取 ===")
print("递归结果(前100字符):")
print(article_div.get_text()[:100])
print("\n非递归结果:")
print(get_text_non_recursive(article_div)[:100])

3. 字符串表示方法

BeautifulSoup 提供了多种字符串表示方法,每种方法有不同的用途。

3.1 str() 和 unicode()

# str() 方法:返回紧凑格式的HTML
compact_html = str(soup)
print("紧凑格式HTML(前200字符):")
print(compact_html[:200])

# 比较不同表示方法
print("\n=== 不同表示方法比较 ===")
h1_element = soup.h1

print(f"str(h1): {str(h1_element)}")
print(f"repr(h1): {repr(h1_element)}")
print(f"h1.prettify(): {h1_element.prettify()[:50]}...")

3.2 提取属性值的字符串表示

# 获取标签的字符串表示
link = soup.find('a')
if link:
    print(f"链接标签: {str(link)}")
    print(f"链接href属性: {link['href']}")
    print(f"链接文本: {link.string}")

# 处理特殊字符
footer = soup.find('footer')
if footer:
    footer_html = str(footer)
    print(f"\nfooter的HTML: {footer_html}")

    # 解码HTML实体
    import html
    decoded_text = html.unescape(footer_html)
    print(f"解码后: {decoded_text}")

4. 编码处理

BeautifulSoup 可以处理不同编码的文档,并正确输出。

4.1 检测和指定编码

# 查看原始编码
print(f"原始编码: {soup.original_encoding}")

# 强制指定编码
soup.original_encoding = 'utf-8'
print(f"设置后的编码: {soup.original_encoding}")

# 重新编码输出
output_gb2312 = soup.encode('gb2312')
print(f"GB2312编码输出类型: {type(output_gb2312)}")
print(f"字节长度: {len(output_gb2312)}")

# 解码查看
try:
    decoded = output_gb2312.decode('gb2312')
    print(f"解码后前100字符: {decoded[:100]}")
except UnicodeDecodeError:
    print("解码失败")

4.2 处理不同编码的文档

# 示例:处理GBK编码的文档
gbk_html = """
<html>
<head>
    <meta charset="GBK">
    <title>GBK编码示例</title>
</head>
<body>
    <h1>中文标题</h1>
    <p>这是一个GBK编码的页面</p>
</body>
</html>
""".encode('gbk')

# 使用from_encoding参数指定编码
soup_gbk = BeautifulSoup(gbk_html, 'lxml', from_encoding='gbk')
print(f"GBK文档的标题: {soup_gbk.title.text if soup_gbk.title else '无'}")

# 转换为UTF-8输出
utf8_output = soup_gbk.encode('utf-8')
print(f"UTF-8输出长度: {len(utf8_output)}")
print(f"解码后: {utf8_output.decode('utf-8')[:100]}")

5. 格式化选项和技巧

5.1 控制空白字符处理

# 创建包含空白字符的示例
whitespace_html = """
<div>
    <p>  前后有空白  </p>
    <p>中间  有多个  空格</p>
    <p>
        多行
        文本
    </p>
</div>
"""

soup_ws = BeautifulSoup(whitespace_html, 'lxml')
div = soup_ws.div

print("=== 不同空白处理方式 ===")
print("原始get_text():")
print(repr(div.get_text()))

print("\n使用strip参数:")
print(repr(div.get_text(strip=True)))

print("\n使用separator和strip:")
print(repr(div.get_text(separator=' ', strip=True)))

# 自定义空白处理函数
def clean_whitespace(text):
    """清理多余的空白字符"""
    import re
    # 将多个空白字符替换为单个空格
    text = re.sub(r'\s+', ' ', text)
    # 去除两端空白
    return text.strip()

cleaned_text = clean_whitespace(div.get_text())
print(f"\n自定义清理后: {repr(cleaned_text)}")

5.2 保留特定标签的结构

def get_text_preserve_structure(element, preserved_tags=None):
    """
    提取文本但保留某些标签的结构
    preserved_tags: 要保留的标签列表,如 ['p', 'br', 'div']
    """
    if preserved_tags is None:
        preserved_tags = []

    result = []

    for child in element.children:
        if isinstance(child, str):
            # 文本节点
            text = child.strip()
            if text:
                result.append(text)
        elif hasattr(child, 'name'):
            if child.name in preserved_tags:
                # 保留标签,递归处理其内容
                inner_text = get_text_preserve_structure(child, preserved_tags)
                if inner_text:
                    result.append(inner_text)
                # 添加换行(如果是块级元素)
                if child.name in ['p', 'div', 'br', 'h1', 'h2', 'h3', 'li']:
                    result.append('\n')
            else:
                # 不保留的标签,只提取文本
                text = child.get_text(strip=True)
                if text:
                    result.append(text)

    return ' '.join(result).strip()

# 使用示例
article = soup.find('div', class_='article')
print("=== 保留段落结构的文本提取 ===")
structured_text = get_text_preserve_structure(article, preserved_tags=['p', 'h2', 'li'])
print(structured_text)

6. 输出到文件

BeautifulSoup 可以方便地将格式化结果保存到文件。

6.1 保存美化HTML到文件

# 保存美化后的HTML
with open('formatted_output.html', 'w', encoding='utf-8') as f:
    f.write(soup.prettify())

print("已保存美化HTML到 formatted_output.html")

# 只保存部分内容
with open('article_content.html', 'w', encoding='utf-8') as f:
    article_div = soup.find('div', class_='article')
    if article_div:
        f.write(article_div.prettify())
        print("已保存文章内容到 article_content.html")

6.2 保存纯文本到文件

# 保存纯文本
with open('extracted_text.txt', 'w', encoding='utf-8') as f:
    full_text = soup.get_text(separator='\n', strip=True)
    f.write(full_text)
    print("已保存纯文本到 extracted_text.txt")

# 保存特定元素的文本
with open('article_text.txt', 'w', encoding='utf-8') as f:
    article = soup.find('div', class_='article')
    if article:
        article_text = article.get_text(separator='\n', strip=True)
        f.write(article_text)
        print("已保存文章文本到 article_text.txt")

7. 高级格式化功能

7.1 自定义格式化器

class CustomFormatter:
    """自定义HTML格式化器"""

    def __init__(self, indent_width=4, max_line_length=80):
        self.indent_width = indent_width
        self.max_line_length = max_line_length
        self.current_indent = 0

    def format_element(self, element):
        """格式化单个元素"""
        if not hasattr(element, 'name'):
            return str(element)

        # 构建开始标签
        tag_name = element.name
        attrs = self._format_attributes(element)
        start_tag = f"<{tag_name}{attrs}>"
        end_tag = f"</{tag_name}>" if not self._is_void_element(tag_name) else ""

        # 处理内容
        if element.contents:
            content = self._format_contents(element)
            return f"{start_tag}{content}{end_tag}"
        else:
            return start_tag + end_tag

    def _format_attributes(self, element):
        """格式化属性"""
        if not element.attrs:
            return ""

        attrs = []
        for key, value in element.attrs.items():
            if isinstance(value, list):
                value = ' '.join(value)
            attrs.append(f'{key}="{value}"')

        return ' ' + ' '.join(attrs)

    def _format_contents(self, element):
        """格式化内容"""
        contents = []
        for child in element.contents:
            if hasattr(child, 'name'):
                contents.append(self.format_element(child))
            else:
                contents.append(str(child).strip())

        return ''.join(contents)

    def _is_void_element(self, tag_name):
        """检查是否是空元素(如img, br等)"""
        void_elements = {'img', 'br', 'hr', 'input', 'meta', 'link'}
        return tag_name in void_elements

# 使用自定义格式化器
formatter = CustomFormatter()
custom_formatted = formatter.format_element(soup.find('div', class_='article'))
print("=== 自定义格式化输出(前200字符) ===")
print(custom_formatted[:200])

7.2 提取结构化文本数据

def extract_structured_data(soup):
    """提取结构化的文本数据"""
    data = {
        'title': '',
        'headings': [],
        'paragraphs': [],
        'lists': [],
        'links': []
    }

    # 提取标题
    if soup.title:
        data['title'] = soup.title.get_text(strip=True)

    # 提取所有标题(h1-h6)
    for i in range(1, 7):
        headings = soup.find_all(f'h{i}')
        for heading in headings:
            data['headings'].append({
                'level': i,
                'text': heading.get_text(strip=True),
                'id': heading.get('id', '')
            })

    # 提取段落
    paragraphs = soup.find_all('p')
    for p in paragraphs:
        text = p.get_text(strip=True)
        if text:  # 跳过空段落
            data['paragraphs'].append(text)

    # 提取列表
    lists = soup.find_all(['ul', 'ol'])
    for lst in lists:
        list_items = []
        for li in lst.find_all('li'):
            list_items.append(li.get_text(strip=True))
        if list_items:
            data['lists'].append({
                'type': lst.name,
                'items': list_items
            })

    # 提取链接
    links = soup.find_all('a')
    for link in links:
        text = link.get_text(strip=True)
        href = link.get('href', '')
        if text or href:
            data['links'].append({
                'text': text,
                'href': href
            })

    return data

# 提取结构化数据
structured_data = extract_structured_data(soup)
print("=== 结构化数据 ===")
print(f"标题: {structured_data['title']}")
print(f"标题数量: {len(structured_data['headings'])}")
print(f"段落数量: {len(structured_data['paragraphs'])}")
print(f"列表数量: {len(structured_data['lists'])}")
print(f"链接数量: {len(structured_data['links'])}")

# 显示第一个列表的内容
if structured_data['lists']:
    first_list = structured_data['lists'][0]
    print(f"\n第一个列表类型: {first_list['type']}")
    print("列表项:")
    for item in first_list['items']:
        print(f"  - {item}")

8. 实际应用案例

8.1 提取文章内容并格式化

def extract_and_format_article(soup):
    """提取文章内容并进行格式化"""
    # 查找文章容器
    article_div = soup.find('div', class_='article')
    if not article_div:
        return None

    result = {
        'raw_html': str(article_div),
        'pretty_html': article_div.prettify(),
        'plain_text': article_div.get_text(strip=True),
        'formatted_text': '',
        'metadata': {}
    }

    # 提取元数据
    result['metadata']['headings'] = [
        h.get_text(strip=True) for h in article_div.find_all(['h1', 'h2', 'h3'])
    ]

    result['metadata']['paragraph_count'] = len(article_div.find_all('p'))
    result['metadata']['list_count'] = len(article_div.find_all(['ul', 'ol']))

    # 生成格式化文本(保留结构)
    formatted_lines = []

    for element in article_div.children:
        if hasattr(element, 'name'):
            if element.name.startswith('h'):
                # 标题:添加空行和标记
                text = element.get_text(strip=True)
                formatted_lines.append('\n' + '#' * int(element.name[1]) + ' ' + text + '\n')
            elif element.name == 'p':
                # 段落
                text = element.get_text(strip=True)
                formatted_lines.append(text + '\n')
            elif element.name in ['ul', 'ol']:
                # 列表
                for li in element.find_all('li'):
                    text = li.get_text(strip=True)
                    prefix = '- ' if element.name == 'ul' else '1. '
                    formatted_lines.append(prefix + text)
                formatted_lines.append('')  # 空行

    result['formatted_text'] = '\n'.join(formatted_lines)
    return result

# 执行提取和格式化
article_data = extract_and_format_article(soup)
if article_data:
    print("=== 文章格式化结果 ===")
    print(f"段落数量: {article_data['metadata']['paragraph_count']}")
    print(f"列表数量: {article_data['metadata']['list_count']}")
    print("\n=== 格式化文本 ===")
    print(article_data['formatted_text'][:300] + "...")

8.2 生成内容摘要

def generate_summary(soup, max_length=200):
    """生成内容摘要"""
    # 获取所有文本
    full_text = soup.get_text(strip=True)

    if len(full_text) <= max_length:
        return full_text

    # 查找重要内容(标题和第一段)
    summary_parts = []

    # 添加主标题
    h1 = soup.find('h1')
    if h1:
        summary_parts.append(h1.get_text(strip=True))

    # 添加第一个段落
    first_p = soup.find('p')
    if first_p:
        first_text = first_p.get_text(strip=True)
        if first_text not in summary_parts:  # 避免重复
            summary_parts.append(first_text)

    # 如果还没有足够内容,添加更多段落
    if len(' '.join(summary_parts)) < max_length:
        paragraphs = soup.find_all('p')[1:3]  # 取第2-3段
        for p in paragraphs:
            text = p.get_text(strip=True)
            current_summary = ' '.join(summary_parts)
            if len(current_summary + ' ' + text) <= max_length:
                summary_parts.append(text)
            else:
                break

    summary = ' '.join(summary_parts)

    # 如果还是太长,截断
    if len(summary) > max_length:
        summary = summary[:max_length].rsplit(' ', 1)[0] + '...'

    return summary

# 生成摘要
summary = generate_summary(soup, max_length=150)
print("=== 内容摘要 ===")
print(summary)

9. 性能优化建议

技巧1:避免不必要的prettify()调用
# 不推荐:对大型文档频繁调用prettify()
large_soup = BeautifulSoup("<div>" * 1000 + "内容" + "</div>" * 1000, 'lxml')
# 这会创建整个文档的格式化副本,内存占用高

# 推荐:只在需要时格式化特定部分
if need_formatted_output:
    # 只格式化需要的部分
    important_section = large_soup.find(id='important')
    if important_section:
        formatted = important_section.prettify()
        print(formatted)
技巧2:合理使用get_text()参数
# 根据需求选择合适的参数
soup = BeautifulSoup(html_doc, 'lxml')

# 如果需要清理的文本,使用strip=True
clean_text = soup.get_text(strip=True)

# 如果需要保留某些结构,使用separator
structured_text = soup.get_text(separator='\n')

# 对于大文档,避免频繁调用get_text()
# 可以先提取需要的部分
content_div = soup.find(id='content')
if content_div:
    text = content_div.get_text()  # 只处理部分内容
技巧3:批量处理文本提取
# 批量提取多个元素的文本
def extract_texts_batch(elements):
    """批量提取文本,减少方法调用开销"""
    texts = []
    for elem in elements:
        if hasattr(elem, 'get_text'):
            texts.append(elem.get_text(strip=True))
        else:
            texts.append(str(elem).strip())
    return texts

# 使用示例
paragraphs = soup.find_all('p')
paragraph_texts = extract_texts_batch(paragraphs)
print(f"提取了 {len(paragraph_texts)} 个段落文本")

10. 常见问题与解决方案

问题1:prettify()输出中文乱码怎么办?
# 解决方案:指定正确的编码
# 方法1:在prettify()中指定编码
pretty_bytes = soup.prettify(encoding='utf-8')
pretty_str = pretty_bytes.decode('utf-8')

# 方法2:确保BeautifulSoup正确识别了编码
print(f"当前编码: {soup.original_encoding}")
if soup.original_encoding != 'utf-8':
    # 重新解析时指定编码
    html_bytes = str(soup).encode(soup.original_encoding)
    soup = BeautifulSoup(html_bytes, 'lxml', from_encoding='utf-8')

# 方法3:保存到文件时指定编码
with open('output.html', 'w', encoding='utf-8') as f:
    f.write(soup.prettify())
问题2:如何提取特定标签的文本但排除其他内容?
def extract_specific_tags_text(soup, tag_names, exclude_tags=None):
    """
    提取特定标签的文本,排除不需要的标签
    tag_names: 要提取的标签列表,如 ['p', 'h1', 'h2']
    exclude_tags: 要排除的标签列表,如 ['script', 'style']
    """
    if exclude_tags is None:
        exclude_tags = ['script', 'style', 'noscript']

    # 创建文档副本
    soup_copy = BeautifulSoup(str(soup), 'lxml')

    # 删除不需要的标签
    for tag_name in exclude_tags:
        for tag in soup_copy.find_all(tag_name):
            tag.decompose()

    # 提取特定标签的文本
    texts = []
    for tag_name in tag_names:
        for tag in soup_copy.find_all(tag_name):
            text = tag.get_text(strip=True)
            if text:
                texts.append(text)

    return texts

# 使用示例
main_texts = extract_specific_tags_text(
    soup,
    tag_names=['h1', 'h2', 'p'],
    exclude_tags=['script', 'style', 'footer']
)
print(f"提取了 {len(main_texts)} 个主要文本块")
for i, text in enumerate(main_texts[:3], 1):
    print(f"{i}. {text[:50]}...")
问题3:如何保持原始格式的同时提取文本?
def extract_text_with_formatting(element):
    """提取文本并保持基本格式"""
    result = []

    def process_node(node, indent_level=0):
        if isinstance(node, str):
            text = node.strip()
            if text:
                result.append('  ' * indent_level + text)
        elif hasattr(node, 'name'):
            # 处理块级元素(添加换行)
            if node.name in ['p', 'div', 'h1', 'h2', 'h3', 'li']:
                result.append('')  # 空行

            # 递归处理子节点
            for child in node.children:
                process_node(child, indent_level + 1 if node.name == 'li' else indent_level)

            # 块级元素后添加换行
            if node.name in ['p', 'div', 'h1', 'h2', 'h3']:
                result.append('')

    process_node(element)
    return '\n'.join(filter(None, result))  # 过滤空字符串

# 使用示例
article = soup.find('div', class_='article')
if article:
    formatted_text = extract_text_with_formatting(article)
    print("=== 保持格式的文本 ===")
    print(formatted_text[:300] + "...")
本章总结:BeautifulSoup的输出格式化功能强大且灵活。prettify()方法可以生成美观的HTML输出,get_text()方法可以提取纯净的文本内容。通过合理使用参数和编码处理,可以确保输出结果的正确性和可读性。在实际应用中,根据具体需求选择合适的格式化方法,并注意性能优化,可以高效地处理和展示HTML文档内容。