1. 表格型页面采集的核心挑战
表格数据采集是爬虫开发中最常见的需求之一,但也是最容易遇到问题的场景。不同于普通的文本抓取,表格数据具有结构化特征,同时又面临跨页、动态加载等复杂情况。我们先来看一个典型的电商价格对比表格:
<table id="price-list"> <thead> <tr> <th>型号</th> <th>店铺</th> <th>价格</th> <th>销量</th> </tr> </thead> <tbody> <tr> <td>iPhone 15</td> <td>Apple旗舰店</td> <td>¥6999</td> <td>2.5万</td> </tr> <!-- 更多行数据... --> </tbody> </table>这类表格看似简单,但在实际采集时会遇到几个典型问题:
- 多列数据关联性:每行的各列数据需要保持对应关系,不能错位
- 跨页处理:当表格数据分页显示时,需要自动识别并处理分页逻辑
- 表头一致性:跨页表格需要确保每页都包含正确的表头信息
- 动态加载:现代网页常使用AJAX动态加载表格数据
提示:在开始采集前,务必检查网站的robots.txt文件,合理设置爬取间隔,避免对目标网站造成过大访问压力。
2. 基础表格解析技术
2.1 使用BeautifulSoup解析静态表格
对于简单的静态HTML表格,BeautifulSoup是最直接的选择。下面是一个完整的解析示例:
from bs4 import BeautifulSoup import requests url = "http://example.com/price-table" response = requests.get(url) soup = BeautifulSoup(response.text, 'html.parser') table = soup.find('table', {'id': 'price-list'}) headers = [th.text.strip() for th in table.find('thead').find_all('th')] data = [] for row in table.find('tbody').find_all('tr'): row_data = [td.text.strip() for td in row.find_all('td')] data.append(dict(zip(headers, row_data))) print(data)这种方法适用于:
- 表格结构规整,有明确的thead和tbody分区
- 数据量不大,不需要处理分页
- 没有复杂的JavaScript渲染
2.2 使用pandas直接读取HTML表格
对于数据分析场景,pandas提供了更便捷的接口:
import pandas as pd tables = pd.read_html("http://example.com/price-table") df = tables[0] # 获取第一个表格 print(df.head())pandas的read_html函数会自动:
- 识别页面中的所有表格
- 处理基本的表格结构
- 将数据转换为DataFrame格式
但它的局限性在于:
- 无法处理动态加载的内容
- 对复杂表格结构的适应性较差
- 分页需要额外处理
3. 高级表格处理技巧
3.1 处理动态加载的表格
现代网页大量使用JavaScript动态加载表格数据。以某电商网站为例,表格数据是通过AJAX请求获取的JSON数据:
import requests import json # 分析页面找到实际的API接口 api_url = "https://api.example.com/products?page=1&size=50" headers = { "User-Agent": "Mozilla/5.0", "X-Requested-With": "XMLHttpRequest" } response = requests.get(api_url, headers=headers) data = response.json() # 解析JSON数据 products = data['data']['list'] for product in products: print(f"{product['name']} - {product['price']}")关键点:
- 使用浏览器开发者工具(F12)分析网络请求
- 找到实际传输数据的API接口
- 模拟必要的请求头(如X-Requested-With)
- 处理可能存在的反爬机制
3.2 跨页表格的自动处理
跨页表格的处理需要解决两个核心问题:
- 分页逻辑识别
- 数据合并
典型的分页处理代码结构:
base_url = "https://example.com/products?page={}" all_data = [] for page in range(1, 6): # 假设采集前5页 url = base_url.format(page) response = requests.get(url) soup = BeautifulSoup(response.text, 'html.parser') # 解析当前页数据 table = soup.find('table', {'class': 'data-table'}) for row in table.find_all('tr')[1:]: # 跳过表头 cells = row.find_all('td') all_data.append({ 'name': cells[0].text, 'price': cells[1].text }) # 检查是否还有下一页 next_page = soup.find('a', {'class': 'next-page'}) if not next_page: break print(f"共采集到{len(all_data)}条记录")4. 实战:完整的多页表格采集系统
让我们构建一个完整的表格采集系统,包含以下功能:
- 自动分页处理
- 异常重试机制
- 数据去重
- 结果导出
import requests from bs4 import BeautifulSoup import pandas as pd from urllib.parse import urljoin import time class TableScraper: def __init__(self, base_url): self.base_url = base_url self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0' }) self.all_data = [] self.seen_ids = set() def scrape_page(self, page=1): url = self.base_url if page == 1 else f"{self.base_url}?page={page}" 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_table(self, html): soup = BeautifulSoup(html, 'html.parser') table = soup.find('table', {'class': 'data-table'}) if not table: return False headers = [th.text.strip() for th in table.find('thead').find_all('th')] for row in table.find('tbody').find_all('tr'): cells = row.find_all('td') if len(cells) != len(headers): continue row_data = {headers[i]: cells[i].text.strip() for i in range(len(headers))} item_id = row.get('data-id') or row_data.get('ID') if item_id and item_id not in self.seen_ids: self.seen_ids.add(item_id) self.all_data.append(row_data) return True def has_next_page(self, html): soup = BeautifulSoup(html, 'html.parser') return bool(soup.find('a', {'class': 'next-page'})) def scrape_all(self, max_pages=10): current_page = 1 while current_page <= max_pages: print(f"正在采集第 {current_page} 页...") html = self.scrape_page(current_page) if not html: break if not self.parse_table(html): print("未找到表格数据") break if not self.has_next_page(html): break current_page += 1 time.sleep(1) # 礼貌性延迟 return pd.DataFrame(self.all_data) # 使用示例 scraper = TableScraper("https://example.com/products") df = scraper.scrape_all(max_pages=5) df.to_csv('products.csv', index=False) print("数据采集完成,已保存到products.csv")这个系统实现了:
- 会话保持(Session)提高效率
- 自动分页检测
- 基于ID的数据去重
- 异常处理和延迟设置
- 结果导出为CSV
5. 常见问题与解决方案
5.1 表格结构不规整
当遇到不规范的表格时(如缺少thead、行列合并等),可以采用以下策略:
def parse_irregular_table(table): # 尝试从第一行提取表头 headers = [] first_row = table.find('tr') for cell in first_row.find_all(['th', 'td']): colspan = int(cell.get('colspan', 1)) headers.extend([cell.text.strip()] * colspan) # 处理数据行 data = [] for row in table.find_all('tr')[1:]: row_data = {} col_index = 0 for cell in row.find_all('td'): colspan = int(cell.get('colspan', 1)) rowspan = int(cell.get('rowspan', 1)) for i in range(colspan): if col_index + i < len(headers): row_data[headers[col_index + i]] = cell.text.strip() col_index += colspan data.append(row_data) return data5.2 反爬机制应对
常见反爬手段及对策:
User-Agent检测:
headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }IP限制:
- 使用代理IP池
- 设置合理的请求间隔
验证码:
- 降低请求频率
- 使用第三方验证码识别服务
行为检测:
import random import time def random_delay(): time.sleep(random.uniform(1, 3))
5.3 大数据量处理
当处理大量数据时,考虑:
增量采集:
last_id = load_last_id() # 从上次停止的位置继续数据分片存储:
if len(data) > 1000: save_to_file(data) data = []内存优化:
import csv with open('output.csv', 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=headers) writer.writeheader() for row in data_generator(): writer.writerow(row)
6. 性能优化技巧
并发采集:
import concurrent.futures def scrape_page(page): # 单页采集逻辑 pass with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: executor.map(scrape_page, range(1, 11))缓存已采集页面:
from requests_cache import CachedSession session = CachedSession('demo_cache', expire_after=3600)选择性解析:
from bs4 import SoupStrainer only_tables = SoupStrainer('table') soup = BeautifulSoup(html, 'html.parser', parse_only=only_tables)使用lxml解析器:
soup = BeautifulSoup(html, 'lxml') # 比html.parser更快
7. 数据清洗与验证
采集后的数据通常需要清洗:
def clean_data(df): # 去除空值 df = df.dropna() # 统一货币格式 df['price'] = df['price'].str.replace('[^\d.]', '', regex=True).astype(float) # 规范日期格式 df['date'] = pd.to_datetime(df['date'], errors='coerce') # 去除重复 df = df.drop_duplicates(subset=['id']) return df验证数据质量:
def validate_data(df): assert not df.empty, "数据为空" assert 'price' in df.columns, "缺少价格列" assert df['price'].between(0, 100000).all(), "价格异常" return True8. 自动化部署方案
对于需要定期运行的采集任务,可以考虑:
Windows任务计划:
schtasks /create /tn "DailyScrape" /tr "python scrape.py" /sc daily /st 02:00Linux crontab:
0 2 * * * /usr/bin/python3 /path/to/scrape.py云函数部署(以AWS Lambda为例):
import boto3 def lambda_handler(event, context): scraper = TableScraper("https://example.com/products") df = scraper.scrape_all() s3 = boto3.client('s3') s3.put_object(Bucket='my-bucket', Key='products.csv', Body=df.to_csv()) return {"status": "success"}
9. 法律与伦理考量
遵守robots.txt:
from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url("https://example.com/robots.txt") rp.read() if not rp.can_fetch("*", target_url): print("禁止采集")数据使用限制:
- 仅采集公开数据
- 不采集个人隐私信息
- 遵守网站的服务条款
访问频率控制:
import time time.sleep(random.uniform(1, 3)) # 随机延迟
在实际项目中,我通常会创建一个配置对象来集中管理这些参数:
class Config: DELAY = (1, 3) # 随机延迟范围 MAX_PAGES = 100 # 最大采集页数 TIMEOUT = 10 # 请求超时时间 USER_AGENT = "Mozilla/5.0" # 默认User-Agent OUTPUT_FILE = "data.csv" # 输出文件名这样在代码中可以通过Config.DELAY等方式统一访问配置,便于维护和修改。