Python爬虫核心技巧:两步法高效提取HTML数据
本文解决爬虫新手最常遇到的两个问题:
- 面对几千个div标签,如何精准定位目标数据?
- 为什么标题和内容总是对应不上?
一、问题场景
假设你要爬取招聘网站的详情页,页面有以下信息:
- 单位简介
- 招聘简章
- 招聘职位
- 宣讲会信息
每部分都有标题和内容,你需要把它们全部提取出来。
难点:
- 页面HTML有10万多字符
- 有3000多个`
`标签
- 如何快速定位?如何保证标题和内容对应?
二、传统做法的问题
❌ 错误方法1:遍历所有div
all_divs = soup.find_all('div') # 找到3000多个div
for div in all_divs:
if '单位简介' in div.get_text():
1. 找到了...但其他呢?
问题:
- 效率低下,需要遍历3000多个元素
- 逻辑复杂,难以维护
❌ 错误方法2:分别查找标题和内容
1. 找所有标题
titles = soup.find_all('div', class_='dm-tit')
1. 结果:["单位简介", "招聘简章", "招聘职位"]
1. 找所有内容
contents = soup.find_all('div', class_='dm-cont')
1. 结果:["深圳能源集团...", "2024年招聘...", "软件工程师..."]
1. 尝试对应
for i, title in enumerate(titles):
print(f"{title}: {contents[i]}")
问题:
- 看起来能工作,但非常脆弱
- 如果网站插入其他元素,顺序就乱了
- 无法保证对应关系
三、正确方法:两步法
核心思想
第一步:初步定位 - 找到数据容器(外层盒子)
第二步:细节提取 - 在容器内提取标题和内容
3.1 第一步:初步定位
用浏览器找规律
- 打开网页,右键点击”单位简介” → 选择”检查”
- 观察HTML结构:
<div class="detail-module">
<div class="dm-tit">单位简介</div>
<div class="dm-cont">深圳能源集团股份有限公司...</div>
</div>
<div class="detail-module">
<div class="dm-tit">招聘简章</div>
<div class="dm-cont">2024年招聘...</div>
</div>
<div class="detail-module">
<div class="dm-tit">招聘职位</div>
<div class="dm-cont">软件工程师...</div>
</div>
发现规律
✅ 关键发现:
- 所有数据都在
class="detail-module"的容器里 - 每个容器包含一个标题(
dm-tit)和一个内容(dm-cont) - 这是一个重复的结构
用代码定位
import requests
from bs4 import BeautifulSoup
1. 获取页面
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
1. 第一步:找到所有容器
modules = soup.find_all('div', class_='detail-module')
print(f"找到 {len(modules)} 个模块") # 输出:找到 4 个模块
这一步的本质:
- 从3000个div中,精准定位到4个目标容器
- 大幅缩小搜索范围
3.2 第二步:细节提取
在容器内提取数据
data = {}
for module in modules: # 遍历每个模块
1. 在当前模块内找标题
title = module.find('div', class_='dm-tit')
1. 在当前模块内找内容
content = module.find('div', class_='dm-cont')
1. 提取文本
if title and content:
title_text = title.get_text(strip=True)
content_text = content.get_text(strip=True)
1. 保存数据
data[title_text] = content_text
print(data)
输出:
{
"单位简介": "深圳能源集团股份有限公司成立于1991年...",
"招聘简章": "2024年校园招聘计划...",
"招聘职位": "软件工程师、产品经理...",
"宣讲会信息": "宣讲时间:2025年10月11日..."
}
四、关键理解:为什么要在容器内查找?
对比两种方法
❌ 直接查找(不推荐)
titles = soup.find_all('div', class_='dm-tit')
contents = soup.find_all('div', class_='dm-cont')
1. 假设网站结构变化,插入了其他元素:
<div class="detail-module">
<div class="dm-tit">单位简介</div>
<div class="dm-cont">深圳能源集团...</div>
</div>
<div class="other-section">
<div class="dm-tit">注意事项</div> <!-- 插入的无关内容 -->
</div>
<div class="detail-module">
<div class="dm-tit">招聘简章</div>
<div class="dm-cont">2024年招聘...</div>
</div>
titles = ["单位简介", "注意事项", "招聘简章"]
contents = ["深圳能源集团...", "2024年招聘..."]
1. 对应关系乱了!
1. titles[1] = "注意事项"
1. contents[1] = "2024年招聘..." ← 错误!
✅ 容器内查找(推荐)
modules = soup.find_all('div', class_='detail-module') # 只找目标容器
for module in modules:
title = module.find('div', class_='dm-tit') # 在容器内找
content = module.find('div', class_='dm-cont') # 在容器内找
1. title 和 content 永远来自同一个 module
1. 对应关系100%正确!
无论插入多少其他元素,都不受影响!
五、完整代码示例
5.1 提取招聘详情页数据
import requests
from bs4 import BeautifulSoup
def extract_career_detail(career_id):
"""提取招聘详情页数据"""
url = f"https://csust.bysjy.com.cn/detail/career?id={career_id}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
1. 获取页面
response = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
detail = {}
1. ==========================================
1. 第一步:初步定位 - 找到所有模块容器
1. ==========================================
modules = soup.find_all('div', class_='detail-module')
print(f"✓ 找到 {len(modules)} 个模块")
1. ==========================================
1. 第二步:细节提取 - 遍历每个模块
1. ==========================================
for module in modules:
1. 在模块内找标题
title_elem = module.find('div', class_='dm-tit')
1. 在模块内找内容
content_elem = module.find('div', class_='dm-cont')
if title_elem and content_elem:
title = title_elem.get_text(strip=True)
content = content_elem.get_text(strip=True)
detail[title] = content
print(f" - 提取:{title} ({len(content)} 字符)")
return detail
1. 测试
data = extract_career_detail('668868')
print(f"\n提取到 {len(data)} 个字段")
输出:
✓ 找到 4 个模块
- 提取:宣讲会信息 (149 字符)
- 提取:单位简介 (397 字符)
- 提取:招聘简章 (822 字符)
- 提取:招聘职位 (8471 字符)
提取到 4 个字段
5.2 批量提取多个页面
def batch_extract(career_ids):
"""批量提取多个页面"""
results = []
for i, career_id in enumerate(career_ids):
print(f"\n[{i+1}/{len(career_ids)}] 正在提取 ID={career_id}")
try:
detail = extract_career_detail(career_id)
results.append({
'career_id': career_id,
**detail # 展开详情数据
})
except Exception as e:
print(f" ✗ 提取失败: {e}")
1. 礼貌延迟,避免被封IP
import time
time.sleep(0.5)
return results
1. 测试
ids = ['667200', '667839', '668868']
data = batch_extract(ids)
1. 保存为JSON
import json
with open('careers.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"\n✓ 成功提取 {len(data)} 条数据")
六、核心概念速查
6.1 三个核心函数
| 函数 | 作用 | 返回值 |
|---|---|---|
find_all() |
找所有符合条件的元素 | 列表 [elem1, elem2, ...] |
find() |
找第一个符合条件的元素 | 单个元素 ` |
… ` |
||
get_text(strip=True) |
提取文本并去除空格 | 字符串 "深圳能源集团..." |
6.2 查找语法
1. 按class查找
soup.find('div', class_='detail-module')
soup.find_all('div', class_='dm-tit')
1. 按id查找
soup.find('div', id='header')
1. CSS选择器(更强大)
soup.select_one('div.detail-module') # 等同于 find()
soup.select('div.dm-tit') # 等同于 find_all()
1. 嵌套查找
container = soup.find('div', class_='container')
title = container.find('h2', class_='title') # 在容器内查找
6.3 提取数据
1. 提取文本
elem.get_text() # " 深圳能源 \n"
elem.get_text(strip=True) # "深圳能源" (推荐)
1. 提取属性
link = soup.find('a', class_='job-link')
href = link.get('href') # 获取链接
title = link.get('title') # 获取title属性
1. 判断元素是否存在
elem = soup.find('div', class_='optional')
if elem:
text = elem.get_text(strip=True)
else:
text = "未找到"
七、两步法流程图
┌─────────────────────────────────────────────────────────┐
│ 第一步:初步定位 │
├─────────────────────────────────────────────────────────┤
│ │
│ soup.find_all('div', class_='detail-module') │
│ ↓ │
│ [模块1, 模块2, 模块3, 模块4] │
│ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 第二步:细节提取 │
├─────────────────────────────────────────────────────────┤
│ │
│ for module in modules: │
│ ├─ title = module.find('div', class_='dm-tit') │
│ ├─ content = module.find('div', class_='dm-cont') │
│ └─ data[title.get_text()] = content.get_text() │
│ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 提取结果 │
├─────────────────────────────────────────────────────────┤
│ { │
│ "单位简介": "深圳能源集团股份有限公司...", │
│ "招聘简章": "2024年招聘...", │
│ "招聘职位": "软件工程师...", │
│ "宣讲会信息": "宣讲时间:2025年10月11日..." │
│ } │
└─────────────────────────────────────────────────────────┘
八、实战技巧
8.1 如何快速找到class名称?
使用浏览器开发者工具:
- 按
F12打开开发者工具 - 点击左上角的”选择元素”图标(或按
Ctrl+Shift+C) - 鼠标悬停在网页内容上,会高亮对应的HTML
- 点击,查看右侧的HTML代码
- 找到
class="xxx"就是你要用的class名称
8.2 处理可选字段
有些页面可能缺少某些字段,需要判断:
for module in modules:
title_elem = module.find('div', class_='dm-tit')
content_elem = module.find('div', class_='dm-cont')
1. 判断是否存在
if title_elem and content_elem:
title = title_elem.get_text(strip=True)
content = content_elem.get_text(strip=True)
detail[title] = content
else:
print(" ⚠ 模块缺少标题或内容,跳过")
8.3 处理多种class名称
有些网站使用不同的class名称:
1. 尝试多个可能的class
title = (module.find('div', class_='dm-tit') or
module.find('div', class_='title') or
module.find('h2', class_='section-title'))
if title:
title_text = title.get_text(strip=True)
8.4 提取嵌套内容
1. 有些内容有多层嵌套
content_elem = module.find('div', class_='dm-cont')
1. 提取所有段落
paragraphs = content_elem.find_all('p')
full_text = '\n'.join(p.get_text(strip=True) for p in paragraphs)
1. 提取列表
items = content_elem.find_all('li')
item_list = [item.get_text(strip=True) for item in items]
九、常见问题
Q1: 为什么find_all()返回空列表?
可能原因:
- class名称写错了(区分大小写)
- 元素是动态加载的(需要用Playwright等工具)
- 页面还没加载完
解决方法:
modules = soup.find_all('div', class_='detail-module')
if not modules:
print("未找到模块,检查class名称是否正确")
print("页面前1000字符:", response.text[:1000])
Q2: 提取的文本有很多空格和换行?
解决:使用 strip=True
1. ❌ 不好
text = elem.get_text() # " 深圳能源 \n 集团 \n"
1. ✅ 好
text = elem.get_text(strip=True) # "深圳能源集团"
1. ✅ 更好(替换多余空白)
import re
text = elem.get_text(strip=True)
text = re.sub(r'\s+', ' ', text) # 把多个空格/换行替换成单个空格
Q3: 如何处理表格数据?
table = soup.find('table', class_='job-table')
rows = table.find_all('tr')
data = []
for row in rows:
cells = row.find_all(['td', 'th'])
row_data = [cell.get_text(strip=True) for cell in cells]
data.append(row_data)
1. 结果
1. [
1. ["职位", "薪资", "学历"],
1. ["软件工程师", "20K-30K", "本科"],
1. ["产品经理", "25K-35K", "本科"]
1. ]
十、总结
核心要点
-
两步法:
- 第一步:初步定位容器(
find_all) - 第二步:在容器内细节提取(
find)
- 第一步:初步定位容器(
-
为什么要用容器:
- 缩小搜索范围
- 保证数据对应关系
- 提高代码健壮性
-
关键函数:
find_all()– 找所有find()– 找第一个get_text(strip=True)– 提取文本
最佳实践
1. ✅ 推荐的完整流程
def extract_data(url):
1. 1. 获取页面
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
1. 2. 初步定位:找容器
containers = soup.find_all('div', class_='container')
1. 3. 细节提取:在容器内查找
results = []
for container in containers:
item = {}
1. 在容器内查找各个字段
title = container.find('h2', class_='title')
content = container.find('div', class_='content')
date = container.find('span', class_='date')
1. 提取文本(判断是否存在)
if title:
item['title'] = title.get_text(strip=True)
if content:
item['content'] = content.get_text(strip=True)
if date:
item['date'] = date.get_text(strip=True)
results.append(item)
return results
附录:完整项目代码
完整的两阶段爬虫代码(获取列表 + 提取详情)可在我的GitHub仓库查看:
核心代码:
crawl_details_without_api.py– 完整爬虫教程文件/– 详细教程和示例
希望这篇文章能帮助你掌握高效的HTML数据提取方法!如果有任何问题,欢迎在评论区留言。