【Python入门】爬虫实战:Requests + XPath 从基础到实战

文章目录

  • 一、环境准备
    • [1.1 安装依赖](#1.1 安装依赖)
    • [1.2 查看版本](#1.2 查看版本)
  • [二、Requests 库基础](#二、Requests 库基础)
    • [2.1 发送第一个请求](#2.1 发送第一个请求)
    • [2.2 Response 对象的核心属性](#2.2 Response 对象的核心属性)
    • [2.3 常见 HTTP 状态码](#2.3 常见 HTTP 状态码)
  • [三、GET 请求与 POST 请求](#三、GET 请求与 POST 请求)
    • [3.1 GET 请求](#3.1 GET 请求)
    • [3.2 POST 请求](#3.2 POST 请求)
    • [3.3 data 与 json 参数的区别](#3.3 data 与 json 参数的区别)
  • [四、代理 IP 与异常处理](#四、代理 IP 与异常处理)
    • [4.1 为什么需要代理](#4.1 为什么需要代理)
    • [4.2 代理配置](#4.2 代理配置)
    • [4.3 常见异常类型](#4.3 常见异常类型)
  • 五、数据解析之正则表达式
  • [六、XPath 基础语法](#六、XPath 基础语法)
    • [6.1 节点类型](#6.1 节点类型)
    • [6.2 路径表达式](#6.2 路径表达式)
    • [6.3 谓词(筛选条件)](#6.3 谓词(筛选条件))
    • [6.4 常用函数](#6.4 常用函数)
    • [6.5 逻辑运算符](#6.5 逻辑运算符)
  • [七、XPath 解析本地文件与网页](#七、XPath 解析本地文件与网页)
    • [7.1 解析本地 HTML 文件](#7.1 解析本地 HTML 文件)
    • [7.2 解析服务器响应(重点)](#7.2 解析服务器响应(重点))
    • [7.3 parse() 与 HTML() 的区别](#7.3 parse() 与 HTML() 的区别)
  • 八、实战项目一:站长素材图片爬取
    • [8.1 页面 URL 规律分析](#8.1 页面 URL 规律分析)
    • [8.2 完整代码](#8.2 完整代码)
    • [8.3 关键技术点](#8.3 关键技术点)
  • [九、实战项目二:豆瓣 Top250 爬取](#九、实战项目二:豆瓣 Top250 爬取)
    • [9.1 页面 URL 规律](#9.1 页面 URL 规律)
    • [9.2 完整代码](#9.2 完整代码)
    • [9.3 关键技术点](#9.3 关键技术点)
  • 十、总结

一、环境准备

1.1 安装依赖

bash 复制代码
pip install requests      # HTTP 请求库
pip install lxml          # XPath 解析库(底层用 C 实现,速度快)

1.2 查看版本

python 复制代码
import requests
print(requests.__version__)  # 例如 2.34.2

二、Requests 库基础

2.1 发送第一个请求

python 复制代码
import requests

url = "https://www.baidu.com/"
response = requests.get(url)
print(type(response))  # <class 'requests.models.Response'>

requests.get() 返回一个 Response 对象,封装了服务器的全部响应信息。

2.2 Response 对象的核心属性

属性 说明 示例
response.text 以字符串形式返回网页源码(自动猜测编码) '<html>...'
response.content 以二进制形式返回(用于图片、文件等) b'\xff\xd8...'
response.encoding 设置/获取响应编码 response.encoding = 'utf-8'
response.status_code HTTP 状态码 200
response.url 最终请求的 URL(可能有重定向) 'https://www.baidu.com/'
response.headers 响应头(字典形式) {'Content-Type': 'text/html'}
response.json() 直接解析 JSON 响应(返回 dict) {'key': 'value'}
response.cookies 响应的 Cookies RequestsCookieJar
python 复制代码
import requests

url = "https://www.baidu.com/"
response = requests.get(url)

response.encoding = 'utf-8'   # 设置编码
print(response.text)          # 字符串形式源码
print(response.content)       # 二进制数据(b 开头)
print(response.url)           # 请求的 URL
print(response.status_code)   # 状态码 200
print(response.headers)       # 响应头

2.3 常见 HTTP 状态码

状态码 含义
200 请求成功
301 / 302 永久/临时重定向
400 请求语法错误,服务器无法理解
401 需要身份认证
403 服务器拒绝访问(常见于反爬)
404 资源未找到
500 服务器内部错误
504 网关超时

补充知识点response.raise_for_status() 会在状态码非 2xx 时主动抛出 HTTPError 异常,是判断请求是否成功的推荐方式。


三、GET 请求与 POST 请求

3.1 GET 请求

GET 请求的参数直接拼接在 URL 中(?key=value&key2=value2),使用 params 参数传入字典即可,Requests 会自动编码拼接。

python 复制代码
import requests

url = "https://www.baidu.com/s"
params = {"wd": "迪丽热巴"}

# 请求头:伪装成浏览器,最基础的反爬手段
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/150.0.0.0 Safari/537.36"
}

response = requests.get(url=url, params=params, headers=headers)
response.encoding = 'utf-8'
print(response.text)

说明params 中的中文会被自动 URL 编码,无需手动处理。请求头中 User-Agent(简称 UA)是最常用的反爬字段,不设置时很多网站会返回 403 或空页面。

3.2 POST 请求

POST 请求的参数放在请求体中,使用 data(表单数据)或 json(JSON 数据)参数传递。

python 复制代码
import requests

url = "https://fanyi.baidu.com/sug"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/150.0.0.0 Safari/537.36"
}
data = {"kw": "蜘蛛"}

response = requests.post(url=url, data=data, headers=headers)

# 方式一:手动解析 JSON
# import json
# obj = json.loads(response.text)

# 方式二(推荐):直接用 response.json()
obj = response.json()
print(obj)

3.3 data 与 json 参数的区别

参数 Content-Type 适用场景
data={"key": "value"} application/x-www-form-urlencoded 表单提交
json={"key": "value"} application/json API 接口(RESTful)

四、代理 IP 与异常处理

4.1 为什么需要代理

频繁请求同一网站时,本机 IP 可能被封禁。使用代理 IP 可以隐藏真实 IP,降低被封风险。

4.2 代理配置

python 复制代码
import requests

url = "https://cn.bing.com/search"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/150.0.0.0 Safari/537.36"
}
params = {"q": "python"}

# 代理配置(推荐带上协议前缀)
proxies = {
    "http": "http://101.37.23.47:21056",
    "https": "http://101.37.23.47:21056"
}

try:
    response = requests.get(
        url=url, params=params, headers=headers,
        proxies=proxies, timeout=8
    )
    response.encoding = "utf-8"
    print(f"状态码:{response.status_code}")
    print(response.text[:500])
except requests.exceptions.ProxyError:
    print("代理不可用!")
except requests.exceptions.ConnectTimeout:
    print("连接代理超时!")
except requests.exceptions.RequestException as e:
    print(f"其他异常:{e}")

4.3 常见异常类型

异常类 触发场景
requests.exceptions.ProxyError 代理连接失败
requests.exceptions.ConnectTimeout 连接超时
requests.exceptions.ReadTimeout 读取响应超时
requests.exceptions.HTTPError raise_for_status() 检测到非 2xx 状态码
requests.exceptions.ConnectionError 网络连接错误(DNS 失败、拒绝连接等)
requests.exceptions.RequestException 所有 requests 异常的基类

五、数据解析之正则表达式

在学习 XPath 之前,先体验一下用正则表达式解析网页的方式。

python 复制代码
import re
import requests

url = "https://www.dytt8899.com/"
response = requests.get(url)
response.encoding = 'gb2312'   # 该网站使用 gb2312 编码

# 编译正则模式
# 2026新片精品:定位锚点
# .*?:非贪婪匹配任意字符
# (?P<ul>.*?):命名分组,提取 <ul> 和 </ul> 之间的内容
# re.S:使 . 可以匹配换行符
pattern = re.compile(r"2026新片精品.*?<ul>(?P<ul>.*?)</ul>", re.S)

# finditer 返回迭代器
result = pattern.finditer(response.text)
for i in result:
    print(i.group("ul"))

正则核心知识点:

符号 含义
.* 贪婪匹配,尽可能多匹配
.*? 非贪婪匹配,尽可能少匹配
re.S 使 . 匹配换行符
(?P<name>...) 命名分组,通过 group('name') 提取
finditer() 返回迭代器,适合处理大量匹配结果

六、XPath 基础语法

XPath(XML Path Language)是一种在 XML / HTML 文档中查找信息的语言,是爬虫数据解析的核心工具。

6.1 节点类型

  • 元素节点 :HTML 标签,如 <div></div>
  • 属性节点 :元素的属性,如 class="container"
  • 文本节点 :标签内的文本,如 <a>点击这里</a> 中的"点击这里"
  • 根节点 :整个文档的根元素,如 <html>
  • 父/子/兄弟节点:节点之间的层级关系

6.2 路径表达式

表达式 含义
/ 从根节点选取,直接找子节点(一层)
// 从任意位置选取,找所有子孙节点(不限层级)
. 当前节点
.. 当前节点的父节点
@ 选取属性
text() 选取文本

绝对路径(从根开始,层级严格):

复制代码
/html/body/div[1]/div[2]/a

相对路径(从任意位置匹配,推荐使用):

复制代码
//div[@class='container']//a

6.3 谓词(筛选条件)

谓词写在 [] 中,用于精确筛选节点。

按位置筛选:

xpath 复制代码
//ul/li[1]              # 第一个 li
//ul/li[last()]         # 最后一个 li
//ul/li[position() < 5] # 前 4 个 li

按属性筛选:

xpath 复制代码
//button[@id='submit']            # id 为 submit 的 button
//ul/li[@class]                   # 有 class 属性的 li
//ul/li[@id='zwq']/@class         # id 为 zwq 的 li 的 class 属性值

6.4 常用函数

函数 含义 示例
contains(@attr, 'str') 属性是否包含子串 //li[contains(@id, 'l')]
starts-with(@attr, 'str') 属性是否以子串开头 //li[starts-with(@id, 'l')]
string-length(@attr) 字符串长度 //li[string-length(@id) = 3]
text() 取文本 //a/text()
last() 最后一个 //li[last()]
position() 当前位置 //li[position() < 5]

6.5 逻辑运算符

xpath 复制代码
//ul/li[@id='lhf' or @id='zwq']   # id 为 lhf 或 zwq
//ul/li[@id='lhf' and @class='a'] # 同时满足两个条件
//ul/li[not(@id)]                 # 没有 id 属性

七、XPath 解析本地文件与网页

7.1 解析本地 HTML 文件

使用 lxml.etree.parse() 解析本地文件:

python 复制代码
from lxml import etree

# 解析本地文件,返回 ElementTree 对象
html_tree = etree.parse("hello.html")

# 查找 ul 下的 li(相对路径,推荐)
li_list = html_tree.xpath("//ul/li")

# 取文本
texts = html_tree.xpath("//ul/li[@id]/text()")

# 取属性值
classes = html_tree.xpath("//ul/li[@id='zwq']/@class")

# 包含匹配
result = html_tree.xpath("//ul/li[contains(@id, 'l')]/text()")

print(type(li_list))  # <class 'list'>,xpath 始终返回列表

注意xpath() 方法始终返回列表 ,即使只匹配到一个元素。取单个值时需要加 [0],并建议先判断列表是否为空。

7.2 解析服务器响应(重点)

使用 lxml.etree.HTML() 解析网页源码字符串:提取百度热搜榜标题

python 复制代码
import requests
from lxml import etree

url = "https://www.baidu.com/"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/150.0.0.0 Safari/537.36"
}

response = requests.get(url=url, headers=headers)
response.encoding = "utf-8"

# 解析网页源码
tree = etree.HTML(response.text)

# 提取"百度一下"按钮的 value 属性
btn_value = tree.xpath("//input[@id='su']/@value")[0]
print(btn_value)  # 百度一下

# 提取百度热搜榜标题
hot_titles = tree.xpath(
    '//ul[@id="hotsearch-content-wrapper"]'
    '/li/a/span[@class="title-content-title"]/text()'
)
for title in hot_titles:
    print(title)

7.3 parse() 与 HTML() 的区别

方法 输入 适用场景
etree.parse(filepath) 文件路径或文件对象 解析本地 XML/HTML 文件
etree.HTML(text) 字符串 解析网页源码字符串(爬虫最常用)

八、实战项目一:站长素材图片爬取

目标网站https://sc.chinaz.com/tupian/jianzhutupian.html

目标:批量下载建筑分类图片,支持多页爬取。

8.1 页面 URL 规律分析

复制代码
第1页:https://sc.chinaz.com/tupian/jianzhutupian.html
第2页:https://sc.chinaz.com/tupian/jianzhutupian_2.html
第3页:https://sc.chinaz.com/tupian/jianzhutupian_3.html

8.2 完整代码

python 复制代码
import re
import os
import time
import requests
from urllib.parse import urljoin
from lxml import etree

# ========== 基础配置 ==========
SAVE_DIR = "img"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/150.0.0.0 Safari/537.36"
}
os.makedirs(SAVE_DIR, exist_ok=True)  # 目录不存在则创建


def safe_filename(name):
    """清理文件名中的非法字符(Windows 不允许 \\/:*?"<>|)"""
    return re.sub(r'[\\/:*?"<>|]', "_", name)


def get_page_url(page):
    """根据页码构造 URL"""
    if page == 1:
        return "https://sc.chinaz.com/tupian/jianzhutupian.html"
    return f"https://sc.chinaz.com/tupian/jianzhutupian_{page}.html"


def get_content(page_url):
    """发送请求,返回网页源码"""
    response = requests.get(page_url, headers=headers, timeout=10)
    response.raise_for_status()   # 非 2xx 则抛异常
    response.encoding = 'utf-8'
    return response.text


def download_images(html):
    """解析并下载图片"""
    tree = etree.HTML(html)

    # 图片名称(alt 属性)
    name_list = tree.xpath("//div[@class='container']//img/@alt")
    # 图片真实地址(懒加载用 data-original,而非 src)
    img_list = tree.xpath("//div[@class='container']//img/@data-original")

    for name, src in zip(name_list, img_list):
        filename = safe_filename(name) + ".jpg"
        img_url = urljoin("https:", src)   # 补全协议头
        save_path = os.path.join(SAVE_DIR, filename)

        try:
            # stream=True 流式下载,适合大文件
            img_resp = requests.get(
                img_url, headers=headers, timeout=10, stream=True
            )
            img_resp.raise_for_status()
            with open(save_path, "wb") as f:
                for chunk in img_resp.iter_content(1024):  # 分块写入
                    f.write(chunk)
            print(f"下载成功:{filename}")
        except Exception as e:
            print(f"下载失败:{filename},原因:{e}")


if __name__ == '__main__':
    start_page = int(input("请输入起始页页码:"))
    end_page = int(input("请输入结束页页码:"))

    for page in range(start_page, end_page + 1):
        try:
            page_url = get_page_url(page)
            print(f"正在抓取第 {page} 页:{page_url}")
            html = get_content(page_url)
            download_images(html)
            time.sleep(1)   # 礼貌延迟,避免请求过快
        except Exception as e:
            print(f"第 {page} 页处理失败:{e}")

8.3 关键技术点

  1. 懒加载图片 :很多网站图片使用 data-original 属性存放真实地址,src 是占位图,需注意区分。
  2. URL 补全urljoin("https:", src)//xxx.com/img.jpg 补全为 https://xxx.com/img.jpg
  3. 流式下载stream=True + iter_content(1024) 分块写入,避免大文件占满内存。
  4. 文件名清洗 :Windows 文件名不允许 \/:*?"<>|,需替换为合法字符。

九、实战项目二:豆瓣 Top250 爬取

目标网站https://movie.douban.com/top250

目标:爬取电影名称、导演、主演、年份、国家、类型、评分、评价人数、摘引,保存为 CSV。

9.1 页面 URL 规律

复制代码
第1页:https://movie.douban.com/top250?start=0
第2页:https://movie.douban.com/top250?start=25
第3页:https://movie.douban.com/top250?start=50
...
range(0, 250, 25)  # 共 10 页

9.2 完整代码

python 复制代码
import csv
import time
import requests
from lxml import etree

# 请求头:伪装成浏览器,绕过基础反爬
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/150.0.0.0 Safari/537.36"
}

# 打开(或创建)CSV文件,使用utf-8编码,newline=""避免空行
with open("douban_250.csv", "w", encoding="utf-8", newline="") as file:
    writer = csv.writer(file)
    # 写入表头(字段名)
    writer.writerow([
        "电影名称", "电影导演", "电影主演", "电影年份",
        "电影国家", "电影类型", "电影评分", "评价人数", "电影摘引"
    ])

    # 豆瓣Top250每页25条,共10页,start从0开始,步长25
    for start in range(0, 250, 25):
        url = f"https://movie.douban.com/top250?start={start}"

        # ---- 发送请求 ----
        try:
            response = requests.get(url=url, headers=headers, timeout=10)
            response.raise_for_status()   # 如果状态码不是200,抛出HTTPError
        except Exception as e:
            print(f"页面请求失败:{url} -> {e}")
            continue   # 跳过当前页,继续下一页

        # ---- 解析HTML ----
        html = etree.HTML(response.text)          # 将HTML字符串解析为Element对象
        items = html.xpath("//div[@class='item']")  # 选取所有包含电影信息的div容器

        # 遍历当前页的每一部电影
        for item in items:
            try:
                # 1. 电影名称:取第一个span[@class='title'],即中文名
                title_list = item.xpath(".//span[@class='title'][1]/text()")
                title = title_list[0].strip() if title_list else "暂无名称"

                # 2. 导演、主演、年份、国家、类型:这些信息在同一个p标签内,分两行
                info_lines = item.xpath(".//div[@class='bd']/p[1]/text()")
                # 清洗:去除行首行尾空白,并过滤掉空字符串
                info_lines = [line.strip() for line in info_lines if line.strip()]

                # 初始化默认值
                director = actors = year = country = movie_type = "暂无数据"

                if info_lines:
                    # 第一行:导演: xxx  主演: xxx (中间可能包含\xa0不间断空格)
                    parts = info_lines[0].split("主演:")
                    director = parts[0].replace("导演:", "").strip()
                    director = director.replace("\xa0", "").strip()   # 去除特殊空格

                    if len(parts) > 1:
                        actors = parts[1].strip().replace("\xa0", "").strip()

                    # 第二行:年份 / 国家 / 类型
                    if len(info_lines) > 1:
                        yct = info_lines[1].split("/")
                        if len(yct) >= 3:
                            year = yct[0].strip()
                            country = yct[1].strip()
                            movie_type = yct[2].strip()

                # 3. 评分(例如 9.7)
                rating_list = item.xpath(".//span[@class='rating_num']/text()")
                rating = rating_list[0].strip() if rating_list else "暂无评分"

                # 4. 评价人数(例如 3306774人评价)
                rating_num_list = item.xpath(
                    ".//div[@class='bd']/div/span[4]/text()"
                )
                rating_num = (
                    rating_num_list[0].replace("人评价", "").strip()
                    if rating_num_list else "0"
                )

                # 5. 电影摘引(经典台词)
                quote_list = item.xpath(".//p[@class='quote']/span/text()")
                quote = quote_list[0].strip() if quote_list else "暂无摘引"

                # 将当前电影的所有数据写入CSV的一行
                writer.writerow([
                    title, director, actors, year, country,
                    movie_type, rating, rating_num, quote
                ])
            except Exception as e:
                # 如果某部电影解析失败,打印错误并继续下一部
                print(f"解析失败:{e}")
                continue

        print(f"已抓取第 {start // 25 + 1} 页数据")
        time.sleep(3)   # 豆瓣反爬严,每页间隔3秒,降低被封风险

print("抓取完成!")

9.3 关键技术点

  1. 相对 XPath :在循环中对每个 item 使用 .// 开头的相对路径,. 表示当前节点,避免匹配到整个文档的其他节点。
  2. 空列表判断xpath() 始终返回列表,取 [0] 前必须判断是否为空,否则会抛 IndexError
  3. \xa0 处理 :网页中常见不间断空格(&nbsp; 的 Unicode 表示),需用 .replace("\xa0", "") 清理。
  4. CSV 写入newline="" 防止 Windows 下出现空行,encoding="utf-8" 保证中文正常。

十、总结

本文系统梳理了 Python 爬虫的两大核心工具:

Requests 库

  • get() / post() 发送请求,params / data / json 传递参数
  • Response 对象的 text / content / json() / status_code 等属性
  • headers 伪装浏览器、proxies 代理 IP、timeout 超时控制
  • try-except 捕获各类请求异常

XPath 解析

  • /// 的区别、... 的用法
  • 谓词 [] 按位置、属性筛选
  • contains() / starts-with() / text() / @attr 等函数
  • etree.HTML() 解析网页、etree.parse() 解析本地文件
  • xpath() 始终返回列表,取 [0] 前需判空

两个实战项目覆盖了图片爬取(流式下载、懒加载处理)和结构化数据爬取(CSV 保存、多页遍历、数据清洗),是爬虫入门的典型场景。

温馨提示 :爬取数据时请遵守目标网站的 robots.txt 协议和相关法律法规,控制请求频率,避免对服务器造成压力。本文仅用于学习交流。

相关推荐
高洁0113 分钟前
Teacher Forcing技术解析
人工智能·python·深度学习·transformer·知识图谱
2601_9620652522 分钟前
从零创建一个 Django 项目
后端·python·django
吴声子夜歌25 分钟前
Java——开发中通用的方法和准则(二)
java·开发语言·php
江屿风37 分钟前
C++OJ题经验总结(竞赛)5
开发语言·c++
溪语流沙40 分钟前
【Python项目实战】虚拟环境与依赖管理:venv / pip / requirements.txt实操
开发语言·python·pip
神威难绷泪40 分钟前
Linux应用软件编程:线程分离属性 互斥机制 同步机制 死锁
linux·开发语言·算法·线程
研☆香1 小时前
数组方法 splice讲解 拓展
开发语言·前端·javascript
梯度下降者1 小时前
CukeTest 自动化测试工具2023年度回顾白皮书
自动化测试·python·cuketest·qtquick/qml·linuxatk
峥嵘life1 小时前
Android16 系统 APEX 模块说明
android·大数据·开发语言