网络爬虫、API调用与数据清洗技术
在进行数据采集之前,首先需要了解数据的来源和类型。根据数据的组织方式,可以将数据分为以下三大类:
| 数据类型 | 说明 | 典型示例 |
|---|---|---|
| 结构化数据 | 有固定的模式或 schema,可以用二维表格表示 | 数据库表、Excel、CSV |
| 半结构化数据 | 有一定的层次结构,但没有严格的 schema | JSON、XML、HTML |
| 非结构化数据 | 没有预定义的结构或格式 | 文本、图片、视频、音频 |
HTTP(HyperText Transfer Protocol)是互联网上应用最为广泛的协议,用于客户端与服务器之间的通信。HTTPS 则是在 HTTP 的基础上加入了 SSL/TLS 加密层,保障数据传输安全。
一次完整的 HTTP 通信过程如下:
URL(Uniform Resource Locator)是互联网上资源的唯一地址。一个完整的 URL 由以下部分组成:
https://www.example.com:443/path/to/page?name=value&sort=asc#section1
\___/ \_____________/ \_/ \___________/ \_____________________/ \______/
| | | | | |
协议 域名 端口 路径 查询参数 锚点
HTTP 请求由请求行、请求头和请求体三部分组成。常用的请求方法如下:
| 方法 | 用途 | 是否包含请求体 |
|---|---|---|
| GET | 获取资源 | 否 |
| POST | 提交数据 | 是 |
| PUT | 更新资源(全量替换) | 是 |
| DELETE | 删除资源 | 通常否 |
| HEAD | 获取响应头(不返回响应体) | 否 |
HTTP 响应同样由状态行、响应头和响应体组成。常见的状态码包括:
1xx 信息类 — 请求已接收,继续处理
2xx 成功类 — 请求已成功处理(200 OK, 201 Created)
3xx 重定向类 — 需要进一步操作(301 永久重定向, 302 临时重定向)
4xx 客户端错误 — 请求有误(400 Bad Request, 404 Not Found, 403 Forbidden)
5xx 服务器错误 — 服务器处理失败(500 Internal Server Error, 502 Bad Gateway)
Requests 是 Python 中最流行的 HTTP 库,以其简洁优雅的 API 著称。使用 pip 即可安装:
pip install requests
安装完成后,即可在 Python 中导入并使用:
import requests
# 发送一个简单的 GET 请求
response = requests.get('https://www.example.com')
# 查看响应内容
print(response.text) # 响应体的文本内容
print(response.status_code) # 状态码,如 200
print(response.url) # 最终请求的 URL
print(response.encoding) # 响应编码
requests.get(url, proxies={'http': 'http://127.0.0.1:7890'})GET 请求用于从服务器获取数据,参数通过 URL 传递;POST 请求用于向服务器提交数据,参数放在请求体中。
import requests
# --- GET 请求:通过 params 传递查询参数 ---
payload = {'q': 'python爬虫', 'page': 1}
response = requests.get('https://httpbin.org/get', params=payload)
print("GET 请求的完整 URL:", response.url)
# 输出: https://httpbin.org/get?q=python%E7%88%AC%E8%99%AB&page=1
# --- POST 请求:通过 data 或 json 传递请求体 ---
form_data = {'username': 'admin', 'password': '123456'}
response = requests.post('https://httpbin.org/post', data=form_data)
print("POST 表单响应:", response.json()['form'])
json_data = {'title': '测试文章', 'content': '这是正文内容'}
response = requests.post('https://httpbin.org/post', json=json_data)
print("POST JSON 响应:", response.json()['json'])
很多网站会通过 User-Agent 等请求头来判断访问来源。设置合理的请求头可以模拟浏览器行为,避免被服务器拒绝。
import requests
# 自定义请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Referer': 'https://www.google.com/'
}
response = requests.get('https://httpbin.org/headers', headers=headers)
print(response.json())
Requests 的 Response 对象提供了丰富的属性和方法来处理服务器返回的数据:
import requests
response = requests.get('https://httpbin.org/json')
# --- 状态码判断 ---
if response.status_code == 200:
print("请求成功!")
elif response.status_code == 404:
print("页面不存在!")
# 更推荐使用内置的断言方法
response.raise_for_status() # 如果状态码不是 2xx,会抛出 HTTPError
# --- 编码处理 ---
print("自动检测编码:", response.encoding)
response.encoding = 'utf-8' # 手动指定编码
print("响应文本前100字符:", response.text[:100])
# --- JSON 响应解析 ---
data = response.json()
print("解析后的数据类型:", type(data))
print("幻灯片标题:", data['slideshow']['title'])
# --- 二进制内容(如图片) ---
img_response = requests.get('https://httpbin.org/image/png')
with open('image.png', 'wb') as f:
f.write(img_response.content)
print("图片已保存,大小:", len(img_response.content), "字节")
Session 对象可以跨请求保持某些参数(如 Cookie),在需要登录或维持会话状态的场景中非常有用。
import requests
# 创建 Session 对象
session = requests.Session()
# 设置全局请求头(后续所有请求都会携带)
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36'
})
# 模拟登录
login_data = {'username': 'your_user', 'password': 'your_pass'}
session.post('https://example.com/login', data=login_data)
# 登录后访问需要认证的页面(自动携带 Cookie)
response = session.get('https://example.com/dashboard')
print("登录后页面内容:", response.text[:200])
# 使用完毕后关闭 Session
session.close()
with 语句管理 Session,这样可以确保即使发生异常也能正确关闭连接:with requests.Session() as session: ...HTML(HyperText Markup Language)是网页的基本结构语言。理解 HTML 的层级关系是进行网页解析的前提。
<!DOCTYPE html>
<html>
<head>
<title>示例页面</title>
</head>
<body>
<div class="container">
<h1 id="title">文章标题</h1>
<p class="intro">这是一段介绍文字。</p>
<ul class="item-list">
<li data-id="1"><a href="/item/1">项目一</a></li>
<li data-id="2"><a href="/item/2">项目二</a></li>
<li data-id="3"><a href="/item/3">项目三</a></li>
</ul>
</div>
</body>
</html>
HTML 中的关键概念:
BeautifulSoup 是 Python 中最常用的 HTML/XML 解析库,配合 lxml 解析器使用效果最佳。
pip install beautifulsoup4 lxml
from bs4 import BeautifulSoup
html_doc = """
<div class="article">
<h2 class="title">Python 数据采集入门</h2>
<div class="meta">
<span class="author">张三</span>
<span class="date">2025-01-15</span>
</div>
<p class="content">本文介绍如何使用 Python 进行数据采集。</p>
</div>
"""
# 创建 BeautifulSoup 对象
soup = BeautifulSoup(html_doc, 'lxml')
# --- 基本查找 ---
print("标题:", soup.h2.string) # Python 数据采集入门
print("作者:", soup.find('span', class_='author').string) # 张三
print("日期:", soup.find('span', class_='date').string) # 2025-01-15
# --- find_all 查找所有匹配元素 ---
all_spans = soup.find_all('span')
for span in all_spans:
print(f"{span.get('class')}: {span.string}")
# --- 获取属性 ---
print("div 的 class:", soup.div.get('class')) # ['article']
prettify() 方法可以将解析后的 HTML 以格式化的方式输出,在调试时非常方便查看文档结构。BeautifulSoup 的 select() 方法支持 CSS 选择器语法,对于熟悉前端开发的开发者来说非常直观。
from bs4 import BeautifulSoup
html_doc = """
<div id="products">
<div class="product">
<h3 class="name">商品A</h3>
<span class="price">99.00</span>
</div>
<div class="product">
<h3 class="name">商品B</h3>
<span class="price">199.00</span>
</div>
<div class="product featured">
<h3 class="name">商品C</h3>
<span class="price">299.00</span>
</div>
</div>
"""
soup = BeautifulSoup(html_doc, 'lxml')
# 通过 class 选择
names = soup.select('.product .name')
for name in names:
print("商品名:", name.string)
# 通过 id 选择
products = soup.select('#products .product')
print(f"共找到 {len(products)} 个商品")
# 组合选择器:选择带有 featured 类的商品
featured = soup.select('.product.featured .price')
print("推荐商品价格:", featured[0].string)
# 属性选择器
links = soup.select('a[href^="https"]') # href 以 https 开头的 a 标签
XPath 是另一种强大的 XML/HTML 路径查询语言,配合 lxml 库使用。相比 CSS 选择器,XPath 支持更复杂的查询条件。
from lxml import etree
html_doc = """
<table>
<tr><th>姓名</th><th>年龄</th><th>城市</th></tr>
<tr><td>张三</td><td>25</td><td>北京</td></tr>
<tr><td>李四</td><td>30</td><td>上海</td></tr>
<tr><td>王五</td><td>28</td><td>广州</td></tr>
</table>
"""
tree = etree.HTML(html_doc)
# --- 基本路径表达式 ---
# 选取所有 td 元素
tds = tree.xpath('//td')
for td in tds:
print("单元格:", td.text)
# --- 带条件筛选 ---
# 选取第二个 tr 中的所有 td
row2 = tree.xpath('//tr[2]/td/text()')
print("第二行数据:", row2)
# --- 属性筛选 ---
# 选取所有含 class="highlight" 的元素
highlights = tree.xpath('//*[@class="highlight"]')
# --- 轴选择(高级用法) ---
# 选取 td 的父元素 tr
parents = tree.xpath('//td/parent::tr')
print(f"共有 {len(parents)} 行数据")
# --- 内置函数 ---
# 获取最后一个 tr
last_row = tree.xpath('//tr[last()]/td/text()')
print("最后一行:", last_row)
下面是一个综合实战案例,演示如何采集一个图书列表页面并提取结构化数据:
import requests
from bs4 import BeautifulSoup
import json
# 1. 发送请求获取网页
url = 'https://books.toscrape.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36'
}
response = requests.get(url, headers=headers)
response.encoding = 'utf-8'
# 2. 解析 HTML
soup = BeautifulSoup(response.text, 'lxml')
# 3. 提取数据
books = []
book_items = soup.select('article.product_pod')
for item in book_items:
title = item.h3.a['title'] # 书名
price = item.select_one('.price_color').string # 价格
rating = item.select_one('p.star-rating')['class'][1] # 评分
availability = item.select_one('.availability').string.strip() # 库存
books.append({
'title': title,
'price': price,
'rating': rating,
'availability': availability
})
# 4. 输出结果
print(f"共采集到 {len(books)} 本书")
for book in books[:5]:
print(f" 《{book['title']}》 - {book['price']} - 评分: {book['rating']}")
# 5. 保存为 JSON
with open('books.json', 'w', encoding='utf-8') as f:
json.dump(books, f, ensure_ascii=False, indent=2)
print("数据已保存到 books.json")
从网络采集到的原始数据通常存在各种质量问题,直接使用会导致分析结果不准确。数据清洗是数据处理流程中至关重要的一步,主要解决以下三类问题:
| 问题类型 | 描述 | 常见处理方式 |
|---|---|---|
| 缺失值 | 某些字段为空或 NaN | 填充默认值、删除、插值 |
| 异常值 | 数据明显偏离正常范围 | 过滤、截断、替换 |
| 重复值 | 存在完全相同的记录 | 去重(保留第一条或最后一条) |
此外,数据清洗还包括格式统一(日期格式、字符串大小写)、类型转换(字符串转数值)、去除空白字符和特殊符号等操作。
Pandas 是 Python 数据处理的核心库,提供了丰富的数据清洗功能。以下演示常见的数据清洗操作:
import pandas as pd
import numpy as np
# --- 创建示例数据 ---
data = {
'姓名': ['张三', '李四', '王五', '赵六', '张三', '钱七'],
'年龄': [25, 30, np.nan, 150, 25, 28],
'城市': ['北京', '上海', '广州', '北京', '北京', '深圳'],
'薪资': ['8000', '12000', '9500', 'abc', '8000', '11000']
}
df = pd.DataFrame(data)
print("原始数据:")
print(df)
print()
# --- 1. 处理缺失值 ---
print("缺失值统计:")
print(df.isnull().sum())
# 填充缺失值
df['年龄'] = df['年龄'].fillna(df['年龄'].mean())
print("\n填充缺失值后:")
print(df)
# --- 2. 处理异常值 ---
# 年龄范围:18-65,超出范围的替换为 NaN 再填充
df.loc[(df['年龄'] < 18) | (df['年龄'] > 65), '年龄'] = np.nan
df['年龄'] = df['年龄'].fillna(df['年龄'].mean())
# --- 3. 处理重复值 ---
print(f"\n去重前: {len(df)} 条记录")
df = df.drop_duplicates()
print(f"去重后: {len(df)} 条记录")
# --- 4. 类型转换 ---
df['薪资'] = pd.to_numeric(df['薪资'], errors='coerce')
print("\n类型转换后:")
print(df.dtypes)
pd.to_numeric() 的 errors='coerce' 参数可以将无法转换的值设为 NaN,而不是报错。这在处理混合类型数据时非常实用。数据格式化确保数据的一致性和可读性,常见的格式化操作包括字符串处理、日期格式化和数值格式化。
import pandas as pd
# 创建示例数据
data = {
'商品名': [' 苹果手机 ', '华为笔记本', '小米平板', 'OPPO耳机 '],
'价格': [5999.0, 6999.0, 2499.0, 399.0],
'日期': ['2025/01/15', '2025-02-20', '2025.03.10', '2025/4/5'],
'分类': ['手机', '手机', '平板', '耳机']
}
df = pd.DataFrame(data)
# --- 字符串清洗 ---
df['商品名'] = df['商品名'].str.strip() # 去除首尾空格
df['分类'] = df['分类'].str.upper() # 转大写
# --- 日期格式化 ---
df['日期'] = pd.to_datetime(df['日期'], format='mixed')
df['年月'] = df['日期'].dt.strftime('%Y-%m') # 提取年月
df['星期'] = df['日期'].dt.day_name() # 获取星期几
# --- 数值格式化 ---
df['价格(万元)'] = (df['价格'] / 10000).round(4) # 转换为万元
df['价格区间'] = pd.cut(df['价格'], bins=[0, 1000, 5000, 10000],
labels=['低价', '中价', '高价'])
print(df)
清洗完成的数据需要持久化存储。CSV 和 JSON 是两种最常用的轻量级数据存储格式。
import pandas as pd
import json
# 示例数据
data = {
'姓名': ['张三', '李四', '王五'],
'年龄': [25, 30, 28],
'城市': ['北京', '上海', '广州']
}
df = pd.DataFrame(data)
# --- 存储为 CSV ---
df.to_csv('output/people.csv', index=False, encoding='utf-8-sig')
print("CSV 文件已保存")
# 读取 CSV
df_csv = pd.read_csv('output/people.csv', encoding='utf-8-sig')
print("从 CSV 读取的数据:")
print(df_csv)
# --- 存储为 JSON ---
# 方式1:使用 Pandas
df.to_json('output/people.json', orient='records',
force_ascii=False, indent=2)
print("\nJSON 文件已保存")
# 方式2:使用 json 模块(更灵活)
records = df.to_dict(orient='records')
with open('output/people_custom.json', 'w', encoding='utf-8') as f:
json.dump({
'total': len(records),
'data': records
}, f, ensure_ascii=False, indent=2)
# 读取 JSON
df_json = pd.read_json('output/people.json', encoding='utf-8')
print("从 JSON 读取的数据:")
print(df_json)
encoding='utf-8-sig'(带 BOM 的 UTF-8),这样用 Excel 打开时不会出现中文乱码问题。对于大量结构化数据,存储到数据库是更好的选择。SQLite 是 Python 内置的轻量级数据库,无需额外安装服务。
import sqlite3
import pandas as pd
# --- 示例数据 ---
data = {
'姓名': ['张三', '李四', '王五', '赵六'],
'年龄': [25, 30, 28, 35],
'城市': ['北京', '上海', '广州', '深圳'],
'薪资': [8000, 12000, 9500, 15000]
}
df = pd.DataFrame(data)
# --- 方式1:使用 Pandas 直接写入 SQLite ---
conn = sqlite3.connect('output/data.db')
df.to_sql('employees', conn, if_exists='replace', index=False)
print("数据已写入 SQLite 数据库")
# 使用 Pandas 读取
result = pd.read_sql('SELECT * FROM employees WHERE 薪资 > 10000', conn)
print("\n薪资大于10000的员工:")
print(result)
# --- 方式2:使用 SQL 语句操作 ---
cursor = conn.cursor()
# 插入新记录
cursor.execute(
"INSERT INTO employees (姓名, 年龄, 城市, 薪资) VALUES (?, ?, ?, ?)",
('钱七', 32, '杭州', 11000)
)
conn.commit()
# 更新记录
cursor.execute("UPDATE employees SET 薪资 = 薪资 * 1.1 WHERE 城市 = '北京'")
conn.commit()
# 查询并统计
cursor.execute("SELECT 城市, COUNT(*), AVG(薪资) FROM employees GROUP BY 城市")
for row in cursor.fetchall():
print(f"城市: {row[0]}, 人数: {row[1]}, 平均薪资: {row[2]:.0f}")
# 关闭连接
conn.close()
to_sql() 方法支持 if_exists 参数,可选值为 'fail'(默认,表已存在则报错)、'replace'(替换原表)和 'append'(追加数据)。在增量采集场景中,使用 'append' 可以方便地将新数据追加到已有表中。