第109集_简单的网络爬虫

学习目标

  1. 了解网络爬虫的基本概念和工作原理
  2. 掌握网络爬虫的基本架构和组成部分
  3. 学习使用Python构建简单的网络爬虫
  4. 了解不同的爬取策略(深度优先、广度优先)
  5. 掌握如何处理网页分页和动态内容
  6. 学习数据存储的方法(文件、数据库)
  7. 了解网络爬虫的伦理问题和最佳实践
  8. 能够独立开发一个简单的网络爬虫应用

一、什么是网络爬虫

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 lxml

3.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接口。

步骤

  1. 打开浏览器开发者工具
  2. 切换到"网络"(Network)选项卡
  3. 刷新页面或触发动态加载
  4. 查找XHR或Fetch类型的请求
  5. 分析请求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()

十、总结

本集我们学习了简单网络爬虫的基础知识,包括:

  1. 网络爬虫的概念和应用场景
  2. 网络爬虫的基本架构(URL管理器、网页下载器、网页解析器、数据存储器)
  3. 使用Python构建简单网络爬虫的步骤
  4. 不同的爬取策略(深度优先、广度优先)
  5. 如何处理网页分页和动态内容
  6. 数据存储的方法(文件、数据库)
  7. 网络爬虫的伦理问题和最佳实践
  8. 完整的简单网络爬虫示例

网络爬虫是Python在实际应用中的重要领域,掌握好网络爬虫技术,可以帮助我们从互联网获取丰富的数据资源。在开发网络爬虫时,我们需要遵守相关的法律法规和网站规则,尊重版权和隐私,做一个负责任的爬虫开发者。

十一、课后练习

  1. 基础练习:开发一个简单的网络爬虫,爬取某个新闻网站的首页新闻标题和链接
  2. 进阶练习:开发一个爬虫,爬取某个电商网站的商品信息(名称、价格、评价数量等),并保存到CSV文件
  3. 综合练习
    • 开发一个爬虫,爬取某个博客网站的所有文章
    • 实现分页爬取功能
    • 将文章内容保存到本地文件
    • 实现简单的内容搜索功能
  4. 挑战练习:使用Selenium爬取一个使用JavaScript动态加载内容的网站

通过这些练习,你将能够熟练掌握简单网络爬虫的开发技巧,并能够应用到实际的数据获取任务中。

« 上一篇 网页解析基础 下一篇 » 网络编程综合练习