每天学习一点Python——用三种方法提取网页标题
大家好,今天我们来做一个非常实用的练习:如何从一个网页中提取出标题。这看起来简单,但里面却藏着不少容易踩的坑。让我们通过实际代码一步步来探索。
准备工作:获取网页HTML
首先,我们需要从网上下载一个页面的HTML代码。这里我们用Python内置的urllib.request模块来完成任务。
from urllib.request import urlopen
url = "http://olympus.realpython.org/profiles/aphrodite"
page = urlopen(url)
html_bytes = page.read()
html = html_bytes.decode("utf-8")
print(html)
代码讲解:
- •
from urllib.request import urlopen:从Python标准库中导入urlopen函数,这个函数可以打开一个网络地址。 - •
url = "...":定义要访问的网址,这里是一个专门用来练习的测试页面。 - •
page = urlopen(url):调用urlopen()打开网页,返回一个HTTPResponse对象,它包含了服务器的响应。 - •
html_bytes = page.read():使用.read()方法读取响应内容。注意,返回的是字节序列(bytes类型),而不是字符串。 - •
html = html_bytes.decode("utf-8"):将字节序列解码成字符串。"utf-8"是最常用的字符编码,绝大多数网页都使用它。 - •
print(html):打印出整个HTML源码。
执行结果:
<html>
<head>
<title>Profile: Aphrodite</title>
</head>
<body bgcolor="yellow">
<center>
<br><br>
<img src="/static/aphrodite.gif" />
<h2>Name: Aphrodite</h2>
<br><br>
Favorite animal: Dove
<br><br>
Favorite color: Red
<br><br>
Hometown: Mount Olympus
</center>
</body>
</html>
可以看到,我们成功获取了完整的HTML代码。现在,我们的目标是从中提取出<title>标签里的内容"Profile: Aphrodite"。
方法一:字符串查找(最容易踩坑)
第一种思路是直接用字符串的.find()方法定位标签位置,然后用切片提取。
步骤1:查找<title>标签的位置
title_index = html.find("<title>")
print(title_index)
执行结果:
14
.find()方法返回子字符串第一次出现的起始索引。这里返回14,意思是字符串"<title>"从索引14开始。
💡 小Tip:初学者最容易困惑的地方
为什么是14?因为HTML中<html>和<head>后面各有一个换行符\n,它们也是字符串的一部分,占用索引位置。
具体索引分布:
字符串中的每个字符(包括空格、换行)都计入索引!
步骤2:计算标题开始位置
start_index = title_index + len("<title>")
print(start_index)
执行结果:
21
len("<title>")返回7(因为<title>有7个字符)。14 + 7 = 21,所以标题内容从索引21开始。
步骤3:查找</title>标签的位置
end_index = html.find("</title>")
print(end_index)
执行结果:
39
</title>的起始位置是39。
步骤4:提取标题
title = html[start_index:end_index]
print(title)
执行结果:
Profile: Aphrodite
切片html[21:39]成功提取出了标题!
💡 小Tip:切片规则
切片[start:end]包含start索引的字符,但不包含end索引的字符。所以索引39的<没有被包含在内,这正是我们想要的。
方法二:正则表达式(更灵活)
字符串查找的方法虽然可行,但太脆弱了——如果HTML格式稍有变化(比如<title>中间多了个空格),代码就会出错。这时候,正则表达式就派上用场了。
先来熟悉正则表达式基础
import re
print(re.findall("ab*c", "ac"))
print(re.findall("ab*c", "abcd"))
print(re.findall("ab*c", "acc"))
print(re.findall("ab*c", "abcac"))
print(re.findall("ab*c", "abdc"))
执行结果:
['ac']
['abc']
['ac']
['abc', 'ac']
[]
代码讲解:
- •
import re:导入Python的正则表达式模块。 - •
re.findall(模式, 字符串):在字符串中查找所有匹配模式的子串,返回列表。 - •
"ab*c":这是一个正则表达式模式,它的含义是: - • 所以
"ab*c"匹配的是:以a开头、以c结尾、中间有0个或多个b的字符串。
逐行解析:
- •
"ac":a + 0个b + c ✅ → ['ac'] - •
"abcd":a + 1个b + c → "abc" ✅ → ['abc'] - •
"acc":a + 0个b + c ✅ → ['ac'] - •
"abcac":找到两个匹配:"abc"和"ac" ✅ → ['abc', 'ac'] - •
"abdc":a后面是b,但再后面是d(不是c),所以不匹配 → []
忽略大小写匹配
print(re.findall("ab*c", "ABC"))
print(re.findall("ab*c", "ABC", re.IGNORECASE))
执行结果:
[]
['ABC']
代码讲解:
- • 第一个
re.findall()没有指定标志位,默认区分大小写,所以找不到匹配。 - • 第二个
re.findall()增加了第三个参数re.IGNORECASE,这个标志让匹配忽略大小写,所以能找到"ABC"。
点号匹配任意字符
print(re.findall("a.c", "abc"))
print(re.findall("a.c", "abbc"))
执行结果:
['abc']
[]
"a.c"中的.代表任意单个字符,所以:
- •
"abc":a + b(任意字符) + c ✅ - •
"abbc":a + b,但下一个字符是b不是c(模式只匹配三个字符)❌
.*匹配任意字符任意次数
print(re.findall("a.*c", "abc"))
print(re.findall("a.*c", "abbc"))
print(re.findall("a.*c", "ac"))
执行结果:
['abc']
['abbc']
['ac']
"a.*c"中的.*代表任意字符出现任意次数,所以:
使用re.search()获取匹配对象
match_results = re.search("ab*c", "ABC", re.IGNORECASE)
print(match_results.group())
执行结果:
ABC
代码讲解:
- •
re.search()和re.findall()不同,它返回第一个匹配的Match对象,而不是列表。 - •
match_results.group()从Match对象中提取出匹配的字符串。
re.sub()替换文本——小心贪婪匹配
string = "Everything is <replaced> if it's in <tags>."
string = re.sub("<.*>", "ELEPHANTS", string)
print(string)
执行结果:
Everything is ELEPHANTS.
代码讲解:
- •
re.sub(模式, 替换文本, 原字符串):将字符串中匹配模式的部分替换成指定文本。 - •
"<.*>":这个模式匹配从第一个<到最后一个>之间的所有内容。 - • 问题在于
.*是贪婪匹配,它会尽可能匹配最长的字符串。所以它匹配了整个"<replaced> if it's in <tags>",而不是分别匹配两个标签。
使用非贪婪匹配.*?
string = "Everything is <replaced> if it's in <tags>."
string = re.sub("<.*?>", "ELEPHANTS", string)
print(string)
执行结果:
Everything is ELEPHANTS if it's in ELEPHANTS.
代码讲解:
- •
"<.*?>":.*?是非贪婪匹配,它会尽可能匹配最短的字符串。 - • 现在它分别匹配了
<replaced>和<tags>两个标签,分别替换成"ELEPHANTS"。
💡 小Tip:贪婪 vs 非贪婪
- •
*、+等量词默认是贪婪的,会匹配尽可能多的字符。 - • 在量词后面加
?变成非贪婪(如*?、+?),会匹配尽可能少的字符。 - • 在处理HTML标签时,通常需要用非贪婪匹配,避免把多个标签一起匹配掉。
用正则表达式提取标题
import re
from urllib.request import urlopen
url = "http://olympus.realpython.org/profiles/aphrodite"
page = urlopen(url)
html = page.read().decode("utf-8")
pattern = "<title.*?>.*?</title.*?>"
match_results = re.search(pattern, html, re.IGNORECASE)
title = match_results.group()
title = re.sub("<.*?>", "", title)
print(title)
执行结果:
Profile: Aphrodite
代码讲解:
- •
pattern = "<title.*?>.*?</title.*?>":这个模式分三部分理解: - 1.
<title.*?>:匹配<title开头,后面可能有任意字符(如空格、属性),非贪婪匹配到第一个>结束。 - 3.
</title.*?>:匹配</title开头,后面可能有任意字符,非贪婪匹配到第一个>结束。
- •
re.search(pattern, html, re.IGNORECASE):在html中搜索匹配pattern的内容,忽略大小写(这样即使HTML中是<TITLE>也能匹配)。 - •
match_results.group():从Match对象中提取出匹配的完整字符串,比如"<title>Profile: Aphrodite</title>"。 - •
re.sub("<.*?>", "", title):用空字符串替换所有HTML标签,只保留文本内容。 - •
print(title):打印最终提取出的标题。
方法三:BeautifulSoup HTML解析器(最专业)
正则表达式虽然强大,但处理复杂的HTML时容易出错。专业的HTML解析器BeautifulSoup是更好的选择。
首先需要安装:
pip install beautifulsoup4
创建BeautifulSoup对象
from bs4 import BeautifulSoup
from urllib.request import urlopen
url = "http://olympus.realpython.org/profiles/dionysus"
page = urlopen(url)
html = page.read().decode("utf-8")
soup = BeautifulSoup(html, "html.parser")
print(soup)
代码讲解:
- •
from bs4 import BeautifulSoup:从bs4模块导入BeautifulSoup类。 - •
soup = BeautifulSoup(html, "html.parser"):创建BeautifulSoup对象。第一个参数是要解析的HTML字符串,第二个参数"html.parser"指定使用Python内置的HTML解析器。 - •
print(soup):打印解析后的对象,会显示格式化后的HTML。
执行结果:
<html>
<head>
<title>Profile: Dionysus</title>
</head>
<body bgcolor="yellow">
<center>
<br/><br/>
<img src="/static/dionysus.jpg"/>
<h2>Name: Dionysus</h2>
<img src="/static/grapes.png"/><br/><br/>
Hometown: Mount Olympus
<br/><br/>
Favorite animal: Leopard <br/>
<br/>
Favorite Color: Wine
</center>
</body>
</html>
提取纯文本
print(soup.get_text())
代码讲解:
- •
.get_text():提取HTML文档中的所有文本内容,自动去除HTML标签。注意方法名是小写的get_text,不是get_Text。
执行结果:
Profile: Dionysus
Name: Dionysus
Hometown: Mount Olympus
Favorite animal: Leopard
Favorite Color: Wine
💡 小Tip:常见拼写错误
很多初学者会写成soup.get_Text(),但Python是区分大小写的,正确的写法是soup.get_text()(小写t)。
如果写成soup.get_Text(),会得到错误:
TypeError: 'NoneType' object is not callable
这是因为get_Text不存在,Python找到了一个叫get_Text的属性(值是None),然后试图调用它,当然会失败。
查找所有图片标签
img_tags = soup.find_all("img")
print(img_tags)
执行结果:
[<img src="/static/dionysus.jpg"/>, <img src="/static/grapes.png"/>]
代码讲解:
- •
soup.find_all("img"):查找文档中所有<img>标签,返回一个列表。列表中的每个元素都是Tag对象,而不是字符串。
解包Tag对象并查看标签名
image1, image2 = soup.find_all("img")
print(image1.name)
执行结果:
img
代码讲解:
- •
image1, image2 = soup.find_all("img"):将返回的列表解包赋值给两个变量。 - •
image1.name:每个Tag对象都有.name属性,返回标签名(这里是"img")。
访问标签属性
print(image1["src"])
print(image2["src"])
执行结果:
/static/dionysus.jpg
/static/grapes.png
代码讲解:
- •
image1["src"]:通过字典风格的键访问,获取<img>标签的src属性值。
直接访问特定标签
print(soup.title)
print(soup.title.string)
执行结果:
<title>Profile: Dionysus</title>
Profile: Dionysus
代码讲解:
- •
soup.title:直接获取<title>标签,返回Tag对象。 - •
soup.title.string:获取标签内的文本内容。
带条件的查找
specific_img = soup.find_all("img", src="/static/dionysus.jpg")
print(specific_img)
执行结果:
[<img src="/static/dionysus.jpg"/>]
代码讲解:
- •
soup.find_all("img", src="/static/dionysus.jpg"):查找所有<img>标签中,src属性等于指定值的标签。 - • 这是BeautifulSoup的强大功能,可以根据标签属性精确定位。
三种方法对比
推荐:日常工作中,除非是非常简单的任务,否则建议直接用BeautifulSoup,省时省力又稳定。
总结
今天我们通过一个简单的“提取网页标题”的任务,学习了:
- 1. 字符串方法的基本操作:
.find()查找位置、切片提取内容,以及要注意的索引计数问题 - 2. 正则表达式的基础知识:元字符含义、贪婪与非贪婪、常用函数
findall()、search()、sub() - 3. BeautifulSoup的基本使用:创建对象、提取文本、查找标签、访问属性
这三种方法各有千秋,掌握它们,你就拥有了处理各种HTML解析任务的工具箱。
📦 资源获取提示
关注「码农自习室」,后台回复关键词 Python学习,即可获取本文完整代码,一起动手掌握高效编程的核心技巧!
❤️ 支持我们
如果觉得本文对你有帮助,欢迎点赞 + 关注,您的支持是我们持续创作优质内容的最大动力!
📚 学习资源说明
本文内容整理自《Python基础教程(第3版)》第16章,所有命令均已实测。