Python自动化大师第3篇:网页爬虫基础
Python Automation Master Part 3: Web Scraping Basics
序言:什么是网页爬虫?
网页爬虫(Web Scraping)是从网站自动提取所需数据的技术。也称为网络爬虫(Web Crawling),由于能够高效收集大量网页数据,被广泛应用于数据分析、价格监控、新闻收集等各种领域。
在这一篇中,我们将学习使用Python进行网页爬虫的基础知识。逐步了解如何使用requests库获取网页,使用BeautifulSoup解析HTML并提取所需数据。
1. 法律/道德考量
在开始网页爬虫之前,必须了解一些法律和道德考量。
1.1 检查robots.txt
robots.txt是位于网站根目录的文件,它指定了网页爬虫可以访问和不能访问的页面。
# robots.txt示例 (https://example.com/robots.txt)
User-agent: *
Disallow: /private/
Disallow: /admin/
Allow: /public/
Crawl-delay: 10
- User-agent:指定规则适用的爬虫。*表示所有爬虫。
- Disallow:指定禁止爬虫的路径。
- Allow:指定允许爬虫的路径。
- Crawl-delay:指定请求之间的等待时间(秒)。
1.2 网页爬虫道德准则
- 遵守robots.txt:务必检查并遵守网站的robots.txt规则。
- 最小化服务器负载:在请求之间设置适当的延迟,不要给服务器造成过大负载。
- 检查服务条款:在网站的服务条款中检查与爬虫相关的条款。
- 保护个人隐私:不要未经授权收集个人信息。
- 尊重版权:尊重收集数据的版权并适当使用。
1.3 使用Python检查robots.txt
from urllib.robotparser import RobotFileParser
def check_robots_txt(url, user_agent='*'):
"""检查robots.txt并返回是否可以爬取。"""
# 生成robots.txt URL
from urllib.parse import urlparse
parsed = urlparse(url)
robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
# 设置RobotFileParser
rp = RobotFileParser()
rp.set_url(robots_url)
rp.read()
# 检查该URL是否可以爬取
can_fetch = rp.can_fetch(user_agent, url)
crawl_delay = rp.crawl_delay(user_agent)
return {
'can_fetch': can_fetch,
'crawl_delay': crawl_delay
}
# 使用示例
result = check_robots_txt('https://www.google.com/search')
print(f"可以爬取:{result['can_fetch']}")
print(f"爬取延迟:{result['crawl_delay']}秒")
2. requests库
requests是Python中发送HTTP请求最流行的库。它提供简单直观的API,可以轻松获取网页。
2.1 安装
# 使用pip安装
pip install requests
2.2 HTTP方法
了解HTTP协议中使用的主要方法以及在requests中的使用方法。
GET请求
最常见的请求方式,用于从服务器获取数据。
import requests
# 基本GET请求
response = requests.get('https://httpbin.org/get')
print(response.text)
# 带查询参数的GET请求
params = {
'search': 'python',
'page': 1,
'limit': 10
}
response = requests.get('https://httpbin.org/get', params=params)
print(response.url) # https://httpbin.org/get?search=python&page=1&limit=10
POST请求
用于向服务器发送数据。可以发送表单数据或JSON数据。
import requests
# 发送表单数据
form_data = {
'username': 'user123',
'password': 'pass456'
}
response = requests.post('https://httpbin.org/post', data=form_data)
print(response.json())
# 发送JSON数据
json_data = {
'name': '张三',
'email': 'zhang@example.com'
}
response = requests.post('https://httpbin.org/post', json=json_data)
print(response.json())
设置请求头
可以设置User-Agent等HTTP请求头来自定义请求。
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/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-US;q=0.8,en;q=0.7',
'Referer': 'https://www.google.com/'
}
response = requests.get('https://httpbin.org/headers', headers=headers)
print(response.json())
2.3 响应处理
requests提供多种处理响应的功能。
import requests
response = requests.get('https://httpbin.org/get')
# 检查状态码
print(f"状态码:{response.status_code}") # 200
# 检查是否成功
if response.ok: # 当status_code在200-299范围内时为True
print("请求成功!")
# 响应正文(文本)
print(response.text)
# 响应正文(JSON)
data = response.json()
print(data)
# 响应正文(二进制)- 用于图片等
content = response.content
# 响应头
print(response.headers)
print(response.headers['Content-Type'])
# 编码
print(response.encoding) # UTF-8
response.encoding = 'utf-8' # 指定编码
HTTP状态码
| 状态码 | 含义 | 说明 |
|---|---|---|
| 200 | OK | 请求成功 |
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 400 | Bad Request | 错误的请求 |
| 403 | Forbidden | 无访问权限 |
| 404 | Not Found | 页面未找到 |
| 500 | Internal Server Error | 服务器内部错误 |
2.4 错误处理和超时
import requests
from requests.exceptions import RequestException, Timeout, HTTPError
def safe_request(url, timeout=10):
"""执行安全的HTTP请求。"""
try:
response = requests.get(url, timeout=timeout)
response.raise_for_status() # 4xx、5xx错误时抛出异常
return response
except Timeout:
print(f"超时:{url}")
except HTTPError as e:
print(f"HTTP错误:{e.response.status_code}")
except RequestException as e:
print(f"请求失败:{e}")
return None
# 使用示例
response = safe_request('https://httpbin.org/get')
if response:
print(response.text)
3. 使用BeautifulSoup解析HTML
BeautifulSoup是用于解析HTML和XML文档的Python库。可以轻松地从复杂的HTML结构中提取所需的数据。
3.1 安装
# 安装BeautifulSoup和lxml解析器
pip install beautifulsoup4 lxml
3.2 基本用法
from bs4 import BeautifulSoup
import requests
# 获取网页
url = 'https://example.com'
response = requests.get(url)
# 创建BeautifulSoup对象
soup = BeautifulSoup(response.text, 'lxml') # 或 'html.parser'
# 查看HTML结构(美化输出)
print(soup.prettify())
3.3 通过标签查找元素
from bs4 import BeautifulSoup
html = """
<html>
<head><title>测试页面</title></head>
<body>
<h1>主标题</h1>
<p>第一段</p>
<p>第二段</p>
<a href="https://example.com">链接1</a>
<a href="https://google.com">链接2</a>
</body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')
# 查找第一个标签
title = soup.find('title')
print(title.text) # 测试页面
h1 = soup.find('h1')
print(h1.text) # 主标题
# 查找所有标签
paragraphs = soup.find_all('p')
for p in paragraphs:
print(p.text)
links = soup.find_all('a')
for link in links:
print(link.text, link['href'])
3.4 通过class和ID查找元素
from bs4 import BeautifulSoup
html = """
<html>
<body>
<div id="header">头部区域</div>
<div class="content">
<p class="intro">介绍</p>
<p class="main-text">正文内容</p>
</div>
<div class="sidebar">侧边栏</div>
<ul class="menu">
<li class="menu-item active">首页</li>
<li class="menu-item">介绍</li>
<li class="menu-item">联系我们</li>
</ul>
</body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')
# 通过ID查找
header = soup.find(id='header')
print(header.text) # 头部区域
# 通过class查找
content = soup.find(class_='content')
print(content.text)
# 通过class名查找多个元素
menu_items = soup.find_all(class_='menu-item')
for item in menu_items:
print(item.text)
# 复合条件查找
active_item = soup.find('li', class_='active')
print(active_item.text) # 首页
# 使用CSS选择器(select)
main_text = soup.select_one('.content .main-text')
print(main_text.text) # 正文内容
all_menu_items = soup.select('ul.menu li')
for item in all_menu_items:
print(item.text)
3.5 访问属性和提取文本
from bs4 import BeautifulSoup
html = """
<html>
<body>
<a href="https://example.com" title="示例链接" data-id="123">
<span>链接</span> 文本
</a>
<img src="image.jpg" alt="图片描述">
<div class="article">
<h2>标题</h2>
<p>第一段</p>
<p>第二段</p>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')
# 访问属性
link = soup.find('a')
print(link['href']) # https://example.com
print(link['title']) # 示例链接
print(link.get('data-id')) # 123
print(link.get('class')) # None(没有则返回None)
# 获取所有属性
print(link.attrs) # {'href': 'https://example.com', 'title': '示例链接', 'data-id': '123'}
# 图片标签属性
img = soup.find('img')
print(img['src']) # image.jpg
print(img['alt']) # 图片描述
# 提取文本
print(link.text) # 链接 文本(包括所有子元素的文本)
print(link.string) # None(有多个子元素时返回None)
print(link.get_text()) # 链接 文本
# 去除空白
print(link.get_text(strip=True)) # 链接 文本
# 用分隔符连接
article = soup.find(class_='article')
print(article.get_text(separator=' | ', strip=True))
# 标题 | 第一段 | 第二段
3.6 CSS选择器高级用法
from bs4 import BeautifulSoup
html = """
<html>
<body>
<table id="data-table">
<tr><th>姓名</th><th>年龄</th><th>职业</th></tr>
<tr><td>张三</td><td>30</td><td>开发者</td></tr>
<tr><td>李四</td><td>25</td><td>设计师</td></tr>
<tr><td>王五</td><td>35</td><td>经理</td></tr>
</table>
<div class="products">
<div class="product" data-price="10000">
<span class="name">商品A</span>
</div>
<div class="product" data-price="20000">
<span class="name">商品B</span>
</div>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')
# 后代选择器(空格)
cells = soup.select('#data-table td')
for cell in cells:
print(cell.text)
# 直接子元素选择器(>)
rows = soup.select('#data-table > tr')
# 属性选择器
products = soup.select('div[data-price]')
for product in products:
name = product.select_one('.name').text
price = product['data-price']
print(f"{name}:{price}元")
# 选择第n个元素
first_row = soup.select_one('#data-table tr:nth-child(2)') # 第一个数据行
print([td.text for td in first_row.find_all('td')])
# 复合选择器
products_with_high_price = soup.select('.product[data-price="20000"]')
for product in products_with_high_price:
print(product.select_one('.name').text)
4. 实战示例:创建简单的爬虫
现在让我们综合所学内容创建实战爬虫。
4.1 新闻标题收集器
import requests
from bs4 import BeautifulSoup
import time
class NewsHeadlineScraper:
"""收集新闻标题的爬虫"""
def __init__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def fetch_page(self, url):
"""获取网页。"""
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.text
except requests.RequestException as e:
print(f"页面请求失败:{e}")
return None
def parse_headlines(self, html, selector):
"""从HTML中提取标题。"""
soup = BeautifulSoup(html, 'lxml')
headlines = []
elements = soup.select(selector)
for element in elements:
title = element.get_text(strip=True)
link = element.get('href', '')
if title:
headlines.append({
'title': title,
'link': link
})
return headlines
def scrape(self, url, selector, delay=1):
"""从给定URL爬取标题。"""
print(f"开始爬取:{url}")
html = self.fetch_page(url)
if not html:
return []
headlines = self.parse_headlines(html, selector)
print(f"找到{len(headlines)}个标题。")
time.sleep(delay) # 防止服务器过载
return headlines
# 使用示例(实际使用时请检查该网站的服务条款)
if __name__ == '__main__':
scraper = NewsHeadlineScraper()
# 示例(根据目标网站修改实际URL和选择器)
headlines = scraper.scrape(
url='https://example.com/news',
selector='a.headline'
)
for idx, headline in enumerate(headlines, 1):
print(f"{idx}. {headline['title']}")
print(f" 链接:{headline['link']}")
4.2 表格数据提取器
import requests
from bs4 import BeautifulSoup
import csv
def extract_table_data(url, table_selector='table'):
"""提取网页中的表格数据。"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'lxml')
table = soup.select_one(table_selector)
if not table:
print("找不到表格。")
return []
data = []
rows = table.find_all('tr')
for row in rows:
# 提取表头单元格或数据单元格
cells = row.find_all(['th', 'td'])
row_data = [cell.get_text(strip=True) for cell in cells]
if row_data:
data.append(row_data)
return data
def save_to_csv(data, filename):
"""将数据保存为CSV文件。"""
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(data)
print(f"数据已保存到{filename}。")
# 使用示例
if __name__ == '__main__':
# 示例URL(实际使用时请更换为适当的URL)
data = extract_table_data(
url='https://example.com/data-table',
table_selector='#main-table'
)
if data:
for row in data[:5]: # 只输出前5行
print(row)
save_to_csv(data, 'extracted_data.csv')
4.3 图片下载器
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import os
import time
class ImageDownloader:
"""从网页下载图片的类"""
def __init__(self, download_dir='images'):
self.download_dir = download_dir
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
# 创建下载目录
if not os.path.exists(download_dir):
os.makedirs(download_dir)
def get_image_urls(self, page_url, img_selector='img'):
"""从页面提取图片URL。"""
response = requests.get(page_url, headers=self.headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'lxml')
images = soup.select(img_selector)
image_urls = []
for img in images:
src = img.get('src') or img.get('data-src')
if src:
# 将相对URL转换为绝对URL
full_url = urljoin(page_url, src)
image_urls.append(full_url)
return image_urls
def download_image(self, url, filename=None):
"""下载图片。"""
try:
response = requests.get(url, headers=self.headers, timeout=30)
response.raise_for_status()
# 如果没有文件名,从URL中提取
if not filename:
parsed = urlparse(url)
filename = os.path.basename(parsed.path) or 'image.jpg'
filepath = os.path.join(self.download_dir, filename)
with open(filepath, 'wb') as f:
f.write(response.content)
print(f"下载完成:{filename}")
return filepath
except requests.RequestException as e:
print(f"下载失败({url}):{e}")
return None
def download_all(self, page_url, img_selector='img', delay=1):
"""下载页面中的所有图片。"""
image_urls = self.get_image_urls(page_url, img_selector)
print(f"找到{len(image_urls)}张图片。")
downloaded = []
for idx, url in enumerate(image_urls, 1):
print(f"[{idx}/{len(image_urls)}] 正在下载...")
filepath = self.download_image(url)
if filepath:
downloaded.append(filepath)
time.sleep(delay) # 防止服务器过载
return downloaded
# 使用示例
if __name__ == '__main__':
downloader = ImageDownloader(download_dir='downloaded_images')
# 示例URL(实际使用时请更换为适当的URL)
downloaded_files = downloader.download_all(
page_url='https://example.com/gallery',
img_selector='div.gallery img',
delay=2
)
print(f"\n共下载了{len(downloaded_files)}张图片。")
5. 技巧和注意事项
5.1 高效爬虫的技巧
- 使用Session:向同一网站发送多次请求时,使用requests.Session()复用连接。
- 适当的延迟:使用time.sleep()在请求之间设置延迟,不要给服务器造成过大负载。
- 错误处理:为网络错误、超时等添加适当的异常处理。
- 利用缓存:缓存结果以避免重复请求同一页面。
- 日志记录:记录爬虫过程,便于问题发生时进行调试。
5.2 常见问题和解决方案
- 编码问题:明确设置response.encoding或使用chardet库。
- 403 Forbidden:设置User-Agent请求头或添加其他请求头。
- 动态内容:通过JavaScript加载的内容无法用requests获取。下一篇将学习使用Selenium的解决方法。
- IP封禁:降低请求频率或使用代理。
总结
在这一篇中,我们学习了Python网页爬虫的基础知识。掌握了如何使用requests库获取网页,使用BeautifulSoup解析HTML并提取所需数据。
在下一篇第4篇中,我们将处理更复杂的网页爬虫场景。将学习使用Selenium处理JavaScript动态生成的内容、爬取需要登录的网站,以及保存收集数据的各种方法。
系列指南:Python自动化大师系列还在继续。在下一篇中学习更深入的网页爬虫技术!