Python爬虫基础

本篇将从零开始,介绍爬虫最核心的基础知识。学习如何像一个真正的爬虫工程师一样思考,并熟练运用数据请求、解析和存储的“三板斧”。

如何分析目标网站

在编写任何代码之前,最重要的一步是分析目标网站。爬虫工程师的“听诊器”就是浏览器的开发者工具(通常按 F12Ctrl+Shift+I 打开)。

区分静态页面与动态页面

  • 静态页面:你看到的页面内容,在 HTML 源码(右键 -> 查看网页源码)里都能找到。这种页面最简单,数据一次性返回,适合直接爬取。

  • 动态页面 (AJAX):页面初次加载时可能只是一个框架,具体数据是通过 JavaScript 在后台异步请求并填充到页面上的。

    应对策略:在开发者工具的 Network (网络) 标签页中,筛选 Fetch/XHR 请求。这些通常就是承载着核心数据的 API 接口。找到这些接口后,我们可以直接模拟请求这些接口,获取干净的 JSON 数据,这远比处理复杂的 HTML 更高效!

开发者工具的核心用途

  • Elements (元素):查看和分析页面最终渲染出的 HTML 结构。你可以在这里测试你的 CSS 选择器或 XPath 路径是否正确。
  • Network (网络):抓取所有网络请求。这是分析动态网页、查找数据接口、查看请求头和 Cookies 的关键所在。

核心思维:爬虫的目标是获取数据,而不是模仿浏览器。能直接请求数据接口,就绝不费力去解析 HTML。


数据请求与requests库

requests 是一个优雅、简洁的 HTTP 客户端库。它被誉为 “HTTP for Humans”,极大地简化了发送 HTTP 请求的过程。

requests vs urllib

urllib 是 Python 的内置库,功能强大但 API 相对繁琐。requests 作为一个第三方库,在易用性上完胜。

特性 requests urllib
GET 请求 requests.get(url, params=payload) urllib.request.urlopen(url + '?' + urlencode(payload))
POST 请求 requests.post(url, data=payload) data = urlencode(payload).encode('utf-8'); req = Request(url, data=data)
JSON 处理 响应体自带 .json() 方法 json.loads(response.read().decode('utf-8'))
Cookies 自动处理,通过 session 对象轻松管理 需要手动管理 http.cookiejar.CookieJar
API 设计 直观、人性化、链式调用 功能分散在多个子模块,API 复杂

在新项目中,无条件推荐使用 requests。了解 urllib 主要是为了能看懂一些旧代码或标准库的源码。


requests核心用法

安装: pip install requests

GET 请求

python
import requests

# 基本 GET 请求
response = requests.get('https://api.github.com/events')
print(f"状态码: {response.status_code}")

# 带参数的 GET 请求
payload = {'key1': 'value1', 'key2': 'value2'}
response = requests.get('https://httpbin.org/get', params=payload)
print(response.url) # 查看最终请求的 URL

POST 请求

python
# 发送表单数据
payload = {'key1': 'value1', 'key2': 'value2'}
response = requests.post('https://httpbin.org/post', data=payload)

# 发送 JSON 数据
payload_json = {'some': 'data'}
response = requests.post('https://httpbin.org/post', json=payload_json)

自定义请求头 (Headers) 模拟浏览器是爬虫的基本功。最常见的是设置 User-Agent

python
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'
}
response = requests.get('https://httpbin.org/headers', headers=headers)
print(response.json())

处理响应

python
response = requests.get('https://api.github.com/events')

# 获取文本内容(requests 会自动解码)
print(response.text)

# 获取二进制内容(如图片、文件)
# print(response.content)

# 如果响应是 JSON,直接解析为 Python 字典
print(response.json())

使用 Session 维持会话 如果你需要向同一个网站发送多个请求(如模拟登录后的操作),使用 Session 对象会自动为你管理 Cookies。

python
session = requests.Session()

# 首次请求,网站可能会设置 cookie
session.get('https://httpbin.org/cookies/set/sessioncookie/123456789')

# 再次请求,会自动带上之前的 cookie
response = session.get('https://httpbin.org/cookies')
print(response.json()) # {'cookies': {'sessioncookie': '123456789'}}

其他常用功能

  • 代理: proxies = {'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080'} requests.get(url, proxies=proxies)
  • 超时: requests.get(url, timeout=5) # 设置超时时间为5秒
  • 异常处理: 使用 try...except requests.exceptions.RequestException 来捕获所有可能的请求异常。

数据解析及相关库

获取到 HTML 源码后,下一步就是从中提取出我们需要的数据。两大神器是 BeautifulSouplxml

BeautifulSoup vs. lxml+XPath 如何选择?

  • 新手入门/快速开发BeautifulSoup + CSS 选择器,API 友好,代码可读性高。
  • 追求极致性能/复杂解析lxml + XPath,速度更快,XPath 在处理复杂的父子/兄弟节点关系时更灵活。
  • 最佳实践:掌握两者。简单场景用 BS4,复杂场景或性能瓶颈时换用 lxml

BeautifulSoup4

BeautifulSoup (BS4) 的强项在于其简洁、符合 Python 哲学的 API,非常适合新手。它能容忍不规范的 HTML 代码。

安装: pip install beautifulsoup4 lxml

核心用法:

python
from bs4 import BeautifulSoup

html_doc = """
<html><head><title>一个简单的故事</title></head>
<body>
<p class="story">从前有三个小姐妹,她们的名字是
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 和
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
她们住在一个小池塘的底部。</p>

<p class="story">...</p>
</body>
</html>
"""

# 创建 BeautifulSoup 对象
soup = BeautifulSoup(html_doc, 'lxml')

# 1. 按标签名查找
print(soup.title)          # <title>一个简单的故事</title>
print(soup.title.string)   # 一个简单的故事

# 2. find(): 查找第一个符合条件的标签
link1 = soup.find('a')
print(link1) # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

# 3. find_all(): 查找所有符合条件的标签,返回一个列表
all_links = soup.find_all('a')
for link in all_links:
    print(link.get('href')) # 获取属性值

# 4. 按 CSS 类名查找
story_p = soup.find('p', class_='story')
print(story_p.get_text()) # 获取标签内的所有文本

# 5. 使用 CSS 选择器 (最推荐的方式!)
#    CSS 选择器语法简洁强大,前端开发者非常熟悉。
# 查找 id 为 link2 的标签
link2_by_css = soup.select_one('#link2')
print(link2_by_css)

# 查找所有 class 为 sister 的 a 标签
sisters_by_css = soup.select('a.sister')
for sister in sisters_by_css:
    print(f"姓名: {sister.string}, 链接: {sister['href']}")

lxml与XPath

lxml 是一个基于 C 语言的高性能库。它原生支持 XPath,这是一种用于在 XML/HTML 文档中定位节点的语言,功能比 CSS 选择器更强大。

XPath 常用语法

  • /:从根节点开始选择。
  • //:从当前节点开始,选择任意位置的后代节点。
  • .:选取当前节点。
  • ..:选取当前节点的父节点。
  • @:选取属性。
  • *:通配符,匹配任何元素节点。
  • text():获取节点的文本内容。
  • [ ]:谓语,用于筛选,如 [1], [@class="story"]

核心用法:

python
from lxml import etree

html_doc = """
<html><head><title>一个简单的故事</title></head>
<body>
<p class="story">从前有三个小姐妹,她们的名字是
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 和
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
她们住在一个小池塘的底部。</p>

<p class="story">...</p>
</body>
</html>
"""

# 注意:lxml 需要的是字节串,或者让它自己处理编码
# 使用 requests 获取的 response.content 是字节串,可以直接传入
# 这里我们用字符串来模拟
html_tree = etree.HTML(html_doc)

# 1. 查找所有 a 标签的 href 属性
hrefs = html_tree.xpath('//a/@href')
print(hrefs) # ['http://example.com/elsie', 'http://example.com/lacie', ...]

# 2. 查找所有 class 为 sister 的 a 标签的文本
names = html_tree.xpath('//a[@class="sister"]/text()')
print(names) # ['Elsie', 'Lacie', 'Tillie']

# 3. 查找 id 为 link3 的 a 标签
link3 = html_tree.xpath('//a[@id="link3"]')[0] # xpath 返回列表,需要取第一个
print(etree.tostring(link3, encoding='unicode')) # 打印标签

数据存储(文件或数据库)

最简单的数据持久化方式是存入文件。当数据量增大或需要复杂查询时,将数据存入数据库是更好的选择。

存储为文件

存储为 CSV 文件

CSV (Comma-Separated Values) 文件非常适合存储表格型数据,可以用 Excel 打开。

python
import csv

data_to_save = [
    {'name': 'Elsie', 'link': 'http://example.com/elsie'},
    {'name': 'Lacie', 'link': 'http://example.com/lacie'},
    {'name': 'Tillie', 'link': 'http://example.com/tillie'}
]

# newline='' 是为了防止写入时出现空行
with open('sisters.csv', 'w', newline='', encoding='utf-8-sig') as f:
    # utf-8-sig 可以让 Excel 正确识别中文
    fieldnames = ['name', 'link']
    writer = csv.DictWriter(f, fieldnames=fieldnames)

    writer.writeheader() # 写入表头
    writer.writerows(data_to_save) # 写入多行数据

print("数据已保存到 sisters.csv")

存储为 JSON 文件

JSON (JavaScript Object Notation) 格式非常适合存储嵌套的、非结构化的数据,且易于程序读取。

python
import json

data_to_save = [
    {'name': 'Elsie', 'link': 'http://example.com/elsie'},
    {'name': 'Lacie', 'link': 'http://example.com/lacie'},
    {'name': 'Tillie', 'link': 'http://example.com/tillie'}
]

# ensure_ascii=False 保证中文字符能正常显示
# indent=4 让 JSON 文件格式化,易于阅读
with open('sisters.json', 'w', encoding='utf-8') as f:
    json.dump(data_to_save, f, ensure_ascii=False, indent=4)

print("数据已保存到 sisters.json")

存储到数据库

存储到 MySQL

安装驱动: pip install mysql-connector-python

python
import mysql.connector

# 数据库连接配置
db_config = {
    'host': 'localhost',
    'user': 'your_username',
    'password': 'your_password',
    'database': 'crawler_db'
}

data_to_save = [{'name': 'Elsie', 'link': 'http://example.com/elsie'}, {'name': 'Lacie', 'link': 'http://example.com/lacie'}]

try:
    conn = mysql.connector.connect(**db_config)
    cursor = conn.cursor()
    
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS sisters (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        link VARCHAR(255)
    )
    ''')
    
    sql = "INSERT INTO sisters (name, link) VALUES (%s, %s)"
    for item in data_to_save:
        cursor.execute(sql, (item['name'], item['link']))
    
    conn.commit()
    print("数据已保存到 MySQL")

except mysql.connector.Error as err:
    print(f"MySQL 错误: {err}")
finally:
    if 'conn' in locals() and conn.is_connected():
        cursor.close()
        conn.close()

存储到 PostgreSQL

安装驱动: pip install psycopg2-binary

python
import psycopg2

db_config = {
    'host': 'localhost',
    'dbname': 'crawler_db',
    'user': 'your_username',
    'password': 'your_password'
}

data_to_save = [{'name': 'Elsie', 'link': 'http://example.com/elsie'}, {'name': 'Lacie', 'link': 'http://example.com/lacie'}]

try:
    conn = psycopg2.connect(**db_config)
    cursor = conn.cursor()
    
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS sisters (
        id SERIAL PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        link VARCHAR(255)
    )
    ''')
    
    sql = "INSERT INTO sisters (name, link) VALUES (%s, %s)"
    for item in data_to_save:
        cursor.execute(sql, (item['name'], item['link']))
        
    conn.commit()
    print("数据已保存到 PostgreSQL")

except psycopg2.Error as err:
    print(f"PostgreSQL 错误: {err}")
finally:
    if 'conn' in locals():
        cursor.close()
        conn.close()

存储到 MongoDB

安装驱动: pip install pymongo

python
from pymongo import MongoClient

data_to_save = [{'name': 'Elsie', 'link': 'http://example.com/elsie'}, {'name': 'Lacie', 'link': 'http://example.com/lacie'}]

client = MongoClient('mongodb://localhost:27017/')
db = client['crawler_db']
collection = db['sisters']

if data_to_save:
    try:
        collection.insert_many(data_to_save)
        print("数据已保存到 MongoDB")
    except Exception as e:
        print(f"MongoDB 存储失败: {e}")

client.close()

实战:豆瓣电影 Top 250

目标:获取电影的排名、名称、链接和评分。

python
import requests
from bs4 import BeautifulSoup
import csv
import time

def fetch_top250_movies():
    """爬取豆瓣电影Top 250的所有页面,并返回电影信息列表"""
    base_url = "https://movie.douban.com/top250"
    all_movies = []
    
    # 豆瓣Top250共10页
    for i in range(0, 250, 25):
        params = {'start': i, 'filter': ''}
        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'
        }
        
        try:
            print(f"正在爬取第 {i//25 + 1} 页...")
            response = requests.get(base_url, params=params, headers=headers)
            response.raise_for_status() # 如果请求失败,抛出异常
            time.sleep(1) # 增加延时,做一个有礼貌的爬虫
            
            soup = BeautifulSoup(response.text, 'lxml')
            
            # 使用 CSS 选择器定位到每个电影的 li 标签
            movie_items = soup.select('div.article ol.grid_view li')
            
            for item in movie_items:
                rank = item.select_one('div.pic em').get_text()
                title = item.select_one('div.info div.hd a span.title').get_text()
                link = item.select_one('div.info div.hd a')['href']
                rating = item.select_one('div.info div.bd div.star span.rating_num').get_text()
                
                movie_data = {
                    'rank': rank,
                    'title': title,
                    'link': link,
                    'rating': rating
                }
                all_movies.append(movie_data)
                
        except requests.RequestException as e:
            print(f"请求失败: {e}")
            break # 如果一页失败,则停止
            
    return all_movies

def save_to_csv(movies, filename='douban_top250.csv'):
    """将电影数据保存到 CSV 文件"""
    with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
        fieldnames = ['rank', 'title', 'link', 'rating']
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        if movies:
            writer.writerows(movies)
    print(f"数据已成功保存到 {filename}")


if __name__ == "__main__":
    movies_list = fetch_top250_movies()
    if movies_list:
        save_to_csv(movies_list)

运行这个脚本,将在同目录下得到一个 douban_top250.csv 文件,里面包含了所有电影信息!