Python 操作 Hive 完整教程

Python 操作 Hive 完整教程

本教程基于 hive-app/ 目录下的 7 个教学文件,

系统讲解 Python 操作 Hive 的 4 种主流方式, 以及如何把 PySpark 脚本提交到本地/集群.

所有源码均已逐行注释, 适合初学者作为训练案例.

教程涵盖:

7 个 Python 脚本(hive_connection_.py / 01 ~ 05)

2 个 spark-submit 启动脚本(local / cluster)

1 个依赖清单(requirements.txt)

1 个示例数据 CSV(emp.csv)

每个代码块均与 hive-app/ 目录下的真实文件 逐字符一致,包含完整的中文注释。


目录

  • [第 0 章: 项目结构与目标](#第 0 章: 项目结构与目标)
  • [第 1 章: 创建项目目录与虚拟环境](#第 1 章: 创建项目目录与虚拟环境)
  • [第 2 章: 编写 requirements.txt](#第 2 章: 编写 requirements.txt)
  • [第 3 章: 准备 Hive 数据(emp.csv + 建库建表)](#第 3 章: 准备 Hive 数据(emp.csv + 建库建表))
  • [第 4 章: 编写 7 个 Python 脚本](#第 4 章: 编写 7 个 Python 脚本)
  • [第 5 章: 编写 2 个 spark-submit 脚本](#第 5 章: 编写 2 个 spark-submit 脚本)
  • [第 6 章: 验证整套项目](#第 6 章: 验证整套项目)
  • [附录: 文件清单与依赖版本](#附录: 文件清单与依赖版本)

第 0 章: 项目结构与目标

text 复制代码
hive-app/
├── requirements.txt                # Python 依赖清单
├── emp.csv                         # 示例数据
├── hive_connection_.py             # 阶段 0: 连接诊断与冒烟测试
├── 01_pyhive_sql_basics.py         # 阶段 1: PyHive 原生 SQL (无 pandas)
├── 02_pandas_pyhive_dbapi.py       # 阶段 2: pandas 3.x + DBAPI
├── 03_sqlalchemy_pyhive_engine.py  # 阶段 3: SQLAlchemy 2.x + pyhive dialect
├── 04_pyspark_hdfs_analysis.py     # 阶段 4a: PySpark + HDFS 数据源
├── 05_pyspark_hive_analysis.py     # 阶段 4b: PySpark + Hive 数据源 (PyHive 桥接)
├── spark_submit_local.sh           # spark-submit 本地模式
├── spark_submit_cluster.sh         # spark-submit 集群模式
└── venv/                           # Python 虚拟环境

目标环境

依赖 版本
Python 3.12+
JDK 21
Hadoop 3.x
Hive 4.x (HiveServer2 默认端口 10000)
Spark 4.x
主机名 lihaozhe (与 /etc/hosts 中 192.168.10.100 对应)
HS2 用户 lhz (NONE 认证)

第 1 章: 创建项目目录与虚拟环境

1.1 创建目录

bash 复制代码
mkdir -p /home/lhz/opt/hive-app
cd /home/lhz/opt/hive-app

1.2 创建虚拟环境

bash 复制代码
python -m venv venv

1.3 激活虚拟环境

bash 复制代码
source /home/lhz/opt/hive-app/venv/bin/activate

后续所有 pip installpython xxx.py 命令都在该虚拟环境中执行。


第 2 章: 编写 requirements.txt

将下面这段内容保存为 requirements.txt

text 复制代码
# hive-app 教学项目依赖
# 全部使用最新版本, 不指定具体版本号

# 阶段 0/1: 连接与原生 SQL
pyhive
thrift
pure-sasl
thrift_sasl

# 阶段 2: pandas + DBAPI
pandas

# 阶段 3: SQLAlchemy 引擎
sqlalchemy

# 阶段 4: PySpark (HDFS / Hive)
pyspark

安装:

bash 复制代码
pip install --upgrade pip
pip install -r /home/lhz/opt/hive-app/requirements.txt

# PySpark 4.x 启动期强校验还需要这些依赖
pip install pyarrow grpcio grpcio-status zstandard

第 3 章: 准备 Hive 数据(emp.csv + 建库建表)

3.1 编写 emp.csv

将下面这段内容保存为 emp.csv (UTF-8 编码, 15 行 5 列, 第 15 行 dept_id 为空, 用于演示缺失值与主键重复):

text 复制代码
7369,张三,研发,800.00,30
7499,李四,财务,1600.00,20
7521,王五,行政,1250.00,10
7566,赵六,销售,2975.00,40
7654,侯七,研发,1250.00,30
7698,马八,研发,2850.00,30
7782,金九,行政,2450.0,30
7788,银十,行政,3000.00,10
7839,小芳,销售,5000.00,40
7844,小明,销售,1500.00,40
7876,小李,行政,1100.00,10
7900,小元,讲师,950.00,30
7902,小海,行政,3000.00,10
7934,小红明,讲师,1300.00,30
7934,小红,讲师,1300.00,

3.2 在 Hive 中建库建表

启动 HiveServer2 后,通过 beeline 进入:

bash 复制代码
beeline -u jdbc:hive2://lihaozhe:10000 -n lhz

依次执行以下 SQL(与原项目 pyhive.md 中一致的内容, 直接列在教程中, 无需复制 pyhive.md):

sql 复制代码
create database db_hive location '/db_hive';
use db_hive;

create external table emp (
    emp_id      int          comment '员工ID',
    emp_name    string       comment '员工姓名',
    emp_job     string       comment '员工岗位',
    emp_salary  decimal(8,2) comment '员工薪资',
    dept_id     int          comment '员工隶属部门ID'
)
row format delimited fields terminated by ','
lines terminated by '\n'
stored as textfile
location '/db_hive/emp';

load data local inpath '/home/lhz/data02/emp.csv' into table emp;

select * from emp;

第 4 章: 编写 7 个 Python 脚本

本章节所有 Python 文件内容均 完整嵌入 在文档中. 直接复制 markdown 代码块,

保存为对应文件名即可.

4.1 阶段 0: hive_connection_.py

将下面这段代码保存为 hive_connection_.py:

python 复制代码
# -*- coding: utf-8 -*-
# 指定源文件编码: utf-8, 允许源码中直接出现中文字符
"""hive-app 教学项目 --- 阶段 0: 连接诊断与冒烟测试.

================================================================================
学习目标
================================================================================
- 掌握 PyHive 连接 HiveServer2 的 4 个核心参数: ``host`` / ``port`` /
  ``username`` / ``auth``
- 学会排查最常见的 3 类失败:
    1) DNS 解析失败 (主机名拼错 / /etc/hosts 未配)
    2) 端口不通   (HiveServer2 未启动 / 防火墙)
    3) 认证失败   (auth 模式与 hive-site.xml 不一致)
- 验证 thrift_sasl 依赖完整性 (PyHive 启动时强制 import)

================================================================================
环境要求
================================================================================
- Python       : 3.12+
- HiveServer2  : lihaozhe:10000, auth=NONE
- 用户         : lhz

安装依赖 (最新版本, 不指定具体版本号)
====================================
使用系统包管理器安装 SASL 原生库 (可选, 若需支持 SASL 认证):

.. code-block:: bash

    sudo apt-get install -y libsasl2-dev

激活虚拟环境后执行:

.. code-block:: bash

    source hive-app/venv/bin/activate
    pip install --upgrade pip
    pip install pyhive thrift pure-sasl thrift_sasl

说明
----
- ``pyhive``           : Hive 官方推荐的 Python 客户端
- ``thrift``           : Thrift 协议依赖
- ``pure-sasl``        : 纯 Python 实现的 SASL, 作为 ``thrift_sasl`` 的后端
- ``thrift_sasl``      : PyHive 内部会无条件 import, 必须安装

注意: 原生 ``sasl`` C 扩展在 Python 3.12 编译失败 (``longintrepr.py`` 已移除),
故使用 ``pure-sasl`` 替代, PyHive 在 ``NONE`` 认证场景下工作正常.

运行
====
::

    source hive-app/venv/bin/activate
    python hive-app/hive_connection_.py

================================================================================
前置学习
================================================================================
无 (这是第一个文件)

================================================================================
下一步
================================================================================
``01_pyhive_sql_basics.py`` --- 原生 SQL/DDL/DML
"""

# 启用 PEP 563 风格的延迟注解求值, 避免循环引用与启动开销
from __future__ import annotations

# 引入 socket 标准库: 用于 DNS 解析 (gethostbyname) 与 TCP 端口探测
import socket
# 引入 sys 标准库: 用于诊断失败时以非 0 退出码终止脚本
import sys

# 引入 pyhive 库的 hive 子模块: 提供 DBAPI 2.0 风格的 Connection / Cursor
from pyhive import hive


# --------------------------------------------------------------------------- #
# 配置 (集中放在这里, 便于跨脚本复用)
# --------------------------------------------------------------------------- #
# HiveServer2 主机名, 与 /etc/hosts 中 192.168.10.100 对应
# 注意: 早期曾误写成 lihaohe, 会触发 gaierror 错误, 见 Step 1
HIVE_HOST = "lihaozhe"
# HiveServer2 Thrift 监听端口 (默认值), 也可在 hive-site.xml 的
# hive.server2.thrift.port 中修改
HIVE_PORT = 10000
# 连接用户名: NONE 认证下 HiveServer2 不会校验密码,
# 但仍要求给一个非空字符串作为客户端标识
HIVE_USER = "lhz"
# 初始数据库: 连接成功后会话默认进入的 schema, 等价于 SQL 的 USE <db>
HIVE_DB = "default"
# 认证模式: 必须与 hive-site.xml 的 hive.server2.authentication 一致
# 本机未设置该项, HiveServer2 默认采用 NONE, 故传 "NONE"
HIVE_AUTH = "NONE"


# --------------------------------------------------------------------------- #
# 通用工具
# --------------------------------------------------------------------------- #
def section(title: str) -> None:
    """打印带分隔线的章节标题, 用于在终端中区分诊断步骤."""
    # 先输出一个空行, 让上一节的输出与本节标题之间有视觉间隔
    print()
    # 60 个等号组成的水平分隔线
    print("=" * 60)
    # 用 f-string 在居中位置插入标题, 两边各有 2 个空格的 padding
    print(f"  {title}")
    # 再输出一条分隔线, 形成一个 "框" 的视觉效果
    print("=" * 60)


# --------------------------------------------------------------------------- #
# 诊断步骤
# --------------------------------------------------------------------------- #
def check_dns() -> bool:
    """Step 1: 验证主机名能否被解析为 IP 地址.

    返回 True 表示解析成功, False 表示失败.
    """
    # 打印 Step 1 的章节标题
    section("Step 1: DNS 解析检查")
    # 用 try / except 捕获解析异常 (主机名拼错会触发 socket.gaierror)
    try:
        # 调用 socket.gethostbyname, 它会先查 /etc/hosts, 再走 DNS
        ip = socket.gethostbyname(HIVE_HOST)
        # 解析成功: 打印 "主机名 -> IP"
        print(f"[OK] {HIVE_HOST} -> {ip}")
        # 返回 True 表示本步通过
        return True
    except socket.gaierror as e:
        # 解析失败: 常见原因包括拼写错误 / /etc/hosts 未配置 / DNS 服务器不可达
        print(f"[FAIL] {HIVE_HOST} 解析失败: {e}")
        # 给出修复建议
        print("       请检查 /etc/hosts 或使用 IP 直连")
        # 返回 False 表示本步失败
        return False


def check_port() -> bool:
    """Step 2: 验证 HiveServer2 的 TCP 端口是否可达.

    通过创建一次 TCP 短连接判断端口是否被监听.
    返回 True 表示端口可达, False 表示不通.
    """
    # 打印 Step 2 的章节标题
    section("Step 2: TCP 端口连通性")
    try:
        # socket.create_connection 会发起 TCP 三次握手
        # - 第一个参数: (host, port) 元组
        # - timeout=5: 5 秒超时, 避免长时间阻塞
        # 用 with 上下文确保 socket 句柄被自动关闭
        with socket.create_connection((HIVE_HOST, HIVE_PORT), timeout=5) as s:
            # 创建成功说明 HiveServer2 正在监听, 端口可达
            print(f"[OK] {HIVE_HOST}:{HIVE_PORT} TCP 可达")
            return True
    except OSError as e:
        # OSError 是 socket.create_connection 失败时的统一异常类
        # 常见原因: HiveServer2 未启动 / 防火墙拦截 / 端口写错
        print(f"[FAIL] {HIVE_HOST}:{HIVE_PORT} TCP 不可达: {e}")
        # 给出排错提示: 用 ps 命令检查 HS2 进程
        print("       请检查 HiveServer2 是否启动: ps -ef | grep HiveServer2")
        return False


def check_thrift_sasl() -> bool:
    """Step 3: 验证 thrift_sasl 模块是否已安装.

    PyHive 在源码内部有 ``import thrift_sasl``, 即使使用 NONE 认证也会触发.
    如果 thrift_sasl 缺失, hive.Connection() 会抛出 ModuleNotFoundError.
    """
    # 打印 Step 3 的章节标题
    section("Step 3: 关键依赖 (thrift_sasl) 检查")
    try:
        # 延迟 import thrift_sasl, 仅作存在性校验
        # noqa: F401 告诉 flake8 该 import 无副作用, 不必报警
        import thrift_sasl  # noqa: F401
        # 打印模块路径, 便于用户确认安装位置
        print(f"[OK] thrift_sasl 已安装: {thrift_sasl.__file__}")
        return True
    except ImportError as e:
        # 缺失时打印错误信息与修复命令
        print(f"[FAIL] thrift_sasl 缺失: {e}")
        print("       执行: pip install thrift_sasl")
        return False


def check_hive_connection() -> bool:
    """Step 4: 真实连接 HiveServer2 并执行冒烟 SQL.

    成功执行 SELECT 1 / SHOW DATABASES / current_user 才算通过.
    返回 True 表示真实连接可用, False 表示失败.
    """
    # 打印 Step 4 的章节标题
    section("Step 4: HiveServer2 连接 + 冒烟测试")
    # 第一阶段: 建立连接
    try:
        # hive.Connection 接受 host / port / username / database / auth 5 个参数
        # 注意: NONE 模式下若 username 为空字符串会触发 ValueError
        conn = hive.Connection(
            host=HIVE_HOST,
            port=HIVE_PORT,
            username=HIVE_USER,
            database=HIVE_DB,
            auth=HIVE_AUTH,
        )
    except Exception as e:
        # 连接本身失败 (Thrift 握手错误 / 认证不匹配等)
        print(f"[FAIL] 连接失败: {e}")
        return False

    # 第二阶段: 执行冒烟 SQL
    # 用 try/finally 确保无论如何都会关闭连接, 避免 HS2 端 Session 泄露
    try:
        # conn.cursor() 走 DBAPI 协议, 返回符合 PEP 249 的游标对象
        with conn.cursor() as cursor:
            # 1. 最简单的查询: 验证 Thrift 协议完整
            cursor.execute("SELECT 1")
            # cursor.fetchone() 返回单条结果, 预期是 (1,)
            print(f"[OK] SELECT 1 -> {cursor.fetchone()}")

            # 2. 元数据查询: 列出所有 schema
            cursor.execute("SHOW DATABASES")
            # 取出结果集中每行的第 0 列 (schema 名) 组成列表
            dbs = [row[0] for row in cursor.fetchall()]
            print(f"[OK] 数据库列表: {dbs}")

            # 3. 会话上下文查询: 验证会话状态正确
            cursor.execute("SELECT current_database(), current_user()")
            # fetchone() 返回 (default, lhz) 这样的元组
            print(f"[OK] 当前信息: {cursor.fetchone()}")
        # 全部 SQL 成功执行才返回 True
        return True
    finally:
        # 不管成功还是异常, 都关闭 Thrift 连接释放 HS2 端资源
        conn.close()
        print("[OK] 连接已关闭")


# --------------------------------------------------------------------------- #
# 主流程
# --------------------------------------------------------------------------- #
def main() -> None:
    """依次执行 4 步诊断, 任何一步失败立即以退出码 1 终止."""
    # 打印目标连接 URL, 便于一眼看清要诊断的目标
    print(f"目标: hive://{HIVE_USER}@{HIVE_HOST}:{HIVE_PORT}/{HIVE_DB}  (auth={HIVE_AUTH})")

    # 前 3 步是 "快速诊断" (DNS / 端口 / 依赖), 任一失败直接退出
    # 用字典记录每步结果, 既便于调试也便于扩展更多步骤
    results = {
        "DNS":  check_dns(),   # 调用 Step 1
        "Port": check_port(),  # 调用 Step 2
        "依赖": check_thrift_sasl(),  # 调用 Step 3
    }
    # all() 对字典 values 求逻辑与; 任一项为 False 整体为 False
    if not all(results.values()):
        print("\n[ERROR] 诊断失败, 请先修复再继续.")
        # 退出码 1 表示异常退出, 方便上层脚本检测
        sys.exit(1)

    # 第 4 步是 "真实连接", 只有前 3 步全过才有意义执行
    ok = check_hive_connection()
    if not ok:
        print("\n[ERROR] 连接失败, 请检查 hive-site.xml 与 HS2 日志.")
        sys.exit(1)

    # 全部通过: 提示用户进入下一阶段
    print("\n[完成] 全部诊断通过, 可以进入 01_pyhive_sql_basics.py")


# Python 入口惯例: 直接运行该文件时才执行 main()
# 若被 import, 不会自动执行 (便于单元测试与复用)
if __name__ == "__main__":
    main()

4.2 阶段 1: 01_pyhive_sql_basics.py

将下面这段代码保存为 01_pyhive_sql_basics.py:

python 复制代码
# -*- coding: utf-8 -*-
# 指定源文件编码: utf-8, 允许源码中直接出现中文字符
"""hive-app 教学项目 --- 阶段 1: PyHive 原生 SQL 方式操作 Hive (仅 SQL, 不使用 pandas).

================================================================================
学习目标
================================================================================
- 掌握 ``hive.Connection`` 的构造与 ``Connection.cursor()`` 用法
- 熟悉 PyHive 的 ``cursor.execute(sql)`` / ``cursor.fetchall()`` / ``description``
- 覆盖 Hive DDL / DQL / DML 全部基础 SQL 操作
- 理解 Hive **执行引擎** 在 DDL/DQL 中的差异
    - 简单 ``SELECT`` / ``DESCRIBE`` / ``LIMIT`` 走 fetch task (无需 MR)
    - ``GROUP BY`` / ``ORDER BY`` / ``INSERT`` / ``CREATE TABLE AS`` 需 MR/Tez/Spark

================================================================================
前置学习
================================================================================
``hive_connection_.py`` --- 连接诊断与冒烟测试

================================================================================
本案例演示
================================================================================
1. 连接 HiveServer2 (lihaozhe:10000)
2. 数据库级操作: SHOW DATABASES / USE
3. 表级元数据: SHOW TABLES / DESCRIBE
4. DQL:  SELECT ... LIMIT / WHERE / ORDER BY
5. 聚合查询:  COUNT / GROUP BY / AVG / MAX / MIN
6. DML:  INSERT INTO ... SELECT  (建临时中间表 demo)
7. DDL:  CREATE TABLE / DROP TABLE
8. 关闭连接

注意
====
本机 Hive 的 MR / Tez / Spark 执行引擎均不可用, 因此包含 GROUP BY / ORDER BY
不经过 reducer 时仍能成功 (LIMIT 触发的 ``hive.limit.query.max.entries`` 优化,
或纯 fetch 任务). 一旦需要 MapReduce 任务, 会失败 --- 案例 6/7/8 中会做降级处理.

运行
====
::

    source hive-app/venv/bin/activate
    python hive-app/01_pyhive_sql_basics.py

================================================================================
下一步
================================================================================
``02_pandas_pyhive_dbapi.py`` --- 在 PyHive 之上接入 pandas (DBAPI 方式)
"""

# 启用 PEP 563 风格的延迟注解求值, 避免循环引用与启动开销
from __future__ import annotations

# 引入 pyhive 库的 hive 子模块: 提供 DBAPI 2.0 风格的 Connection / Cursor
from pyhive import hive


# --------------------------------------------------------------------------- #
# 连接配置 (集中放在这里, 便于跨脚本复用)
# --------------------------------------------------------------------------- #
# HiveServer2 主机名, 与 /etc/hosts 中 192.168.10.100 对应
HIVE_HOST = "lihaozhe"
# HiveServer2 Thrift 监听端口 (默认值)
HIVE_PORT = 10000
# 连接用户名: NONE 认证下 HiveServer2 不会校验密码
HIVE_USER = "lhz"
# 初始数据库: 这里直接指向业务库 db_hive, 跳过 USE
HIVE_DB = "db_hive"
# 认证模式: 与 hive-site.xml 的 hive.server2.authentication 一致
HIVE_AUTH = "NONE"


# --------------------------------------------------------------------------- #
# 通用工具
# --------------------------------------------------------------------------- #
def section(title: str) -> None:
    """打印带分隔线的章节标题, 用于在终端中区分案例小节."""
    # 先输出一个空行, 让上一节的输出与本节标题之间有视觉间隔
    print()
    # 60 个等号组成的水平分隔线
    print("=" * 60)
    # 用 f-string 在居中位置插入标题, 两边各有 2 个空格的 padding
    print(f"  {title}")
    # 再输出一条分隔线, 形成一个 "框" 的视觉效果
    print("=" * 60)


def dump_cursor(cursor) -> None:
    """打印游标当前结果集的所有行, 字段以 ' | ' 分隔.

    参数:
        cursor : 已 execute() 但未取结果的游标
    """
    # cursor.description 是 DBAPI 标准属性, 在 execute 后才会填充
    # 每个元素形如 (name, type_code, display_size, internal_size, precision, scale, null_ok)
    if cursor.description:
        # 取列名列表
        cols = [d[0] for d in cursor.description]
        # 用 " | " 连接列名, 每列宽度 14, 右对齐
        print(" | ".join(f"{c:>14}" for c in cols))
        # 分隔线长度 = (列宽 + 分隔宽) * 列数
        print("-" * (17 * len(cols)))
    # fetchall() 取全部结果行 (list[tuple])
    for row in cursor.fetchall():
        # 每行的每个值都用 str() 转换后右对齐 14 列
        print(" | ".join(f"{str(v):>14}" for v in row))


# --------------------------------------------------------------------------- #
# 主流程
# --------------------------------------------------------------------------- #
def main() -> None:
    """演示 PyHive 的连接 / 元数据查询 / DQL / DDL / DML 完整用法."""
    # 打印目标连接信息, 便于一眼看清本次要连哪个 HS2
    print(f"连接 HiveServer2: {HIVE_HOST}:{HIVE_PORT} user={HIVE_USER} auth={HIVE_AUTH}")
    # hive.Connection 走 Thrift 协议, 参数:
    #   - host        : HS2 主机名
    #   - port        : HS2 端口
    #   - username    : 用户标识
    #   - database    : 默认 schema
    #   - auth        : 认证方式
    conn = hive.Connection(
        host=HIVE_HOST,
        port=HIVE_PORT,
        username=HIVE_USER,
        database=HIVE_DB,
        auth=HIVE_AUTH,
    )
    # 用 try / finally 保证连接最终会被关闭
    try:
        # conn.cursor() 返回符合 PEP 249 的游标对象
        cursor = conn.cursor()

        # ---- 1. SHOW DATABASES ---- #
        # 打印第 1 小节标题
        section("1. SHOW DATABASES")
        # 执行元数据查询, 返回的列名是 database_name
        cursor.execute("SHOW DATABASES")
        # fetchall() 返回 list[tuple], 每行只有一列
        # 这里用列表推导取出每行的第 0 列组成 list[str]
        dbs = [r[0] for r in cursor.fetchall()]
        # 打印所有可见 schema 名
        print("数据库列表:", dbs)

        # ---- 2. SHOW TABLES ---- #
        section("2. SHOW TABLES (current db = db_hive)")
        # 当前连接的 database 是 db_hive, SHOW TABLES 只列当前 schema 的表
        cursor.execute("SHOW TABLES")
        # 同样是单列结果, 抽取 tab_name 字段
        tables = [r[0] for r in cursor.fetchall()]
        print("表列表:", tables)

        # ---- 3. DESCRIBE ---- #
        section("3. DESCRIBE emp")
        # DESCRIBE 返回 (col_name, data_type, comment) 三列
        cursor.execute("DESCRIBE emp")
        # 逐行打印, 格式化为 "列名 类型"
        for col_name, col_type, _comment in cursor.fetchall():
            # _comment 用 _ 前缀告诉读者这里忽略第三个值
            print(f"  {col_name:<12} {col_type}")

        # ---- 4. 简单 DQL ---- #
        section("4. SELECT * FROM emp LIMIT 5")
        # LIMIT 5 触发 hive.limit.query.max.entries 优化, 走简单 fetch 路径
        cursor.execute("SELECT * FROM emp LIMIT 5")
        # 用 dump_cursor 打印当前游标的结果集 (含列名表头)
        dump_cursor(cursor)

        # ---- 5. 条件查询 ---- #
        section("5. SELECT WHERE 薪资 > 2000  (无 ORDER BY, 走简单 fetch 路径)")
        try:
            # WHERE 谓词下推到存储层, 同样不触发 MR
            cursor.execute(
                "SELECT emp_id, emp_name, emp_job, emp_salary, dept_id "
                "FROM emp WHERE emp_salary > 2000"
            )
            # dump_cursor 会调用 fetchall 一次性取完
            dump_cursor(cursor)
        except Exception as e:
            # 任何异常都打印首行 (避免多行堆栈淹没关键信息)
            print(f"[错误] {str(e).splitlines()[0]}")

        # ---- 5b. 带 ORDER BY (会触发 MR, 本机引擎不可用) ---- #
        section("5b. SELECT WHERE ... ORDER BY emp_salary DESC  (需 MR, 演示失败模式)")
        try:
            # ORDER BY 全局排序在 Hive 中必须走 MR / Tez / Spark
            cursor.execute(
                "SELECT emp_id, emp_name, emp_job, emp_salary, dept_id "
                "FROM emp WHERE emp_salary > 2000 ORDER BY emp_salary DESC"
            )
            dump_cursor(cursor)
        except Exception as e:
            # 用方括号标注 [预期失败], 把异常转为教学示例
            print(f"[预期失败] ORDER BY 触发 MR: {str(e).splitlines()[0]}")

        # ---- 6. 聚合查询 (在 MR 不可用时, 通过 LIMIT 限制触发的简单路径) ---- #
        section("6. 聚合查询 (按 emp_job 分组) --- 需 MR, 本机可能失败")
        try:
            # GROUP BY 是典型的 reducer 操作, 必须有可用的执行引擎
            cursor.execute(
                "SELECT emp_job, COUNT(*) AS cnt, AVG(emp_salary) AS avg_sal "
                "FROM emp GROUP BY emp_job"
            )
            dump_cursor(cursor)
        except Exception as e:
            # 这里预期失败, 提示用户当前环境缺执行引擎
            print(f"[跳过] MR 不可用, 错误: {str(e).splitlines()[0]}")

        # ---- 7. CREATE TABLE AS SELECT (CTAS) ---- #
        section("7. CREATE TABLE emp_top AS SELECT * FROM emp WHERE emp_salary > 2000")
        try:
            # 先确保临时表不存在 (DDL 是幂等的, 重复运行也不会报错)
            cursor.execute("DROP TABLE IF EXISTS emp_top")
            # CTAS 把子查询结果写入新表, 同样需要 MR
            cursor.execute(
                "CREATE TABLE emp_top AS "
                "SELECT * FROM emp WHERE emp_salary > 2000"
            )
            # 验证新表行数
            cursor.execute("SELECT COUNT(*) FROM emp_top")
            # fetchone() 返回单行单列, 取 [0] 得到整数
            print("emp_top 行数:", cursor.fetchone()[0])

            # 展示新表前若干行
            cursor.execute("SELECT * FROM emp_top ORDER BY emp_salary DESC")
            dump_cursor(cursor)
        except Exception as e:
            print(f"[跳过] CTAS 需要 MR, 错误: {str(e).splitlines()[0]}")

        # ---- 8. INSERT INTO 演示 (使用 LIMIT 触发的轻量路径) ---- #
        section("8. INSERT INTO ... SELECT 演示")
        try:
            # 创建结构相同的空表作为 INSERT 目标
            cursor.execute("DROP TABLE IF EXISTS emp_copy")
            cursor.execute("CREATE TABLE emp_copy LIKE emp")
            # 注意: 这里 INSERT ... LIMIT 5 在某些 Hive 版本下仍可能需要 MR
            cursor.execute("INSERT INTO emp_copy SELECT * FROM emp LIMIT 5")
            cursor.execute("SELECT COUNT(*) FROM emp_copy")
            print("emp_copy 行数:", cursor.fetchone()[0])
        except Exception as e:
            print(f"[跳过] INSERT 错误: {str(e).splitlines()[0]}")

        # ---- 9. 清理 ---- #
        section("9. 清理临时表")
        # 不论前面是否成功, 都尝试清理, 避免污染 db_hive
        for t in ("emp_top", "emp_copy"):
            try:
                # DROP TABLE IF EXISTS 总是成功
                cursor.execute(f"DROP TABLE IF EXISTS {t}")
                print(f"  DROP TABLE {t} OK")
            except Exception as e:
                # 防御性捕获: 即使失败也不影响主流程结束
                print(f"  DROP TABLE {t} 失败: {e}")

        # 显式关闭游标
        cursor.close()
        print("\n[完成] PyHive SQL 案例结束.")
    finally:
        # 关闭 Thrift 连接释放 HS2 端资源
        conn.close()


# Python 入口惯例: 直接运行该文件时才执行 main()
if __name__ == "__main__":
    main()

4.3 阶段 2: 02_pandas_pyhive_dbapi.py

将下面这段代码保存为 02_pandas_pyhive_dbapi.py:

python 复制代码
# -*- coding: utf-8 -*-
# 指定源文件编码: utf-8, 允许源码中直接出现中文字符
"""hive-app 教学项目 --- 阶段 2: pandas 3.x + pyhive DBAPI 操作 Hive.

================================================================================
学习目标
================================================================================
- 理解 DBAPI 2.0 协议 (PEP 249): 任何符合 DBAPI 的连接都能被 pandas 接受
- 掌握 ``pd.read_sql`` / ``pd.read_sql_query`` / ``pd.read_sql_table`` 的区别
- 看到 pandas 2.x/3.x 在 DBAPI 模式下抛出的 ``UserWarning``, 理解为何生产推荐 SQLAlchemy
- 认识 ``df.to_sql`` 在 Hive 上的局限

================================================================================
前置学习
================================================================================
``01_pyhive_sql_basics.py`` --- 原生 PyHive 用法

================================================================================
版本说明
================================================================================
本脚本针对 **pandas 3.x** (当前环境为 3.0.3) 编写. pandas 3.x 在 DBAPI 模式下
依然保留 ``UserWarning``, 推荐使用 SQLAlchemy 引擎 (见 ``03_sqlalchemy_pyhive_engine.py``).

================================================================================
本案例演示
================================================================================
1. ``pd.read_sql(sql, conn)`` 直接吃 pyhive 的 DBAPI 连接
2. ``pd.read_sql_query()`` / ``pd.read_sql_table()`` 两种语义化入口
3. 写入: ``df.to_sql()`` 通过临时 SQLAlchemy 引擎 (Hive 不支持原生 to_sql, 演示会失败)
4. 配合 Python 内置聚合, 实现 "Hive 拉数 + pandas 分析"

注意
====
- pandas 3.x 在 ``read_sql`` 传入 DBAPI 连接时会发出 ``UserWarning``:
  推荐使用 SQLAlchemy 引擎. 本案例保留 DBAPI 路径用于教学对比.
- Hive 不支持 ``DataFrame.to_sql()`` 的标准写入, 仅可演示报错信息.

运行
====
::

    source hive-app/venv/bin/activate
    pip install pandas
    python hive-app/02_pandas_pyhive_dbapi.py

================================================================================
下一步
================================================================================
``03_sqlalchemy_pyhive_engine.py`` --- 用 SQLAlchemy 引擎消除 UserWarning,
                                          并支持 ``inspect`` / ``MetaData`` 反射
"""

# 启用 PEP 563 风格的延迟注解求值, 避免循环引用与启动开销
from __future__ import annotations

# 引入 warnings 标准库: 用于在 read_sql 抛 UserWarning 时选择性静音 / 保留
import warnings

# 引入 pandas 库: 教学目标中的 "在 Hive 之上做数据分析" 的主角 (版本 3.x)
import pandas as pd

# 引入 pyhive 库的 hive 子模块: 提供 DBAPI 2.0 风格 Connection
from pyhive import hive


# --------------------------------------------------------------------------- #
# 连接配置
# --------------------------------------------------------------------------- #
# HiveServer2 主机名
HIVE_HOST = "lihaozhe"
# HiveServer2 Thrift 端口
HIVE_PORT = 10000
# 连接用户名
HIVE_USER = "lhz"
# 初始数据库
HIVE_DB = "db_hive"
# 认证模式
HIVE_AUTH = "NONE"


# --------------------------------------------------------------------------- #
# 通用工具
# --------------------------------------------------------------------------- #
def section(title: str) -> None:
    """打印带分隔线的章节标题, 复用本案例的 5 个 section."""
    # 空行 + 双横线, 与阶段 1 风格保持一致
    print()
    print("=" * 60)
    print(f"  {title}")
    print("=" * 60)


def section_default(version: str) -> None:
    """打印含 pandas 版本号的章节标题, 让运行结果自带版本信息."""
    print()
    print("=" * 60)
    # 在标题前插入 pandas 版本, 既美观又便于核对环境
    print(f"  [pandas {version}] {title if False else ''}")


# --------------------------------------------------------------------------- #
# 主流程
# --------------------------------------------------------------------------- #
def main() -> None:
    """演示 pandas 通过 DBAPI 接入 Hive, 演示 UserWarning 与写入限制."""
    # 打印目标连接信息
    print(f"连接 HiveServer2: {HIVE_HOST}:{HIVE_PORT} user={HIVE_USER} auth={HIVE_AUTH}")
    # 打印 pandas 版本, 与本案例的目标版本 3.x 对照
    print(f"pandas 版本: {pd.__version__}")
    # hive.Connection 同时是 DBAPI connection 对象, 可被 pandas 接受
    conn = hive.Connection(
        host=HIVE_HOST,
        port=HIVE_PORT,
        username=HIVE_USER,
        database=HIVE_DB,
        auth=HIVE_AUTH,
    )
    # 用 try / finally 保证连接最终会被关闭
    try:
        # ---- 1. pd.read_sql(sql, conn) ---- #
        section("1. pd.read_sql(sql, conn) - 完整 SQL")
        # pandas 2.x/3.x 在传入 DBAPI (非 SQLAlchemy) 连接时会发出 UserWarning
        # 这里用 catch_warnings + simplefilter("always") 显式保留, 用于教学
        with warnings.catch_warnings():
            # "always" 让所有警告都显示, 而不只是同源同 message 的第一次
            warnings.simplefilter("always")
            # pd.read_sql(sql, conn) 内部会:
            # 1) 调用 conn.cursor() 获取游标
            # 2) cursor.execute(sql) 执行 SQL
            # 3) cursor.description 推断列名
            # 4) cursor.fetchall() 拉数据
            # 5) 用 cursor.description 拼成 DataFrame
            df = pd.read_sql("SELECT * FROM emp", conn)
        # 打印每一列的 dtype, 注意 PyHive 会把列名变成 "emp.emp_id" 形式
        print("dtypes:")
        print(df.dtypes)
        # 打印前 5 行, 验证数据确实拉到了本地
        print()
        print("前 5 行:")
        print(df.head())

        # 清理 pyhive 给每列加的 "emp." 表名前缀
        # 例如 "emp.emp_id" -> "emp_id"
        df.columns = [c.split(".")[-1] for c in df.columns]
        # 把 emp_id 转成 pandas 扩展类型 Int64 (可空 int), 便于后续聚合
        df["emp_id"] = df["emp_id"].astype("Int64")
        # dept_id 也是可空的, 同样转 Int64
        df["dept_id"] = df["dept_id"].astype("Int64")
        # emp_salary 用 Decimal 返回, 转 float 便于可视化与算术运算
        df["emp_salary"] = df["emp_salary"].astype(float)

        # ---- 2. pd.read_sql_query ---- #
        section("2. pd.read_sql_query(sql, conn)")
        # read_sql_query 与 read_sql(sql, conn) 等价, 是更明确语义的版本
        with warnings.catch_warnings():
            # 这里用 ignore 静音, 只展示用法不重复警告
            warnings.simplefilter("ignore")
            # 同样只查 3 列, 演示列投影
            df2 = pd.read_sql_query("SELECT emp_id, emp_name, emp_salary FROM emp", conn)
        # 打印前 5 行
        print(df2.head())
        # 同样去掉 "emp." 前缀
        df2.columns = [c.split(".")[-1] for c in df2.columns]

        # ---- 3. pd.read_sql_table  ---- #
        section("3. pd.read_sql_table('emp', conn)")
        # read_sql_table(name, conn) 是 "按表名读整表" 的语义化入口
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            try:
                # pandas 在内部会尝试通过 reflection 推断 schema
                df3 = pd.read_sql_table("emp", conn)
                print(df3.head())
            except Exception as e:
                # DBAPI 模式没有 SQLAlchemy 反射能力, 这里通常会抛 TypeError
                print(f"[提示] read_sql_table 在 DBAPI 模式下不可用: {type(e).__name__}")
                # 给出修复建议: 用 SQLAlchemy 引擎
                print("       (read_sql_table 需要 SQLAlchemy 引擎)")

        # ---- 4. 参数化查询 (防 SQL 注入演示) ---- #
        section("4. 参数化查询: 部门 = 30 的员工")
        # 注意: 这里 pandas 内部用的是 DBAPI 的参数化机制
        # %(dept)s 是命名占位符, params 提供字典
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            df4 = pd.read_sql(
                "SELECT * FROM emp WHERE dept_id = %(dept)s",
                conn,
                params={"dept": 30},
            )
        # 同样清理列名前缀
        df4.columns = [c.split(".")[-1] for c in df4.columns]
        # 打印命中 dept_id=30 的所有员工
        print(df4)

        # ---- 5. 用 pandas 做分析 (Hive 端 GROUP BY 在本机不可用, 用 Python 端聚合) ---- #
        section("5. 用 pandas 做本地分析")
        # 由于本机 Hive 没有可用执行引擎, GROUP BY / AVG 等会失败
        # 改为把数据拉回 pandas, 用 DataFrame.agg 做本地聚合
        print("按 emp_job 聚合:")
        # groupby("emp_job") 分组; agg 同时算三个聚合; sort_values 按平均薪资降序
        # 关键参数:
        #   - 人数=("emp_id", "count") : 用 emp_id 列做 count
        #   - 薪资总额=("emp_salary", "sum") : 用 emp_salary 列做 sum
        #   - 平均薪资=("emp_salary", "mean") : 用 emp_salary 列做 mean
        print(
            df.groupby("emp_job")
            .agg(人数=("emp_id", "count"),
                 薪资总额=("emp_salary", "sum"),
                 平均薪资=("emp_salary", "mean"))
            .sort_values("平均薪资", ascending=False)
        )

        print("\n按 dept_id 聚合 (含缺失值):")
        # dropna=False 让空值也作为一组保留
        # sort_values 默认 NaN 排到最后, 所以这里 ascending=False 也能正常显示
        print(
            df.groupby("dept_id", dropna=False)
            .agg(人数=("emp_id", "count"), 平均薪资=("emp_salary", "mean"))
            .sort_values("平均薪资", ascending=False)
        )

        # ---- 6. 演示 to_sql 写入 (Hive 不支持, 预期失败) ---- #
        section("6. df.to_sql 演示 (Hive 不支持, 预期失败)")
        # df.to_sql 在没有 SQLAlchemy 引擎时, 会尝试用 DBAPI 的 cursor.execute 拼 SQL
        # 但由于底层 DBAPI 不支持统一的 schema 操作, 直接抛 TypeError
        try:
            # if_exists="replace" 表示同名表存在则替换; index=False 不写 DataFrame 索引
            df.head(3).to_sql("emp_tmp_should_fail", conn, if_exists="replace", index=False)
        except Exception as e:
            # 打印异常类型与首行消息
            print(f"[预期失败] {type(e).__name__}: {str(e).splitlines()[0]}")

        print("\n[完成] pandas + pyhive 案例结束.")
    finally:
        # 关闭 Thrift 连接
        conn.close()


# Python 入口惯例
if __name__ == "__main__":
    main()

4.4 阶段 3: 03_sqlalchemy_pyhive_engine.py

将下面这段代码保存为 03_sqlalchemy_pyhive_engine.py:

python 复制代码
# -*- coding: utf-8 -*-
# 指定源文件编码: utf-8, 允许源码中直接出现中文字符
"""hive-app 教学项目 --- 阶段 3: SQLAlchemy 2.x + pyhive dialect 操作 Hive.

================================================================================
学习目标
================================================================================
- 掌握 SQLAlchemy 2.x 的 ``Engine`` / ``Connection`` / ``Session`` / ``MetaData``
- 用 ``create_engine("hive://...")`` 创建 pyhive dialect 引擎
- 利用 ``inspect()`` / ``Table(autoload_with=engine)`` 反射元数据
- 用 ``text()`` / ``select()`` 编写类型化 SQL
- 理解 pyhive 方言的三大限制:
    1. URL 中 :password 仅 LDAP/CUSTOM 模式可用
    2. 不支持 bind 参数
    3. ``to_sql`` 不可写

================================================================================
前置学习
================================================================================
``02_pandas_pyhive_dbapi.py`` --- DBAPI 模式与 UserWarning 来源

================================================================================
本案例演示
================================================================================
1. ``create_engine("hive://...")`` 创建 SQLAlchemy 引擎
2. ``engine.connect()`` / ``engine.begin()`` 获取连接, 执行 ``text(sql)``
3. ``pandas.read_sql()`` 通过引擎读取 (无 UserWarning, 相比案例 2 更规范)
4. ``inspect()`` 反射元数据 (表/列)
5. ``Table`` 对象 + ``select()`` / ``insert()`` Core 写法
6. ``session.execute()`` ORM 风格执行 SQL

依赖
====
::

    pip install sqlalchemy pyhive thrift pure-sasl thrift_sasl

注意
====
- Hive 没有原生主键 / 外键, ``Table`` 反射只拿到列.
- ``engine.execute()`` 在 SQLAlchemy 2.x 已移除, 改用 ``conn.execute()``.
- ``to_sql`` 仍然不直接支持 Hive (DML 需要走 MR).

运行
====
::

    source hive-app/venv/bin/activate
    python hive-app/03_sqlalchemy_pyhive_engine.py

================================================================================
下一步
================================================================================
``04_pyspark_hdfs_analysis.py`` / ``05_pyspark_hive_analysis.py`` --- 大数据集成
``spark_submit_local.sh`` / ``spark_submit_cluster.sh`` --- 提交到 Spark 集群
"""

# 启用 PEP 563 风格的延迟注解求值, 避免循环引用与启动开销
from __future__ import annotations

# 引入 pandas 库: 用于通过 SQLAlchemy 引擎读 Hive
import pandas as pd

# 引入 SQLAlchemy 2.x 的核心 API
from sqlalchemy import (
    MetaData,            # 元数据容器, 反射时使用
    Table,               # 表对象, 也用于反射
    create_engine,       # 构造 SQLAlchemy Engine
    inspect,             # 元数据反射工具
    select,              # Core select() 构造器
    text,                # 原生 SQL 字符串包装
)
# 引入 Engine 类型用于类型标注
from sqlalchemy.engine import Engine

# 关键: 显式导入 pyhive 的 sqlalchemy 方言
# SQLAlchemy 通过 entry_points 自动发现, 但显式 import 让教学更直观
# noqa: F401 表示该 import 仅用于副作用, 不需要直接使用 sqlalchemy_hive 符号
from pyhive import sqlalchemy_hive  # noqa: F401  (导入即注册)


# --------------------------------------------------------------------------- #
# 配置
# --------------------------------------------------------------------------- #
# HiveServer2 主机名
HIVE_HOST = "lihaozhe"
# HiveServer2 Thrift 端口
HIVE_PORT = 10000
# 连接用户名
HIVE_USER = "lhz"
# NONE 认证模式下, HIVE_PASS 必须为空字符串, 否则 pyhive 会抛 ValueError
HIVE_PASS = ""
# 初始数据库
HIVE_DB = "db_hive"
# 认证模式, 通过 connect_args 传给 pyhive
HIVE_AUTH = "NONE"

# SQLAlchemy URL
# 重要: NONE 认证下 URL 中 **不能** 带 ":password" 段
# 错误写法: hive://lhz:@host:port/db   <- 触发 ValueError
# 正确写法: hive://lhz@host:port/db     <- 用户名后直接 @
HIVE_URL = f"hive://{HIVE_USER}@{HIVE_HOST}:{HIVE_PORT}/{HIVE_DB}"


# --------------------------------------------------------------------------- #
# 通用工具
# --------------------------------------------------------------------------- #
def section(title: str) -> None:
    """打印带分隔线的章节标题, 复用本案例的 7 个 section."""
    # 空行 + 双横线, 与阶段 1/2 风格保持一致
    print()
    print("=" * 60)
    print(f"  {title}")
    print("=" * 60)


def build_engine() -> Engine:
    """创建 SQLAlchemy Engine, 通过 connect_args 把 auth 传给 pyhive.

    返回值: Engine 实例, 全案例共用.
    """
    # create_engine 接受 URL + 各种 kwargs:
    # - connect_args : 透传给底层 DBAPI (pyhive) 的 connect() 关键字参数
    # - pool_pre_ping : 每次取出连接前先发 SELECT 1 校验 (默认 False)
    #                  Hive 端 session 不适合频繁探测, 这里关掉
    # - pool_recycle  : 连接最长存活时间 (秒), 超过会被回收重建
    return create_engine(
        HIVE_URL,
        connect_args={"auth": HIVE_AUTH},
        pool_pre_ping=False,   # Hive session 不适合 pre_ping
        pool_recycle=3600,     # 1 小时回收
    )


# --------------------------------------------------------------------------- #
# 主流程
# --------------------------------------------------------------------------- #
def main() -> None:
    """演示 SQLAlchemy Engine / Connection / Session / MetaData 完整用法."""
    # 先打印构造出的 URL, 便于排错
    print(f"SQLAlchemy URL: {HIVE_URL}")
    # 一次性构造 engine, 后续所有 Connection / Session 都从它派生
    engine = build_engine()
    # 用 try / finally 确保最后调用 engine.dispose() 释放连接池
    try:
        # ---- 1. engine.connect() + text(sql) ---- #
        section("1. engine.connect() + text(sql)")
        # engine.connect() 返回 Connection 对象 (走 context manager 自动 close)
        with engine.connect() as conn:
            # text(sql) 把原生 SQL 字符串包成 SQLAlchemy 可执行的 ClauseElement
            # conn.execute() 返回 Result, fetchone() 取首行
            result = conn.execute(text("SELECT current_database(), current_user()"))
            # 打印 (db, user) 元组
            print("current_database / current_user :", result.fetchone())

        # ---- 2. inspect() 反射 ---- #
        section("2. inspect() 反射元数据")
        # inspect(engine) 返回 Inspector 对象, 提供高层元数据查询 API
        insp = inspect(engine)
        # get_schema_names() 等价于 SHOW DATABASES, 返回 schema 名列表
        schemas = insp.get_schema_names()
        print("schemas:", schemas)

        # get_table_names(schema="db_hive") 等价于 SHOW TABLES FROM db_hive
        tables = insp.get_table_names(schema=HIVE_DB)
        print(f"tables in {HIVE_DB}:", tables)

        # get_columns("emp", schema="db_hive") 反射单表的列信息
        cols = insp.get_columns("emp", schema=HIVE_DB)
        print("emp columns:")
        # 每列返回 dict: {'name': ..., 'type': ..., 'nullable': ..., 'default': ...}
        for c in cols:
            print(f"  {c['name']:<12} {c['type']}")

        # ---- 3. Table 反射 + select ---- #
        section("3. MetaData 反射 Table + Core select")
        # MetaData 是元数据容器, 多个 Table 可挂载到同一个 MetaData 上
        meta = MetaData()
        # Table(name, meta, autoload_with=engine, schema="db_hive")
        # autoload_with 让 SQLAlchemy 在创建 Table 时立刻去数据库反射列定义
        # 反射完成后, emp_t.columns 就是 Column 对象列表, 可用于类型化 SQL
        emp_t = Table("emp", meta, autoload_with=engine, schema=HIVE_DB)
        # 打印 Table 的 repr (含 schema.name)
        print("反射表:", emp_t)
        # 打印所有反射出来的列名
        print("列:", [c.name for c in emp_t.columns])

        # 反射得到的 Table 可以直接参与 select() 构造
        # 但 pyhive 方言不支持 bind 参数, 这里改用 text() + 字面量
        with engine.connect() as conn:
            # 注意: pyhive 方言不支持参数化 bind, 只能用 text() 或字面量
            stmt = text(
                "SELECT emp_id, emp_name, emp_salary FROM db_hive.emp "
                "WHERE emp_salary > 2000"
            )
            # fetchall() 取全部结果
            for row in conn.execute(stmt).fetchall():
                print(row)

        # ---- 4. pandas 通过 SQLAlchemy 读 (无 UserWarning) ---- #
        section("4. pd.read_sql(sql, engine)  (无 UserWarning)")
        # 与阶段 2 不同, 这里传入的是 SQLAlchemy Engine, pandas 不再发警告
        df = pd.read_sql("SELECT * FROM emp", engine)
        # 清理 "emp." 前缀
        df.columns = [c.split(".")[-1] for c in df.columns]
        # Int64 是 pandas 的可空整数扩展类型, 兼容 Hive 的 NULL
        df["emp_id"] = df["emp_id"].astype("Int64")
        df["dept_id"] = df["dept_id"].astype("Int64")
        # Decimal -> float
        df["emp_salary"] = df["emp_salary"].astype(float)
        print("dtypes:")
        print(df.dtypes)
        print()
        print("前 5 行:")
        print(df.head())

        # ---- 5. 字面量查询 (pyhive 不支持参数化 bind, 这里用 f-string 拼接) ---- #
        section("5. 字面量查询: 部门 = 40 (对比案例 4 的参数化方式)")
        with engine.connect() as conn:
            # 注意: pyhive 方言不支持参数化 bind, 这里用字面量
            dept = 40
            # f-string 拼接 dept 进 SQL, 简单但有 SQL 注入风险 (演示用)
            stmt = text(f"SELECT * FROM emp WHERE dept_id = {dept}")
            for row in conn.execute(stmt).fetchall():
                print(row)
            # 给出生产建议
            print("\n[说明] pyhive 方言不支持 :param 形式的 bind, ")
            print("       生产中如需参数化, 建议改用原生 pyhive.Connection.cursor.execute(sql, params)")

        # ---- 6. engine.begin() 事务 (DDL) ---- #
        section("6. engine.begin() 事务 --- 演示建/删临时视图")
        # engine.begin() 与 engine.connect() 的区别:
        #   - begin() 在退出 with 时自动 commit
        #   - 出异常时自动 rollback
        # 这里用于事务性 DDL (CREATE + DROP 必须配对)
        with engine.begin() as conn:
            try:
                # 防御性: 先确保视图不存在
                conn.execute(text("DROP VIEW IF EXISTS v_emp_demo"))
                # 创建一张只包含高薪员工的视图
                conn.execute(text(
                    "CREATE VIEW v_emp_demo AS "
                    "SELECT emp_id, emp_name, emp_salary FROM emp WHERE emp_salary > 2000"
                ))
                print("[OK] CREATE VIEW v_emp_demo")
                # 验证视图可读
                rows = conn.execute(text("SELECT * FROM v_emp_demo")).fetchall()
                print(f"v_emp_demo 行数: {len(rows)}")
            except Exception as e:
                # 视图如果走 MR 会失败
                print(f"[提示] 视图可能需要 MR: {str(e).splitlines()[0]}")
            finally:
                # 无论是否成功都尝试 DROP, 防止视图残留污染环境
                try:
                    conn.execute(text("DROP VIEW IF EXISTS v_emp_demo"))
                    print("[OK] DROP VIEW v_emp_demo")
                except Exception:
                    # DROP 也失败就只能忽略, 不影响主流程退出
                    pass

        # ---- 7. ORM Session 风格 (可选) ---- #
        section("7. ORM-style session.execute (教学演示)")
        # ORM Session 是 SQLAlchemy 2.x 推荐的高级 API
        # 即便没有定义 ORM Model, 也能通过 session.execute(text(sql)) 执行原生 SQL
        from sqlalchemy.orm import Session
        # Session(engine) 进入上下文自动 close
        with Session(engine) as session:
            # 用字面量查询, 不触发 MR
            result = session.execute(text("SELECT * FROM emp LIMIT 1"))
            # fetchone() 返回单行元组
            row = result.fetchone()
            print("first row:", row)
            # result.keys() 返回列名列表
            print("row keys :", result.keys())

        print("\n[完成] SQLAlchemy + pyhive 案例结束.")
    finally:
        # dispose() 关闭连接池中的所有连接, 释放底层 socket
        engine.dispose()


# Python 入口惯例
if __name__ == "__main__":
    main()

4.5 阶段 4a: 04_pyspark_hdfs_analysis.py

将下面这段代码保存为 04_pyspark_hdfs_analysis.py:

python 复制代码
# -*- coding: utf-8 -*-
# 指定源文件编码: utf-8, 允许源码中直接出现中文字符
"""hive-app 教学项目 --- 阶段 4a: PySpark + HDFS 数据源分析.

================================================================================
学习目标
================================================================================
- 掌握 SparkSession 的本地构造 (``local[*]``) 与 HDFS 环境变量配置
- 用 ``spark.read.csv(path, schema=...)`` 读取 HDFS 文件, 显式定义 schema
- 熟练使用 DataFrame API: ``groupBy`` / ``agg`` / ``pivot`` / ``orderBy`` /
  ``Window`` / ``withColumn`` / ``when`` / ``percentile_approx``
- 理解 ``df.cache()`` 在多次聚合场景下的作用

================================================================================
前置学习
================================================================================
``03_sqlalchemy_pyhive_engine.py`` --- SQLAlchemy 反射与 Connection 模式

================================================================================
数据源
================================================================================
HDFS 路径 ``hdfs:///db_hive/emp/emp.csv`` (5 列, 15 行, 无表头, UTF-8,
含中文, 最后一行 dept_id 为空).

列定义
======
- emp_id      INT
- emp_name    STRING
- emp_job     STRING
- emp_salary  DECIMAL(8,2)  (CSV 中是字符串, 自行转 decimal)
- dept_id     INT           (可空)

运行
====
::

    source hive-app/venv/bin/activate
    export JAVA_HOME=/home/lhz/opt/jdk-21
    export HADOOP_HOME=/home/lhz/opt/hadoop-3
    export HADOOP_CONF_DIR=/home/lhz/opt/hadoop-3/etc/hadoop
    pip install pyspark
    python hive-app/04_pyspark_hdfs_analysis.py

================================================================================
下一步
================================================================================
``05_pyspark_hive_analysis.py`` --- 相同分析逻辑, 数据源换成 Hive, 通过 PyHive 桥接
"""

# 启用 PEP 563 风格的延迟注解求值, 避免循环引用与启动开销
from __future__ import annotations

# 引入 os 标准库: 用于环境变量与目录创建
import os
# 引入 shutil 标准库: 用于递归删除目录
import shutil
# 引入 sys 标准库: 用于 sys.exit 传递退出码
import sys

# 引入 PySpark DataFrame / SparkSession / Window
from pyspark.sql import DataFrame, SparkSession, Window
# 引入 PySpark SQL 函数对象 (惯例别名为 F)
from pyspark.sql import functions as F
# 引入 Spark SQL 类型, 用于显式定义 schema
from pyspark.sql.types import (
    DecimalType,    # 对应 Hive DECIMAL(8,2)
    IntegerType,    # 对应 Hive INT
    StringType,     # 对应 Hive STRING
    StructField,    # schema 字段定义
    StructType,     # schema 整体结构
)


# --------------------------------------------------------------------------- #
# 配置
# --------------------------------------------------------------------------- #
# HDFS 上的输入路径, hdfs:/// 是 URI 前缀, 实际指向 namenode 的 /db_hive/emp/emp.csv
HDFS_PATH = "hdfs:///db_hive/emp/emp.csv"
# 以下字段在 HDFS 版不直接使用, 保留是为了与 05_pyspark_hive_analysis.py 保持一致
HIVE_HOST = "lihaozhe"
HIVE_PORT = 10000
HIVE_USER = "lhz"
HIVE_DB = "db_hive"

# Spark 本地 warehouse 目录, 用于 SQL 相关的元数据 (CTAS / CREATE VIEW 等)
# 这里清空是为了防止上次运行的元数据残留
WAREHOUSE_DIR = "/tmp/spark-warehouse-hdfs"
# 保留字段: 在 Hive 版中会用作 metastore_db
METASTORE_DB = "/tmp/spark-metastore-hdfs"


# 显式定义 CSV 的 schema (没有 header, 必须给出列名 + 类型)
# 注意: emp_id 在数据中无空值, 故 nullable=False
#      emp_salary / dept_id 在数据中有空, 故 nullable=True
SCHEMA = StructType([
    StructField("emp_id", IntegerType(), nullable=False),
    StructField("emp_name", StringType(), nullable=True),
    StructField("emp_job", StringType(), nullable=True),
    StructField("emp_salary", DecimalType(8, 2), nullable=True),
    StructField("dept_id", IntegerType(), nullable=True),
])


def build_spark() -> SparkSession:
    """构建 SparkSession (本地模式, 不启用 Hive)."""
    # 清理可能残留的本地元数据, 保证每次启动都是干净状态
    for d in (WAREHOUSE_DIR, METASTORE_DB):
        # 存在就递归删除
        if os.path.exists(d):
            shutil.rmtree(d)
    # 重建 warehouse 目录
    os.makedirs(WAREHOUSE_DIR, exist_ok=True)

    # 链式构造 SparkSession
    spark = (
        SparkSession.builder       # Builder 模式入口
        .appName("emp_analysis_pyspark_hdfs")  # 应用名, UI 上可见
        .master("local[*]")        # 本地模式, * 表示用所有 CPU 核
        .config("spark.sql.warehouse.dir", WAREHOUSE_DIR)  # warehouse 位置
        .config("spark.driver.memory", "1g")                # Driver 堆内存
        # 显式禁用 Spark Connect, 强制走经典 SparkContext
        # 否则 PySpark 4.x 默认会尝试启动 SparkConnectServer 进程, 报错
        .config("spark.connect.enabled", "false")
        .getOrCreate()             # 已存在则复用, 否则新建
    )
    # 把 Spark 内部日志降到 WARN 级别, 减少 INFO 噪音
    spark.sparkContext.setLogLevel("WARN")
    return spark


# --------------------------------------------------------------------------- #
# 通用展示工具
# --------------------------------------------------------------------------- #
def section(title: str) -> None:
    """打印带分隔线的章节标题 (复用本案例的 8 个 section)."""
    # 70 个等号, 比前 3 阶段宽, 因为 Spark 表头更长
    print()
    print("=" * 70)
    print(f"  {title}")
    print("=" * 70)


def show(df: DataFrame, n: int = 50, truncate: bool = False) -> None:
    """DataFrame 美化打印, 调用 df.show() 但限定参数."""
    # n: 最多显示行数; truncate: 是否截断长字段 (False 表示完整显示)
    df.show(n=n, truncate=truncate)


# --------------------------------------------------------------------------- #
# 分析模块
# --------------------------------------------------------------------------- #
def overview(df: DataFrame) -> None:
    """1. 全表概览: 行数 / 总额 / 均值 / 中位数 / 标准差 / 最大 / 最小."""
    section("1. 全表概览")
    # df.agg(...) 接受多个聚合表达式, 返回单行 DataFrame
    s = df.agg(
        F.count("*").alias("行数"),                              # 总行数
        F.sum("emp_salary").alias("薪资总额"),                   # 薪资总和
        F.round(F.avg("emp_salary"), 2).alias("薪资均值"),       # 平均薪资 (保留 2 位)
        F.round(
            F.expr("percentile_approx(emp_salary, 0.5)"), 2     # 中位数 (近似算法)
        ).alias("薪资中位数"),
        F.round(F.stddev("emp_salary"), 2).alias("薪资标准差"),  # 样本标准差
        F.max("emp_salary").alias("最高薪资"),                   # 最大值
        F.min("emp_salary").alias("最低薪资"),                   # 最小值
    )
    show(s)


def by_job(df: DataFrame) -> None:
    """2. 按职位 (emp_job) 统计."""
    section("2. 按职位 (emp_job) 统计")
    out = (
        df.groupBy("emp_job")          # 按职位分组
        .agg(
            F.count("*").alias("人数"),                # 每个职位的人数
            F.sum("emp_salary").alias("薪资总额"),
            F.round(F.avg("emp_salary"), 2).alias("平均薪资"),
            F.max("emp_salary").alias("最高薪资"),
            F.min("emp_salary").alias("最低薪资"),
        )
        .orderBy(F.col("平均薪资").desc())  # 按平均薪资降序
    )
    show(out)


def by_dept(df: DataFrame) -> None:
    """3. 按部门 (dept_id) 统计."""
    section("3. 按部门 (dept_id) 统计")
    out = (
        df.groupBy("dept_id")
        .agg(
            F.count("*").alias("人数"),
            F.sum("emp_salary").alias("薪资总额"),
            F.round(F.avg("emp_salary"), 2).alias("平均薪资"),
        )
        # desc_nulls_last: NULL 值排在最后 (dept_id 为空的员工放在末尾)
        .orderBy(F.col("平均薪资").desc_nulls_last())
    )
    show(out)


def pivot_job_dept(df: DataFrame) -> None:
    """4. 部门 × 职位 人数交叉透视."""
    section("4. 部门 × 职位 人数交叉透视")
    out = (
        df.groupBy("emp_job")          # 行: emp_job
        .pivot("dept_id")              # 列: dept_id 自动展开
        .agg(F.count(F.lit(1)))        # 聚合: 每个交叉单元格的计数
        # 注意: pivot 不支持 count(*), 必须用 lit(1) 显式给出非空表达式
    )
    show(out)


def top_bottom(df: DataFrame, k: int = 5) -> None:
    """5/6. 高薪 Top k 与 低薪 Bottom k."""
    section(f"5. 高薪 Top {k}")
    # orderBy(...desc()).limit(k): 全局 Top k
    show(df.orderBy(F.col("emp_salary").desc()).limit(k), truncate=False)

    section(f"6. 低薪 Bottom {k}")
    # asc() 升序就是最小值在前
    show(df.orderBy(F.col("emp_salary").asc()).limit(k), truncate=False)


def rank_within_dept(df: DataFrame) -> None:
    """7. 各部门内薪资排名 (窗口函数)."""
    section("7. 各部门内薪资排名")
    # Window.partitionBy 按部门分组
    # orderBy 按薪资降序
    w = Window.partitionBy("dept_id").orderBy(F.col("emp_salary").desc())
    # withColumn("部门内排名", row_number().over(w))
    # row_number 给每行在窗口内一个唯一序号, 从 1 开始
    out = (
        df.withColumn("部门内排名", F.row_number().over(w))
        .select("dept_id", "部门内排名", "emp_id", "emp_name", "emp_job", "emp_salary")
        .orderBy(F.col("dept_id").asc_nulls_last(), "部门内排名")
    )
    show(out, truncate=False)


def data_quality(df: DataFrame) -> None:
    """8. 数据质量: 缺失值统计 + 主键重复检测."""
    section("8. 数据质量")

    # 8.1 各列缺失数
    # 用列表推导为每列构造一个 when().otherwise() 求和表达式
    # sum(case when col is null then 1 else 0 end) 等价于 count null
    null_df = df.select([
        F.sum(F.when(F.col(c).isNull(), 1).otherwise(0)).alias(c)
        for c in df.columns
    ])
    section("8.1 各列缺失数")
    show(null_df)

    # 8.2 主键重复检测: 按 emp_id group, 过滤 count>1 的
    section("8.2 主键 emp_id 重复明细")
    dup = (
        df.groupBy("emp_id")
        .agg(F.count("*").alias("重复数"))
        .filter(F.col("重复数") > 1)   # 只保留重复键
    )
    show(dup, truncate=False)
    # 如果有重复, 列出重复键对应的所有行明细
    if dup.count() > 0:
        # collect() 把 Driver 端的聚合结果收集为 list[Row]
        dup_ids = [r["emp_id"] for r in dup.collect()]
        # 用 isin() 把这些 emp_id 对应的所有行拿出来
        show(df.filter(F.col("emp_id").isin(dup_ids)).orderBy("emp_id"), truncate=False)


# --------------------------------------------------------------------------- #
# 入口
# --------------------------------------------------------------------------- #
def main() -> None:
    """脚本主流程: 构建 Spark -> 读 HDFS -> 8 个分析维度 -> 停止 Spark."""
    # 先构造 SparkSession
    spark = build_spark()
    # try/finally 保证 spark.stop() 一定被调用
    try:
        print(f"[INFO] Spark version : {spark.version}")
        print(f"[INFO] HDFS input     : {HDFS_PATH}")

        # 链式调用读 HDFS CSV
        df = (
            spark.read                                # DataFrameReader
            .option("header", "false")                # CSV 无表头
            .schema(SCHEMA)                           # 用前面定义的 schema
            .csv(HDFS_PATH)                           # 路径
        )
        # count() 触发一次完整计算, 顺便验证数据已经成功读取
        print(f"[OK] 拉取 {df.count()} 行")

        # 依次执行 8 个分析维度
        overview(df)
        by_job(df)
        by_dept(df)
        pivot_job_dept(df)
        top_bottom(df, k=5)
        rank_within_dept(df)
        data_quality(df)

        print("\n分析完成.")
    finally:
        # spark.stop() 关闭 SparkSession, 释放 JVM 进程
        spark.stop()


# Python 入口惯例
# sys.exit(main()) 把 main() 的返回值作为进程退出码
if __name__ == "__main__":
    sys.exit(main())

4.6 阶段 4b: 05_pyspark_hive_analysis.py

将下面这段代码保存为 05_pyspark_hive_analysis.py:

python 复制代码
# -*- coding: utf-8 -*-
# 指定源文件编码: utf-8, 允许源码中直接出现中文字符
"""hive-app 教学项目 --- 阶段 4b: PySpark + Hive 数据源分析.

================================================================================
学习目标
================================================================================
- 看清 Spark 4.x 与 Hive 4.x metastore 的 Thrift API 不兼容问题
- 掌握 "Hive 作数据源, PyHive 取数 + Spark 算力" 的工程化桥接方案
- 体会 Decimal 类型规范化 (避免 Schema 校验失败)
- 与阶段 4a 对比: 相同分析逻辑, 仅数据源不同

================================================================================
前置学习
================================================================================
``04_pyspark_hdfs_analysis.py`` --- HDFS 数据源 + Spark DataFrame API

================================================================================
数据源
================================================================================
Hive 表 ``db_hive.emp`` (HiveServer2 = lihaozhe:10000).

================================================================================
实现方式
================================================================================
本机环境 (Hive 4.2 metastore + Spark 4.1) 存在已知的 HMS Thrift API 不兼容问题:
Spark 4.x 自带的 Hive 客户端调 ``get_table`` 会被 Hive 4.2 metastore 拒绝
(``Invalid method name: 'get_table'``). 直接 ``spark.read.table()`` / ``spark.sql("DESCRIBE ...")``
都会失败.

因此采用 "Hive 作数据源, PyHive 取数 + Spark 算力" 的工程化方案:

    +-----------+      Thrift       +-----------+   DataFrame   +--------------+
    | Hive 4.2  |  <--------------> |   PyHive  |  ---------->  | Spark 4.1    |
    | HS2:10000 |     SQL 查询       |  (client) |    rows       | (local mode) |
    +-----------+                    +-----------+               +--------------+

- 数据来源是 Hive (满足 "Hive 作数据源" 的诉求)
- 全部聚合/排序/窗口/透视均由 Spark DataFrame API 完成
- 不依赖 pandas / numpy

字段
====
- emp_id      INT
- emp_name    STRING
- emp_job     STRING
- emp_salary  DECIMAL(8,2)
- dept_id     INT

运行
====
::

    source hive-app/venv/bin/activate
    export JAVA_HOME=/home/lhz/opt/jdk-21
    export HADOOP_HOME=/home/lhz/opt/hadoop-3
    pip install pyhive thrift pure-sasl thrift_sasl pyspark
    python hive-app/05_pyspark_hive_analysis.py

================================================================================
完成 --- 学习路径建议
================================================================================
回头对比 ``01_pyhive_sql_basics.py`` ~ ``03_sqlalchemy_pyhive_engine.py``,
理解 "同一份数据 → 4 种访问方式" 的取舍:

1. 原生 SQL   : 最直接, 但聚合需 MR (本机不可用)
2. +pandas    : 快速本地分析, DBAPI 模式有 UserWarning
3. +SQLAlchemy: 工程化首选, 支持反射与 ORM
4. +PySpark   : 大数据 + 复杂分析, 数据源 HDFS 或 Hive 皆可
"""

# 启用 PEP 563 风格的延迟注解求值, 避免循环引用与启动开销
from __future__ import annotations

# 引入 os 标准库: 用于目录创建与判断
import os
# 引入 shutil 标准库: 用于递归删除目录
import shutil
# 引入 sys 标准库: 用于退出码
import sys
# 引入 Decimal 类型: 用于规范化 Hive 返回的 Decimal 字段
from decimal import Decimal
# 引入 typing.Any: 用于 fetch_from_hive 返回值的元素类型标注
from typing import Any

# 引入 PyHive 用于从 Hive 取数
from pyhive import hive
# 引入 PySpark DataFrame / SparkSession / Window
from pyspark.sql import DataFrame, SparkSession, Window
# 引入 PySpark SQL 函数
from pyspark.sql import functions as F
# 引入 PySpark SQL 类型
from pyspark.sql.types import (
    DecimalType,    # 对应 Hive DECIMAL(8,2)
    IntegerType,    # 对应 Hive INT
    StringType,     # 对应 Hive STRING
    StructField,    # schema 字段定义
    StructType,     # schema 整体结构
)


# --------------------------------------------------------------------------- #
# 配置
# --------------------------------------------------------------------------- #
# HiveServer2 主机名
HIVE_HOST = "lihaozhe"
# HiveServer2 Thrift 端口
HIVE_PORT = 10000
# 连接用户名
HIVE_USER = "lhz"
# 数据库
HIVE_DB = "db_hive"
# 表名 (不带 db 前缀, 后续 SQL 里拼成 db_hive.emp)
HIVE_TABLE = "emp"
# 认证模式
HIVE_AUTH = "NONE"

# Spark 本地 warehouse 目录 (与 04 版隔开, 避免元数据冲突)
WAREHOUSE_DIR = "/tmp/spark-warehouse-hive"

# 显式 schema, 必须与 Hive 表一致
SCHEMA = StructType([
    StructField("emp_id", IntegerType(), nullable=False),
    StructField("emp_name", StringType(), nullable=True),
    StructField("emp_job", StringType(), nullable=True),
    StructField("emp_salary", DecimalType(8, 2), nullable=True),
    StructField("dept_id", IntegerType(), nullable=True),
])


# --------------------------------------------------------------------------- #
# 数据获取 (PyHive -> Python rows)
# --------------------------------------------------------------------------- #
def fetch_from_hive() -> list[tuple]:
    """用 PyHive 走 Thrift 从 HiveServer2 取 emp 表全量数据.

    返回值: list[tuple], 每个 tuple 对应一行, 列顺序与 SELECT 一致.
    """
    # 用 pyhive 建立 Thrift 连接
    conn = hive.Connection(
        host=HIVE_HOST,
        port=HIVE_PORT,
        username=HIVE_USER,
        database=HIVE_DB,
        auth=HIVE_AUTH,
    )
    try:
        # 用 cursor 执行 SELECT *
        with conn.cursor() as cursor:
            cursor.execute(f"SELECT * FROM {HIVE_TABLE}")
            # fetchall() 一次取回所有行 (list[tuple])
            rows = cursor.fetchall()
    finally:
        # 关闭 Thrift 连接
        conn.close()

    # 规范化行数据, 让每一列的类型与 SCHEMA 完全对齐
    # 原因: PyHive 返回 Decimal('800.00'), float, int 混合,
    #       Spark schema 校验对 DecimalType 严格, 必须归一化
    out = []
    for r in rows:
        normalized = []
        for v in r:
            if isinstance(v, Decimal):
                # quantize(Decimal("0.01")) 把 Decimal 量化到 2 位小数
                # 例如 Decimal('800') -> Decimal('800.00')
                normalized.append(Decimal(v).quantize(Decimal("0.01")))
            elif v is None:
                # 空值直接保留 None
                normalized.append(None)
            elif isinstance(v, int):
                # 整数透传
                normalized.append(v)
            else:
                # 字符串 / 其他透传 (emp_name, emp_job 是 str)
                normalized.append(v)
        out.append(tuple(normalized))
    return out


# --------------------------------------------------------------------------- #
# Spark Session
# --------------------------------------------------------------------------- #
def build_spark() -> SparkSession:
    """构建本地模式 SparkSession (不启用 Hive support, 避免 API 不兼容)."""
    # 清理可能残留的 warehouse 目录
    if os.path.exists(WAREHOUSE_DIR):
        shutil.rmtree(WAREHOUSE_DIR)
    os.makedirs(WAREHOUSE_DIR, exist_ok=True)

    # 注意: 这里 **没有** 调用 .enableHiveSupport()
    # 因为 Spark 4.x 自带的 Hive 客户端与 Hive 4.2 metastore API 不兼容
    spark = (
        SparkSession.builder
        .appName("emp_analysis_pyspark_hive")
        .master("local[*]")
        .config("spark.sql.warehouse.dir", WAREHOUSE_DIR)
        .config("spark.driver.memory", "1g")
        # 显式禁用 Spark Connect, 强制走经典 SparkContext
        .config("spark.connect.enabled", "false")
        .getOrCreate()
    )
    spark.sparkContext.setLogLevel("WARN")
    return spark


# --------------------------------------------------------------------------- #
# 通用展示
# --------------------------------------------------------------------------- #
def section(title: str) -> None:
    """打印带分隔线的章节标题."""
    print()
    print("=" * 70)
    print(f"  {title}")
    print("=" * 70)


def show(df: DataFrame, n: int = 50, truncate: bool = False) -> None:
    """DataFrame 美化打印."""
    df.show(n=n, truncate=truncate)


# --------------------------------------------------------------------------- #
# 分析模块 (与 HDFS 版本一致, 保证输出一致)
# --------------------------------------------------------------------------- #
def overview(df: DataFrame) -> None:
    """1. 全表概览."""
    section("1. 全表概览")
    s = df.agg(
        F.count("*").alias("行数"),
        F.sum("emp_salary").alias("薪资总额"),
        F.round(F.avg("emp_salary"), 2).alias("薪资均值"),
        F.round(F.expr("percentile_approx(emp_salary, 0.5)"), 2).alias("薪资中位数"),
        F.round(F.stddev("emp_salary"), 2).alias("薪资标准差"),
        F.max("emp_salary").alias("最高薪资"),
        F.min("emp_salary").alias("最低薪资"),
    )
    show(s)


def by_job(df: DataFrame) -> None:
    """2. 按职位 (emp_job) 统计."""
    section("2. 按职位 (emp_job) 统计")
    out = (
        df.groupBy("emp_job")
        .agg(
            F.count("*").alias("人数"),
            F.sum("emp_salary").alias("薪资总额"),
            F.round(F.avg("emp_salary"), 2).alias("平均薪资"),
            F.max("emp_salary").alias("最高薪资"),
            F.min("emp_salary").alias("最低薪资"),
        )
        .orderBy(F.col("平均薪资").desc())
    )
    show(out)


def by_dept(df: DataFrame) -> None:
    """3. 按部门 (dept_id) 统计."""
    section("3. 按部门 (dept_id) 统计")
    out = (
        df.groupBy("dept_id")
        .agg(
            F.count("*").alias("人数"),
            F.sum("emp_salary").alias("薪资总额"),
            F.round(F.avg("emp_salary"), 2).alias("平均薪资"),
        )
        .orderBy(F.col("平均薪资").desc_nulls_last())
    )
    show(out)


def pivot_job_dept(df: DataFrame) -> None:
    """4. 部门 × 职位 人数交叉透视."""
    section("4. 部门 × 职位 人数交叉透视")
    out = (
        df.groupBy("emp_job")
        .pivot("dept_id")
        .agg(F.count(F.lit(1)))
    )
    show(out)


def top_bottom(df: DataFrame, k: int = 5) -> None:
    """5/6. 高薪 Top k 与 低薪 Bottom k."""
    section(f"5. 高薪 Top {k}")
    show(df.orderBy(F.col("emp_salary").desc()).limit(k), truncate=False)

    section(f"6. 低薪 Bottom {k}")
    show(df.orderBy(F.col("emp_salary").asc()).limit(k), truncate=False)


def rank_within_dept(df: DataFrame) -> None:
    """7. 各部门内薪资排名."""
    section("7. 各部门内薪资排名")
    w = Window.partitionBy("dept_id").orderBy(F.col("emp_salary").desc())
    out = (
        df.withColumn("部门内排名", F.row_number().over(w))
        .select("dept_id", "部门内排名", "emp_id", "emp_name", "emp_job", "emp_salary")
        .orderBy(F.col("dept_id").asc_nulls_last(), "部门内排名")
    )
    show(out, truncate=False)


def data_quality(df: DataFrame) -> None:
    """8. 数据质量."""
    section("8. 数据质量")

    null_df = df.select([
        F.sum(F.when(F.col(c).isNull(), 1).otherwise(0)).alias(c)
        for c in df.columns
    ])
    section("8.1 各列缺失数")
    show(null_df)

    section("8.2 主键 emp_id 重复明细")
    dup = (
        df.groupBy("emp_id")
        .agg(F.count("*").alias("重复数"))
        .filter(F.col("重复数") > 1)
    )
    show(dup, truncate=False)
    if dup.count() > 0:
        dup_ids = [r["emp_id"] for r in dup.collect()]
        show(df.filter(F.col("emp_id").isin(dup_ids)).orderBy("emp_id"), truncate=False)


# --------------------------------------------------------------------------- #
# 入口
# --------------------------------------------------------------------------- #
def main() -> None:
    """主流程: 构建 Spark -> PyHive 取数 -> createDataFrame -> 8 维分析 -> 停止 Spark."""
    spark = build_spark()
    try:
        print(f"[INFO] Spark version       : {spark.version}")
        print(f"[INFO] Hive source         : jdbc:hive2://{HIVE_HOST}:{HIVE_PORT}/{HIVE_DB}.{HIVE_TABLE}")
        print(f"[INFO] Bridge              : PyHive (Thrift)")

        # 1) PyHive 拉数
        rows: list[tuple[Any, ...]] = fetch_from_hive()
        print(f"[OK] 从 Hive 拉取 {len(rows)} 行")

        # 2) Spark 建 DataFrame
        # createDataFrame(data, schema) 把 Python 列表转成 Spark DataFrame
        # rows 里的元素已被规范化为 Decimal(8,2) / int / str / None, 与 SCHEMA 严格对齐
        df = spark.createDataFrame(rows, schema=SCHEMA)
        # cache(): 把 DataFrame 物化到内存, 后续 8 次分析复用同一份数据
        # 在本场景下数据量很小, cache 是教学演示用
        df.cache()
        # count(): 触发 lazy evaluation, 让 cache 真正生效
        df.count()

        overview(df)
        by_job(df)
        by_dept(df)
        pivot_job_dept(df)
        top_bottom(df, k=5)
        rank_within_dept(df)
        data_quality(df)

        print("\n分析完成.")
    finally:
        # 释放 Spark 资源
        spark.stop()


# Python 入口惯例
if __name__ == "__main__":
    sys.exit(main())

第 5 章: 编写 2 个 spark-submit 脚本

5.1 本地模式: spark_submit_local.sh

将下面这段脚本保存为 spark_submit_local.sh, 然后 chmod +x spark_submit_local.sh:

bash 复制代码
#!/usr/bin/env bash
# ==============================================================================
# hive-app 教学项目 --- Spark 本地模式提交脚本
# ==============================================================================
# 用途:
#   把本地的 PySpark 脚本当作普通程序运行, 集群管理器为 "local[N]".
#   适合教学/开发阶段单机调试.
#
# 与 cluster 脚本的区别:
#   --master local[*]   : 全部线程在当前进程内运行, 不连接 ResourceManager
#   --deploy-mode client: Driver 跑在提交节点 (本机), 适合交互式日志
#
# 用法:
#   bash spark_submit_local.sh <script.py> [args...]
#
# 例子:
#   bash spark_submit_local.sh 04_pyspark_hdfs_analysis.py
#   bash spark_submit_local.sh 05_pyspark_hive_analysis.py
# ==============================================================================

# set -e: 任何命令失败立刻退出
set -e

# ------------------------------------------------------------------------------
# 0. 校验参数
# ------------------------------------------------------------------------------
# $1 是脚本第一个位置参数, -z 判断字符串为空
if [ -z "$1" ]; then
  # 把用法输出到 stderr, 让上层脚本可以区分正常输出与错误
  echo "Usage: $0 <spark_script.py> [args...]" >&2
  # 退出码 2 表示参数错误 (与 bash 惯例一致)
  exit 2
fi

# ------------------------------------------------------------------------------
# 1. 路径解析
# ------------------------------------------------------------------------------
# 当前脚本所在目录的绝对路径 (即使从其他目录调用也能解析)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Hive-app 项目根目录 (与脚本同级)
PROJECT_DIR="$SCRIPT_DIR"

# 待提交的 PySpark 脚本 (首个参数), 解析为绝对路径
SPARK_SCRIPT="$SCRIPT_DIR/$1"
# 把它从参数列表里移除, 后续用 "$@" 把剩余参数透传给 PySpark
shift

# 校验目标脚本存在且可读
if [ ! -f "$SPARK_SCRIPT" ]; then
  echo "[ERROR] 文件不存在: $SPARK_SCRIPT" >&2
  exit 3
fi

# ------------------------------------------------------------------------------
# 2. 环境变量
# ------------------------------------------------------------------------------
# 这些变量是 Hadoop / Spark / Hive 客户端必须依赖的
# 如果已在 ~/.bashrc 或调用方 export 过, 这里赋值会沿用 (若已为空则用默认值)
export JAVA_HOME="${JAVA_HOME:-/home/lhz/opt/jdk-21}"
export HADOOP_HOME="${HADOOP_HOME:-/home/lhz/opt/hadoop-3}"
# HADOOP_CONF_DIR 指向 Hadoop 客户端配置, 内含 core-site.xml / hdfs-site.xml
export HADOOP_CONF_DIR="${HADOOP_CONF_DIR:-/home/lhz/opt/hadoop-3/etc/hadoop}"
# 让 pyspark 找得到 hive-site.xml, 这是 Spark 访问 HiveServer2 元数据所必需的
export HIVE_HOME="${HIVE_HOME:-/home/lhz/opt/hive-4}"
# SPARK_HOME 是 spark-submit 启动的位置, 若系统 PATH 已有 spark-submit 可省略
export SPARK_HOME="${SPARK_HOME:-/home/lhz/opt/spark-4}"

# 把 HADOOP 用户配置目录加入 PYTHONPATH, 让 PyHive / PySpark 都能 import
export PYTHONPATH="${PYTHONPATH:-}:${HADOOP_HOME}/etc/hadoop"

# ------------------------------------------------------------------------------
# 3. Python 环境
# ------------------------------------------------------------------------------
# 优先使用虚拟环境内的 Python 解释器, 避免污染系统 Python
VENV_PY="${PROJECT_DIR}/venv/bin/python"
if [ ! -x "$VENV_PY" ]; then
  echo "[ERROR] 未找到虚拟环境 Python: $VENV_PY" >&2
  echo "        请先执行: python -m venv venv" >&2
  exit 4
fi

# 找到 spark-submit 可执行文件
SPARK_SUBMIT="${SPARK_HOME}/bin/spark-submit"
if [ ! -x "$SPARK_SUBMIT" ]; then
  echo "[ERROR] 未找到 spark-submit: $SPARK_SUBMIT" >&2
  exit 5
fi

# ------------------------------------------------------------------------------
# 4. 资源参数 (教学场景下都偏小, 方便单机跑)
# ------------------------------------------------------------------------------
# --master local[*]    : 用所有 CPU 核在当前进程跑 Spark
# --driver-memory 1g   : Driver 堆内存
# --conf spark.sql.shuffle.partitions=4 : 减少分区数, 加速小数据测试
# --conf spark.ui.showConsoleProgress=false : 关闭进度条, 输出更干净
SPARK_OPTS=(
  "--master" "local[*]"
  "--driver-memory" "1g"
  "--conf" "spark.sql.shuffle.partitions=4"
  "--conf" "spark.ui.showConsoleProgress=false"
  # 强制走经典 SparkContext, 不启用 Spark Connect (Spark 4.x 默认 API mode 取决于 spark.api.mode)
  # 否则 PySpark 会去加载 spark-connect jar, 本地缺失时抛 ClassNotFoundException
  "--conf" "spark.api.mode=classic"
  "--conf" "spark.connect.enabled=false"
)

# ------------------------------------------------------------------------------
# 5. 执行
# ------------------------------------------------------------------------------
echo "[INFO] PROJECT_DIR  : $PROJECT_DIR"
echo "[INFO] SPARK_SCRIPT : $SPARK_SCRIPT"
echo "[INFO] JAVA_HOME    : $JAVA_HOME"
echo "[INFO] HADOOP_HOME  : $HADOOP_HOME"
echo "[INFO] SPARK_HOME   : $SPARK_HOME"
echo "[INFO] VENV_PY      : $VENV_PY"
echo "[INFO] 透传参数     : $*"
echo "----------------------------------------------------------------"

# 把虚拟环境 bin 加到 PATH 最前, 让 spark-submit 启动的 Python 解释器是 venv 的
# 这样 pyspark 能 import 到 venv 内已安装的 pandas / pyhive / sqlalchemy 等
export PATH="$PROJECT_DIR/venv/bin:$PATH"

# 强制让 PySpark 走经典 SparkContext, 不启用 Spark Connect
# Spark 4.x 默认 API mode 取决于 SPARK_API_MODE 环境变量
# (--conf spark.api.mode 会被 Spark 视为非法 SQLConf 而忽略)
export SPARK_API_MODE="classic"

# 最终调用: spark-submit <opts> <script> <args>
# 双引号包裹数组元素以保留空格, "${SPARK_OPTS[@]}" 展开为多个独立参数
"$SPARK_SUBMIT" \
  "${SPARK_OPTS[@]}" \
  "$SPARK_SCRIPT" \
  "$@"

5.2 集群模式: spark_submit_cluster.sh

将下面这段脚本保存为 spark_submit_cluster.sh, 然后 chmod +x spark_submit_cluster.sh:

bash 复制代码
#!/usr/bin/env bash
# ==============================================================================
# hive-app 教学项目 --- Spark Standalone / YARN 集群模式提交脚本
# ==============================================================================
# 用途:
#   把本地的 PySpark 脚本提交到远端 Spark 集群 (Standalone 或 YARN).
#   Driver 会作为独立进程在集群中运行, 适合生产环境.
#
# 支持模式 (通过 SPARK_MASTER 环境变量切换):
#   - spark://host:7077    : Spark Standalone
#   - yarn                 : Hadoop YARN
#   - k8s://https://host   : Kubernetes (需要额外配置)
#
# 与 local 脚本的区别:
#   --master spark://...   : 连接到集群 ResourceManager / Master
#   --deploy-mode cluster  : Driver 跑在集群内某一 Worker, 提交方只负责拉起
#   --executor-memory      : Executor 堆内存
#   --num-executors        : Executor 数量
#
# 用法:
#   export SPARK_MASTER="spark://master:7077"
#   bash spark_submit_cluster.sh <script.py> [args...]
#
# 例子:
#   bash spark_submit_cluster.sh 04_pyspark_hdfs_analysis.py
#   bash spark_submit_cluster.sh 05_pyspark_hive_analysis.py
# ==============================================================================

# set -euo pipefail 比 set -e 更严格:
#   -e  : 任一命令失败即退出
#   -u  : 引用未定义变量即报错
#   -o pipefail : 管道中任一命令失败即整体失败
set -euo pipefail

# ------------------------------------------------------------------------------
# 0. 校验参数
# ------------------------------------------------------------------------------
# $1 是脚本第一个位置参数, -z 判断字符串为空
if [ -z "${1:-}" ]; then
  echo "Usage: SPARK_MASTER=spark://host:7077 $0 <spark_script.py> [args...]" >&2
  exit 2
fi

# ------------------------------------------------------------------------------
# 1. 路径解析
# ------------------------------------------------------------------------------
# 当前脚本所在目录的绝对路径 (即使从其他目录调用也能解析)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Hive-app 项目根目录 (与脚本同级)
PROJECT_DIR="$SCRIPT_DIR"

# 待提交的 PySpark 脚本 (首个参数), 解析为绝对路径
SPARK_SCRIPT="$SCRIPT_DIR/$1"
# 把它从参数列表里移除, 后续用 "$@" 把剩余参数透传给 PySpark
shift

# 校验目标脚本存在且可读
if [ ! -f "$SPARK_SCRIPT" ]; then
  echo "[ERROR] 文件不存在: $SPARK_SCRIPT" >&2
  exit 3
fi

# ------------------------------------------------------------------------------
# 2. 环境变量
# ------------------------------------------------------------------------------
# Hive 客户端必须依赖的环境变量 (即使 Driver 在集群跑, 也得在提交端设置,
# 因为 spark-submit 本地会拉起 Driver 的 Python 进程)
export JAVA_HOME="${JAVA_HOME:-/home/lhz/opt/jdk-21}"
export HADOOP_HOME="${HADOOP_HOME:-/home/lhz/opt/hadoop-3}"
export HADOOP_CONF_DIR="${HADOOP_CONF_DIR:-/home/lhz/opt/hadoop-3/etc/hadoop}"
export HIVE_HOME="${HIVE_HOME:-/home/lhz/opt/hive-4}"
export SPARK_HOME="${SPARK_HOME:-/home/lhz/opt/spark-4}"

# SPARK_MASTER 必填, 默认值仅作 fallback, 实际使用时请显式 export
export SPARK_MASTER="${SPARK_MASTER:-spark://localhost:7077}"

# ------------------------------------------------------------------------------
# 3. 依赖分发
# ------------------------------------------------------------------------------
# 集群模式下需要把 Python 虚拟环境打包, 避免每个 Executor 重新安装
# 教学场景下用 --py-files 传递单个脚本即可
PY_FILES=(
  # PyHive 取数脚本中若 import 了自定义模块, 在此添加
  # "${PROJECT_DIR}/some_helper.py"
)

# --jars: 把额外的 jar 加入 Spark classpath
# 集群模式访问 Hive 4 时, 需要 hive-exec / hive-metastore 等 jar
JARS=(
  "${HIVE_HOME}/lib/hive-exec-*.jar"
  "${HIVE_HOME}/lib/hive-metastore-*.jar"
  "${HIVE_HOME}/lib/hive-common-*.jar"
  "${HIVE_HOME}/lib/hive-serde-*.jar"
)

# glob 展开 (${JARS[@]} 仍含通配符, spark-submit 内部不会展开)
# 所以这里显式展开成真实存在的文件
JARS_EXPANDED=()
for pattern in "${JARS[@]}"; do
  # shellcheck disable=SC2206  (允许数组 split)
  expanded=( $pattern )
  if [ ${#expanded[@]} -gt 0 ] && [ -e "${expanded[0]}" ]; then
    JARS_EXPANDED+=( "${expanded[0]}" )
  fi
done

# ------------------------------------------------------------------------------
# 4. 资源参数 (集群场景下通常更大)
# ------------------------------------------------------------------------------
# --master spark://... : 连接到 Spark 集群 Master
# --deploy-mode cluster: Driver 跑在集群某 Worker 节点
# --executor-memory 2g : 每个 Executor 堆内存
# --executor-cores 2   : 每个 Executor 使用 2 核
# --num-executors 4    : 启动 4 个 Executor
# --driver-memory 2g   : Driver 进程堆内存
SPARK_OPTS=(
  "--master" "${SPARK_MASTER}"
  "--deploy-mode" "cluster"
  "--driver-memory" "2g"
  "--executor-memory" "2g"
  "--executor-cores" "2"
  "--num-executors" "4"
  "--conf" "spark.sql.shuffle.partitions=8"
  "--conf" "spark.ui.showConsoleProgress=false"
  "--conf" "spark.yarn.maxAppAttempts=2"
  # 禁用 Spark Connect, 集群中若未部署 spark-connect 服务会报错
  "--conf" "spark.connect.enabled=false"
)

# 如果有 jar 就追加 --jars 参数 (spark-submit 接受逗号分隔列表)
if [ ${#JARS_EXPANDED[@]} -gt 0 ]; then
  # IFS=, 把数组元素拼成逗号分隔字符串
  JARS_CSV="$( IFS=,; echo "${JARS_EXPANDED[*]}" )"
  SPARK_OPTS+=( "--jars" "$JARS_CSV" )
fi

# 如果有 py-files 就追加 --py-files
if [ ${#PY_FILES[@]} -gt 0 ]; then
  PY_FILES_CSV="$( IFS=,; echo "${PY_FILES[*]}" )"
  SPARK_OPTS+=( "--py-files" "$PY_FILES_CSV" )
fi

# ------------------------------------------------------------------------------
# 5. 执行
# ------------------------------------------------------------------------------
echo "[INFO] PROJECT_DIR  : $PROJECT_DIR"
echo "[INFO] SPARK_SCRIPT : $SPARK_SCRIPT"
echo "[INFO] SPARK_MASTER : $SPARK_MASTER"
echo "[INFO] JAVA_HOME    : $JAVA_HOME"
echo "[INFO] HADOOP_HOME  : $HADOOP_HOME"
echo "[INFO] HIVE_HOME    : $HIVE_HOME"
echo "[INFO] 透传参数     : $*"
echo "----------------------------------------------------------------"

# 把虚拟环境 bin 加到 PATH 最前, 让 spark-submit 启动的 Python 解释器是 venv 的
# 这样 pyspark 能 import 到 venv 内已安装的 pandas / pyhive / sqlalchemy 等
export PATH="$PROJECT_DIR/venv/bin:$PATH"

# 找到 spark-submit 可执行文件
SPARK_SUBMIT="${SPARK_HOME}/bin/spark-submit"
if [ ! -x "$SPARK_SUBMIT" ]; then
  echo "[ERROR] 未找到 spark-submit: $SPARK_SUBMIT" >&2
  exit 5
fi

# 最终调用: spark-submit <opts> <script> <args>
# cluster 模式下 spark-submit 提交后立即返回, 输出在 cluster WebUI 查看
"$SPARK_SUBMIT" \
  "${SPARK_OPTS[@]}" \
  "$SPARK_SCRIPT" \
  "$@"

第 6 章: 验证整套项目

6.1 文件清单

执行以下命令, 应当得到与本章开头相同的目录树:

bash 复制代码
cd /home/lhz/opt/hive-app
ls -la

预期输出 (关键文件):

text 复制代码
-rw-r--r-- 1 lhz lhz   273  requirements.txt
-rw-r--r-- 1 lhz lhz   463  emp.csv
-rw-r--r-- 1 lhz lhz  11K   hive_connection_.py
-rw-r--r-- 1 lhz lhz  12K   01_pyhive_sql_basics.py
-rw-r--r-- 1 lhz lhz  11K   02_pandas_pyhive_dbapi.py
-rw-r--r-- 1 lhz lhz  13K   03_sqlalchemy_pyhive_engine.py
-rw-r--r-- 1 lhz lhz  13K   04_pyspark_hdfs_analysis.py
-rw-r--r-- 1 lhz lhz  14K   05_pyspark_hive_analysis.py
-rwxr-xr-x 1 lhz lhz  6.0K  spark_submit_local.sh
-rwxr-xr-x 1 lhz lhz  7.0K  spark_submit_cluster.sh
drwxr-xr-x 6 lhz lhz  4.0K  venv/

6.2 阶段 0: 连接诊断

bash 复制代码
source /home/lhz/opt/hive-app/venv/bin/activate
python /home/lhz/opt/hive-app/hive_connection_.py

预期: Step 1~4 全部打印 [OK], 末尾提示进入下一阶段.

6.3 阶段 1: PyHive 原生 SQL

bash 复制代码
python /home/lhz/opt/hive-app/01_pyhive_sql_basics.py

预期: 9 个 section 中 1~5 与 9 成功, 6/7/8 标注 [跳过] (MR 不可用).

6.4 阶段 2: pandas + DBAPI

bash 复制代码
python /home/lhz/opt/hive-app/02_pandas_pyhive_dbapi.py

预期: 6 个 section 全部跑通, 第 6 步 df.to_sql 标注 [预期失败].

6.5 阶段 3: SQLAlchemy 引擎

bash 复制代码
python /home/lhz/opt/hive-app/03_sqlalchemy_pyhive_engine.py

预期: 7 个 section 全部跑通, 第 6 步视图创建可能 [提示] (MR 不可用).

6.6 阶段 4a: PySpark + HDFS

bash 复制代码
bash /home/lhz/opt/hive-app/spark_submit_local.sh 04_pyspark_hdfs_analysis.py

预期: exit code 0, 输出 8 个分析维度 (全表概览 / 按职位 / 按部门 / 透视 / Top k / Bottom k / 排名 / 数据质量).

6.7 阶段 4b: PySpark + Hive

bash 复制代码
bash /home/lhz/opt/hive-app/spark_submit_local.sh 05_pyspark_hive_analysis.py

预期: 与 4a 输出一致, 数据源改为 Hive.


附录: 文件清单与依赖版本

完整文件清单

路径 大小 用途
requirements.txt ~270 B Python 依赖清单
emp.csv ~460 B 15 行员工示例数据
hive_connection_.py ~11 KB 阶段 0: 连接诊断
01_pyhive_sql_basics.py ~12 KB 阶段 1: 原生 SQL
02_pandas_pyhive_dbapi.py ~11 KB 阶段 2: pandas + DBAPI
03_sqlalchemy_pyhive_engine.py ~13 KB 阶段 3: SQLAlchemy
04_pyspark_hdfs_analysis.py ~13 KB 阶段 4a: PySpark + HDFS
05_pyspark_hive_analysis.py ~14 KB 阶段 4b: PySpark + Hive
spark_submit_local.sh ~6 KB spark-submit 本地模式
spark_submit_cluster.sh ~7 KB spark-submit 集群模式

已验证的依赖版本

复制代码
pandas             3.0.3
pyspark            4.1.2
pyarrow            24.0.0
grpcio             1.81.1
grpcio-status      1.81.1
zstandard          0.25.0
pyhive             0.7.0
thrift             0.23.0
pure-sasl          0.6.2
thrift_sasl        0.4.3
sqlalchemy         2.0.51

跨平台说明

  • Linux / WSL: 完全兼容, 上面所有命令直接可用.
  • macOS : 需将 apt-get 改为 brew install, 路径从 /home/lhz 改为 /Users/xxx.
  • Windows (原生): 建议使用 WSL2, 或使用 Git Bash + Anaconda Python 3.12.