BeautifulSoup 提供了多种输出格式化的方法,可以将解析后的HTML以美观、易读的形式输出,或者提取纯净的文本内容。本章将详细介绍 prettify()、get_text() 等格式化方法的使用。
为了演示各种输出格式化方法,我们使用以下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>版权所有 © 2024</p>
<p>联系方式: <a href="mailto:contact@example.com">contact@example.com</a></p>
</footer>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'lxml')
prettify() 方法可以将HTML文档格式化为标准的缩进格式,使其更易读。
# 美化整个文档
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])
# 指定输出编码
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]}")
# 默认缩进(使用空格)
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)}")
get_text() 方法用于提取HTML中的纯文本内容,忽略所有标签。
# 提取整个文档的文本
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()}")
# 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])
# 递归提取所有后代文本(默认)
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])
BeautifulSoup 提供了多种字符串表示方法,每种方法有不同的用途。
# 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]}...")
# 获取标签的字符串表示
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}")
BeautifulSoup 可以处理不同编码的文档,并正确输出。
# 查看原始编码
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("解码失败")
# 示例:处理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]}")
# 创建包含空白字符的示例
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)}")
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)
BeautifulSoup 可以方便地将格式化结果保存到文件。
# 保存美化后的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")
# 保存纯文本
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")
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])
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}")
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] + "...")
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)
# 不推荐:对大型文档频繁调用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)
# 根据需求选择合适的参数
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() # 只处理部分内容
# 批量提取多个元素的文本
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)} 个段落文本")
# 解决方案:指定正确的编码
# 方法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())
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]}...")
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] + "...")
prettify()方法可以生成美观的HTML输出,get_text()方法可以提取纯净的文本内容。通过合理使用参数和编码处理,可以确保输出结果的正确性和可读性。在实际应用中,根据具体需求选择合适的格式化方法,并注意性能优化,可以高效地处理和展示HTML文档内容。