Python 爬虫基础

110 阅读15分钟

Python 爬虫基础

1. 爬虫基础概念

1.1 什么是网络爬虫?

网络爬虫(Web Crawler),也称为网络蜘蛛(Web Spider)或网络机器人(Web Robot),是一种按照一定规则,自动地抓取万维网信息的程序或脚本。它们模拟浏览器行为,向服务器发送请求,获取网页内容,并从中提取所需数据。

1.2 爬虫的组成部分

一个典型的网络爬虫通常包含以下几个核心部分:

  • 请求模块 (Downloader) :负责向目标网站发送 HTTP/HTTPS 请求,获取网页的原始 HTML/JSON/XML 内容。
  • 解析模块 (Parser) :负责解析获取到的网页内容,从中提取出目标数据。
  • 数据存储模块 (Pipeline/Storage) :负责将提取到的数据保存到文件、数据库或其他存储介质中。
  • 调度器 (Scheduler) :管理待抓取的 URL 队列,决定下一个要抓取的页面。
  • URL 管理器 (URL Manager) :负责管理已抓取和待抓取的 URL,确保不重复抓取,并处理新的 URL。

2. HTTP/HTTPS 基础

网络爬虫的核心是模拟浏览器发送 HTTP/HTTPS 请求。了解 HTTP/HTTPS 的基本原理对于编写高效、健壮的爬虫至关重要。

2.1 请求方法

  • GET: 从服务器获取资源。最常用的方法,用于请求网页、图片等。参数通常附加在 URL 后面(查询字符串)。
  • POST: 向服务器提交数据,例如提交表单数据、上传文件。参数通常放在请求体中。
  • PUT: 向服务器上传完整的新资源,或替换现有资源。
  • DELETE: 请求删除指定资源。
  • HEAD: 类似于 GET 请求,但只返回响应头,不返回响应体。常用于检查资源是否存在或获取元数据。

2.2 状态码 (Status Codes)

服务器响应 HTTP 请求时返回的三位数字代码,表示请求的处理结果。

  • 2xx (成功) :

    • 200 OK: 请求成功。
    • 201 Created: 请求成功,并创建了新资源。
    • 204 No Content: 请求成功,但没有返回任何内容。
  • 3xx (重定向) :

    • 301 Moved Permanently: 资源永久移动到新位置。
    • 302 Found: 资源临时移动到新位置。
    • 304 Not Modified: 资源未修改,可以使用缓存。
  • 4xx (客户端错误) :

    • 400 Bad Request: 客户端请求语法错误。
    • 401 Unauthorized: 未经授权,需要身份验证。
    • 403 Forbidden: 服务器拒绝请求,通常是权限问题。
    • 404 Not Found: 服务器找不到请求的资源。
    • 429 Too Many Requests: 短时间内发送了太多请求(被限流)。
  • 5xx (服务器错误) :

    • 500 Internal Server Error: 服务器内部错误。
    • 503 Service Unavailable: 服务器暂时无法处理请求(可能过载或维护)。

2.3 请求头 (Request Headers)

请求头包含关于请求或客户端的附加信息。爬虫中常用的请求头:

  • User-Agent: 标识客户端的类型,模拟浏览器访问可以避免被识别为爬虫。
  • Referer: 表示请求的来源页面。
  • Cookie: 包含会话信息,用于维持登录状态等。
  • Accept-Encoding: 告知服务器客户端支持的编码方式(如 gzip, deflate)。

2.4 响应 (Response)

服务器对请求的回应,包含状态行、响应头和响应体。

  • 响应体 (Response Body) : 实际的网页内容(HTML、JSON、图片数据等)。

3. 常用 Python 爬虫库

Python 拥有丰富的第三方库,使得编写爬虫变得非常高效。

3.1 requests:HTTP 请求库

requests 是一个简洁而强大的 HTTP 库,用于发送各种 HTTP 请求。

  • 安装: pip install requests

  • 发送 GET 请求:

    import requests
    
    url = "https://www.example.com"
    try:
        response = requests.get(url)
        # 检查响应状态码
        response.raise_for_status() # 如果状态码不是200,则抛出HTTPError异常
    
        # 获取响应内容
        print(f"URL: {url}")
        print(f"状态码: {response.status_code}")
        print(f"响应头: {response.headers}")
        print(f"编码: {response.encoding}")
        print(f"响应文本 (前500字符):\n{response.text[:500]}...")
        print(f"响应字节内容 (前50字节): {response.content[:50]}...")
    except requests.exceptions.RequestException as e:
        print(f"请求发生错误: {e}")
    
  • 发送带参数的 GET 请求:

    import requests
    
    url = "https://httpbin.org/get" # 一个用于测试的公共API
    params = {
        "name": "Python",
        "version": "3.9"
    }
    response = requests.get(url, params=params)
    print("\n--- 带参数的 GET 请求 ---")
    print(response.url) # 打印完整的请求URL
    print(response.json()) # 打印JSON响应内容
    
  • 发送 POST 请求:

    import requests
    
    url = "https://httpbin.org/post"
    data = {
        "username": "testuser",
        "password": "testpassword"
    }
    response = requests.post(url, data=data) # data 参数用于表单数据
    print("\n--- POST 请求 (表单数据) ---")
    print(response.json())
    
    # 发送 JSON 数据
    json_data = {
        "item": "book",
        "price": 29.99
    }
    response = requests.post(url, json=json_data) # json 参数用于发送JSON格式数据
    print("\n--- POST 请求 (JSON 数据) ---")
    print(response.json())
    
  • 设置请求头:

    import requests
    
    url = "https://httpbin.org/headers"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
        "Referer": "https://www.google.com"
    }
    response = requests.get(url, headers=headers)
    print("\n--- 设置请求头 ---")
    print(response.json())
    
  • 超时设置:

    import requests
    
    url = "https://httpbin.org/delay/5" # 模拟5秒延迟的URL
    try:
        response = requests.get(url, timeout=3) # 设置3秒超时
        print("\n--- 超时设置 ---")
        print(response.text)
    except requests.exceptions.Timeout:
        print("\n--- 超时设置 ---")
        print("请求超时!")
    except requests.exceptions.RequestException as e:
        print(f"请求发生错误: {e}")
    
  • 会话 (Session) : requests.Session 对象允许你在多个请求之间保持某些参数(如 Cookie、请求头)。这对于需要登录或维持会话状态的网站非常有用。

    import requests
    
    session = requests.Session()
    # 假设登录操作会设置Cookie
    login_url = "https://httpbin.org/cookies/set/sessioncookie/12345"
    session.get(login_url)
    
    # 再次请求时,Session会自动携带上一次请求设置的Cookie
    check_cookie_url = "https://httpbin.org/cookies"
    response = session.get(check_cookie_url)
    print("\n--- 会话 (Session) ---")
    print(response.json())
    

3.2 BeautifulSoup:HTML/XML 解析库

BeautifulSoup 是一个用于从 HTML 或 XML 文件中提取数据的 Python 库。它能够将复杂的 HTML 文档转换成一个复杂的树形结构,每个节点都是 Python 对象。

  • 安装: pip install beautifulsoup4 (通常也需要安装一个解析器,如 lxmlhtml5lib) pip install lxml

  • 基本用法:

    from bs4 import BeautifulSoup
    
    html_doc = """
    <html><head><title>测试页面</title></head>
    <body>
        <p class="title"><b>页面标题</b></p>
        <a href="http://example.com/link1" class="sister" id="link1">链接1</a>
        <a href="http://example.com/link2" class="sister" id="link2">链接2</a>
        <p>
            这是一个段落。
            <span class="highlight">高亮文本</span>
            另一个文本。
        </p>
    </body></html>
    """
    soup = BeautifulSoup(html_doc, 'lxml') # 使用 lxml 解析器
    
    print("\n--- BeautifulSoup 基本用法 ---")
    print(f"页面标题: {soup.title.string}") # 获取 <title> 标签的文本内容
    print(f"第一个 p 标签: {soup.p}") # 获取第一个 <p> 标签
    print(f"p 标签的 class: {soup.p['class']}") # 获取 p 标签的 class 属性
    
    # 获取所有链接
    for link in soup.find_all('a'):
        print(f"链接文本: {link.get_text()}, URL: {link.get('href')}")
    
  • 查找元素:

    • find(name, attrs, recursive, string, **kwargs): 查找第一个匹配的标签。

    • find_all(name, attrs, recursive, string, limit, **kwargs): 查找所有匹配的标签,返回一个列表。

      • name: 标签名 (如 'a', 'p')。
      • attrs: 属性字典 (如 {'class': 'title'}{'id': 'link1'})。
      • string: 匹配标签的文本内容。
      • limit: 限制返回结果的数量。
    print("\n--- BeautifulSoup 查找元素 ---")
    # 查找所有 class 为 'sister' 的 <a> 标签
    sisters = soup.find_all('a', class_='sister') # class_ 是因为 class 是Python关键字
    for s in sisters:
        print(f"找到 sister 链接: {s.get_text()}")
    
    # 查找 id 为 'link1' 的标签
    link1 = soup.find(id='link1')
    print(f"找到 id 为 link1 的标签: {link1.get_text()}")
    
    # 查找文本为 "高亮文本" 的 span 标签
    highlight_span = soup.find('span', string="高亮文本")
    if highlight_span:
        print(f"找到高亮文本: {highlight_span.get_text()}")
    
  • CSS 选择器 (推荐) : select() 方法允许你使用 CSS 选择器来查找元素,语法与前端 CSS 类似,非常强大和直观。

    print("\n--- BeautifulSoup CSS 选择器 ---")
    # 查找所有 class 为 'sister' 的 <a> 标签
    links_by_css = soup.select('a.sister')
    for link in links_by_css:
        print(f"CSS选择器找到链接: {link.get_text()}")
    
    # 查找 id 为 'link1' 的标签
    link1_by_css = soup.select_one('#link1') # select_one 类似 find,只返回第一个
    print(f"CSS选择器找到 id 为 link1 的标签: {link1_by_css.get_text()}")
    
    # 查找 p 标签下的 span 标签
    span_in_p = soup.select('p span.highlight')
    for s in span_in_p:
        print(f"CSS选择器找到 p 下的 span: {s.get_text()}")
    

3.3 lxml:高性能解析库 (常与 XPath 结合)

lxml 是一个高性能的 XML 和 HTML 解析库,它提供了 Pythonic 的 API,并且支持 XPath 和 CSS 选择器。在处理大型或复杂的 HTML/XML 文档时,lxml 通常比 BeautifulSoup 更快。

  • 安装: pip install lxml

  • 基本用法 (XPath) : XPath 是一种在 XML 文档中查找信息的语言。它提供了一种简洁的方式来定位文档中的节点。

    from lxml import etree
    
    html_doc = """
    <html><head><title>测试页面</title></head>
    <body>
        <div id="container">
            <p class="intro">这是一个介绍段落。</p>
            <ul>
                <li>项目1</li>
                <li class="item-active">项目2</li>
                <li>项目3</li>
            </ul>
            <a href="/about.html">关于我们</a>
            <a href="/contact.html">联系我们</a>
        </div>
    </body></html>
    """
    # 从字符串解析HTML
    html_element = etree.HTML(html_doc)
    
    print("\n--- lxml XPath 基本用法 ---")
    # 查找所有 <p> 标签
    paragraphs = html_element.xpath('//p')
    for p in paragraphs:
        print(f"找到段落: {p.text}")
    
    # 查找 class 为 'item-active' 的 <li> 标签
    active_item = html_element.xpath('//li[@class="item-active"]')
    if active_item:
        print(f"找到活跃项目: {active_item[0].text}")
    
    # 查找所有 <a> 标签的 href 属性
    links_href = html_element.xpath('//a/@href')
    print(f"所有链接的 href 属性: {links_href}")
    
    # 查找 id 为 'container' 的 div 下的所有 <a> 标签的文本
    container_links_text = html_element.xpath('//div[@id="container"]/a/text()')
    print(f"container 下链接文本: {container_links_text}")
    

3.4 Scrapy:专业爬虫框架

Scrapy 是一个强大的 Python 爬虫框架,用于快速、高效地抓取网站并从网页中提取结构化数据。它提供了完整的爬虫解决方案,包括请求调度、中间件、管道、分布式部署等。适用于构建复杂和大规模的爬虫项目。

  • 安装: pip install scrapy

  • 核心组件:

    • Spider (爬虫) : 定义如何抓取网站以及如何从网页中提取数据。
    • Item (数据项) : 定义要从网页中提取的数据结构。
    • Pipeline (管道) : 处理 Item,例如清理、验证、存储数据。
    • Downloader Middleware (下载器中间件) : 在请求发送前和响应接收后处理请求和响应(如设置代理、User-Agent)。
    • Spider Middleware (爬虫中间件) : 在 Spider 处理输入和输出时进行处理。
  • 基本流程:

    1. 创建一个 Scrapy 项目 (scrapy startproject myproject)。
    2. 在项目中创建一个 Spider (scrapy genspider example example.com)。
    3. 在 Spider 中定义起始 URL、如何解析响应以及如何提取数据。
    4. 运行爬虫 (scrapy crawl example)。
  • 示例 (概念性代码,非完整可运行) :

    # myproject/myproject/spiders/example_spider.py
    import scrapy
    
    class ExampleSpider(scrapy.Spider):
        name = 'example'
        start_urls = ['http://quotes.toscrape.com/'] # 示例网站
    
        def parse(self, response):
            # 使用 CSS 选择器提取数据
            quotes = response.css('div.quote')
            for quote in quotes:
                yield {
                    'text': quote.css('span.text::text').get(),
                    'author': quote.css('small.author::text').get(),
                    'tags': quote.css('div.tags a.tag::text').getall(),
                }
    
            # 翻页逻辑
            next_page = response.css('li.next a::attr(href)').get()
            if next_page is not None:
                yield response.follow(next_page, callback=self.parse)
    
    # 在项目根目录运行: scrapy crawl example -o quotes.json
    

3.5 selenium:自动化浏览器

selenium 主要用于 Web 应用程序的自动化测试,但它也可以用于爬取那些依赖 JavaScript 动态加载内容的网站。它会启动一个真实的浏览器(如 Chrome, Firefox),模拟用户的行为(点击、滚动、填写表单),然后获取渲染后的页面内容。

  • 安装: pip install selenium (还需要下载对应浏览器的 WebDriver,例如 ChromeDriver)

  • 基本用法:

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.chrome.service import Service
    from selenium.webdriver.chrome.options import Options
    import time
    
    # 配置 ChromeDriver 路径 (根据你的实际路径修改)
    # service = Service(executable_path='/path/to/chromedriver')
    
    # 无头模式 (不显示浏览器界面)
    chrome_options = Options()
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--disable-gpu") # 禁用GPU硬件加速
    chrome_options.add_argument("--no-sandbox") # 禁用沙盒模式,用于某些环境
    chrome_options.add_argument("--disable-dev-shm-usage") # 解决Docker等环境下的/dev/shm问题
    
    # driver = webdriver.Chrome(service=service, options=chrome_options) # 如果需要指定路径
    driver = webdriver.Chrome(options=chrome_options) # 如果chromedriver在PATH中
    
    url = "https://www.baidu.com" # 示例网站
    try:
        driver.get(url)
        time.sleep(2) # 等待页面加载完成,或使用显式等待
    
        print("\n--- Selenium 自动化浏览器 ---")
        print(f"当前页面标题: {driver.title}")
    
        # 查找搜索框并输入内容
        search_box = driver.find_element(By.ID, "kw")
        search_box.send_keys("Python 爬虫")
    
        # 查找搜索按钮并点击
        search_button = driver.find_element(By.ID, "su")
        search_button.click()
    
        time.sleep(3) # 等待搜索结果加载
    
        print(f"搜索结果页面标题: {driver.title}")
        # 获取渲染后的页面HTML
        # print(driver.page_source[:1000]) # 打印部分HTML
    
    except Exception as e:
        print(f"Selenium 操作发生错误: {e}")
    finally:
        driver.quit() # 关闭浏览器
    

4. 爬虫流程

一个完整的爬虫通常遵循以下步骤:

  1. 确定目标: 明确要抓取哪个网站,以及需要哪些数据。

  2. 分析网页:

    • 使用浏览器开发者工具(F12)分析网页结构(HTML、CSS、JavaScript)。
    • 识别目标数据所在的 HTML 元素(标签、属性、ID、Class)。
    • 观察网络请求,了解数据是如何加载的(同步加载、异步 AJAX 加载)。
    • 检查 robots.txt 文件,了解网站的爬取规则。
  3. 发送请求: 使用 requests 库向目标 URL 发送 HTTP/HTTPS 请求,获取网页内容。

    • 根据分析结果,选择 GET 或 POST 方法。
    • 设置适当的请求头(User-Agent、Cookie、Referer 等)。
    • 处理请求参数。
  4. 解析响应:

    • 对于 HTML 页面,使用 BeautifulSouplxml 将原始 HTML 字符串解析成可操作的对象。
    • 对于 JSON 数据,直接使用 response.json() 解析成 Python 字典或列表。
  5. 提取数据:

    • 使用解析库提供的选择器(如 BeautifulSoupfind(), find_all(), select()lxml 的 XPath)定位目标数据。
    • 提取标签的文本内容、属性值等。
  6. 数据清洗与处理:

    • 对提取到的数据进行格式化、去重、类型转换等操作。
    • 处理缺失值或异常数据。
  7. 数据存储: 将清洗后的数据保存到本地文件(CSV, JSON, TXT)、数据库(MySQL, MongoDB, SQLite)或其他存储系统。

  8. 循环与翻页: 如果目标数据分布在多个页面,需要实现翻页逻辑,循环执行上述步骤,直到所有页面都被抓取。

  9. 异常处理: 捕获并处理网络错误、解析错误、反爬机制触发的错误等。

  10. 遵守规则: 始终遵守网站的 robots.txt 协议,设置合理的抓取频率,避免对网站服务器造成过大压力。

5. 反爬机制与应对策略

网站为了保护自身资源和防止滥用,通常会采取各种反爬机制。了解这些机制并采取相应的应对策略是爬虫工程师的必备技能。

5.1 常见的反爬机制

  • 基于 User-Agent 的检测: 服务器检查请求头中的 User-Agent 字段,如果不是常见的浏览器 UA,则拒绝访问或返回错误。
  • 基于 IP 地址的限制: 短时间内来自同一 IP 地址的请求过多,服务器会暂时或永久封禁该 IP。
  • 基于 Cookie/Session 的验证: 网站通过 Cookie 跟踪用户会话,如果请求没有携带正确的 Cookie 或会话过期,则拒绝访问。
  • 验证码: 网站在请求频率过高时弹出验证码,要求用户手动输入以证明是人类。
  • 动态加载内容 (JavaScript 渲染) : 网站内容通过 JavaScript 异步加载(AJAX),直接请求 HTML 无法获取完整数据。
  • 登录/认证: 某些数据需要用户登录后才能访问。
  • 数据加密: 传输的数据可能经过加密,需要解密才能获取真实内容。
  • 蜜罐 (Honeypot) : 在网页中设置一些隐藏的链接,只有爬虫会访问,一旦访问则被识别并封禁。
  • 请求参数加密/签名: 请求中的某些参数可能经过加密或签名,防止直接构造请求。

5.2 应对策略

  • 伪装 User-Agent: 在请求头中设置常见的浏览器 User-Agent。可以使用 fake_useragent 等库随机生成。

    # headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"}
    # requests.get(url, headers=headers)
    
  • 使用代理 IP 池: 当一个 IP 被封禁时,切换到另一个 IP。可以购买付费代理服务,或搭建自己的代理池。

    # proxies = {
    #     "http": "http://user:password@proxy.example.com:8080",
    #     "https": "https://user:password@proxy.example.com:8080",
    # }
    # requests.get(url, proxies=proxies)
    
  • 处理 Cookie/Session: 使用 requests.Session 维护会话,或手动管理 Cookie。

  • 验证码识别:

    • 简单验证码: 使用 Pillow (PIL) 进行图像处理,结合 pytesseract (Tesseract OCR) 进行识别。
    • 复杂验证码: 使用第三方打码平台(付费服务),或深度学习模型进行识别。
  • 处理动态加载内容:

    • 分析 AJAX 请求: 观察开发者工具中的 XHR/Fetch 请求,直接模拟这些请求获取 JSON 数据。
    • 使用 selenium: 启动真实浏览器,等待页面完全渲染后再获取 HTML 内容。
    • 使用 PlaywrightPuppeteer (Node.js) : 类似的无头浏览器自动化工具。
  • 模拟登录: 通过分析登录流程,模拟 POST 请求发送用户名和密码进行登录,获取会话 Cookie。

  • 设置请求间隔: 模拟人类行为,在请求之间添加随机的延迟 (time.sleep()),避免请求频率过高。

  • 分布式爬虫: 使用 Scrapy 框架或自定义分布式架构,将爬取任务分发到多台机器或多个 IP 上。

  • User-Agent 轮换: 使用一个 User-Agent 列表,每次请求随机选择一个。

  • 请求失败重试: 对于临时的网络问题或服务器错误,可以设置重试机制。

6. 道德与法律

在进行网络爬虫时,务必遵守相关的道德规范和法律法规,避免不必要的麻烦。

6.1 道德规范

  • 遵守 robots.txt 协议: 这是一个网站约定俗成的文件,指示爬虫哪些页面可以抓取,哪些不可以。虽然不是强制性的,但遵守它是对网站的尊重。
  • 设置合理的抓取频率: 不要给目标网站服务器造成过大的压力,避免被认为是拒绝服务攻击 (DoS)。
  • 尊重网站版权: 抓取到的数据可能受版权保护,未经授权不得用于商业用途或公开发布。
  • 保护用户隐私: 不要抓取和泄露用户的个人敏感信息。
  • 避免恶意行为: 不进行注入、篡改、破坏网站等行为。

6.2 法律法规

  • 数据隐私法: 如 GDPR (欧盟通用数据保护条例)、CCPA (加州消费者隐私法案) 等,规定了个人数据的收集、处理和存储。
  • 版权法: 网站内容可能受版权保护,未经授权的复制和分发可能构成侵权。
  • 不正当竞争法: 恶意爬取商业数据可能被视为不正当竞争行为。
  • 网络安全法: 某些国家和地区有严格的网络安全法律,禁止未经授权的访问和数据获取。

在开始爬取任何网站之前,请务必仔细阅读该网站的使用条款和服务协议。