前言

大家好,今天想和大家分享一个非常实用的话题——网络爬虫入门。很多人听到"爬虫"两个字,第一反应可能是"写代码抓数据",觉得门槛很高。其实不然,爬虫的核心原理非常简单:模拟浏览器向服务器发送请求,然后解析服务器返回的网页内容,从中提取出我们想要的数据

这篇文章是入门级的内容,适合对爬虫感兴趣的零基础读者。我会尽量用通俗的语言,配合完整的代码示例,带大家走完一个爬虫从发送请求到存储数据的完整流程。


一、网络爬虫概述

1.1 什么是网络爬虫?

网络爬虫(Web Crawler / Web Scraper)是一种自动化程序,它按照一定的规则,自动地浏览万维网并获取信息。我们可以把它理解成一个"机器人",这个机器人不知疲倦地访问网页、下载页面内容,然后从中提取出我们需要的数据。

爬虫的规模可以很小,小到只抓取一个网页获取某条新闻标题;也可以非常庞大,大到像 Google、百度这样的搜索引擎,它们每天爬取数十亿个网页,建立索引供用户搜索。

1.2 爬虫的应用场景

爬虫技术的应用非常广泛,这里列举几个常见的场景:

  • **搜索引擎**:Google、Bing、百度等搜索引擎的核心就是爬虫
  • **价格监控**:电商平台的价格对比、降价提醒
  • **数据采集**:学术研究、市场调研、舆情分析
  • **内容聚合**:新闻聚合、RSS 订阅更新
  • **SEO 监测**:检查网站排名、外链情况
  • **自动化测试**:模拟用户操作测试网站功能

1.3 法律与道德边界

学习爬虫,首先要建立正确的法律意识。这里几条红线一定要记住:

  • **遵守 robots.txt**:网站根目录下的 `robots.txt` 文件声明了哪些路径允许爬取
  • **尊重版权**:爬取的数据如果涉及版权内容,不可随意商用
  • **控制频率**:不要对服务器造成压力,这是基本的网络礼仪
  • **遵守《数据安全法》**:在中国,爬取个人信息、商业机密等受法律保护的数据可能构成违法
  • **绕过反爬措施可能违法**:破解验证码、绕过登录认证等措施,可能触犯《刑法》中"非法获取计算机信息系统数据罪"
**一句话总结:学爬虫是技术,用爬虫是法律。技术本身中立,但使用方式决定了是否合规。**

二、HTTP协议基础

爬虫的本质就是发送 HTTP 请求并处理响应,所以理解 HTTP 协议非常关键。

2.1 什么是 HTTP?

HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最广泛的协议。浏览器和服务器之间通过 HTTP 进行通信:浏览器发送"请求"(Request),服务器返回"响应"(Response)。

一个典型的 HTTP 请求包含:


GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

2.2 请求方法(HTTP Methods)

最常用的请求方法有:

| 方法 | 含义 | 爬虫中的用途 |

|------|------|------------|

| GET | 获取资源 | 最常用,获取网页内容 |

| POST | 提交数据 | 登录、搜索、表单提交 |

| HEAD | 获取响应头 | 探测链接是否有效 |

| PUT | 更新资源 | 较少使用 |

| DELETE | 删除资源 | 极少使用 |

编写爬虫时,绝大多数场景下我们只需要使用 GET 和 `POST`。

2.3 状态码(Status Codes)

服务器返回的响应中会包含一个状态码,表示请求的处理结果:

  • **200 OK**:请求成功,正常返回页面内容
  • **301/302 Moved**:资源被重定向到其他 URL
  • **403 Forbidden**:服务器拒绝访问(常见于反爬)
  • **404 Not Found**:资源不存在
  • **500 Internal Server Error**:服务器内部错误
  • **503 Service Unavailable**:服务器暂时无法处理请求

# Python中检查状态码
import requests

response = requests.get('https://httpbin.org/status/200')
if response.status_code == 200:
    print('请求成功!')
elif response.status_code == 403:
    print('被拒绝了,需要加请求头或使用代理')
elif response.status_code == 404:
    print('页面不存在')

2.4 请求头(Headers)

请求头是爬虫中非常关键的一环。服务器通过请求头来判断请求来源,其中最重要的几个字段:

  • **`User-Agent`**:标识客户端类型,如浏览器版本、操作系统等
  • **`Referer`**:来源页面,告诉服务器是从哪个页面跳转过来的
  • **`Cookie`**:携带用户的身份信息和状态
  • **`Accept`**:告诉服务器客户端能处理的内容类型
  • **`Authorization`**:携带认证信息(如 Token)

很多网站会检查 User-Agent,如果检测到是爬虫工具(如 Python-requests)就会拒绝访问。所以我们通常需要伪装成真实浏览器:


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,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Referer': 'https://www.google.com/',
}
response = requests.get('https://example.com', headers=headers)

2.5 Cookie 与 Session

HTTP 是"无状态"协议,服务器默认不知道两次请求是否来自同一个用户。Cookie 和 Session 就是用来解决这个问题的:

  • **Cookie**:存储在客户端(浏览器)的小段文本,每次请求会自动附带
  • **Session**:存储在服务器端的用户状态数据,通过 Session ID 关联

爬虫处理登录状态的常见方式:


import requests

# 创建一个 Session 对象,可以自动保存 Cookie
session = requests.Session()

# 模拟登录,提交表单
login_data = {'username': 'user', 'password': 'pass'}
session.post('https://example.com/login', data=login_data)

# 后续请求会自动带上登录后的 Cookie
response = session.get('https://example.com/dashboard')
print(response.text)

三、Python爬虫工具链

Python 是爬虫领域最受欢迎的语言,生态非常丰富。这里对比三款最主流的 HTTP 请求库:

3.1 requests —— 简单易用的首选

requests 是 Python 爬虫的入门标配,API 设计人性化,使用非常简洁。


import requests

# GET 请求
resp = requests.get('https://api.github.com')
print(resp.json())  # 自动解析 JSON

# POST 请求
resp = requests.post('https://httpbin.org/post', data={'key': 'value'})

# 带参数的 GET 请求
params = {'q': 'python', 'page': 1}
resp = requests.get('https://httpbin.org/get', params=params)

优点:API 简洁、文档丰富、社区庞大

缺点:同步阻塞、不支持 HTTP/2

3.2 httpx —— requests 的现代替代者

httpx 兼容 `requests` 的 API,同时支持异步和 HTTP/2,是近年来的热门选择。


import httpx

# 同步方式(和 requests 类似)
resp = httpx.get('https://httpbin.org/get')
print(resp.status_code)

# 异步方式(需要 async/await)
async def fetch():
    async with httpx.AsyncClient() as client:
        resp = await client.get('https://httpbin.org/get')
        return resp.text

# 支持 HTTP/2
resp = httpx.get('https://httpbin.org/get', http2=True)

优点:支持异步和 HTTP/2、API 兼容 requests

缺点:部分场景下性能不如 aiohttp

3.3 aiohttp —— 高性能异步爬虫

aiohttp 是专为异步而生 HTTP 库,适合大规模并发爬取。


import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'https://httpbin.org/get')
        print(html)

asyncio.run(main())

优点:异步性能好、支持 WebSocket

缺点:API 相对底层、学习曲线稍陡

3.4 如何选择?

| 场景 | 推荐库 |

|------|--------|

| 快速原型、简单爬虫 | requests |

| 需要 HTTP/2 或异步 | httpx |

| 大规模并发爬取 | aiohttp |

对于初学者,我建议 先从 requests 入手,等需要大规模爬取时再学习异步方案。


四、页面解析技术

拿到 HTML 源码后,下一步就是从中提取我们需要的数据。以下是几种常用的页面解析方案:

4.1 正则表达式

正则表达式是提取文本的"瑞士军刀",性能好,但可读性较差,不适合解析复杂的 HTML 结构。


import re
import requests

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

# 提取标题
title_pattern = r'

(.*?)

' match = re.search(title_pattern, html) if match: print('标题:', match.group(1)) # 提取所有链接 link_pattern = r'href=["\'](https?://[^"\']+)["\']' links = re.findall(link_pattern, html) print('找到链接:', links[:5])

适用场景:简单的文本提取、日志分析

不适用场景:复杂的嵌套 HTML、属性提取

4.2 BeautifulSoup —— 入门首选

BeautifulSoup 是 Python 最流行的 HTML/XML 解析库,API 非常友好,适合初学者。


from bs4 import BeautifulSoup
import requests

html = requests.get('https://httpbin.org/html').text
soup = BeautifulSoup(html, 'html.parser')

# 通过标签名获取
h1 = soup.find('h1')
print('标题:', h1.text)

# 通过 class 查找
items = soup.find_all(class_='item')
for item in items:
    print(item.text)

# 通过属性查找
link = soup.find('a', href=True)
print('链接:', link['href'])

# CSS 选择器(通过 select 方法)
titles = soup.select('div.content h2')
for title in titles:
    print(title.text.strip())

解析器选择

  • `html.parser`:Python 内置,不需要额外安装
  • `lxml`:需要安装(`pip install lxml`),速度更快
  • `html5lib`:最慢但最容错

建议:直接上 lxml,速度最快。

4.3 lxml + XPath —— 精准高效

lxml 是底层用 C 语言实现的 XML/HTML 解析库,速度极快,配合 XPath 语法可以非常精准地定位元素。


from lxml import html
import requests

resp = requests.get('https://httpbin.org/html')
tree = html.fromstring(resp.content)

# XPath 定位
title = tree.xpath('//h1/text()')
print('标题:', title[0] if title else '')

# 通过 class 定位
items = tree.xpath('//div[@class="item"]')
for item in items:
    print(item.text)

# 提取属性值
links = tree.xpath('//a/@href')
print('所有链接:', links[:5])

# 多条件组合
result = tree.xpath('//div[@id="main"]//a[contains(@href, "article")]/text()')
print(result)

XPath 速查表

| 表达式 | 含义 |

|--------|------|

| //div | 选取所有 div 元素 |

| //div[@class="foo"] | 选取 class 为 foo 的 div |

| //a/@href | 提取所有 a 标签的 href 属性 |

| //div[contains(@class, "bar")] | class 包含 bar 的 div |

| //div/p[1] | div 下的第一个 p 标签 |

4.4 四种解析方案对比

| 方案 | 学习难度 | 性能 | HTML容错 | 推荐指数 |

|------|---------|------|---------|---------|

| 正则表达式 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐ |

| BeautifulSoup | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |

| lxml + XPath | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |

| CSS 选择器 | ⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |

**个人建议**:新手先学 BeautifulSoup 上手,然后深入学习 lxml + XPath 应对复杂场景。正则表达式可以作为辅助工具偶尔使用。

五、动态页面处理

现实中的很多网站都是"动态页面"——页面内容不是直接写在 HTML 里的,而是通过 JavaScript 加载渲染出来的。对于这类页面,单纯请求 HTML 源码是拿不到数据的。

5.1 判断页面是否动态

最简单的判断方法:在浏览器中右键查看"网页源码",如果内容很少,但实际页面看起来很丰富,大概率是动态渲染。

或者用 Python 请求一下看看:


import requests
from bs4 import BeautifulSoup

resp = requests.get('https://example.com')
soup = BeautifulSoup(resp.text, 'html.parser')
# 如果这里找不到内容,但浏览器打开能看到,说明是动态渲染
print(soup.prettify()[:1000])

5.2 寻找 API 接口(推荐方案)

很多 SPA(单页应用)网站的数据是通过 XHR/Fetch 请求从 API 获取的。如果能直接调用 API,就不需要渲染页面了。

如何发现 API

1. 打开浏览器开发者工具(F12)

2. 切换到 Network → Fetch/XHR 标签

3. 刷新页面,观察请求

4. 找到返回 JSON 数据的请求,直接调用


import requests

# 直接调用 API 获取数据,比渲染页面高效得多
api_url = 'https://example.com/api/articles?page=1'
headers = {
    'User-Agent': 'Mozilla/5.0 ...',
    'Accept': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
}
resp = requests.get(api_url, headers=headers)
data = resp.json()  # 直接拿到结构化的 JSON 数据
print(data)

5.3 Selenium —— 模拟真实浏览器

当无法找到 API 时,就需要使用浏览器自动化工具了。Selenium 是最经典的选择。


from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time

# 配置无头浏览器(不显示界面)
options = Options()
options.add_argument('--headless')  # 无头模式
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('User-Agent=Mozilla/5.0 ...')

driver = webdriver.Chrome(options=options)

# 访问页面
driver.get('https://example.com')

# 等待 JavaScript 渲染完成
time.sleep(3)  # 简单粗暴
# 或者使用显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'content')))

# 获取渲染后的页面源码
html = driver.page_source

# 传给 BeautifulSoup 解析
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
print(soup.find('h1').text)

driver.quit()

5.4 Playwright —— 新一代浏览器自动化

Playwright 是微软开发的新一代浏览器自动化工具,比 Selenium 更现代、更强大。


from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    
    # 拦截网络请求(可选)
    page.route('**/*.{png,jpg,jpeg,gif}', lambda route: route.abort())
    
    # 访问页面
    page.goto('https://example.com')
    
    # 等待某个元素出现
    page.wait_for_selector('.content', timeout=10000)
    
    # 获取渲染后的内容
    content = page.content()
    
    # 点击和交互
    page.click('button#load-more')
    page.wait_for_timeout(2000)
    
    # 截图(调试用)
    page.screenshot(path='debug.png')
    
    browser.close()

5.5 Puppeteer(Node.js)

如果你使用 Node.js,Puppeteer 是 Google 官方维护的 Chrome 控制库:


const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();
    await page.goto('https://example.com');
    await page.waitForSelector('.content');
    const html = await page.content();
    console.log(html);
    await browser.close();
})();

5.6 动态页面处理方案对比

| 工具 | 语言 | 速度 | 资源占用 | 推荐场景 |

|------|------|------|---------|---------|

| 直接调 API | 任意 | ⭐⭐⭐⭐⭐ | 极低 | 能调 API 就优先选 |

| Selenium | 多语言 | ⭐⭐ | 高 | 传统项目、兼容性要求高 |

| Playwright | 多语言 | ⭐⭐⭐ | 中 | 强烈推荐,新项目首选 |

| Puppeteer | Node.js | ⭐⭐⭐ | 中 | Node.js 生态项目 |


六、数据存储

数据爬取下来后,如何持久化存储?下面是几种常用方案:

6.1 CSV 存储

CSV 是最简单的表格格式,适合结构化数据:


import csv

data = [
    {'title': '文章一', 'url': 'https://example.com/1', 'date': '2025-01-01'},
    {'title': '文章二', 'url': 'https://example.com/2', 'date': '2025-01-02'},
]

with open('articles.csv', 'w', newline='', encoding='utf-8-sig') as f:
    writer = csv.DictWriter(f, fieldnames=['title', 'url', 'date'])
    writer.writeheader()
    writer.writerows(data)

print('已保存到 articles.csv')

6.2 JSON 存储

JSON 格式灵活,支持嵌套结构:


import json

data = {
    'source': 'example.com',
    'count': 2,
    'articles': [
        {'title': '文章一', 'url': 'https://example.com/1'},
        {'title': '文章二', 'url': 'https://example.com/2'},
    ]
}

with open('articles.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

print('已保存到 articles.json')

6.3 SQLite 存储

SQLite 是轻量级嵌入式数据库,无需安装服务端,适合单机爬虫:


import sqlite3

conn = sqlite3.connect('crawler.db')
cursor = conn.cursor()

# 创建表
cursor.execute('''
    CREATE TABLE IF NOT EXISTS articles (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        url TEXT UNIQUE NOT NULL,
        content TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
''')

# 插入数据
cursor.execute(
    'INSERT OR IGNORE INTO articles (title, url, content) VALUES (?, ?, ?)',
    ('文章标题', 'https://example.com/1', '文章内容...')
)
conn.commit()

# 查询数据
cursor.execute('SELECT * FROM articles LIMIT 10')
for row in cursor.fetchall():
    print(row)

conn.close()

6.4 MySQL / PostgreSQL

大规模生产环境需要更强大的数据库,这里以 MySQL 为例:


import pymysql

conn = pymysql.connect(
    host='localhost',
    user='root',
    password='password',
    database='crawler',
    charset='utf8mb4',
)

cursor = conn.cursor()
cursor.execute('''
    CREATE TABLE IF NOT EXISTS articles (
        id INT AUTO_INCREMENT PRIMARY KEY,
        title VARCHAR(500) NOT NULL,
        url VARCHAR(2000) UNIQUE NOT NULL,
        content LONGTEXT,
        crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''')

# 批量插入
sql = 'INSERT IGNORE INTO articles (title, url) VALUES (%s, %s)'
data = [('文章一', 'https://example.com/1'), ('文章二', 'https://example.com/2')]
cursor.executemany(sql, data)
conn.commit()

conn.close()

6.5 存储方案选择

| 方案 | 适用场景 | 优点 | 缺点 |

|------|---------|------|------|

| CSV | 简单报表、Excel 兼容 | 通用性强、可读性好 | 不支持复杂结构 |

| JSON | API 数据、嵌套结构 | 灵活、易于调试 | 不适合大数据量 |

| SQLite | 单机爬虫、原型 | 零配置、轻量 | 不支持并发写入 |

| MySQL/PG | 生产环境、大量数据 | 性能好、支持并发 | 需要维护数据库服务 |


七、完整的爬虫示例

下面我们编写一个完整的爬虫示例:从 Hacker News 首页抓取热门文章的标题、链接和分数,并存储到 SQLite 数据库中。


"""
Hacker News 热门文章爬虫
功能:抓取首页文章列表,提取标题、链接、分数,存入 SQLite
"""

import requests
from bs4 import BeautifulSoup
import sqlite3
import time
import re


def setup_database():
    """创建数据库和表"""
    conn = sqlite3.connect('hackernews.db')
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS stories (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            url TEXT,
            score INTEGER DEFAULT 0,
            author TEXT,
            comments INTEGER DEFAULT 0,
            crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    conn.commit()
    return conn


def fetch_page(url):
    """发送 HTTP 请求,获取页面内容"""
    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,*/*;q=0.8',
        'Accept-Language': 'en-US,en;q=0.5',
    }

    print(f'正在请求: {url}')
    try:
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()  # 如果状态码不是 200,抛出异常
        return response.text
    except requests.RequestException as e:
        print(f'请求失败: {e}')
        return None


def parse_stories(html):
    """解析 HTML,提取文章信息"""
    soup = BeautifulSoup(html, 'html.parser')
    stories = []

    # Hacker News 每一行是一个 
    for row in soup.select('tr.athing'):
        title_cell = row.find('td', class_='title')
        if not title_cell:
            continue

        # 标题和链接
        link_tag = title_cell.find('a')
        if not link_tag:
            continue

        title = link_tag.get_text(strip=True)
        url = link_tag.get('href', '')

        # 如果是相对路径,拼接完整 URL
        if url.startswith('item?'):
            url = f'https://news.ycombinator.com/{url}'

        # 获取分数和评论数(在下一行  中)
        subtext_row = row.find_next_sibling('tr')
        score = 0
        author = ''
        comments = 0

        if subtext_row:
            subtext = subtext_row.find('td', class_='subtext')
            if subtext:
                score_span = subtext.find('span', class_='score')
                if score_span:
                    score_text = score_span.get_text()
                    score = int(re.search(r'\d+', score_text).group())

                author_tag = subtext.find('a', class_='hnuser')
                if author_tag:
                    author = author_tag.get_text(strip=True)

                # 评论数
                links = subtext.find_all('a')
                for link in links:
                    text = link.get_text()
                    if 'comment' in text:
                        num = re.search(r'\d+', text)
                        if num:
                            comments = int(num.group())

        stories.append({
            'title': title,
            'url': url,
            'score': score,
            'author': author,
            'comments': comments,
        })

    return stories


def save_to_database(conn, stories):
    """将文章数据存入数据库"""
    cursor = conn.cursor()
    inserted = 0

    for story in stories:
        try:
            cursor.execute('''
                INSERT OR IGNORE INTO stories (title, url, score, author, comments)
                VALUES (?, ?, ?, ?, ?)
            ''', (
                story['title'],
                story['url'],
                story['score'],
                story['author'],
                story['comments'],
            ))
            if cursor.rowcount > 0:
                inserted += 1
        except sqlite3.Error as e:
            print(f'数据库写入错误: {e}')

    conn.commit()
    return inserted


def main():
    """主函数:执行爬虫流程"""
    print('=' * 50)
    print('Hacker News 热门文章爬虫')
    print('=' * 50)

    # 1. 初始化数据库
    conn = setup_database()

    # 2. 获取页面
    html = fetch_page('https://news.ycombinator.com/')
    if not html:
        conn.close()
        return

    # 3. 解析数据
    stories = parse_stories(html)
    print(f'解析到 {len(stories)} 篇文章')

    # 4. 存储数据
    count = save_to_database(conn, stories)
    print(f'新增了 {count} 条记录')

    # 5. 展示结果
    print('\n抓取结果预览:')
    print('-' * 50)
    for i, story in enumerate(stories[:5], 1):
        print(f'{i}. [{story["score"]}分] {story["title"]}')
        print(f'   作者: {story["author"]} | 评论: {story["comments"]}')
        print(f'   链接: {story["url"][:70]}...')
        print()

    # 6. 关闭数据库连接
    conn.close()
    print(f'数据已保存到 hackernews.db')
    print('爬取完成!')


if __name__ == '__main__':
    main()

运行这个脚本,你会在终端看到抓取过程,同时数据会自动保存到 hackernews.db 中。

运行结果示例


==================================================
Hacker News 热门文章爬虫
==================================================
正在请求: https://news.ycombinator.com/
解析到 30 篇文章
新增了 30 条记录

抓取结果预览:
--------------------------------------------------
1. [423分] Show HN: I built a CLI tool to generate landing pages
   作者: user123 | 评论: 158
   链接: https://example.com/cli-landing-page...
2. [387分] Ask HN: What are you working on?
   作者: techfan | 评论: 342
   ...
数据已保存到 hackernews.db
爬取完成!

八、爬虫礼仪与最佳实践

最后,我们来聊聊写爬虫时应该遵守的一些基本原则和技巧。

8.1 尊重 robots.txt

robots.txt 是网站根目录下的一个文本文件,声明了哪些路径允许或禁止爬虫访问。


# 查看网站的 robots.txt
import requests

resp = requests.get('https://example.com/robots.txt')
print(resp.text)

示例输出


User-agent: *
Disallow: /admin/
Disallow: /private/
Allow: /public/

Sitemap: https://example.com/sitemap.xml

爬虫应该做的事情

  • 爬取前检查 `robots.txt`
  • 遵守 `Disallow` 规则
  • 记录并遵循 `Crawl-delay` 指令

一个简单的 robots.txt 解析示例:


import re
import requests

def check_robots_txt(base_url):
    """检查 robots.txt,返回是否允许爬取"""
    robots_url = base_url.rstrip('/') + '/robots.txt'
    try:
        resp = requests.get(robots_url, timeout=5)
        if resp.status_code == 200:
            print(f'robots.txt 内容:\n{resp.text}')
            # 这里可以用 urllib.robotparser 做更完整的解析
            from urllib.robotparser import RobotFileParser
            rp = RobotFileParser()
            rp.parse(resp.text.splitlines())
            can_fetch = rp.can_fetch('MyCrawler/1.0', base_url)
            print(f'允许爬取首页: {can_fetch}')
            return rp
    except Exception as e:
        print(f'无法读取 robots.txt: {e}')
    return None

8.2 控制请求频率

这是最基本的网络礼仪,也是避免被 IP 封禁的关键。


import time
import random

class PoliteCrawler:
    """有礼貌的爬虫——控制请求频率"""

    def __init__(self, min_delay=1.0, max_delay=3.0):
        self.min_delay = min_delay
        self.max_delay = max_delay
        self.last_request_time = 0

    def wait_before_request(self):
        """在每次请求之前等待"""
        elapsed = time.time() - self.last_request_time
        delay = random.uniform(self.min_delay, self.max_delay)
        if elapsed < delay:
            sleep_time = delay - elapsed
            print(f'等待 {sleep_time:.1f} 秒...')
            time.sleep(sleep_time)
        self.last_request_time = time.time()

    def get(self, url, **kwargs):
        self.wait_before_request()
        print(f'请求: {url}')
        return requests.get(url, **kwargs)


# 使用示例
crawler = PoliteCrawler(min_delay=1.5, max_delay=3.0)
for page in range(1, 6):
    resp = crawler.get(f'https://example.com/articles?page={page}')
    # 处理响应...

8.3 设置合理的 User-Agent

总是使用真实的浏览器 User-Agent,不要暴露自己是用 Python 请求库:


# ❌ 不好的做法
headers = {'User-Agent': 'python-requests/2.31.0'}

# ✅ 好的做法
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'
}

也可以准备一个 User-Agent 池,随机切换:


import random

USER_AGENTS = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...',
    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ...',
]

def get_random_headers():
    return {
        'User-Agent': random.choice(USER_AGENTS),
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    }

8.4 错误重试与异常处理

网络不稳定,爬虫要有容错机制:


import time
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_resilient_session(retries=3, backoff_factor=0.5):
    """创建一个带有重试机制的 Session"""
    session = requests.Session()

    retry_strategy = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist=[500, 502, 503, 504],
    )

    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount('http://', adapter)
    session.mount('https://', adapter)

    return session


# 使用
session = create_resilient_session()
try:
    resp = session.get('https://example.com', timeout=10)
    resp.raise_for_status()
except requests.RequestException as e:
    print(f'多次重试后仍然失败: {e}')

8.5 总结:爬虫礼仪清单

| 实践 | 说明 | 优先级 |

|------|------|--------|

| ✅ 检查 robots.txt | 遵守网站的爬取规则 | 必须 |

| ✅ 控制请求频率 | 设置合理的延迟 | 必须 |

| ✅ 设置 User-Agent | 伪装成真实浏览器 | 必须 |

| ✅ 错误重试 | 网络波动时自动重试 | 建议 |

| ✅ 使用 Session | 保持 Cookie 和连接复用 | 建议 |

| ✅ 数据去重 | 避免重复爬取相同内容 | 建议 |

| ✅ 日志记录 | 记录请求和错误信息 | 建议 |

| ❌ 暴力爬取 | 大量并发请求压垮服务器 | 禁止 |

| ❌ 爬取敏感数据 | 个人信息、版权内容 | 禁止 |


结语

至此,我们从 HTTP 协议基础开始,一路走到了 Python 爬虫的工具链、页面解析技术、动态页面处理和数据存储,最后还写了一个完整的爬虫示例,并讨论了爬虫礼仪。

回顾一下核心要点:

1. 爬虫的本质就是发送 HTTP 请求 + 解析页面内容

2. requests + BeautifulSoup/lxml 是入门阶段的最佳组合

3. 先找 API,不行再用浏览器自动化

4. 控制频率、遵守规则是爬虫能长期运行的基础

5. 数据存储根据场景选择 CSV/JSON/SQLite/MySQL

爬虫是一个非常实用的技能,但力量越大责任越大。希望大家在学习和使用爬虫技术时,始终记得尊重数据、尊重网站、遵守法律。

如果这篇文章对你有帮助,欢迎收藏和分享。有任何问题也欢迎在评论区交流讨论!


*本文发布于 [博客名称] · 转载请注明出处*