🔥 本期专栏《Python爬虫实战》已收录,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:
-
-
- [🌟 开篇语](#🌟 开篇语)
- [📌 上期回顾](#📌 上期回顾)
- [🎯 本节目标](#🎯 本节目标)
- [一、HTML 文档结构:理解网页的骨架](#一、HTML 文档结构:理解网页的骨架)
-
- [1.1 一个简单的 HTML 示例](#1.1 一个简单的 HTML 示例)
- [1.2 HTML 的核心概念](#1.2 HTML 的核心概念)
- [1.3 DOM 树:理解文档的层级结构](#1.3 DOM 树:理解文档的层级结构)
- [二、CSS 选择器:最常用的定位方式](#二、CSS 选择器:最常用的定位方式)
-
- [2.1 基础选择器](#2.1 基础选择器)
- [2.2 组合选择器(重点)](#2.2 组合选择器(重点))
- [2.3 伪类选择器(实用技巧)](#2.3 伪类选择器(实用技巧))
- [2.4 实战示例:提取新闻字段](#2.4 实战示例:提取新闻字段)
- [2.5 选择器优先级(工程建议)](#2.5 选择器优先级(工程建议))
- 三、XPath:更强大的定位语言
-
- [3.1 XPath 基础语法](#3.1 XPath 基础语法)
- [3.2 XPath 属性和文本筛选](#3.2 XPath 属性和文本筛选)
- [3.3 XPath 实战示例](#3.3 XPath 实战示例)
- [3.4 CSS vs XPath:如何选择?](#3.4 CSS vs XPath:如何选择?)
- 四、浏览器实时测试选择器(必会技能)
-
- [4.1 Chrome DevTools 控制台](#4.1 Chrome DevTools 控制台)
- [4.2 Elements 面板右键菜单](#4.2 Elements 面板右键菜单)
- [4.3 实战演练:分析真实网页](#4.3 实战演练:分析真实网页)
- 五、选择器稳定性:工程化思维
-
- [5.1 什么是"稳定"的选择器?](#5.1 什么是"稳定"的选择器?)
- [5.2 选择器设计的五个原则](#5.2 选择器设计的五个原则)
- 六、常见问题与调试技巧
-
- [6.1 问题:选择器返回 None 或空列表](#6.1 问题:选择器返回 None 或空列表)
- [6.2 问题:提取的文本包含多余空格和换行](#6.2 问题:提取的文本包含多余空格和换行)
- [6.3 问题:元素包含子标签,只想要纯文本](#6.3 问题:元素包含子标签,只想要纯文本)
- [6.4 问题:动态加载的内容抓不到](#6.4 问题:动态加载的内容抓不到)
- 七、本节小结
- [📝 课后作业(必做,验收进入下一节)](#📝 课后作业(必做,验收进入下一节))
- [🔮 下期预告](#🔮 下期预告)
- 🌟文末
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》
订阅后更新会优先推送,按目录学习更高效~
📌 上期回顾
在上一节《网页是怎么工作的:URL、请求、响应、状态码?》中,我们学习了 HTTP 请求响应机制,掌握了如何使用浏览器 Network 面板分析网络请求。现在,你已经知道数据是如何从服务器传输到浏览器的。
但问题来了:服务器返回的 HTML 代码有几千行,我们要的标题、时间、作者这些字段藏在哪里?如何精准提取出来?
这一节,我们将学习网页结构解析的核心技能------这是爬虫工程中最关键的环节之一!
🎯 本节目标
通过本节学习,你将能够:
- 理解 HTML 文档结构和 DOM 树的概念
- 掌握 CSS 选择器的基本语法和优先级
- 学会使用 XPath 表达式定位元素
- 在浏览器中实时测试选择器
- 建立"选择器稳定性"的工程思维
- 交付验收:从一个真实网页手动定位 5 个字段(标题/时间/作者/正文/链接)
一、HTML 文档结构:理解网页的骨架
1.1 一个简单的 HTML 示例
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>新闻详情页</title>
</head>
<body>
<header>
<nav class="navbar">
<a href="/">首页</a>
<a href="/news">新闻</a>
</nav>
</header>
<main>
<article class="news-article" id="article-12345">
<h1 class="title">重大科技突破:量子计算新进展</h1>
<div class="meta-info">
<span class="author">张三</span>
<span class="publish-time" data-timestamp="1705824000">
2025-01-21 10:00:00
</span>
<span class="category">科技</span>
</div>
<div class="content">
<p>研究团队今日宣布...</p>
<p>这一突破意味着...</p>
</div>
<div class="tags">
<a href="/tag/quantum" class="tag">量子计算</a>
<a href="/tag/science" class="tag">科学</a>
</div>
</article>
</main>
<footer>
<p>版权所有 © 2025</p>
</footer>
</body>
</html>
1.2 HTML 的核心概念
标签(Tag):
- 成对出现:
<div>内容</div> - 自闭合:
<img src="photo.jpg" /> - 嵌套关系:子标签在父标签内部
属性(Attribute):
html
<div class="container" id="main" data-page="1">
class:样式类名(可多个,空格分隔)id:唯一标识符(页面内唯一)data-*:自定义数据属性
常见标签及作用:
| 标签 | 作用 | 示例 |
|---|---|---|
<h1> - <h6> |
标题 | <h1>主标题</h1> |
<p> |
段落 | <p>正文内容</p> |
<a> |
链接 | <a href="/news">新闻</a> |
<div> |
块级容器 | <div class="box">...</div> |
<span> |
行内容器 | <span class="label">标签</span> |
<ul> <li> |
列表 | <ul><li>项目1</li></ul> |
<img> |
图片 | <img src="photo.jpg" alt="描述"> |
1.3 DOM 树:理解文档的层级结构
上面的 HTML 在浏览器中被解析为 DOM(文档对象模型)树:
json
html
├── head
│ ├── meta
│ └── title
└── body
├── header
│ └── nav.navbar
│ ├── a
│ └── a
├── main
│ └── article.news-article#article-12345
│ ├── h1.title
│ ├── div.meta-info
│ │ ├── span.author
│ │ ├── span.publish-time
│ │ └── span.category
│ ├── div.content
│ │ ├── p
│ │ └── p
│ └── div.tags
│ ├── a.tag
│ └── a.tag
└── footer
└── p
💡 核心理解:爬虫的本质就是在这棵树上"导航",找到目标节点,提取其文本或属性。
二、CSS 选择器:最常用的定位方式
2.1 基础选择器
1. 标签选择器
css
p /* 选择所有 <p> 标签 */
div /* 选择所有 <div> 标签 */
2. 类选择器(最常用)
css
.title /* 选择 class="title" 的元素 */
.meta-info /* 选择 class="meta-info" 的元素 */
.news-article /* 选择 class="news-article" 的元素 */
3. ID 选择器
css
#article-12345 /* 选择 id="article-12345" 的元素 */
4. 属性选择器
css
[data-timestamp] /* 有 data-timestamp 属性的元素 */
[href="/news"] /* href 等于 "/news" 的元素 */
[class^="news"] /* class 以 "news" 开头 */
[class$="info"] /* class 以 "info" 结尾 */
[class*="meta"] /* class 包含 "meta" */
2.2 组合选择器(重点)
1. 后代选择器(空格)
css
article .title /* article 内部的 .title(任意层级) */
.meta-info span /* .meta-info 内部的所有 span */
2. 子选择器(>)
css
.meta-info > span /* .meta-info 的直接子元素 span */
article > h1 /* article 的直接子元素 h1 */
3. 相邻兄弟选择器(+)
css
h1 + .meta-info /* h1 后面紧邻的 .meta-info */
4. 通用兄弟选择器(~)
css
h1 ~ div /* h1 后面所有同级的 div */
2.3 伪类选择器(实用技巧)
css
li:first-child /* 第一个 li */
li:last-child /* 最后一个 li */
li:nth-child(2) /* 第 2 个 li */
li:nth-child(odd) /* 奇数位的 li */
li:nth-child(even) /* 偶数位的 li */
p:not(.intro) /* 不包含 intro 类的 p */
2.4 实战示例:提取新闻字段
基于前面的 HTML 示例:
python
from bs4 import BeautifulSoup
html = """...(前面的HTML代码)..."""
soup = BeautifulSoup(html, 'lxml')
# 1. 提取标题
title = soup.select_one('article h1.title').text.strip()
# 结果:"重大科技突破:量子计算新进展"
# 2. 提取作者
author = soup.select_one('.meta-info .author').text.strip()
# 结果:"张三"
# 3. 提取时间
time_elem = soup.select_one('.publish-time')
publish_time = time_elem.text.strip()
timestamp = time_elem.get('data-timestamp')
# 结果:"2025-01-21 10:00:00", "1705824000"
# 4. 提取正文(所有段落)
paragraphs = soup.select('.content p')
content = '\n'.join([p.text.strip() for p in paragraphs])
# 5. 提取所有标签
tags = soup.select('.tags a.tag')
tag_list = [tag.text.strip() for tag in tags]
# 结果:['量子计算', '科学']
2.5 选择器优先级(工程建议)
优先级从高到低:
- ID 选择器 :
#article-12345(唯一性高) - 类选择器 :
.news-article(通用性好) - 属性选择器 :
[data-id="12345"](灵活) - 标签选择器 :
article(范围太宽)
⚠️ 工程原则 :优先使用
class和id,避免过度依赖标签名和层级结构(网页改版时容易失效)。
三、XPath:更强大的定位语言
3.1 XPath 基础语法
XPath 是一种在 XML/HTML 文档中导航的语言,功能比 CSS 选择器更强大。
基本路径表达式:
xpath
/html/body/div # 绝对路径(从根开始)
//div # 所有 div 元素(任意位置)
//div[@class="title"] # 带 class="title" 的 div
//div[@id="main"] # 带 id="main" 的 div
轴操作:
xpath
//article # 选择 article 元素
//article/h1 # article 的子元素 h1
//article//span # article 内部所有 span(任意层级)
//h1/following-sibling::div # h1 后面的兄弟 div 元素
//div/parent::article # div 的父元素 article
3.2 XPath 属性和文本筛选
属性匹配:
xpath
//div[@class="news"] # class 完全匹配
//div[contains(@class, "news")] # class 包含 "news"
//div[starts-with(@class, "news")] # class 以 "news" 开头
//a[@href="/news"] # href 等于 "/news"
//span[@data-timestamp] # 有 data-timestamp 属性
文本匹配:
xpath
//h1[text()="标题"] # 文本完全匹配
//p[contains(text(), "关键词")] # 文本包含 "关键词"
//div[string-length(text()) > 10] # 文本长度大于 10
位置索引:
xpath
//li[1] # 第 1 个 li(注意:XPath 索引从 1 开始)
//li[last()] # 最后一个 li
//li[position() < 3] # 前 2 个 li
3.3 XPath 实战示例
python
from lxml import etree
html = """...(前面的HTML代码)..."""
tree = etree.HTML(html)
# 1. 提取标题
title = tree.xpath('//article[@class="news-article"]/h1[@class="title"]/text()')[0]
# 或更简洁:
title = tree.xpath('//h1[@class="title"]/text()')[0]
# 2. 提取作者
author = tree.xpath('//span[@class="author"]/text()')[0]
# 3.')[0]
# 3. 提取时间戳属性
timestamp = tree.xpath('//span[@class="publish-time"]/@data-timestamp')[0]
# 4. 提取正文所有段落
paragraphs = tree.xpath('//div[@class="content"]/p/text()')
content = '\n'.join(paragraphs)
# 5. 提取标签链接和文本
tags = tree.xpath('//div[@class="tags"]/a')
tag_data = [
{'text': tag.text.strip(), 'href': tag.get('href')}
for tag in tags
]
# 6. 复杂场景:提取包含特定关键词的段落
keyword_paras = tree.xpath('//p[contains(text(), "突破")]')
3.4 CSS vs XPath:如何选择?
| 特性 | CSS 选择器 | XPath |
|---|---|---|
| 学习难度 | 简单,前端常用 | 稍复杂,但更强大 |
| 文本筛选 | 持 | ✅ 支持 |
| 属性获取 | ⚠️ 需额外方法 | ✅ 直接获取 @href |
| 父节点访问 | ❌ 不支持 | ✅ 支持 parent:: |
| 性能 | 稍快 | 稍慢(可忽略) |
| 浏览器测试 | ✅ 直接支持 | ✅ 支持($x()) |
💡 建议:
- 简单场景用 CSS(代码更简洁)
- 复杂筛选用 XPath(功能更强大)
- 两者结合使用(根据具体场景选择)
四、浏览器实时测试选择器(必会技能)
4.1 Chrome DevTools 控制台
步骤:
- 按
F12打开开发者工具 - 切换到 Console 标签页
- 使用以下命令测试
CSS 选择器测试:
javascript
// 返回第一个匹配元素
document.querySelector('.title')
// 返回所有匹配元素(NodeList)
document.querySelectorAll('.meta-info span')
// 简写形式(Chrome 提供)
$('.title') // 等同于 querySelector
$$('.meta-info span') // 等同于 querySelectorAll
XPath 测试:
javascript
// Chrome 提供的 XPath 测试函数
$x('//h1[@class="title"]') // 返回数组
$x('//span[@class="author"]/text()') // 提取文本节点
4.2 Elements 面板右键菜单
复制选择器:
- 在 Elements 面板右键点击目标元素
- 选择 "Copy" → "Copy selector"(自动生成 CSS 选择器)
- 选择 "Copy" → "Copy XPath"(自动生成 XPath)
注意:浏览器生成的选择器通常过于具体,需要手动简化:
css
/* 浏览器生成(过于复杂)*/
body > div.wrapper > main > article:nth-child(1) > h1.title
/* 简化后(更稳定)*/
article h1.title
/* 或 */
.news-article .title
4.3 实战演练:分析真实网页
任务 :打开 https://news.sina.com.cn/ 并提取新闻标题
javascript
// 1. 在控制台测试
$$('.news-item h2 a') // 假设这是标题所在位置
// 2. 遍历提取文本
$$('.news-item h2 a').forEach(el => console.log(el.innerText))
// 3. 提取链接
$$('.news-item h2 a').forEach(el => console.log(el.href))
五、选择器稳定性:工程化思维
5.1 什么是"稳定"的选择器?
不稳定的示例:
css
/* ❌ 依赖层级和顺序,改版易失效 */
body > div:nth-child(3) > div:nth-child(2) > p:first-child
/* ❌ 依赖样式类,可能被随机化 */
.css-1xw2kqp-Title
/* ❌ 过于泛化,容易误匹配 */
div span
稳定的示例:
css
/* ✅ 语义化类名 */
.article-title
.publish-date
.author-name
/* ✅ 数据属性 */
[data-article-id]
[data-role="title"]
/* ✅ 适度组合 */
article.news .title
.meta-info .author
5.2 选择器设计的五个原则
原则1:优先使用语义化的 class 和 id
python
# ✅ 好
soup.select_one('.article-title')
# ❌ 差
soup.select_one('div > div > h1')
原则2:避免过度依赖层级结构
python
# ✅ 好(适度层级)
soup.select('.article-content p')
# ❌ 差(过深层级)
soup.select('body > main > section > article > div > div > p')
原则3:用属性选择器应对动态类名
python
# 某些网站的类名是动态生成的
# <div class="css-1ab2cd3-Title">标题</div>
# ❌ 不稳定
soup.select_one('.css-1ab2cd3-Title')
# ✅ 用属性或结构
soup.select_one('[data-testid="title"]')
soup.select_one('article > h1')
原则4:多重选择器作为 fallback
python
def extract_title(soup):
"""提取标题,支持多种选择器"""
selectors = [
'h1.article-title', # 首选
'[data-role="title"]', # 备选1
'article > h1', # 备选2
'h1' # 兜底
]
for selector in selectors:
element = soup.select_one(selector)
if element:
return element.text.strip()
return None # 所有选择器都失效
原则5:留下注释说明选择逻辑
python
# 提取发布时间
# 网站结构:<span class="time" data-timestamp="...">2025-01-21</span>
# 优先获取 data-timestamp 属性(Unix 时间戳)
# 如果没有,则解析显示文本
time_elem = soup.select_one('.meta-info .time')
if time_elem and time_elem.get('data-timestamp'):
timestamp = int(time_elem['data-timestamp'])
else:
time_text = time_elem.text.strip() # 需要进一步解析
六、常见问题与调试技巧
6.1 问题:选择器返回 None 或空列表
排查步骤:
python
# 1. 检查 HTML 是否正确获取
print(response.text[:500]) # 查看前 500 字符
# 2. 保存 HTML 到文件,用浏览器打开对比
with open('debug.html', 'w', encoding='utf-8') as f:
f.write(response.text)
# 3. 在保存的文件中测试选择器
soup = BeautifulSoup(open('debug.html', encoding='utf-8'), 'lxml')
result = soup.select('.title')
print(f"找到 {len(result)} 个元素")
# 4. 逐步简化选择器
soup.select('h1') # 是否能找到任何 h1?
soup.select('.title') # 是否有这个类?
soup.select('article .title') # 组合是否正确?
6.2 问题:提取的文本包含多余空格和换行
清洗方法:
python
import re
# 原始文本
raw_text = " \n 这是标题 \n "
# 方法1:strip() 去除首尾空白
clean1 = raw_text.strip()
# 结果:"这是标题"
# 方法2:正则替换多余空白
clean2 = re.sub(r'\s+', ' ', raw_text).strip()
# 结果:"这是标题"
# 方法3:处理 HTML 实体
from html import unescape
text_with_entity = "这是 标题<test>"
clean3 = unescape(text_with_entity)
# 结果:"这是 标题<test>"
6.3 问题:元素包含子标签,只想要纯文本
python
# HTML: <div class="content">正文<span>(来源:网站)</span></div>
# ❌ 错误做法(会包含 span 的文本)
text = soup.select_one('.content').text
# 结果:"正文(来源:网站)"
# ✅ 方法1:只取直接文本节点
element = soup.select_one('.content')
text = ''.join(element.find_all(text=True, recursive=False))
# 结果:"正文"
# ✅ 方法2:移除特定子元素后提取
element = soup.select_one('.content')
for span in element.select('span'):
span.decompose() # 删除 span 标签
text = element.text.strip()
6.4 问题:动态加载的内容抓不到
python
# 症状:浏览器能看到,代码获取的 HTML 里没有
# 原因:内容是 JavaScript 渲染的
# 解决方案1:找到数据接口(推荐)
# 在 Network 面板找 XHR/Fetch 请求,直接请求 JSON 接口
# 解决方案2:使用动态渲染工具(下一章学习)
# 如 Playwright、Selenium
七、本节小结
本节我们系统学习了网页解析的核心技能:
✅ HTML 结构 :理解标签、属性、DOM 树的概念
✅ CSS 选择器 :掌握类、ID、属性、组合选择器的用法
✅ XPath 表达式 :学会更强大的定位和筛选语法
✅ 浏览器调试 :使用控制台实时测试选择器
✅ 稳定性原则 :编写不易失效的选择器
✅ 问题排查:调试技巧和常见错误处理
核心原则:
- 选择器要"刚刚好"------不过度具体,不过度泛化
- 语义化类名 > 层级结构 > 标签名
- 多准备 fallback 选择器,提高鲁棒性
- 遇到问题先保存 HTML,对比浏览器渲染结果
📝 课后作业(必做,验收进入下一节)
任务1:选择器练习
针对以下 HTML 片段,写出选择器并验证:
html
<div class="news-list">
<div class="news-item" data-id="1001">
<h3 class="title"><a href="/news/1001">新闻标题A</a></h3>
<div class="info">
<span class="author">作者A</span>
<span class="date">2025-01-21</span>
<span class="views">1.2万</span>
</div>
</div>
<div class="news-item" data-id="1002">
<h3 class="title"><a href="/news/1002">新闻标题B</a></h3>
<div class="info">
<span class="author">作者B</span>
<span class="date">2025-01-20</span>
<span class="views">8500</span>
</div>
</div>
</div>
要求:写出提取以下内容的选择器(CSS 和 XPath 各一种)
- 所有新闻标题
- 第一条新闻的作者
- data-id 属性为 1002 的新闻链接
- 所有发布日期
- 阅读量超过 1 万的新闻标题
任务1:爬虫代码示例
如下为任务1的爬虫代码,仅供参加:
1)所有新闻标题
CSS
css
.news-item h3.title > a
XPath
xpath
//div[@class="news-item"]//h3[@class="title"]/a/text()
2)第一条新闻的作者
CSS
css
.news-item:first-child .info .author
XPath
xpath
(//div[@class="news-item"])[1]//span[@class="author"]/text()
3)data-id=1002 的新闻链接
CSS
css
.news-item[data-id="1002"] h3.title > a
XPath
xpath
//div[@class="news-item" and @data-id="1002"]//h3[@class="title"]/a/@href
4)所有发布日期
CSS
css
.news-item .info .date
XPath
xpath
//div[@class="news-item"]//span[@class="date"]/text()
5)阅读量超过 1 万的新闻标题(工程化答案)
CSS(先选出每条新闻节点,后续用 Python 过滤)
css
.news-item
XPath(同理,先取每条 news-item 节点,再过滤)
xpath
//div[@class="news-item"]
任务2:真实网站练习
选择一个真实网站(推荐:新浪新闻、知乎、豆瓣),完成:
-
打开浏览器开发者工具
-
定位列表页的 5 个字段:
- 标题
- 发布时间
- 作者/来源
- 链接
- (可选)摘要/标签
-
为每个字段编写 CSS 选择器和 XPath 表达式
-
在控制台测试验证
-
截图保存测试结果
任务2:爬虫代码示例
如下为任务2的爬虫代码,仅供参加:
我用这篇示例文章页来写(你也可以换成你自己选的页面,替换选择器即可):
- 标题在页面顶部(可见为 H1 级标题) (手机科技频道)
- 来源/作者信息紧随其后(可见"爱范儿""爱范儿官方账号 01.21 19:44") (手机科技频道)
- 正文是多段文本直出(适合 requests + bs4 解析) (手机科技频道)
2.1(浏览器控制台)字段定位"通用兜底选择器"
这类移动文章页通常 h1 就是标题 ,正文段落多为
p,你可以先用"兜底选择器"跑通,然后再在 Elements 里 Copy selector 精准化 ✨
js
// 1) 标题(兜底:第一条 h1)
document.querySelector("h1")?.innerText
// 2) 来源/作者(兜底:页面里第一个 h2 或者包含"账号/作者"的块)
// 如果 h2 就是来源:
document.querySelector("h2")?.innerText
// 3) 发布时间(兜底:从页面文本里用正则抽"MM.DD HH:MM")
document.body.innerText.match(/\b\d{2}\.\d{2}\s+\d{2}:\d{2}\b/)?.[0]
// 4) 链接(当前页 URL)
location.href
// 5) 正文(兜底:取所有 p 的文本)
[...document.querySelectorAll("p")].map(p => p.innerText.trim()).filter(Boolean).join("\n")
2.2(Python 抓取)真实网站提取脚本源码:real_site_demo.py
你本地运行需要联网;如遇到"页面需要开启 JavaScript",就换页面或改用 Playwright(下一章会学)。这个示例页正文是直出的,适合 requests。 ([手机科技频道][1])
python
# real_site_demo.py
import re
import requests
from bs4 import BeautifulSoup
from lxml import etree
URL = "https://tech.sina.cn/2026-01-21/detail-inhiaktr5379581.d.html?cid=79649&node_id=79649&vt=4"
def pick_first_text(soup, css_list):
"""多套 CSS 选择器兜底:找到第一个就返回 text"""
for css in css_list:
el = soup.select_one(css)
if el and el.get_text(strip=True):
return el.get_text(strip=True)
return None
def main():
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0 Safari/537.36"
}
r = requests.get(URL, headers=headers, timeout=15)
r.raise_for_status()
r.encoding = r.apparent_encoding
html = r.text
soup = BeautifulSoup(html, "lxml")
tree = etree.HTML(html)
# 1) 标题:兜底优先 h1
title = pick_first_text(soup, ["h1", "header h1", ".article-title", ".art_tit_h1"])
# 2) 来源/作者:兜底 h2 或常见 class
source = pick_first_text(soup, ["h2", ".source", ".article-source", ".art_info .source"])
# 3) 时间:页面里通常出现 "01.21 19:44" 这类格式,直接正则抽
time_text = None
m = re.search(r"\b\d{2}\.\d{2}\s+\d{2}:\d{2}\b", soup.get_text(" ", strip=True))
if m:
time_text = m.group(0)
# 4) 链接:就是 URL
link = URL
# 5) 正文:兜底抓所有 p(过滤太短的段落)
ps = [p.get_text(" ", strip=True) for p in soup.select("p")]
content = "\n".join([t for t in ps if len(t) >= 10])
# 若 p 抓不到,再用 XPath 兜底抓全文(不够精准但能交付)
if not content.strip():
text_nodes = tree.xpath("//body//text()")
content = "\n".join([t.strip() for t in text_nodes if t and t.strip()])
print("=== REAL SITE PARSE RESULT ===")
print("Title:", title)
print("Time:", time_text)
print("Source:", source)
print("Link:", link)
print("\nContent preview:\n", content[:400], "...\n")
if __name__ == "__main__":
main()
任务3:编写提取脚本
创建 parser_demo.py:
python
from bs4 import BeautifulSoup
from lxml import etree
html = """(任务1的HTML代码)"""
# 用 BeautifulSoup + CSS 选择器
soup = BeautifulSoup(html, 'lxml')
print("=== BeautifulSoup + CSS ===")
# 实现提取逻辑...
# 用 lxml + XPath
tree = etree.HTML(html)
print("\n=== lxml + XPath ===")
# 实现提取逻辑...
任务3:爬虫代码示例
下面这份脚本直接用你作业里的 HTML 片段,分别用 BeautifulSoup+CSS 与 lxml+XPath 提取 5 个要求字段,并对"阅读量 > 1 万"做了工程化数值解析。
python
# parser_demo.py
import re
from bs4 import BeautifulSoup
from lxml import etree
HTML = """
<div class="news-list">
<div class="news-item" data-id="1001">
<h3 class="title"><a href="/news/1001">新闻标题A</a></h3>
<div class="info">
<span class="author">作者A</span>
<span class="date">2025-01-21</span>
<span class="views">1.2万</span>
</div>
</div>
<div class="news-item" data-id="1002">
<h3 class="title"><a href="/news/1002">新闻标题B</a></h3>
<div class="info">
<span class="author">作者B</span>
<span class="date">2025-01-20</span>
<span class="views">8500</span>
</div>
</div>
</div>
"""
def parse_views_to_int(s: str) -> int:
"""
把 '1.2万' / '8500' / '12,345' 统一转成整数
"""
if not s:
return 0
s = s.strip().replace(",", "")
# 形如 1.2万
m = re.fullmatch(r"(\d+(?:\.\d+)?)万", s)
if m:
return int(float(m.group(1)) * 10000)
# 纯数字
m = re.fullmatch(r"\d+", s)
if m:
return int(s)
return 0
def bs4_css_demo():
soup = BeautifulSoup(HTML, "lxml")
print("=== BeautifulSoup + CSS ===")
# 1. 所有新闻标题
titles = [a.get_text(strip=True) for a in soup.select(".news-item h3.title > a")]
print("1) Titles:", titles)
# 2. 第一条新闻的作者
first_author = soup.select_one(".news-item:first-child .info .author").get_text(strip=True)
print("2) First author:", first_author)
# 3. data-id=1002 的新闻链接
link_1002 = soup.select_one('.news-item[data-id="1002"] h3.title > a')["href"]
print("3) Link of data-id=1002:", link_1002)
# 4. 所有发布日期
dates = [s.get_text(strip=True) for s in soup.select(".news-item .info .date")]
print("4) Dates:", dates)
# 5. 阅读量超过 1 万的新闻标题(工程化过滤)
over_10k = []
for item in soup.select(".news-item"):
views_text = item.select_one(".views").get_text(strip=True)
views = parse_views_to_int(views_text)
if views > 10000:
over_10k.append(item.select_one("h3.title > a").get_text(strip=True))
print("5) Titles with views > 10k:", over_10k)
def lxml_xpath_demo():
tree = etree.HTML(HTML)
print("\n=== lxml + XPath ===")
# 1. 所有新闻标题
titles = tree.xpath('//div[@class="news-item"]//h3[@class="title"]/a/text()')
titles = [t.strip() for t in titles]
print("1) Titles:", titles)
# 2. 第一条新闻的作者
first_author = tree.xpath('(//div[@class="news-item"])[1]//span[@class="author"]/text()')[0].strip()
print("2) First author:", first_author)
# 3. data-id=1002 的新闻链接
link_1002 = tree.xpath('//div[@class="news-item" and @data-id="1002"]//h3[@class="title"]/a/@href')[0]
print("3) Link of data-id=1002:", link_1002)
# 4. 所有发布日期
dates = tree.xpath('//div[@class="news-item"]//span[@class="date"]/text()')
dates = [d.strip() for d in dates]
print("4) Dates:", dates)
# 5. 阅读量超过 1 万:XPath 抓文本 + Python 数值过滤(最稳)
items = tree.xpath('//div[@class="news-item"]')
over_10k = []
for item in items:
title = item.xpath('.//h3[@class="title"]/a/text()')[0].strip()
views_text = item.xpath('.//span[@class="views"]/text()')[0].strip()
if parse_views_to_int(views_text) > 10000:
over_10k.append(title)
print("5) Titles with views > 10k:", over_10k)
if __name__ == "__main__":
bs4_css_demo()
lxml_xpath_demo()
最终验收方式:在留言区提交:
- 任务1的选择器答案(CSS + XPath)
- 任务2的分析截图和选择器代码
- 任务3的脚本运行截图
- 遇到的困难和解决方法
🔮 下期预告
下一节《接口数据基础:JSON 是什么?分页是什么?》,我们将学习:
- JSON 数据格式的结构和解析
- 列表接口的三种分页模式(page/offset/cursor)
- 如何用 Postman 测试接口
- 接口参数的逆向分析技巧
- 编写第一个接口采集脚本
预习建议 :
打开你常访问的网站,在 Network 面板筛选 XHR 类型的请求,观察哪些返回的是 JSON 格式数据。尝试理解 JSON 的结构(对象、数组、嵌套关系)。
💬 选择器是爬虫的基本功,多练习才能熟练!加油!
记住:没有完美的选择器,只有适合当前场景的选择器。保持灵活,准备 plan B,你的爬虫会更稳定。
🌟文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。