序言:什么是网页爬虫?

网页爬虫(Web Scraping)是从网站自动提取所需数据的技术。也称为网络爬虫(Web Crawling),由于能够高效收集大量网页数据,被广泛应用于数据分析、价格监控、新闻收集等各种领域。

在这一篇中,我们将学习使用Python进行网页爬虫的基础知识。逐步了解如何使用requests库获取网页,使用BeautifulSoup解析HTML并提取所需数据。

1. 法律/道德考量

在开始网页爬虫之前,必须了解一些法律和道德考量。

1.1 检查robots.txt

robots.txt是位于网站根目录的文件,它指定了网页爬虫可以访问和不能访问的页面。

# robots.txt示例 (https://example.com/robots.txt)
User-agent: *
Disallow: /private/
Disallow: /admin/
Allow: /public/

Crawl-delay: 10
  • User-agent:指定规则适用的爬虫。*表示所有爬虫。
  • Disallow:指定禁止爬虫的路径。
  • Allow:指定允许爬虫的路径。
  • Crawl-delay:指定请求之间的等待时间(秒)。

1.2 网页爬虫道德准则

  • 遵守robots.txt:务必检查并遵守网站的robots.txt规则。
  • 最小化服务器负载:在请求之间设置适当的延迟,不要给服务器造成过大负载。
  • 检查服务条款:在网站的服务条款中检查与爬虫相关的条款。
  • 保护个人隐私:不要未经授权收集个人信息。
  • 尊重版权:尊重收集数据的版权并适当使用。

1.3 使用Python检查robots.txt

from urllib.robotparser import RobotFileParser

def check_robots_txt(url, user_agent='*'):
    """检查robots.txt并返回是否可以爬取。"""
    # 生成robots.txt URL
    from urllib.parse import urlparse
    parsed = urlparse(url)
    robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"

    # 设置RobotFileParser
    rp = RobotFileParser()
    rp.set_url(robots_url)
    rp.read()

    # 检查该URL是否可以爬取
    can_fetch = rp.can_fetch(user_agent, url)
    crawl_delay = rp.crawl_delay(user_agent)

    return {
        'can_fetch': can_fetch,
        'crawl_delay': crawl_delay
    }

# 使用示例
result = check_robots_txt('https://www.google.com/search')
print(f"可以爬取:{result['can_fetch']}")
print(f"爬取延迟:{result['crawl_delay']}秒")

2. requests库

requests是Python中发送HTTP请求最流行的库。它提供简单直观的API,可以轻松获取网页。

2.1 安装

# 使用pip安装
pip install requests

2.2 HTTP方法

了解HTTP协议中使用的主要方法以及在requests中的使用方法。

GET请求

最常见的请求方式,用于从服务器获取数据。

import requests

# 基本GET请求
response = requests.get('https://httpbin.org/get')
print(response.text)

# 带查询参数的GET请求
params = {
    'search': 'python',
    'page': 1,
    'limit': 10
}
response = requests.get('https://httpbin.org/get', params=params)
print(response.url)  # https://httpbin.org/get?search=python&page=1&limit=10

POST请求

用于向服务器发送数据。可以发送表单数据或JSON数据。

import requests

# 发送表单数据
form_data = {
    'username': 'user123',
    'password': 'pass456'
}
response = requests.post('https://httpbin.org/post', data=form_data)
print(response.json())

# 发送JSON数据
json_data = {
    'name': '张三',
    'email': 'zhang@example.com'
}
response = requests.post('https://httpbin.org/post', json=json_data)
print(response.json())

设置请求头

可以设置User-Agent等HTTP请求头来自定义请求。

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
    'Referer': 'https://www.google.com/'
}

response = requests.get('https://httpbin.org/headers', headers=headers)
print(response.json())

2.3 响应处理

requests提供多种处理响应的功能。

import requests

response = requests.get('https://httpbin.org/get')

# 检查状态码
print(f"状态码:{response.status_code}")  # 200

# 检查是否成功
if response.ok:  # 当status_code在200-299范围内时为True
    print("请求成功!")

# 响应正文(文本)
print(response.text)

# 响应正文(JSON)
data = response.json()
print(data)

# 响应正文(二进制)- 用于图片等
content = response.content

# 响应头
print(response.headers)
print(response.headers['Content-Type'])

# 编码
print(response.encoding)  # UTF-8
response.encoding = 'utf-8'  # 指定编码

HTTP状态码

状态码 含义 说明
200 OK 请求成功
301 Moved Permanently 永久重定向
302 Found 临时重定向
400 Bad Request 错误的请求
403 Forbidden 无访问权限
404 Not Found 页面未找到
500 Internal Server Error 服务器内部错误

2.4 错误处理和超时

import requests
from requests.exceptions import RequestException, Timeout, HTTPError

def safe_request(url, timeout=10):
    """执行安全的HTTP请求。"""
    try:
        response = requests.get(url, timeout=timeout)
        response.raise_for_status()  # 4xx、5xx错误时抛出异常
        return response
    except Timeout:
        print(f"超时:{url}")
    except HTTPError as e:
        print(f"HTTP错误:{e.response.status_code}")
    except RequestException as e:
        print(f"请求失败:{e}")
    return None

# 使用示例
response = safe_request('https://httpbin.org/get')
if response:
    print(response.text)

3. 使用BeautifulSoup解析HTML

BeautifulSoup是用于解析HTML和XML文档的Python库。可以轻松地从复杂的HTML结构中提取所需的数据。

3.1 安装

# 安装BeautifulSoup和lxml解析器
pip install beautifulsoup4 lxml

3.2 基本用法

from bs4 import BeautifulSoup
import requests

# 获取网页
url = 'https://example.com'
response = requests.get(url)

# 创建BeautifulSoup对象
soup = BeautifulSoup(response.text, 'lxml')  # 或 'html.parser'

# 查看HTML结构(美化输出)
print(soup.prettify())

3.3 通过标签查找元素

from bs4 import BeautifulSoup

html = """
<html>
<head><title>测试页面</title></head>
<body>
    <h1>主标题</h1>
    <p>第一段</p>
    <p>第二段</p>
    <a href="https://example.com">链接1</a>
    <a href="https://google.com">链接2</a>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# 查找第一个标签
title = soup.find('title')
print(title.text)  # 测试页面

h1 = soup.find('h1')
print(h1.text)  # 主标题

# 查找所有标签
paragraphs = soup.find_all('p')
for p in paragraphs:
    print(p.text)

links = soup.find_all('a')
for link in links:
    print(link.text, link['href'])

3.4 通过class和ID查找元素

from bs4 import BeautifulSoup

html = """
<html>
<body>
    <div id="header">头部区域</div>
    <div class="content">
        <p class="intro">介绍</p>
        <p class="main-text">正文内容</p>
    </div>
    <div class="sidebar">侧边栏</div>
    <ul class="menu">
        <li class="menu-item active">首页</li>
        <li class="menu-item">介绍</li>
        <li class="menu-item">联系我们</li>
    </ul>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# 通过ID查找
header = soup.find(id='header')
print(header.text)  # 头部区域

# 通过class查找
content = soup.find(class_='content')
print(content.text)

# 通过class名查找多个元素
menu_items = soup.find_all(class_='menu-item')
for item in menu_items:
    print(item.text)

# 复合条件查找
active_item = soup.find('li', class_='active')
print(active_item.text)  # 首页

# 使用CSS选择器(select)
main_text = soup.select_one('.content .main-text')
print(main_text.text)  # 正文内容

all_menu_items = soup.select('ul.menu li')
for item in all_menu_items:
    print(item.text)

3.5 访问属性和提取文本

from bs4 import BeautifulSoup

html = """
<html>
<body>
    <a href="https://example.com" title="示例链接" data-id="123">
        <span>链接</span> 文本
    </a>
    <img src="image.jpg" alt="图片描述">
    <div class="article">
        <h2>标题</h2>
        <p>第一段</p>
        <p>第二段</p>
    </div>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# 访问属性
link = soup.find('a')
print(link['href'])        # https://example.com
print(link['title'])       # 示例链接
print(link.get('data-id')) # 123
print(link.get('class'))   # None(没有则返回None)

# 获取所有属性
print(link.attrs)  # {'href': 'https://example.com', 'title': '示例链接', 'data-id': '123'}

# 图片标签属性
img = soup.find('img')
print(img['src'])  # image.jpg
print(img['alt'])  # 图片描述

# 提取文本
print(link.text)          # 链接 文本(包括所有子元素的文本)
print(link.string)        # None(有多个子元素时返回None)
print(link.get_text())    # 链接 文本

# 去除空白
print(link.get_text(strip=True))  # 链接 文本

# 用分隔符连接
article = soup.find(class_='article')
print(article.get_text(separator=' | ', strip=True))
# 标题 | 第一段 | 第二段

3.6 CSS选择器高级用法

from bs4 import BeautifulSoup

html = """
<html>
<body>
    <table id="data-table">
        <tr><th>姓名</th><th>年龄</th><th>职业</th></tr>
        <tr><td>张三</td><td>30</td><td>开发者</td></tr>
        <tr><td>李四</td><td>25</td><td>设计师</td></tr>
        <tr><td>王五</td><td>35</td><td>经理</td></tr>
    </table>
    <div class="products">
        <div class="product" data-price="10000">
            <span class="name">商品A</span>
        </div>
        <div class="product" data-price="20000">
            <span class="name">商品B</span>
        </div>
    </div>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# 后代选择器(空格)
cells = soup.select('#data-table td')
for cell in cells:
    print(cell.text)

# 直接子元素选择器(>)
rows = soup.select('#data-table > tr')

# 属性选择器
products = soup.select('div[data-price]')
for product in products:
    name = product.select_one('.name').text
    price = product['data-price']
    print(f"{name}:{price}元")

# 选择第n个元素
first_row = soup.select_one('#data-table tr:nth-child(2)')  # 第一个数据行
print([td.text for td in first_row.find_all('td')])

# 复合选择器
products_with_high_price = soup.select('.product[data-price="20000"]')
for product in products_with_high_price:
    print(product.select_one('.name').text)

4. 实战示例:创建简单的爬虫

现在让我们综合所学内容创建实战爬虫。

4.1 新闻标题收集器

import requests
from bs4 import BeautifulSoup
import time

class NewsHeadlineScraper:
    """收集新闻标题的爬虫"""

    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
        self.session = requests.Session()
        self.session.headers.update(self.headers)

    def fetch_page(self, url):
        """获取网页。"""
        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_headlines(self, html, selector):
        """从HTML中提取标题。"""
        soup = BeautifulSoup(html, 'lxml')
        headlines = []

        elements = soup.select(selector)
        for element in elements:
            title = element.get_text(strip=True)
            link = element.get('href', '')
            if title:
                headlines.append({
                    'title': title,
                    'link': link
                })

        return headlines

    def scrape(self, url, selector, delay=1):
        """从给定URL爬取标题。"""
        print(f"开始爬取:{url}")

        html = self.fetch_page(url)
        if not html:
            return []

        headlines = self.parse_headlines(html, selector)
        print(f"找到{len(headlines)}个标题。")

        time.sleep(delay)  # 防止服务器过载
        return headlines

# 使用示例(实际使用时请检查该网站的服务条款)
if __name__ == '__main__':
    scraper = NewsHeadlineScraper()

    # 示例(根据目标网站修改实际URL和选择器)
    headlines = scraper.scrape(
        url='https://example.com/news',
        selector='a.headline'
    )

    for idx, headline in enumerate(headlines, 1):
        print(f"{idx}. {headline['title']}")
        print(f"   链接:{headline['link']}")

4.2 表格数据提取器

import requests
from bs4 import BeautifulSoup
import csv

def extract_table_data(url, table_selector='table'):
    """提取网页中的表格数据。"""
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }

    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, 'lxml')
    table = soup.select_one(table_selector)

    if not table:
        print("找不到表格。")
        return []

    data = []
    rows = table.find_all('tr')

    for row in rows:
        # 提取表头单元格或数据单元格
        cells = row.find_all(['th', 'td'])
        row_data = [cell.get_text(strip=True) for cell in cells]
        if row_data:
            data.append(row_data)

    return data

def save_to_csv(data, filename):
    """将数据保存为CSV文件。"""
    with open(filename, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerows(data)
    print(f"数据已保存到{filename}。")

# 使用示例
if __name__ == '__main__':
    # 示例URL(实际使用时请更换为适当的URL)
    data = extract_table_data(
        url='https://example.com/data-table',
        table_selector='#main-table'
    )

    if data:
        for row in data[:5]:  # 只输出前5行
            print(row)

        save_to_csv(data, 'extracted_data.csv')

4.3 图片下载器

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import os
import time

class ImageDownloader:
    """从网页下载图片的类"""

    def __init__(self, download_dir='images'):
        self.download_dir = download_dir
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }

        # 创建下载目录
        if not os.path.exists(download_dir):
            os.makedirs(download_dir)

    def get_image_urls(self, page_url, img_selector='img'):
        """从页面提取图片URL。"""
        response = requests.get(page_url, headers=self.headers, timeout=10)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, 'lxml')
        images = soup.select(img_selector)

        image_urls = []
        for img in images:
            src = img.get('src') or img.get('data-src')
            if src:
                # 将相对URL转换为绝对URL
                full_url = urljoin(page_url, src)
                image_urls.append(full_url)

        return image_urls

    def download_image(self, url, filename=None):
        """下载图片。"""
        try:
            response = requests.get(url, headers=self.headers, timeout=30)
            response.raise_for_status()

            # 如果没有文件名,从URL中提取
            if not filename:
                parsed = urlparse(url)
                filename = os.path.basename(parsed.path) or 'image.jpg'

            filepath = os.path.join(self.download_dir, filename)

            with open(filepath, 'wb') as f:
                f.write(response.content)

            print(f"下载完成:{filename}")
            return filepath

        except requests.RequestException as e:
            print(f"下载失败({url}):{e}")
            return None

    def download_all(self, page_url, img_selector='img', delay=1):
        """下载页面中的所有图片。"""
        image_urls = self.get_image_urls(page_url, img_selector)
        print(f"找到{len(image_urls)}张图片。")

        downloaded = []
        for idx, url in enumerate(image_urls, 1):
            print(f"[{idx}/{len(image_urls)}] 正在下载...")
            filepath = self.download_image(url)
            if filepath:
                downloaded.append(filepath)
            time.sleep(delay)  # 防止服务器过载

        return downloaded

# 使用示例
if __name__ == '__main__':
    downloader = ImageDownloader(download_dir='downloaded_images')

    # 示例URL(实际使用时请更换为适当的URL)
    downloaded_files = downloader.download_all(
        page_url='https://example.com/gallery',
        img_selector='div.gallery img',
        delay=2
    )

    print(f"\n共下载了{len(downloaded_files)}张图片。")

5. 技巧和注意事项

5.1 高效爬虫的技巧

  • 使用Session:向同一网站发送多次请求时,使用requests.Session()复用连接。
  • 适当的延迟:使用time.sleep()在请求之间设置延迟,不要给服务器造成过大负载。
  • 错误处理:为网络错误、超时等添加适当的异常处理。
  • 利用缓存:缓存结果以避免重复请求同一页面。
  • 日志记录:记录爬虫过程,便于问题发生时进行调试。

5.2 常见问题和解决方案

  • 编码问题:明确设置response.encoding或使用chardet库。
  • 403 Forbidden:设置User-Agent请求头或添加其他请求头。
  • 动态内容:通过JavaScript加载的内容无法用requests获取。下一篇将学习使用Selenium的解决方法。
  • IP封禁:降低请求频率或使用代理。

总结

在这一篇中,我们学习了Python网页爬虫的基础知识。掌握了如何使用requests库获取网页,使用BeautifulSoup解析HTML并提取所需数据。

在下一篇第4篇中,我们将处理更复杂的网页爬虫场景。将学习使用Selenium处理JavaScript动态生成的内容、爬取需要登录的网站,以及保存收集数据的各种方法。

系列指南:Python自动化大师系列还在继续。在下一篇中学习更深入的网页爬虫技术!