Python自动化网络爬虫:从入门到实战全覆盖
·
网络爬虫是数据采集的利器,也是很多初学者进阶Python的第一道坎。今天分享一套完整的爬虫实战教程,从基础请求到反爬应对,从单页面到分布式,帮你系统掌握Python爬虫技能。
爬虫基础知识
在开始写代码之前,先搞清楚几个核心概念:
- HTTP协议:理解GET/POST请求、请求头、响应码
- HTML基础:能看懂网页结构,定位元素
- JavaScript渲染:有些数据是JS动态加载的,需要特殊处理
常见的Python爬虫库:
- requests:最基础的HTTP库
- BeautifulSoup:HTML/XML解析
- Scrapy:专业爬虫框架
- Selenium/Playwright:处理JS渲染
- aiohttp:异步爬虫
基础请求封装
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import time
import random
from typing import Optional, Dict
class BaseCrawler:
"""爬虫基类"""
def __init__(self):
self.session = self._create_session()
self.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,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
}
def _create_session(self) -> requests.Session:
"""创建带重试机制的会话"""
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def get(self, url: str, headers: Optional[Dict] = None,
params: Optional[Dict] = None, **kwargs) -> Optional[requests.Response]:
"""
发送GET请求
Args:
url: 目标URL
headers: 自定义请求头
params: URL参数
"""
final_headers = self.headers.copy()
if headers:
final_headers.update(headers)
try:
response = self.session.get(
url=url,
headers=final_headers,
params=params,
timeout=30,
**kwargs
)
response.raise_for_status()
return response
except requests.exceptions.RequestException as e:
print(f"请求失败: {url} - {e}")
return None
def post(self, url: str, data: Dict = None, json: Dict = None,
headers: Optional[Dict] = None) -> Optional[requests.Response]:
"""发送POST请求"""
final_headers = self.headers.copy()
if headers:
final_headers.update(headers)
try:
response = self.session.post(
url=url,
headers=final_headers,
data=data,
json=json,
timeout=30
)
response.raise_for_status()
return response
except requests.exceptions.RequestException as e:
print(f"请求失败: {url} - {e}")
return None
def random_sleep(self, min_seconds=1, max_seconds=3):
"""随机延时"""
time.sleep(random.uniform(min_seconds, max_seconds))
BeautifulSoup解析器
from bs4 import BeautifulSoup
from typing import List, Dict, Optional, Callable
class HTMLParser:
"""HTML解析器"""
def __init__(self, html: str, parser: str = 'html.parser'):
self.soup = BeautifulSoup(html, parser)
def find_one(self, tag: str, attrs: Dict = None,
class_: str = None, id: str = None,
text: str = None) -> Optional[BeautifulSoup]:
"""
查找单个元素
Args:
tag: 标签名,如 'div', 'a', 'span'
attrs: 属性字典,如 {'data-id': '123'}
class_: CSS类名
id: 元素ID
text: 文本内容
"""
return self.soup.find(
tag,
attrs=attrs,
class_=class_,
id=id,
string=text
)
def find_all(self, tag: str, attrs: Dict = None,
class_: str = None, limit: int = None) -> List[BeautifulSoup]:
"""查找所有匹配元素"""
return self.soup.find_all(
tag,
attrs=attrs,
class_=class_,
limit=limit
)
def find_all_safe(self, tag: str, attrs: Dict = None,
class_: str = None, limit: int = None) -> List[BeautifulSoup]:
"""安全查找,返回空列表而非None"""
result = self.find_all(tag, attrs, class_, limit)
return result if result else []
def extract_text(self, tag: str = None, attrs: Dict = None) -> str:
"""提取文本"""
element = self.find_one(tag, attrs)
if element:
return element.get_text(strip=True)
return ''
def extract_links(self, base_url: str = '',
filter_func: Callable[[str], bool] = None) -> List[str]:
"""
提取所有链接
Args:
base_url: 基础URL,用于拼接相对路径
filter_func: 过滤函数,接收href返回bool
"""
links = []
for a in self.soup.find_all('a', href=True):
href = a['href']
# 处理相对路径
if href.startswith('/'):
href = base_url.rstrip('/') + href
elif not href.startswith('http'):
href = base_url + '/' + href
# 应用过滤
if filter_func is None or filter_func(href):
links.append(href)
return list(set(links)) # 去重
def extract_images(self) -> List[str]:
"""提取所有图片URL"""
images = []
for img in self.soup.find_all('img'):
src = img.get('src') or img.get('data-src')
if src:
images.append(src)
return images
实战案例:爬取图片资源
import os
from pathlib import Path
from urllib.parse import urljoin, urlparse
class ImageCrawler(BaseCrawler):
"""图片爬虫"""
def __init__(self, save_dir='images'):
super().__init__()
self.save_dir = Path(save_dir)
self.save_dir.mkdir(parents=True, exist_ok=True)
self.downloaded = set()
def crawl_page(self, url: str, selectors: List[str] = None,
min_size: int = 0) -> List[str]:
"""
爬取页面图片
Args:
url: 页面URL
selectors: CSS选择器列表
min_size: 最小文件大小(KB)
"""
response = self.get(url)
if not response:
return []
parser = HTMLParser(response.text)
domain = urlparse(url).netloc
# 确定选择器
if not selectors:
selectors = ['img', 'picture source', 'a[href$=".jpg"]', 'a[href$=".png"]']
image_urls = set()
for selector in selectors:
elements = parser.find_all(selector.split('[')[0], limit=100)
for elem in elements:
if selector.startswith('img'):
src = elem.get('src') or elem.get('data-src')
elif selector.startswith('picture'):
src = elem.get('srcset', '').split()[0]
else:
src = elem.get('href')
if src and self._is_valid_image(src):
# 绝对路径转换
if src.startswith('//'):
src = 'https:' + src
elif src.startswith('/'):
src = f'https://{domain}{src}'
image_urls.add(src)
# 下载图片
downloaded = []
for img_url in image_urls:
filepath = self.download(img_url, min_size=min_size)
if filepath:
downloaded.append(filepath)
self.random_sleep(0.5, 1.5)
return downloaded
def _is_valid_image(self, url: str) -> bool:
"""验证是否是有效图片URL"""
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']
parsed = urlparse(url.lower())
return any(parsed.path.endswith(ext) for ext in image_extensions)
def download(self, url: str, min_size: int = 0) -> Optional[str]:
"""下载单张图片"""
if url in self.downloaded:
return None
try:
response = self.get(url)
if not response:
return None
content = response.content
# 检查文件大小
size_kb = len(content) / 1024
if min_size > 0 and size_kb < min_size:
print(f"跳过小文件: {url} ({size_kb:.1f}KB)")
return None
# 生成文件名
parsed = urlparse(url)
ext = Path(parsed.path).suffix or '.jpg'
filename = f"{hash(url)}{ext}"
filepath = self.save_dir / filename
with open(filepath, 'wb') as f:
f.write(content)
self.downloaded.add(url)
print(f"✓ 下载成功: {url} -> {filepath} ({size_kb:.1f}KB)")
return str(filepath)
except Exception as e:
print(f"✗ 下载失败: {url} - {e}")
return None
实战案例:爬取文章列表
class ArticleCrawler(BaseCrawler):
"""文章列表爬虫"""
def __init__(self):
super().__init__()
self.items = []
def crawl_list(self, url: str, page_range: tuple = (1, 10),
list_selector: str = 'article',
title_selector: str = 'h2.title',
link_selector: str = 'a[href]',
date_selector: str = 'time') -> List[Dict]:
"""
爬取文章列表
Args:
url: 列表页URL,支持 {page} 占位符
page_range: 页码范围
list_selector: 列表容器选择器
title_selector: 标题选择器
link_selector: 链接选择器
date_selector: 日期选择器
"""
all_items = []
for page in range(page_range[0], page_range[1] + 1):
page_url = url.format(page=page)
print(f"\n正在爬取第 {page} 页: {page_url}")
response = self.get(page_url)
if not response:
continue
parser = HTMLParser(response.text)
items = parser.find_all(list_selector, limit=50)
if not items:
print(f"第 {page} 页没有找到数据")
break
for item in items:
title_elem = item.select_one(title_selector)
link_elem = item.select_one(link_selector)
date_elem = item.select_one(date_selector)
if title_elem:
all_items.append({
'title': title_elem.get_text(strip=True),
'url': link_elem.get('href') if link_elem else '',
'date': date_elem.get_text(strip=True) if date_elem else '',
'page': page
})
print(f"第 {page} 页: 获取 {len(items)} 条")
self.random_sleep(1, 2)
self.items = all_items
return all_items
def crawl_detail(self, url: str) -> Dict:
"""爬取文章详情"""
response = self.get(url)
if not response:
return {}
parser = HTMLParser(response.text)
# 提取正文(根据目标网站结构调整)
content_elem = parser.find_one('div.content') or parser.find_one('article')
if content_elem:
# 清理脚本和样式
for tag in content_elem.find_all(['script', 'style', 'iframe']):
tag.decompose()
return {
'url': url,
'content': content_elem.get_text(separator='\n', strip=True),
'images': parser.extract_images()
}
return {}
def save_to_json(self, filepath: str):
"""保存结果到JSON"""
import json
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(self.items, f, ensure_ascii=False, indent=2)
print(f"✓ 已保存 {len(self.items)} 条数据到 {filepath}")
反爬应对策略
class AntiCrawlerMixin:
"""反爬应对Mixin"""
def rotate_user_agent(self):
"""随机User-Agent"""
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) Firefox/121.0',
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) Mobile/15E148',
]
self.headers['User-Agent'] = random.choice(user_agents)
def use_proxy(self, proxy_pool: List[str]) -> Dict:
"""使用代理IP"""
if not proxy_pool:
return {}
proxy = random.choice(proxy_pool)
return {
'http': f'http://{proxy}',
'https': f'http://{proxy}'
}
def handle_captcha(self, response):
"""检测验证码"""
if 'captcha' in response.text.lower() or response.status_code == 403:
print("⚠ 检测到验证码或被封禁")
return True
return False
完整使用示例
def main():
# 爬取图片
image_crawler = ImageCrawler('./downloaded_images')
image_crawler.crawl_page(
url='https://example.com/gallery',
selectors=['img.photo', 'a.download'],
min_size=10
)
# 爬取文章列表
article_crawler = ArticleCrawler()
articles = article_crawler.crawl_list(
url='https://example.com/articles?page={page}',
page_range=(1, 5)
)
# 保存结果
article_crawler.save_to_json('articles.json')
# 爬取文章详情
for article in articles[:3]: # 只取前3篇
if article['url']:
detail = article_crawler.crawl_detail(article['url'])
print(f"标题: {article['title']}")
print(f"内容长度: {len(detail.get('content', ''))} 字符")
article_crawler.random_sleep(1, 2)
if __name__ == '__main__':
main()
爬虫伦理规范
最后提醒几点:
- 遵守robots.txt:查看网站的爬虫协议
- 控制请求频率:不要对服务器造成压力
- 设置User-Agent:让网站知道你是爬虫
- 不要爬取隐私数据:合规是底线
- 优先使用官方API:很多网站提供合法接口
这套爬虫框架覆盖了常见的采集场景,核心思路是分层封装——请求层、解析层、存储层分离,便于维护和扩展🙃
更多推荐


所有评论(0)