默认内容
- 默认项
BeautifulSoup 不仅用于解析和提取HTML数据,还可以修改文档内容和结构。本章将详细介绍如何修改标签、属性、文本,以及如何添加、删除和移动节点,让你能够动态编辑HTML文档。
为了演示各种修改操作,我们使用以下HTML文档作为基础:
from bs4 import BeautifulSoup
html_doc = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>原始页面</title>
<meta charset="UTF-8">
</head>
<body>
<div id="header" class="page-header">
<h1>原始标题</h1>
<p class="subtitle">原始副标题</p>
</div>
<div id="content" class="main-content">
<article class="post" id="article-1">
<h2>文章标题1</h2>
<p>这是第一篇文章的内容。</p>
<div class="meta">
<span class="author">作者: 张三</span>
<span class="date">发布日期: 2024-01-01</span>
</div>
</article>
<article class="post" id="article-2">
<h2>文章标题2</h2>
<p>这是第二篇文章的内容。</p>
<div class="meta">
<span class="author">作者: 李四</span>
<span class="date">发布日期: 2024-01-02</span>
</div>
</article>
</div>
<div id="sidebar" class="sidebar">
<h3>侧边栏标题</h3>
<ul class="menu">
<li><a href="#home">首页</a></li>
<li><a href="#about">关于</a></li>
<li><a href="#contact">联系</a></li>
</ul>
</div>
<footer id="footer" class="page-footer">
<p>版权所有 © 2024</p>
</footer>
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'lxml')
# 修改h1标签为h2
h1_tag = soup.find('h1')
if h1_tag:
h1_tag.name = 'h2'
print(f"h1标签已修改为: {h1_tag.name}")
# 修改多个标签
for h2 in soup.find_all('h2'):
h2.name = 'h3'
print(f"所有h2标签已修改为h3")
# 查看修改结果
print("\n修改后的标题:")
for tag in soup.find_all(['h2', 'h3']):
print(f"{tag.name}: {tag.text}")
# 修改单个属性
header_div = soup.find('div', id='header')
if header_div:
header_div['class'] = 'new-header'
print(f"header的class已修改为: {header_div['class']}")
# 添加新属性
header_div['data-modified'] = 'true'
print(f"添加的新属性: data-modified={header_div.get('data-modified')}")
# 修改多个属性
article = soup.find('article', id='article-1')
if article:
article['class'] = ['post', 'updated', 'featured']
article['data-status'] = 'published'
print(f"文章1的新属性:")
print(f" class: {article['class']}")
print(f" data-status: {article.get('data-status')}")
# 删除属性
if 'data-modified' in header_div.attrs:
del header_div['data-modified']
print(f"已删除data-modified属性")
# 为所有文章添加data-category属性
for i, article in enumerate(soup.find_all('article'), 1):
article['data-category'] = f'category-{i}'
article['data-index'] = i
print("所有文章的属性:")
for article in soup.find_all('article'):
print(f"文章{article.get('id')}: category={article.get('data-category')}, index={article.get('data-index')}")
# 修改所有链接的target属性
for a in soup.find_all('a'):
a['target'] = '_blank'
a['rel'] = 'noopener noreferrer'
print("\n链接属性修改:")
for a in soup.find_all('a'):
print(f"{a.text}: target={a.get('target')}, rel={a.get('rel')}")
# 修改单个标签的文本
title = soup.find('title')
if title:
title.string = "修改后的页面标题"
print(f"页面标题: {title.string}")
# 修改h1文本
h1 = soup.find('h1')
if not h1:
# 如果没有h1,找到第一个h2修改
h2 = soup.find('h2')
if h2:
h2.string = "新的主标题"
print(f"主标题: {h2.string}")
# 修改段落文本
first_paragraph = soup.find('p')
if first_paragraph:
first_paragraph.string = "这是修改后的第一段内容。"
print(f"第一段落: {first_paragraph.string}")
# 替换整个标签
old_footer = soup.find('footer')
if old_footer:
# 创建新的footer元素
new_footer = soup.new_tag('footer')
new_footer['class'] = 'new-footer'
# 添加内容到新footer
copyright_p = soup.new_tag('p')
copyright_p.string = "© 2024 修改后的版权信息"
new_footer.append(copyright_p)
# 添加联系信息
contact_p = soup.new_tag('p')
contact_p.string = "联系方式: info@example.com"
new_footer.append(contact_p)
# 替换旧的footer
old_footer.replace_with(new_footer)
print("footer已替换")
# 查看新的footer
new_footer = soup.find('footer')
if new_footer:
print("新的footer内容:")
for p in new_footer.find_all('p'):
print(f"- {p.text}")
# 找到并修改特定的文本节点
for span in soup.find_all('span', class_='author'):
# 提取原始文本并修改
old_text = span.string
if old_text and '张三' in old_text:
span.string.replace_with('作者: 王五 (修改后)')
elif old_text and '李四' in old_text:
span.string = '作者: 赵六'
print("修改后的作者信息:")
for span in soup.find_all('span', class_='author'):
print(f"- {span.string}")
# 修改日期格式
for span in soup.find_all('span', class_='date'):
if span.string:
old_date = span.string
# 假设格式转换
new_date = old_date.replace('发布日期: ', '发布时间: ')
span.string = new_date
print(f"日期格式已修改: {new_date}")
# 创建新标签
new_div = soup.new_tag('div')
new_div['class'] = 'new-section'
new_div['id'] = 'new-section-1'
# 创建带文本的标签
new_h3 = soup.new_tag('h3')
new_h3.string = "新添加的标题"
# 创建带属性的标签
new_link = soup.new_tag('a', href='https://example.com', target='_blank')
new_link.string = "点击这里"
print("创建的新标签:")
print(f"div: class={new_div['class']}, id={new_div['id']}")
print(f"h3: {new_h3}")
print(f"a: {new_link}")
# 使用append()添加子节点
content_div = soup.find('div', id='content')
if content_div:
# 创建新文章
new_article = soup.new_tag('article')
new_article['class'] = 'post new'
new_article['id'] = 'article-3'
# 添加标题
new_title = soup.new_tag('h2')
new_title.string = "新添加的文章"
new_article.append(new_title)
# 添加内容
new_content = soup.new_tag('p')
new_content.string = "这是通过BeautifulSoup动态添加的文章内容。"
new_article.append(new_content)
# 添加meta信息
meta_div = soup.new_tag('div', **{'class': 'meta'})
author_span = soup.new_tag('span', **{'class': 'author'})
author_span.string = "作者: 系统管理员"
date_span = soup.new_tag('span', **{'class': 'date'})
date_span.string = "发布日期: 2024-01-15"
meta_div.append(author_span)
meta_div.append(" ") # 添加空格
meta_div.append(date_span)
new_article.append(meta_div)
# 将新文章添加到内容区域
content_div.append(new_article)
print("新文章已添加")
# 验证添加结果
print("\n内容区域的文章数量:", len(content_div.find_all('article')))
# 使用insert()在特定位置插入
sidebar = soup.find('div', id='sidebar')
if sidebar:
# 找到菜单ul
menu_ul = sidebar.find('ul', class_='menu')
if menu_ul:
# 创建新菜单项
new_li = soup.new_tag('li')
new_link = soup.new_tag('a', href='#services')
new_link.string = "服务"
new_li.append(new_link)
# 在第二个位置插入(索引1)
menu_ul.insert(1, new_li)
print("新菜单项已插入在第二个位置")
# 查看菜单项
print("\n当前菜单项:")
for i, li in enumerate(menu_ul.find_all('li'), 1):
print(f"{i}. {li.a.text if li.a else '无链接'}")
# 在特定元素前插入
first_article = soup.find('article', id='article-1')
if first_article:
# 创建分隔线
hr_tag = soup.new_tag('hr')
hr_tag['class'] = 'article-separator'
# 在第一个文章前插入
first_article.insert_before(hr_tag)
print("\n分隔线已插入到第一篇文章前")
# 在特定元素后插入
if first_article:
# 创建广告div
ad_div = soup.new_tag('div', **{'class': 'advertisement'})
ad_p = soup.new_tag('p')
ad_p.string = "广告位招租"
ad_div.append(ad_p)
# 在第一篇文章后插入
first_article.insert_after(ad_div)
print("广告已插入到第一篇文章后")
# 创建多个标签
new_tags = []
for i in range(3):
p = soup.new_tag('p')
p.string = f"这是第{i+1}个动态添加的段落。"
p['class'] = 'dynamic-paragraph'
new_tags.append(p)
# 添加到内容区域
content_div = soup.find('div', id='content')
if content_div:
content_div.extend(new_tags)
print(f"已添加 {len(new_tags)} 个新段落")
# 查看添加的段落
print("\n动态添加的段落:")
for p in content_div.find_all('p', class_='dynamic-paragraph'):
print(f"- {p.text}")
# 使用decompose()完全删除元素
sidebar = soup.find('div', id='sidebar')
if sidebar:
# 删除整个侧边栏
sidebar.decompose()
print("侧边栏已完全删除")
# 检查是否删除成功
sidebar = soup.find('div', id='sidebar')
print(f"查找侧边栏: {'找到' if sidebar else '未找到'}")
# 删除特定元素
old_meta = soup.find('div', class_='meta')
if old_meta:
old_meta.decompose()
print("文章meta信息已删除")
# extract()删除元素但保留副本
footer = soup.find('footer')
if footer:
extracted_footer = footer.extract()
print("footer已提取并删除")
print(f"提取的footer内容: {extracted_footer.text.strip()}")
# 检查原文档中是否还有footer
remaining_footer = soup.find('footer')
print(f"文档中是否还有footer: {'是' if remaining_footer else '否'}")
# extract()可以用于移动元素
first_article = soup.find('article', id='article-1')
second_article = soup.find('article', id='article-2')
if first_article and second_article:
# 提取第一篇文章
extracted_article = first_article.extract()
# 插入到第二篇文章后
second_article.insert_after(extracted_article)
print("文章顺序已调整")
# 清空元素内容但保留标签
content_div = soup.find('div', id='content')
if content_div:
# 保存原始HTML
original_html = str(content_div)
# 清空内容
content_div.clear()
print("内容区域已清空")
print(f"清空前长度: {len(original_html)}")
print(f"清空后长度: {len(str(content_div))}")
print(f"清空后内容: {content_div.text}")
# 添加新内容
new_p = soup.new_tag('p')
new_p.string = "内容已被清空并重新填充。"
content_div.append(new_p)
print(f"重新填充后: {content_div.text}")
# 重新构建示例文档
soup = BeautifulSoup(html_doc, 'lxml')
# 移动元素到新位置
sidebar = soup.find('div', id='sidebar')
content_div = soup.find('div', id='content')
if sidebar and content_div:
# 将侧边栏移动到内容区域前
content_div.insert_before(sidebar)
print("侧边栏已移动到内容区域前")
# 移动元素内的子元素
first_article = soup.find('article', id='article-1')
second_article = soup.find('article', id='article-2')
if first_article and second_article:
# 将第一篇文章的标题移动到第二篇文章
first_title = first_article.find('h2')
if first_title:
# 提取标题
extracted_title = first_title.extract()
# 插入到第二篇文章开头
second_article.insert(0, extracted_title)
print("标题已移动到第二篇文章")
# 使用copy()复制元素
first_article = soup.find('article', id='article-1')
if first_article:
# 复制元素
article_copy = first_article.copy()
# 修改复制的元素
article_copy['id'] = 'article-1-copy'
title = article_copy.find('h2')
if title:
title.string = "复制的文章标题"
# 添加到文档中
first_article.insert_after(article_copy)
print("文章已复制并添加")
# 查看复制结果
print("\n所有文章ID:")
for article in soup.find_all('article'):
print(f"- {article.get('id')}")
# 深拷贝与浅拷贝
from copy import copy, deepcopy
# 浅拷贝
shallow_copy = copy(first_article)
# 深拷贝
deep_copy = deepcopy(first_article)
print(f"\n浅拷贝类型: {type(shallow_copy)}")
print(f"深拷贝类型: {type(deep_copy)}")
# 批量修改所有文章
for article in soup.find_all('article'):
# 添加阅读时间估计
content_length = len(article.text)
reading_time = max(1, content_length // 500) # 假设500字符/分钟
reading_div = soup.new_tag('div', **{'class': 'reading-time'})
reading_span = soup.new_tag('span')
reading_span.string = f"阅读时间: {reading_time}分钟"
reading_div.append(reading_span)
article.append(reading_div)
print("已为所有文章添加阅读时间")
# 批量修改链接
for a in soup.find_all('a'):
# 添加图标
if '#home' in a.get('href', ''):
a.string = f"🏠 {a.string}"
elif '#about' in a.get('href', ''):
a.string = f"ℹ️ {a.string}"
elif '#contact' in a.get('href', ''):
a.string = f"📞 {a.string}"
print("链接已添加图标")
def enhance_article(article):
"""增强文章元素的函数"""
# 添加唯一标识
if not article.get('data-uuid'):
import uuid
article['data-uuid'] = str(uuid.uuid4())
# 添加修改时间
import datetime
article['data-last-modified'] = datetime.datetime.now().isoformat()
# 添加编辑按钮
edit_button = soup.new_tag('button', **{
'class': 'edit-btn',
'data-article-id': article.get('id', '')
})
edit_button.string = "编辑"
article.append(edit_button)
return article
# 应用函数到所有文章
for article in soup.find_all('article'):
enhance_article(article)
print("文章增强完成")
# 查看增强后的文章属性
for article in soup.find_all('article'):
print(f"\n文章 {article.get('id')}:")
print(f" UUID: {article.get('data-uuid')}")
print(f" 最后修改: {article.get('data-last-modified')}")
print(f" 是否有编辑按钮: {'是' if article.find('button', class_='edit-btn') else '否'}")
# 重新组织文档结构
def reorganize_document(soup):
"""重新组织文档结构"""
# 创建新的容器
new_container = soup.new_tag('div', **{'class': 'container', 'id': 'main-container'})
# 收集所有主要部分
header = soup.find('div', id='header')
content = soup.find('div', id='content')
sidebar = soup.find('div', id='sidebar')
footer = soup.find('footer')
# 将各部分添加到新容器
if header:
new_container.append(header)
# 创建主要内容区域
main_content = soup.new_tag('div', **{'class': 'main-wrapper'})
if content:
main_content.append(content)
if sidebar:
# 将侧边栏包装在新的div中
sidebar_wrapper = soup.new_tag('div', **{'class': 'sidebar-wrapper'})
sidebar_wrapper.append(sidebar)
main_content.append(sidebar_wrapper)
new_container.append(main_content)
if footer:
new_container.append(footer)
# 替换body的内容
body = soup.find('body')
if body:
body.clear()
body.append(new_container)
return soup
# 执行重构
reorganized_soup = reorganize_document(soup)
print("文档结构已重构")
# 查看新结构
body = reorganized_soup.find('body')
if body:
print("\n新的body结构:")
for child in body.children:
if hasattr(child, 'name'):
print(f"- {child.name}: class={child.get('class', '无')}")
def build_dynamic_page():
"""从头开始构建HTML页面"""
# 创建新的BeautifulSoup对象
soup = BeautifulSoup('', 'lxml')
# 添加HTML5文档声明
soup.append(soup.new_string('\n'))
# 创建html元素
html = soup.new_tag('html', lang='zh-CN')
soup.append(html)
# 创建head
head = soup.new_tag('head')
html.append(head)
# 添加meta和title
meta_charset = soup.new_tag('meta', charset='UTF-8')
title = soup.new_tag('title')
title.string = '动态生成的页面'
head.extend([meta_charset, title])
# 创建body
body = soup.new_tag('body')
html.append(body)
# 添加header
header = soup.new_tag('header', id='main-header')
h1 = soup.new_tag('h1')
h1.string = '欢迎来到动态页面'
header.append(h1)
body.append(header)
# 添加主要内容
main = soup.new_tag('main', id='content')
# 添加文章列表
articles_section = soup.new_tag('section', **{'class': 'articles'})
h2 = soup.new_tag('h2')
h2.string = '文章列表'
articles_section.append(h2)
# 动态生成文章
articles_data = [
{'title': 'Python基础', 'content': '学习Python编程基础。'},
{'title': 'BeautifulSoup教程', 'content': '掌握HTML解析技巧。'},
{'title': 'Web开发', 'content': '构建现代Web应用。'}
]
for i, article_data in enumerate(articles_data, 1):
article = soup.new_tag('article', **{'class': 'post', 'data-id': f'post-{i}'})
title_tag = soup.new_tag('h3')
title_tag.string = article_data['title']
article.append(title_tag)
content_tag = soup.new_tag('p')
content_tag.string = article_data['content']
article.append(content_tag)
articles_section.append(article)
main.append(articles_section)
body.append(main)
# 添加footer
footer = soup.new_tag('footer')
p = soup.new_tag('p')
p.string = '© 2024 动态生成页面 | 使用BeautifulSoup构建'
footer.append(p)
body.append(footer)
return soup
# 构建页面
dynamic_page = build_dynamic_page()
print("动态页面已构建")
# 输出结果
print("\n=== 生成的HTML结构 ===")
print(dynamic_page.prettify()[:500], "...") # 只显示前500字符
class HTMLTemplate:
"""简单的HTML模板系统"""
def __init__(self, template_html):
self.soup = BeautifulSoup(template_html, 'lxml')
self.placeholders = {}
def set_placeholder(self, name, value):
"""设置占位符值"""
self.placeholders[name] = value
def render(self):
"""渲染模板"""
# 替换所有占位符
for placeholder, value in self.placeholders.items():
# 查找所有占位符元素
placeholder_elements = self.soup.find_all(
attrs={'data-placeholder': placeholder}
)
for element in placeholder_elements:
# 根据占位符类型进行处理
placeholder_type = element.get('data-type', 'text')
if placeholder_type == 'text':
element.string = str(value)
elif placeholder_type == 'html':
# 清空并添加HTML内容
element.clear()
if isinstance(value, str):
# 如果是字符串,解析为HTML
value_soup = BeautifulSoup(value, 'lxml')
element.append(value_soup)
else:
# 如果是BeautifulSoup对象或标签
element.append(value)
elif placeholder_type == 'attribute':
# 设置属性值
attr_name = element.get('data-attr', '')
if attr_name:
element[attr_name] = str(value)
# 移除占位符属性
del element['data-placeholder']
if 'data-type' in element.attrs:
del element['data-type']
if 'data-attr' in element.attrs:
del element['data-attr']
return str(self.soup)
# 使用模板系统
template_html = """
默认标题
默认头部
默认内容
- 默认项
"""
# 创建模板实例
template = HTMLTemplate(template_html)
# 设置占位符值
template.set_placeholder('page_title', '用户个人主页')
template.set_placeholder('header_title', '欢迎,张三!')
# 设置HTML内容
content_html = """
"""
template.set_placeholder('content', content_html)
# 设置属性值
template.set_placeholder('avatar_url', 'https://example.com/avatar.jpg')
template.set_placeholder('username', '张三')
# 设置列表项
items_html = """
个人资料
消息中心
账户设置
退出登录
"""
template.set_placeholder('items', items_html)
# 渲染模板
rendered_html = template.render()
print("模板渲染完成")
# 解析渲染结果查看
rendered_soup = BeautifulSoup(rendered_html, 'lxml')
print("\n渲染后的标题:", rendered_soup.find('title').text)
print("头像URL:", rendered_soup.find('img')['src'])
print("列表项数量:", len(rendered_soup.find_all('li')))
BeautifulSoup的修改操作是原地进行的,会直接改变原始对象。如果需要保留原始文档,请先创建副本。
# 错误:直接修改原始文档
soup = BeautifulSoup(html_doc, 'lxml')
# ... 一系列修改操作
# 原始soup已被改变
# 正确:先创建副本
from copy import deepcopy
original_soup = BeautifulSoup(html_doc, 'lxml')
modified_soup = deepcopy(original_soup)
# 在副本上修改
# original_soup保持不变
class属性是多值属性,需要使用列表进行处理。
# 错误:直接赋值字符串
element['class'] = 'new-class' # 这会覆盖原有的类
# 正确:使用列表
element['class'] = ['existing-class', 'new-class']
# 或者添加类
if 'class' in element.attrs:
element['class'].append('additional-class')
else:
element['class'] = ['additional-class']
注意NavigableString对象和普通字符串的区别,以及Tag对象和字符串在操作上的不同。
# 创建新标签时
tag = soup.new_tag('div')
# 添加字符串
tag.string = "文本内容" # 这会替换所有现有内容
# 添加另一个字符串
tag.append(" 更多内容") # 这会追加内容
# 修改文本内容
tag.string = "新文本" # 替换文本
# 或者
tag.clear()
tag.append("全新内容")