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 install 与 python 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.