1. Python爬虫入门:为什么选择百度作为练习对象

刚开始接触Python爬虫时,很多人会纠结该从哪个网站开始练习。我建议从百度入手,原因很简单:它的反爬机制相对友好,页面结构稳定,特别适合新手练手。记得我第一次写爬虫时,尝试抓取某电商网站数据,结果不到10分钟就被封了IP,那种挫败感至今难忘。

百度搜索结果的HTML结构清晰,关键信息都包裹在class为"result"的div中。这种规律性强的页面,能让我们把精力集中在爬虫核心逻辑上,而不是花大量时间处理各种反爬陷阱。不过要注意,虽然百度相对宽容,我们仍需遵守基本的爬虫礼仪——这也是为什么代码中要设置随机User-Agent和访问间隔。

import requests
from bs4 import BeautifulSoup
import random
import time

# 基础请求函数示例
def simple_request():
    url = "https://www.baidu.com"
    response = requests.get(url)
    print(response.status_code)  # 200表示请求成功

这个最简单的请求示例展示了爬虫的核心三要素:目标URL、请求发送和响应处理。当你能看到输出200这个数字时,说明已经成功迈出了第一步。接下来我们会在这个基础上,逐步添加更多实用功能。

2. 环境准备与基础工具链配置

2.1 必备库的安装与验证

工欲善其事,必先利其器。在开始写爬虫代码前,需要确保环境配置正确。我推荐使用Python 3.6+版本,太老的版本可能会遇到兼容性问题。安装依赖库时,新手常犯的错误是直接使用系统Python,这可能导致权限问题。我的经验是先用virtualenv创建隔离环境:

python -m venv spider_env
source spider_env/bin/activate  # Linux/Mac
spider_env\Scripts\activate  # Windows
pip install requests beautifulsoup4

安装完成后,建议写个简单的测试脚本验证库是否可用。有次我帮同事调试爬虫,折腾两小时才发现他装的bs4版本太旧,导致CSS选择器语法不支持。这个小测试能避免很多无谓的折腾:

def test_libraries():
    print(requests.__version__)  # 应该≥2.22.0
    print(BeautifulSoup.__version__)  # 应该≥4.9.0

2.2 开发工具的选择技巧

虽然可以用记事本写Python,但好的IDE能事半功倍。VS Code和PyCharm都不错,我个人更推荐VS Code,因为它轻量且插件丰富。安装Python扩展后,记得开启自动补全和linting功能。调试爬虫时,这几个功能特别有用:

  • 请求拦截查看(使用F12开发者工具)
  • XPath/CSS选择器测试窗
  • 变量实时监控

刚开始可以先用print调试,等熟悉后建议学习使用debugger。有次我遇到个诡异的问题,明明浏览器能看到元素,但爬虫就是抓不到。后来用debugger逐步跟踪,才发现是动态加载的内容,这个经验让我意识到工具的重要性。

3. 核心爬取逻辑实现详解

3.1 关键词搜索URL的构造技巧

百度搜索的URL构造很有规律,基础格式是https://www.baidu.com/s?wd=关键词。但实际使用时要注意几个细节:

  1. 中文关键词需要URL编码
  2. 可以添加pn参数控制分页(每页10条,pn=0第一页,pn=10第二页)
  3. 特殊符号需要正确处理
from urllib.parse import quote

def build_search_url(keyword, page=1):
    base_url = "https://www.baidu.com/s?wd="
    encoded_keyword = quote(keyword)
    page_param = f"&pn={(page-1)*10}" if page > 1 else ""
    return f"{base_url}{encoded_keyword}{page_param}"

这个改进版的URL构造器可以处理中文和分页。我曾经踩过坑,直接拼接中文关键词导致请求失败,后来才明白URL编码的重要性。分页参数也是个实用技巧,当需要抓取更多结果时非常有用。

3.2 反爬策略的实战应用

即使百度相对友好,基本的反爬措施还是必要的。我们的武器库包括:

  1. User-Agent轮换:准备多个主流浏览器的UA
  2. 请求间隔:随机延迟0.5-3秒
  3. 请求头完善:添加Referer等常见头信息
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...',
    # 至少准备5-10个不同的UA
]

def get_random_headers():
    return {
        'User-Agent': random.choice(user_agents),
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Referer': 'https://www.baidu.com/'
    }

在实际项目中,我发现Referer头特别重要。有次爬虫在服务器上跑不起来,本地却正常,最后发现是缺少Referer头导致的。这种细节问题最容易浪费调试时间。

4. 数据解析与存储方案

4.1 BeautifulSoup的进阶用法

解析HTML时,新手常犯的错误是过度依赖固定的class名。百度的结果容器class确实是"result",但其他网站可能变化。更健壮的做法是结合标签和属性多重过滤:

def parse_results(soup):
    items = []
    for item in soup.select('div[class*="result"]'):
        try:
            title = item.find('h3').get_text(strip=True)
            link = item.find('a')['href']
            # 提取摘要
            abstract = item.find('div', class_=re.compile('c-abstract'))
            abstract = abstract.get_text(strip=True) if abstract else ''
            
            items.append({
                'title': title,
                'link': link,
                'abstract': abstract
            })
        except Exception as e:
            print(f"解析错误: {e}")
            continue
    return items

这个解析器更加健壮,使用了CSS选择器和正则表达式组合匹配。get_text(strip=True)能自动清理多余空白字符,比直接.text更干净。我还添加了异常处理,避免因个别元素解析失败导致整个程序中断。

4.2 数据存储的几种选择

抓取到的数据通常需要持久化存储。根据数据量和使用场景,有几种常用方案:

  1. 本地文件:适合小规模数据

    import json
    
    def save_to_json(data, filename):
        with open(filename, 'a', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
    
  2. CSV文件:适合表格型数据

    import csv
    
    def save_to_csv(items, filename):
        with open(filename, 'a', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['title', 'link', 'abstract'])
            writer.writerows(items)
    
  3. 数据库:适合大规模数据

    import sqlite3
    
    def init_db():
        conn = sqlite3.connect('search_results.db')
        c = conn.cursor()
        c.execute('''CREATE TABLE IF NOT EXISTS results
                     (title text, link text, abstract text)''')
        conn.commit()
        return conn
    

我个人的经验是,小型项目用JSON足够,中型项目考虑CSV,当数据量超过10万条时再上数据库。过早优化是万恶之源,这句话在爬虫存储方案选择上尤其适用。

5. 常见问题排查与优化技巧

5.1 请求失败的常见原因

即使代码看起来完美,实际运行中还是会遇到各种问题。这些是我遇到最多的错误及解决方案:

  1. 403 Forbidden:通常是被网站识别为爬虫

    • 解决方案:完善请求头,添加Cookie
  2. 连接超时:目标服务器响应慢或网络问题

    • 解决方案:设置timeout参数并重试
    try:
        response = requests.get(url, headers=headers, timeout=10)
    except requests.exceptions.Timeout:
        print("请求超时,正在重试...")
        response = requests.get(url, headers=headers, timeout=15)
    
  3. SSL证书错误:常见于某些特定环境

    • 解决方案:添加verify=False参数(但会降低安全性)

5.2 性能优化实战建议

当需要抓取大量数据时,效率就成为关键问题。经过多次优化实践,我总结出这些有效方法:

  1. 会话保持:使用Session对象减少连接开销

    session = requests.Session()
    session.headers.update(get_random_headers())
    response = session.get(url)
    
  2. 并发控制:适当使用多线程(但别太激进)

    from concurrent.futures import ThreadPoolExecutor
    
    def crawl_page(page):
        url = build_search_url(keyword, page)
        # 爬取逻辑...
    
    with ThreadPoolExecutor(max_workers=3) as executor:
        executor.map(crawl_page, range(1, 6))
    
  3. 增量爬取:记录已抓取URL避免重复

    seen_urls = set()
    
    if link not in seen_urls:
        process_link(link)
        seen_urls.add(link)
    

记住一个原则:对目标网站要温柔。我见过有人开50个线程狂爬,结果几分钟就被封IP。合理的做法是从小规模开始,逐步增加并发量,找到那个不触发反爬的甜蜜点。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐