引言
在当今信息时代,互联网上存在着海量的数据资源,这些数据对于商业分析、市场研究、学术研究等领域具有重要价值。然而,这些数据往往分散在不同的网站上,以非结构化的形式存在,手动收集和整理这些数据既费时又容易出错。Web爬虫(Web Spider)作为一种自动化数据采集工具,能够帮助我们高效地从网页中提取所需信息,并将其转换为结构化数据供进一步分析使用。
Python凭借其简洁的语法、丰富的第三方库和强大的生态系统,成为了Web爬虫开发的首选语言。从简单的网页请求到复杂的动态网页处理,从数据提取到存储,Python都能提供相应的解决方案。requests、BeautifulSoup、Scrapy等库使得开发Web爬虫变得更加简单和高效。
在前面的章节中,我们学习了Python的基础语法、数据结构、面向对象编程、文件操作、标准库使用、环境管理、正则表达式、单元测试以及自动化脚本开发等内容。本章将结合这些知识,深入学习Web爬虫开发的核心技术和实践方法,帮助你掌握从网页中提取数据的技能。
学习目标
完成本章学习后,你将能够:
- 理解Web爬虫的基本概念和工作原理
- 掌握HTTP协议和网页请求的基础知识
- 学会使用requests库发送HTTP请求
- 掌握BeautifulSoup库解析HTML文档
- 了解XPath和CSS选择器的使用方法
- 学会处理动态网页和JavaScript渲染内容
- 掌握数据存储和导出的方法
- 了解爬虫的法律和道德规范
- 学会处理反爬虫机制
核心知识点讲解
1. Web爬虫概述
Web爬虫是一种自动化程序,它能够模拟人类浏览器的行为,向Web服务器发送请求,获取网页内容,并从中提取所需的数据。一个典型的爬虫工作流程包括:
爬虫工作流程:
- 发送请求:向目标网站发送HTTP请求
- 接收响应:获取服务器返回的HTML内容
- 解析内容:从HTML中提取所需数据
- 数据存储:将提取的数据保存到文件或数据库
- 循环执行:重复上述过程直到完成所有任务
爬虫类型:
- 通用爬虫:如搜索引擎爬虫,爬取整个互联网
- 聚焦爬虫:针对特定主题或网站的爬虫
- 增量爬虫:只爬取新增或更新的内容
- 深层爬虫:能够处理JavaScript渲染的动态内容
2. HTTP协议基础
HTTP(HyperText Transfer Protocol)是Web浏览器和服务器之间通信的基础协议。理解HTTP协议对于开发Web爬虫至关重要。
HTTP请求方法:
- GET:请求指定资源
- POST:向指定资源提交数据
- PUT:更新指定资源
- DELETE:删除指定资源
HTTP状态码:
- 2xx:成功响应(200 OK, 201 Created等)
- 3xx:重定向(301 Moved Permanently, 302 Found等)
- 4xx:客户端错误(404 Not Found, 403 Forbidden等)
- 5xx:服务器错误(500 Internal Server Error, 503 Service Unavailable等)
3. requests库详解
requests是Python中最受欢迎的HTTP库,它简化了HTTP请求的发送过程,提供了简洁易用的API。
主要功能:
- 发送各种HTTP请求:GET、POST、PUT、DELETE等
- 处理请求头和Cookie:自定义请求头,管理会话状态
- 处理响应内容:文本、JSON、二进制数据等
- 会话管理:保持连接和Cookie状态
- 超时和重试:处理网络异常情况
4. HTML解析与数据提取
获取网页内容后,需要从中提取所需的数据。Python提供了多种HTML解析和数据提取的方法。
BeautifulSoup库:
- HTML解析:将HTML文档转换为树形结构
- 元素查找:通过标签名、属性、CSS选择器等方式查找元素
- 数据提取:获取元素的文本内容、属性值等
- 文档遍历:在HTML树中导航和遍历
XPath和CSS选择器:
- XPath:XML路径语言,用于在XML文档中查找节点
- CSS选择器:用于选择HTML元素的模式
5. 动态网页处理
现代Web应用大量使用JavaScript来动态生成内容,传统的HTML解析方法无法处理这类动态内容。
处理方法:
- Selenium:浏览器自动化工具,能够执行JavaScript
- Playwright:现代化的浏览器自动化库
- Pyppeteer:Python版的Puppeteer,控制Chrome/Chromium
6. 数据存储与导出
爬取的数据需要妥善存储和管理,以便后续分析和使用。
存储方式:
- 文件存储:CSV、JSON、Excel等格式
- 数据库存储:SQLite、MySQL、MongoDB等
- 云存储:AWS S3、Google Cloud Storage等
7. 反爬虫机制与应对
为了保护网站资源,许多网站采用了反爬虫机制,爬虫开发者需要了解这些机制并采取相应的应对措施。
常见反爬虫机制:
- User-Agent检测:检查请求头中的User-Agent
- IP限制:限制同一IP的请求频率
- 验证码:要求用户输入验证码
- JavaScript混淆:通过JavaScript动态生成内容
- 蜜罐陷阱:设置虚假链接诱捕爬虫
应对策略:
- 设置合理的请求头:模拟真实浏览器
- 控制请求频率:添加延时,避免过于频繁的请求
- 使用代理IP:轮换IP地址
- 处理验证码:使用OCR或打码平台
- 执行JavaScript:使用浏览器自动化工具
8. 法律与道德规范
Web爬虫开发需要遵守相关法律法规和道德规范,尊重网站的权益和用户隐私。
注意事项:
- 遵守robots.txt:查看并遵守网站的爬虫协议
- 尊重版权:不侵犯网站的知识产权
- 保护隐私:不收集和传播个人隐私信息
- 合理使用:避免对网站造成过大负担
代码示例与实战
实战1:基础爬虫开发
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
基础Web爬虫示例
爬取静态网页内容并提取数据
"""
import requests
from bs4 import BeautifulSoup
import csv
import json
import time
import logging
from urllib.parse import urljoin, urlparse
import argparse
class BasicSpider:
"""基础爬虫类"""
def __init__(self, base_url, headers=None):
self.base_url = base_url
self.session = requests.Session()
# 设置默认请求头
default_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 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.8,en-US;q=0.5,en;q=0.3',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
}
if headers:
default_headers.update(headers)
self.session.headers.update(default_headers)
self.setup_logging()
def setup_logging(self):
"""设置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('spider.log'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def get_page(self, url, timeout=10):
"""获取网页内容"""
try:
response = self.session.get(url, timeout=timeout)
response.raise_for_status() # 检查HTTP错误
response.encoding = response.apparent_encoding # 自动检测编码
return response
except requests.RequestException as e:
self.logger.error(f"请求失败 {url}: {e}")
return None
def parse_news_list(self, html_content, base_url):
"""解析新闻列表页面"""
soup = BeautifulSoup(html_content, 'html.parser')
news_items = []
# 查找新闻条目(这里以常见的新闻网站结构为例)
# 实际使用时需要根据目标网站的具体结构调整选择器
articles = soup.find_all('div', class_='news-item')
for article in articles:
try:
# 提取标题
title_elem = article.find('h3') or article.find('a')
title = title_elem.get_text(strip=True) if title_elem else "无标题"
# 提取链接
link_elem = article.find('a')
link = link_elem.get('href') if link_elem else ""
if link:
link = urljoin(base_url, link) # 转换为绝对URL
# 提取摘要
summary_elem = article.find('p', class_='summary')
summary = summary_elem.get_text(strip=True) if summary_elem else ""
# 提取发布时间
time_elem = article.find('span', class_='time')
publish_time = time_elem.get_text(strip=True) if time_elem else ""
news_item = {
'title': title,
'link': link,
'summary': summary,
'publish_time': publish_time
}
news_items.append(news_item)
except Exception as e:
self.logger.error(f"解析新闻条目失败: {e}")
continue
return news_items
def parse_news_detail(self, html_content):
"""解析新闻详情页面"""
soup = BeautifulSoup(html_content, 'html.parser')
# 提取标题
title_elem = soup.find('h1') or soup.find('title')
title = title_elem.get_text(strip=True) if title_elem else ""
# 提取正文内容
content_elem = soup.find('div', class_='content') or soup.find('article')
content = content_elem.get_text(strip=True) if content_elem else ""
# 提取作者
author_elem = soup.find('span', class_='author') or soup.find('meta', attrs={'name': 'author'})
author = ""
if author_elem:
if author_elem.name == 'meta':
author = author_elem.get('content', '')
else:
author = author_elem.get_text(strip=True)
# 提取发布时间
time_elem = soup.find('time') or soup.find('span', class_='publish-time')
publish_time = time_elem.get_text(strip=True) if time_elem else ""
return {
'title': title,
'content': content,
'author': author,
'publish_time': publish_time
}
def crawl_news(self, list_urls, output_file='news_data.csv'):
"""爬取新闻数据"""
all_news = []
# 爬取列表页面
for url in list_urls:
self.logger.info(f"正在爬取列表页: {url}")
response = self.get_page(url)
if response:
news_items = self.parse_news_list(response.text, url)
all_news.extend(news_items)
# 添加延时,避免请求过于频繁
time.sleep(1)
# 爬取详情页面
detailed_news = []
for news_item in all_news:
if news_item['link']:
self.logger.info(f"正在爬取详情页: {news_item['link']}")
response = self.get_page(news_item['link'])
if response:
detail = self.parse_news_detail(response.text)
# 合并列表页和详情页信息
full_news = {**news_item, **detail}
detailed_news.append(full_news)
# 添加延时
time.sleep(1)
# 保存数据
self.save_to_csv(detailed_news, output_file)
self.save_to_json(detailed_news, output_file.replace('.csv', '.json'))
self.logger.info(f"爬取完成,共获取 {len(detailed_news)} 条新闻")
return detailed_news
def save_to_csv(self, data, filename):
"""保存数据到CSV文件"""
if not data:
return
fieldnames = data[0].keys()
with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(data)
self.logger.info(f"数据已保存到 CSV 文件: {filename}")
def save_to_json(self, data, filename):
"""保存数据到JSON文件"""
with open(filename, 'w', encoding='utf-8') as jsonfile:
json.dump(data, jsonfile, ensure_ascii=False, indent=2)
self.logger.info(f"数据已保存到 JSON 文件: {filename}")
# 使用示例
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='基础新闻爬虫')
parser.add_argument('--urls', nargs='+', required=True,
help='新闻列表页面URL')
parser.add_argument('--output', default='news_data.csv',
help='输出文件名')
args = parser.parse_args()
# 创建爬虫实例
spider = BasicSpider("https://example-news-site.com")
# 爬取数据
news_data = spider.crawl_news(args.urls, args.output)
# 显示前几条数据
print(f"爬取到 {len(news_data)} 条新闻")
for i, news in enumerate(news_data[:3]):
print(f"\n新闻 {i+1}:")
print(f"标题: {news.get('title', 'N/A')}")
print(f"链接: {news.get('link', 'N/A')}")
print(f"摘要: {news.get('summary', 'N/A')[:100]}...")
if __name__ == '__main__':
main()
实战2:处理动态网页的爬虫
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
动态网页爬虫示例
使用Selenium处理JavaScript渲染的内容
"""
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import time
import csv
import logging
from datetime import datetime
class DynamicSpider:
"""动态网页爬虫类"""
def __init__(self, headless=True):
self.setup_logging()
self.driver = self.setup_driver(headless)
def setup_logging(self):
"""设置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('dynamic_spider.log'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def setup_driver(self, headless=True):
"""设置WebDriver"""
chrome_options = Options()
if headless:
chrome_options.add_argument('--headless') # 无头模式
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--window-size=1920,1080')
chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
try:
driver = webdriver.Chrome(options=chrome_options)
self.logger.info("Chrome WebDriver 初始化成功")
return driver
except Exception as e:
self.logger.error(f"Chrome WebDriver 初始化失败: {e}")
raise
def wait_for_element(self, locator, timeout=10):
"""等待元素出现"""
try:
element = WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located(locator)
)
return element
except Exception as e:
self.logger.error(f"等待元素超时: {locator}")
return None
def scroll_to_bottom(self):
"""滚动到页面底部"""
last_height = self.driver.execute_script("return document.body.scrollHeight")
while True:
# 滚动到页面底部
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 等待新内容加载
time.sleep(2)
# 计算新的滚动高度
new_height = self.driver.execute_script("return document.body.scrollHeight")
# 如果高度没有变化,说明已经到底部
if new_height == last_height:
break
last_height = new_height
def parse_product_list(self):
"""解析商品列表"""
soup = BeautifulSoup(self.driver.page_source, 'html.parser')
products = []
# 查找商品元素(需要根据实际网站结构调整)
product_elements = soup.find_all('div', class_='product-item')
for product_elem in product_elements:
try:
# 提取商品信息
name_elem = product_elem.find('h3', class_='product-name')
name = name_elem.get_text(strip=True) if name_elem else "未知商品"
price_elem = product_elem.find('span', class_='price')
price = price_elem.get_text(strip=True) if price_elem else "价格未知"
# 移除价格中的非数字字符(如¥符号)
import re
price_number = re.search(r'[\d.]+', price)
price_value = float(price_number.group()) if price_number else 0.0
rating_elem = product_elem.find('div', class_='rating')
rating = rating_elem.get_text(strip=True) if rating_elem else "暂无评分"
product = {
'name': name,
'price': price_value,
'rating': rating,
'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
products.append(product)
except Exception as e:
self.logger.error(f"解析商品信息失败: {e}")
continue
return products
def crawl_ecommerce_products(self, url, max_pages=5):
"""爬取电商平台商品数据"""
self.logger.info(f"开始爬取电商网站: {url}")
try:
# 访问目标页面
self.driver.get(url)
# 等待页面加载完成
self.wait_for_element((By.CLASS_NAME, "product-list"))
all_products = []
# 爬取多页数据
for page in range(1, max_pages + 1):
self.logger.info(f"正在爬取第 {page} 页")
# 解析当前页面的商品数据
products = self.parse_product_list()
all_products.extend(products)
self.logger.info(f"第 {page} 页获取到 {len(products)} 个商品")
# 尝试点击下一页按钮
try:
next_button = self.driver.find_element(By.CLASS_NAME, "next-page")
if next_button.is_enabled():
next_button.click()
time.sleep(2) # 等待页面加载
else:
self.logger.info("已经是最后一页")
break
except Exception as e:
self.logger.info("没有找到下一页按钮,可能是最后一页")
break
return all_products
except Exception as e:
self.logger.error(f"爬取过程中发生错误: {e}")
return []
def crawl_infinite_scroll(self, url, scroll_times=3):
"""爬取无限滚动页面"""
self.logger.info(f"开始爬取无限滚动页面: {url}")
try:
# 访问目标页面
self.driver.get(url)
# 等待初始内容加载
self.wait_for_element((By.CLASS_NAME, "content"))
# 模拟滚动加载更多内容
for i in range(scroll_times):
self.logger.info(f"第 {i+1} 次滚动加载")
self.scroll_to_bottom()
time.sleep(2) # 等待新内容加载
# 解析所有加载的内容
products = self.parse_product_list()
return products
except Exception as e:
self.logger.error(f"爬取无限滚动页面时发生错误: {e}")
return []
def save_to_csv(self, data, filename):
"""保存数据到CSV文件"""
if not data:
return
fieldnames = data[0].keys()
with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(data)
self.logger.info(f"数据已保存到 CSV 文件: {filename}")
def close(self):
"""关闭浏览器"""
if self.driver:
self.driver.quit()
self.logger.info("浏览器已关闭")
# 使用示例
def main():
"""主函数"""
spider = None
try:
# 创建爬虫实例(非无头模式,可以看到浏览器操作)
spider = DynamicSpider(headless=False)
# 爬取电商网站商品数据
ecommerce_url = "https://example-ecommerce-site.com/products"
products = spider.crawl_ecommerce_products(ecommerce_url, max_pages=3)
# 保存数据
if products:
spider.save_to_csv(products, 'products.csv')
print(f"成功爬取 {len(products)} 个商品")
# 显示前几个商品信息
for i, product in enumerate(products[:5]):
print(f"\n商品 {i+1}:")
print(f" 名称: {product['name']}")
print(f" 价格: ¥{product['price']}")
print(f" 评分: {product['rating']}")
else:
print("未获取到商品数据")
except Exception as e:
print(f"爬取过程中发生错误: {e}")
finally:
# 确保关闭浏览器
if spider:
spider.close()
if __name__ == '__main__':
main()
实战3:高级爬虫框架 - Scrapy
# scrapy_spider/spiders/news_spider.py
"""
Scrapy新闻爬虫示例
"""
import scrapy
from scrapy.http import Request
from urllib.parse import urljoin
import re
class NewsSpider(scrapy.Spider):
name = 'news'
allowed_domains = ['example-news-site.com']
start_urls = ['https://example-news-site.com/news']
custom_settings = {
'DOWNLOAD_DELAY': 1, # 下载延迟1秒
'RANDOMIZE_DOWNLOAD_DELAY': 0.5, # 随机延迟
'CONCURRENT_REQUESTS': 16, # 并发请求数
'CONCURRENT_REQUESTS_PER_DOMAIN': 8, # 每个域名的并发请求数
'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
def parse(self, response):
"""解析新闻列表页面"""
# 提取新闻链接
news_links = response.css('div.news-item a::attr(href)').getall()
for link in news_links:
absolute_url = urljoin(response.url, link)
yield Request(absolute_url, callback=self.parse_news_detail)
# 处理分页
next_page = response.css('a.next-page::attr(href)').get()
if next_page:
next_url = urljoin(response.url, next_page)
yield Request(next_url, callback=self.parse)
def parse_news_detail(self, response):
"""解析新闻详情页面"""
yield {
'title': response.css('h1.title::text').get(default='').strip(),
'content': ' '.join(response.css('div.content p::text').getall()),
'author': response.css('span.author::text').get(default='').strip(),
'publish_time': response.css('time.publish-time::text').get(default='').strip(),
'url': response.url,
'tags': response.css('div.tags a::text').getall()
}
# scrapy_spider/items.py
"""
定义爬取的数据结构
"""
import scrapy
class NewsItem(scrapy.Item):
title = scrapy.Field()
content = scrapy.Field()
author = scrapy.Field()
publish_time = scrapy.Field()
url = scrapy.Field()
tags = scrapy.Field()
# scrapy_spider/pipelines.py
"""
数据处理管道
"""
import sqlite3
import json
from itemadapter import ItemAdapter
class NewsPipeline:
def __init__(self):
self.conn = None
self.cursor = None
def open_spider(self, spider):
"""爬虫开始时调用"""
self.conn = sqlite3.connect('news.db')
self.cursor = self.conn.cursor()
# 创建表
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS news (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
author TEXT,
publish_time TEXT,
url TEXT UNIQUE,
tags TEXT
)
''')
self.conn.commit()
def close_spider(self, spider):
"""爬虫结束时调用"""
if self.conn:
self.conn.close()
def process_item(self, item, spider):
"""处理每个爬取的项目"""
adapter = ItemAdapter(item)
# 插入数据库
try:
self.cursor.execute('''
INSERT OR IGNORE INTO news
(title, content, author, publish_time, url, tags)
VALUES (?, ?, ?, ?, ?, ?)
''', (
adapter.get('title', ''),
adapter.get('content', ''),
adapter.get('author', ''),
adapter.get('publish_time', ''),
adapter.get('url', ''),
json.dumps(adapter.get('tags', []), ensure_ascii=False)
))
self.conn.commit()
except Exception as e:
spider.logger.error(f"数据库插入失败: {e}")
return item
class JsonWriterPipeline:
def open_spider(self, spider):
self.file = open('news.json', 'w', encoding='utf-8')
self.file.write('[\n')
self.first_item = True
def close_spider(self, spider):
self.file.write('\n]')
self.file.close()
def process_item(self, item, spider):
if not self.first_item:
self.file.write(',\n')
else:
self.first_item = False
line = json.dumps(ItemAdapter(item).asdict(), ensure_ascii=False, indent=2)
self.file.write(line)
return item
# scrapy_spider/settings.py
"""
Scrapy配置文件
"""
BOT_NAME = 'scrapy_spider'
SPIDER_MODULES = ['scrapy_spider.spiders']
NEWSPIDER_MODULE = 'scrapy_spider.spiders'
# 遵守robots.txt规则
ROBOTSTXT_OBEY = True
# 配置管道
ITEM_PIPELINES = {
'scrapy_spider.pipelines.NewsPipeline': 300,
'scrapy_spider.pipelines.JsonWriterPipeline': 400,
}
# 下载延迟
DOWNLOAD_DELAY = 1
RANDOMIZE_DOWNLOAD_DELAY = 0.5
# 并发设置
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8
# 用户代理
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
# 中间件
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 90,
}
# 日志级别
LOG_LEVEL = 'INFO'
# scrapy_spider/middlewares.py
"""
自定义中间件
"""
import random
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
class RotateUserAgentMiddleware(UserAgentMiddleware):
"""轮换User-Agent中间件"""
def __init__(self, user_agent=''):
self.user_agent = user_agent
self.user_agent_list = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0',
]
def process_request(self, request, spider):
ua = random.choice(self.user_agent_list)
request.headers.setdefault('User-Agent', ua)
# scrapy_spider/run_spider.py
"""
运行爬虫的脚本
"""
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
from scrapy_spider.spiders.news_spider import NewsSpider
def run_spider():
"""运行新闻爬虫"""
# 获取Scrapy设置
settings = get_project_settings()
# 创建爬虫进程
process = CrawlerProcess(settings)
# 添加爬虫
process.crawl(NewsSpider)
# 启动爬虫
process.start()
if __name__ == '__main__':
run_spider()
实战4:反爬虫对策和代理使用
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
反爬虫对策和代理使用示例
"""
import requests
import time
import random
from urllib.parse import urljoin
import logging
from fake_useragent import UserAgent
import json
class AntiAntiSpider:
"""反反爬虫爬虫类"""
def __init__(self):
self.session = requests.Session()
self.ua = UserAgent()
self.proxy_pool = self.load_proxy_pool()
self.setup_logging()
def setup_logging(self):
"""设置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('anti_spider.log'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def load_proxy_pool(self):
"""加载代理池(示例数据)"""
# 实际使用时可以从代理服务商API获取
return [
{'http': 'http://proxy1.example.com:8080'},
{'http': 'http://proxy2.example.com:8080'},
{'http': 'http://proxy3.example.com:8080'},
]
def get_random_proxy(self):
"""获取随机代理"""
if self.proxy_pool:
return random.choice(self.proxy_pool)
return None
def get_random_user_agent(self):
"""获取随机User-Agent"""
try:
return self.ua.random
except:
# 如果fake_useragent不可用,使用预定义列表
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0',
]
return random.choice(user_agents)
def make_request(self, url, retries=3, delay_range=(1, 3)):
"""发送请求,包含反爬虫对策"""
for attempt in range(retries):
try:
# 设置随机请求头
headers = {
'User-Agent': self.get_random_user_agent(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
# 随机选择代理
proxy = self.get_random_proxy()
# 随机延时
delay = random.uniform(*delay_range)
time.sleep(delay)
self.logger.info(f"尝试请求 {url} (代理: {proxy})")
response = self.session.get(
url,
headers=headers,
proxies=proxy,
timeout=10
)
# 检查响应状态
if response.status_code == 200:
return response
elif response.status_code == 429: # 请求过于频繁
self.logger.warning("请求过于频繁,增加延时")
time.sleep(random.uniform(5, 10))
continue
else:
self.logger.warning(f"HTTP {response.status_code}: {url}")
except requests.RequestException as e:
self.logger.error(f"请求失败 (尝试 {attempt + 1}/{retries}): {e}")
if attempt < retries - 1:
time.sleep(random.uniform(2, 5))
return None
def handle_captcha(self, captcha_image_url):
"""处理验证码(示例)"""
# 实际使用时可以集成OCR服务或打码平台
self.logger.info(f"检测到验证码: {captcha_image_url}")
# 这里可以调用OCR服务或人工识别
return input("请输入验证码: ")
def crawl_with_session(self, login_url, username, password):
"""带会话的爬取(处理登录)"""
try:
# 获取登录页面
login_page = self.make_request(login_url)
if not login_page:
return False
# 解析登录表单(需要根据实际网站调整)
from bs4 import BeautifulSoup
soup = BeautifulSoup(login_page.text, 'html.parser')
# 获取CSRF token等隐藏字段
csrf_token = soup.find('input', {'name': 'csrf_token'})
if csrf_token:
csrf_token = csrf_token.get('value', '')
# 准备登录数据
login_data = {
'username': username,
'password': password,
'csrf_token': csrf_token
}
# 发送登录请求
login_response = self.session.post(
login_url,
data=login_data,
headers={'User-Agent': self.get_random_user_agent()}
)
# 检查登录是否成功
if login_response.status_code == 200:
self.logger.info("登录成功")
return True
else:
self.logger.error("登录失败")
return False
except Exception as e:
self.logger.error(f"登录过程中发生错误: {e}")
return False
def crawl_rate_limited(self, urls, requests_per_minute=10):
"""限速爬取"""
delay = 60.0 / requests_per_minute
results = []
for i, url in enumerate(urls):
self.logger.info(f"爬取进度: {i+1}/{len(urls)}")
response = self.make_request(url)
if response:
results.append({
'url': url,
'status': response.status_code,
'content_length': len(response.content)
})
else:
results.append({
'url': url,
'status': 'failed',
'content_length': 0
})
# 保持请求间隔
if i < len(urls) - 1: # 不是最后一个请求
time.sleep(delay + random.uniform(0, 1))
return results
# 使用示例
def main():
"""主函数"""
spider = AntiAntiSpider()
# 示例URL列表
urls = [
'https://httpbin.org/delay/1',
'https://httpbin.org/user-agent',
'https://httpbin.org/ip',
]
# 限速爬取
results = spider.crawl_rate_limited(urls, requests_per_minute=5)
# 输出结果
for result in results:
print(f"URL: {result['url']}")
print(f" 状态: {result['status']}")
print(f" 内容长度: {result['content_length']}")
print()
if __name__ == '__main__':
main()
小结与回顾
本章我们深入学习了Web爬虫开发的核心知识和实践方法:
-
爬虫基础概念:理解了Web爬虫的工作原理、类型和应用场景。
-
HTTP协议:掌握了HTTP请求方法、状态码等基础知识。
-
requests库:学会了使用requests库发送HTTP请求和处理响应。
-
HTML解析:掌握了BeautifulSoup库解析HTML文档和提取数据的方法。
-
动态网页处理:了解了使用Selenium处理JavaScript渲染内容的技术。
-
Scrapy框架:学习了专业的爬虫框架Scrapy的使用方法。
-
反爬虫对策:掌握了处理反爬虫机制的策略和技术。
-
数据存储:学会了将爬取的数据保存到文件和数据库中。
通过本章的学习和实战练习,你应该已经掌握了Web爬虫开发的核心技能,并能够在实际项目中运用这些技术来采集网络数据。Web爬虫是一项强大的数据获取技术,但使用时需要遵守相关法律法规和网站的使用条款。
练习与挑战
基础练习
-
开发以下爬虫:
- 新闻网站爬虫:爬取新闻标题、内容、发布时间等信息
- 电商网站爬虫:爬取商品名称、价格、评价等信息
- 社交媒体爬虫:爬取微博、Twitter等平台的公开内容
- 招聘网站爬虫:爬取职位信息、薪资待遇等数据
-
改进爬虫功能:
- 添加异常处理和重试机制
- 实现数据去重功能
- 添加进度显示和日志记录
- 实现断点续爬功能
-
学习使用以下工具:
- Playwright:现代化的浏览器自动化工具
- Scrapy-Splash:处理JavaScript渲染内容
- ProxyPool:代理池管理工具
进阶挑战
-
开发分布式爬虫系统:
- 使用Redis作为任务队列
- 实现多进程/多线程爬取
- 支持动态扩容和负载均衡
- 提供监控和管理界面
-
实现智能爬虫:
- 基于机器学习的内容识别
- 自适应反爬虫策略
- 智能代理IP管理
- 自动验证码识别
-
创建爬虫平台:
- 提供Web界面配置爬虫任务
- 支持多种数据导出格式
- 实现爬虫任务调度和监控
- 集成数据分析和可视化功能
项目实战
开发一个"智能数据采集平台",集成以下功能:
- 多种爬虫引擎支持(静态、动态、API等)
- 可视化爬虫配置界面
- 智能反爬虫对策
- 分布式任务调度
- 数据清洗和预处理
- 多种数据存储后端
- 实时监控和告警
- 数据分析和报告生成
扩展阅读
-
Scrapy官方文档: docs.scrapy.org/
- Python最流行的爬虫框架官方文档
-
《用Python写网络爬虫》 by Richard Lawson:
- 详细介绍Python爬虫开发的书籍
-
Selenium官方文档: www.selenium.dev/documentati…
- 浏览器自动化工具的官方文档
-
BeautifulSoup官方文档: www.crummy.com/software/Be…
- HTML/XML解析库的官方文档
-
《Web Scraping with Python》 by Ryan Mitchell:
- 介绍Web爬虫技术的实用书籍
-
Playwright官方文档: playwright.dev/python/
- 现代化浏览器自动化工具
-
《精通Scrapy网络爬虫》:
- 深入介绍Scrapy框架的中文书籍
-
Robots.txt协议: www.robotstxt.org/
- 网站爬虫协议的标准规范
通过深入学习这些扩展资源,你将进一步巩固对Web爬虫开发的理解,并掌握更多高级用法和最佳实践。Web爬虫开发是数据科学和信息检索领域的重要技能,掌握它将为你在数据分析、市场研究等领域的工作提供强大支持。