第109集_简单的网络爬虫
学习目标
- 了解网络爬虫的基本概念和工作原理
- 掌握网络爬虫的基本架构和组成部分
- 学习使用Python构建简单的网络爬虫
- 了解不同的爬取策略(深度优先、广度优先)
- 掌握如何处理网页分页和动态内容
- 学习数据存储的方法(文件、数据库)
- 了解网络爬虫的伦理问题和最佳实践
- 能够独立开发一个简单的网络爬虫应用
一、什么是网络爬虫
1.1 网络爬虫的定义
网络爬虫(Web Crawler),也称为网络蜘蛛(Spider)或网络机器人(Robot),是一种自动化程序,用于按照一定的规则自动浏览万维网并收集信息。爬虫程序通常从一个或多个初始URL开始,然后沿着网页中的链接继续爬取其他网页,不断重复这个过程,从而建立起一个庞大的网页数据库。
1.2 网络爬虫的应用场景
- 搜索引擎:如Google、百度等,使用爬虫收集网页内容并建立索引
- 数据挖掘:从网站收集数据进行分析和挖掘
- 内容聚合:将多个网站的内容聚合到一个平台,如RSS阅读器
- 价格监控:监控电商网站的商品价格变化
- 新闻采集:自动采集新闻网站的新闻内容
- 舆情监控:监控社交媒体和论坛的舆论动向
- 网站测试:测试网站的性能和可用性
1.3 网络爬虫的分类
- 通用爬虫:如搜索引擎爬虫,目标是爬取整个互联网的内容
- 聚焦爬虫:只爬取特定主题或特定网站的内容
- 增量爬虫:只爬取网站上新增或更新的内容
- 深层爬虫:能够爬取网站深层结构中的内容,如需要登录才能访问的页面
- 无头爬虫:使用无头浏览器(如Chrome Headless)模拟浏览器行为,能够处理动态内容
二、网络爬虫的基本架构
一个典型的网络爬虫通常由以下几个部分组成:
2.1 URL管理器
URL管理器负责管理待爬取的URL队列和已爬取的URL集合,避免重复爬取同一页面。
- 待爬取URL队列:存储即将要爬取的URL
- 已爬取URL集合:存储已经爬取过的URL,用于去重
2.2 网页下载器
网页下载器负责从互联网上下载网页内容。常用的Python库包括:
- requests:简单易用的HTTP库
- urllib:Python标准库中的HTTP客户端
- aiohttp:异步HTTP客户端,用于高并发爬取
- selenium:自动化测试工具,可以模拟浏览器行为
2.3 网页解析器
网页解析器负责从下载的网页中提取有用的信息。常用的Python库包括:
- BeautifulSoup:简单易用的HTML/XML解析库
- lxml:高性能的HTML/XML解析库,支持XPath和CSS选择器
- pyquery:类似jQuery的解析库,使用CSS选择器
- html.parser:Python标准库中的HTML解析器
2.4 数据存储器
数据存储器负责将提取的数据保存到本地文件或数据库中。常用的存储方式包括:
- 文件存储:TXT、CSV、JSON、XML等
- 数据库存储:MySQL、SQLite、MongoDB等
- 缓存存储:Redis、Memcached等
三、Python构建简单网络爬虫的步骤
3.1 步骤一:环境准备
首先需要安装必要的库:
pip install requests beautifulsoup4 lxml3.2 步骤二:获取网页内容
使用requests库获取网页HTML内容:
import requests
url = "https://www.example.com"
try:
response = requests.get(url)
response.raise_for_status() # 检查请求是否成功
response.encoding = response.apparent_encoding # 设置正确的编码
html_content = response.text
print("网页获取成功")
except Exception as e:
print(f"网页获取失败: {e}")3.3 步骤三:解析网页内容
使用BeautifulSoup解析网页并提取信息:
from bs4 import BeautifulSoup
# 创建BeautifulSoup对象
soup = BeautifulSoup(html_content, "lxml")
# 提取网页标题
title = soup.title.string
print(f"网页标题: {title}")
# 提取所有链接
links = soup.find_all("a")
print(f"共找到 {len(links)} 个链接")
for link in links[:5]: # 只显示前5个链接
href = link.get("href")
text = link.string.strip() if link.string else "无文本"
print(f"{text} -> {href}")3.4 步骤四:存储提取的数据
将提取的数据保存到文件:
import json
# 提取链接数据
link_data = []
for link in links:
href = link.get("href")
text = link.string.strip() if link.string else "无文本"
link_data.append({"text": text, "href": href})
# 保存到JSON文件
with open("links.json", "w", encoding="utf-8") as f:
json.dump(link_data, f, ensure_ascii=False, indent=2)
print("数据已保存到links.json文件")四、爬取策略
4.1 深度优先爬取(DFS)
深度优先爬取策略是指爬虫从起始URL开始,沿着一个链接路径一直爬取到最深层,然后再回溯到上一个节点,继续爬取其他路径。
特点:
- 实现简单,使用递归或栈即可
- 可能会陷入某些深层路径无法自拔
- 适合爬取垂直领域的网站
实现示例:
def dfs_crawler(start_url, max_depth=2):
"""深度优先爬取"""
visited = set()
def dfs(url, depth):
if depth > max_depth or url in visited:
return
visited.add(url)
print(f"深度 {depth}: {url}")
try:
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
# 提取链接
for link in soup.find_all("a"):
href = link.get("href")
if href and href.startswith("http"):
dfs(href, depth + 1)
except Exception as e:
print(f"爬取失败: {e}")
dfs(start_url, 0)
# 使用示例
dfs_crawler("https://www.example.com", max_depth=2)4.2 广度优先爬取(BFS)
广度优先爬取策略是指爬虫从起始URL开始,先爬取所有直接链接的网页,然后再爬取这些网页中的链接,逐层进行。
特点:
- 需要使用队列来实现
- 可以更均匀地覆盖网站内容
- 适合爬取大型网站
- 内存占用可能较大
实现示例:
from collections import deque
def bfs_crawler(start_url, max_depth=2):
"""广度优先爬取"""
visited = set()
queue = deque()
queue.append((start_url, 0))
visited.add(start_url)
while queue:
url, depth = queue.popleft()
if depth > max_depth:
continue
print(f"深度 {depth}: {url}")
try:
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
# 提取链接
for link in soup.find_all("a"):
href = link.get("href")
if href and href.startswith("http") and href not in visited:
visited.add(href)
queue.append((href, depth + 1))
except Exception as e:
print(f"爬取失败: {e}")
# 使用示例
bfs_crawler("https://www.example.com", max_depth=2)五、处理网页分页
很多网站会将大量内容分页展示,如新闻列表、商品列表等。处理分页是网络爬虫中的常见需求。
5.1 基于URL参数的分页
有些网站使用URL参数来控制分页,如?page=1、?p=1等。
实现示例:
def crawl_pagination_example():
"""爬取带分页的网站示例"""
base_url = "https://www.example.com/news?page="
max_pages = 5 # 最大爬取页数
for page in range(1, max_pages + 1):
url = f"{base_url}{page}"
print(f"爬取第 {page} 页: {url}")
try:
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
# 提取新闻标题
news_titles = soup.select("h3.news-title")
for title in news_titles:
print(f" - {title.text.strip()}")
except Exception as e:
print(f"爬取失败: {e}")
# 使用示例
crawl_pagination_example()5.2 基于下一页链接的分页
有些网站不使用URL参数,而是通过"下一页"链接来导航。
实现示例:
def crawl_next_page_example(start_url, max_pages=5):
"""通过下一页链接爬取"""
url = start_url
page_count = 0
while url and page_count < max_pages:
page_count += 1
print(f"爬取第 {page_count} 页: {url}")
try:
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
# 提取内容
# ...
# 查找下一页链接
next_page = soup.find("a", class_="next-page")
if next_page:
url = next_page.get("href")
# 如果是相对链接,转换为绝对链接
if not url.startswith("http"):
from urllib.parse import urljoin
url = urljoin(start_url, url)
else:
url = None # 没有下一页了
except Exception as e:
print(f"爬取失败: {e}")
url = None
# 使用示例
crawl_next_page_example("https://www.example.com/news", max_pages=5)六、处理动态内容
现代网站越来越多地使用JavaScript动态加载内容,这种情况下直接爬取HTML源码可能无法获取到完整的内容。
6.1 分析网络请求
可以通过浏览器的开发者工具(F12)分析网站的网络请求,找到动态加载数据的API接口。
步骤:
- 打开浏览器开发者工具
- 切换到"网络"(Network)选项卡
- 刷新页面或触发动态加载
- 查找XHR或Fetch类型的请求
- 分析请求URL、参数和响应格式
实现示例:
def crawl_api_example():
"""爬取API接口示例"""
api_url = "https://www.example.com/api/news"
page = 1
while True:
params = {
"page": page,
"limit": 10,
"category": "tech"
}
print(f"爬取API第 {page} 页")
try:
response = requests.get(api_url, params=params)
response.raise_for_status()
data = response.json()
# 处理数据
news_items = data.get("items", [])
if not news_items:
break # 没有更多数据了
for item in news_items:
print(f" - {item['title']}")
page += 1
except Exception as e:
print(f"爬取失败: {e}")
break
# 使用示例
crawl_api_example()6.2 使用Selenium
对于无法通过API获取数据的网站,可以使用Selenium模拟浏览器行为,获取渲染后的页面内容。
安装:
pip install selenium
# 还需要下载对应的浏览器驱动,如ChromeDriver实现示例:
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
def crawl_dynamic_content():
"""使用Selenium爬取动态内容"""
# 创建浏览器实例
driver = webdriver.Chrome() # 需要下载ChromeDriver并配置到PATH
try:
# 打开网页
driver.get("https://www.example.com/dynamic-content")
# 等待页面加载完成
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "dynamic-item")))
# 模拟滚动加载更多内容
for _ in range(3):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2) # 等待内容加载
# 获取页面内容
html_content = driver.page_source
soup = BeautifulSoup(html_content, "lxml")
# 提取内容
items = soup.find_all("div", class_="dynamic-item")
for item in items:
print(f" - {item.text.strip()}")
finally:
# 关闭浏览器
driver.quit()
# 使用示例
crawl_dynamic_content()七、数据存储
7.1 文件存储
7.1.1 TXT文件
def save_to_txt(data, filename="data.txt"):
"""保存数据到TXT文件"""
with open(filename, "w", encoding="utf-8") as f:
for item in data:
f.write(f"{item['title']}\n{item['content'][:100]}...\n\n")
# 使用示例
data = [{"title": "新闻1", "content": "内容1..."}, {"title": "新闻2", "content": "内容2..."}]
save_to_txt(data)7.1.2 CSV文件
import csv
def save_to_csv(data, filename="data.csv"):
"""保存数据到CSV文件"""
if not data:
return
# 获取字段名
fieldnames = data[0].keys()
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader() # 写入表头
writer.writerows(data) # 写入数据
# 使用示例
data = [{"title": "新闻1", "author": "作者1", "date": "2023-10-01"},
{"title": "新闻2", "author": "作者2", "date": "2023-10-02"}]
save_to_csv(data)7.1.3 JSON文件
import json
def save_to_json(data, filename="data.json"):
"""保存数据到JSON文件"""
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 使用示例
data = [{"title": "新闻1", "content": "内容1..."}, {"title": "新闻2", "content": "内容2..."}]
save_to_json(data)7.2 数据库存储
7.2.1 SQLite
import sqlite3
def save_to_sqlite(data, db_name="crawler.db", table_name="news"):
"""保存数据到SQLite数据库"""
# 连接数据库
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
# 创建表
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {table_name} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
author TEXT,
date TEXT
)
""")
# 插入数据
for item in data:
cursor.execute(f"""
INSERT INTO {table_name} (title, content, author, date)
VALUES (?, ?, ?, ?)
""", (item.get("title"), item.get("content"), item.get("author"), item.get("date")))
# 提交事务
conn.commit()
# 关闭连接
cursor.close()
conn.close()
# 使用示例
data = [
{"title": "新闻1", "content": "内容1...", "author": "作者1", "date": "2023-10-01"},
{"title": "新闻2", "content": "内容2...", "author": "作者2", "date": "2023-10-02"}
]
save_to_sqlite(data)7.2.2 MongoDB
from pymongo import MongoClient
def save_to_mongodb(data, db_name="crawler", collection_name="news"):
"""保存数据到MongoDB数据库"""
# 连接MongoDB
client = MongoClient("mongodb://localhost:27017/")
# 获取数据库和集合
db = client[db_name]
collection = db[collection_name]
# 插入数据
if data:
if isinstance(data, list):
collection.insert_many(data)
else:
collection.insert_one(data)
# 关闭连接
client.close()
# 使用示例
data = [
{"title": "新闻1", "content": "内容1...", "author": "作者1", "date": "2023-10-01"},
{"title": "新闻2", "content": "内容2...", "author": "作者2", "date": "2023-10-02"}
]
save_to_mongodb(data)八、网络爬虫的伦理问题和最佳实践
8.1 遵守Robots协议
Robots协议(也称为爬虫协议)是网站通过robots.txt文件告诉搜索引擎哪些页面可以爬取,哪些页面不可以爬取。
查看方法:在网站域名后加上/robots.txt,如:https://www.example.com/robots.txt
示例robots.txt:
User-agent: *
Disallow: /admin/
Disallow: /private/
Allow: /public/
User-agent: Googlebot
Disallow: /8.2 控制爬取速度
- 设置延迟:在请求之间设置适当的延迟,避免对服务器造成过大压力
- 使用并发限制:控制同时发送的请求数量
- 遵守网站的速率限制:有些网站会在响应头中设置速率限制(如X-RateLimit-Limit)
示例:
import time
def crawl_with_delay(urls, delay=1):
"""带延迟的爬取"""
for url in urls:
try:
response = requests.get(url)
# 处理响应
print(f"爬取成功: {url}")
time.sleep(delay) # 设置延迟
except Exception as e:
print(f"爬取失败: {e}")8.3 伪装请求头
- User-Agent:设置合理的User-Agent,模拟浏览器行为
- Referer:设置适当的Referer头
- 其他头信息:根据需要设置Cookie、Accept等头信息
示例:
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",
"Referer": "https://www.example.com",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
}
response = requests.get(url, headers=headers)8.4 避免过度爬取
- 设置爬取范围:明确爬取目标,不要爬取与目标无关的内容
- 使用增量爬取:只爬取新增或更新的内容
- 定期检查网站政策:遵守网站的使用条款和隐私政策
8.5 数据使用伦理
- 尊重版权:不要随意复制和传播爬取的内容
- 保护隐私:不要爬取和传播个人隐私信息
- 合理使用数据:爬取的数据只能用于合法和道德的目的
九、完整的简单网络爬虫示例
下面是一个完整的简单网络爬虫示例,用于爬取示例网站的新闻内容:
import requests
from bs4 import BeautifulSoup
import csv
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class SimpleNewsCrawler:
def __init__(self, start_url, max_pages=5):
self.start_url = start_url
self.max_pages = max_pages
self.news_data = []
self.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"
}
def get_page_content(self, url):
"""获取页面内容"""
try:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
response.encoding = response.apparent_encoding
return response.text
except Exception as e:
logger.error(f"获取页面内容失败: {e}")
return None
def parse_page(self, html_content):
"""解析页面内容"""
if not html_content:
return
soup = BeautifulSoup(html_content, "lxml")
# 提取新闻列表
news_list = soup.find_all("div", class_="news-item")
for item in news_list:
# 提取标题
title = item.find("h3", class_="news-title").text.strip() if item.find("h3", class_="news-title") else ""
# 提取内容
content = item.find("p", class_="news-content").text.strip() if item.find("p", class_="news-content") else ""
# 提取作者
author = item.find("span", class_="news-author").text.strip() if item.find("span", class_="news-author") else ""
# 提取日期
date = item.find("span", class_="news-date").text.strip() if item.find("span", class_="news-date") else ""
# 添加到数据列表
self.news_data.append({
"title": title,
"content": content,
"author": author,
"date": date
})
def get_next_page(self, html_content):
"""获取下一页链接"""
if not html_content:
return None
soup = BeautifulSoup(html_content, "lxml")
next_page = soup.find("a", class_="next-page")
if next_page:
href = next_page.get("href")
# 如果是相对链接,转换为绝对链接
if not href.startswith("http"):
from urllib.parse import urljoin
href = urljoin(self.start_url, href)
return href
return None
def save_to_csv(self, filename="news.csv"):
"""保存数据到CSV文件"""
if not self.news_data:
logger.info("没有数据可保存")
return
fieldnames = ["title", "content", "author", "date"]
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(self.news_data)
logger.info(f"数据已保存到 {filename},共 {len(self.news_data)} 条记录")
def crawl(self):
"""执行爬取"""
logger.info("开始爬取新闻...")
url = self.start_url
page_count = 0
while url and page_count < self.max_pages:
page_count += 1
logger.info(f"爬取第 {page_count} 页: {url}")
# 获取页面内容
html_content = self.get_page_content(url)
# 解析页面
self.parse_page(html_content)
# 获取下一页
url = self.get_next_page(html_content)
# 设置延迟
if url:
time.sleep(1)
logger.info("爬取完成")
# 保存数据
self.save_to_csv()
# 使用示例
if __name__ == "__main__":
crawler = SimpleNewsCrawler("https://www.example.com/news", max_pages=3)
crawler.crawl()十、总结
本集我们学习了简单网络爬虫的基础知识,包括:
- 网络爬虫的概念和应用场景
- 网络爬虫的基本架构(URL管理器、网页下载器、网页解析器、数据存储器)
- 使用Python构建简单网络爬虫的步骤
- 不同的爬取策略(深度优先、广度优先)
- 如何处理网页分页和动态内容
- 数据存储的方法(文件、数据库)
- 网络爬虫的伦理问题和最佳实践
- 完整的简单网络爬虫示例
网络爬虫是Python在实际应用中的重要领域,掌握好网络爬虫技术,可以帮助我们从互联网获取丰富的数据资源。在开发网络爬虫时,我们需要遵守相关的法律法规和网站规则,尊重版权和隐私,做一个负责任的爬虫开发者。
十一、课后练习
- 基础练习:开发一个简单的网络爬虫,爬取某个新闻网站的首页新闻标题和链接
- 进阶练习:开发一个爬虫,爬取某个电商网站的商品信息(名称、价格、评价数量等),并保存到CSV文件
- 综合练习:
- 开发一个爬虫,爬取某个博客网站的所有文章
- 实现分页爬取功能
- 将文章内容保存到本地文件
- 实现简单的内容搜索功能
- 挑战练习:使用Selenium爬取一个使用JavaScript动态加载内容的网站
通过这些练习,你将能够熟练掌握简单网络爬虫的开发技巧,并能够应用到实际的数据获取任务中。