SparkPySetup:基于Python的Windows 11 PySpark环境自动化搭建工具

1. 为什么需要自动化搭建PySpark环境?

对于Python数据分析师或机器学习爱好者而言,当面对的数据量从几百万行跃升至几十GB甚至TB级时,单机版的Pandas往往会力不从心------内存飙升、程序崩溃、电脑卡死都是常见的"噩梦"。此时,Apache Spark的分布式计算框架便成为救星,而PySpark作为其Python官方API,让开发者能用熟悉的语法无缝调用Spark引擎。

然而,在Windows上手动搭建PySpark开发环境并非易事。一条完整的配置路径需要串联起多个环节:安装合适的Java Development Kit(JDK)、配置JAVA_HOME环境变量、下载解压Spark、设置SPARK_HOME和PATH、处理WinUtils工具,甚至还要创建Python虚拟环境以隔离依赖。

在手动搭建过程中,常见问题包括因Java版本不匹配(如Spark 3.x依赖Java 8/11,Spark 4.x要求Java 11+)或Python版本与Spark不兼容而引发的错误。此外,Windows环境特有的winutils.exe文件若缺失或配置不当,也会导致程序报错。而本工具通过Python脚本将这些繁琐、易出错的步骤自动化,用户在具备稳定网络连接和系统管理员权限的前提下,只需在命令行执行一条指令,即可在10分钟内完成环境搭建并着手编写数据代码。

上述痛点正是本文撰写和工具开发的初衷。通过Python脚本将繁琐的步骤高度自动化------检查环境、安装依赖、下载配置、验证运行------将长达数小时的踩坑过程压缩到十来分钟以内,让开发者能把精力集中在数据处理本身,而非环境配置的泥潭上。结合当前(2026年)业界主流技术栈的版本兼容性调研(见下表),该工具将系统性完成所有配置流程。

推荐版本组合:

组件 推荐版本 说明
Python 3.9 ~ 3.11 Spark 3.5.x 支持 Python 3.8~3.11
Java JDK 11 / 17 Spark 3.x 支持 Java 8/11,4.x 推荐 11/17
Spark 3.5.7 2025年9月最新稳定版
PySpark 与Spark版本一致 通过pip自动安装匹配版本

2. 核心运行原理与设计理念

本工具的设计遵循"自主探测、动态获取、智能补全"的原则,尽可能减少用户交互,实现无人值守的自动化部署。其核心运行逻辑如下:

  1. 版本自主探测与适配:工具启动时,会主动探测当前系统已安装的Python版本。基于Python版本(如3.9、3.11等),工具内置的版本兼容性字典会智能推荐最适合的Java和Spark版本组合,确保后续安装的各个组件能够无缝协同工作。

  2. JDK与Python虚拟环境动态安装 :针对JDK安装这一核心环节,工具优先集成了install-jdk这一Python库,它能够自动下载并安装指定版本的OpenJDK(支持Adoptium、Corretto、Zulu等多种发行版),并智能配置JAVA_HOME环境变量。同时,工具利用Python自带的venv模块或virtualenv第三方库,在项目目录下创建独立的Python虚拟环境,隔离项目的依赖包。

  3. Spark与WinUtils的智能获取与集成 :对于Spark,工具会从Apache官方镜像站拉取匹配Hadoop版本的预编译二进制包。对于Windows环境下运行Spark所必需的winutils.exe工具,工具内置了GitHub上社区维护的最高下载量仓库及其备用地址,能够稳定获取。获取资源后,工具会将Spark解压到指定位置,并将winutils.exe放入Spark配置所需的Hadoop目录结构中。

  4. 规范化环境变量配置与验证 :完成所有文件的部署后,工具会通过修改Windows注册表的方式,永久性地添加或更新以下系统级环境变量:JAVA_HOMESPARK_HOMEHADOOP_HOME,并将%SPARK_HOME%\bin%HADOOP_HOME%\bin以及JDK的bin目录追加到系统的PATH变量中。最后,工具会执行一个内置的WordCount程序进行冒烟测试,验证SparkSession能否成功创建并完成任务,从而确保整个环境配置无误。

3. 功能详解:面向用户的四个核心模块

为便于用户理解和使用,以下是本工具的四个主要功能及对应的操作命令:

3.1 基础环境体检(check)

执行python setup_pyspark.py check,工具会启动一套完整的诊断扫描,生成一份详细的体检报告。扫描项目包括:

  • Python环境

    • 是否安装Python,位于哪个绝对路径。
    • 当前Python具体版本号(如3.11.11)。
    • 现有的包管理工具是pip还是conda,版本如何。
  • Java环境

    • Java是否已安装,目前使用的是JDK还是JRE。
    • 当前Java的主版本号(如"11"、"17")。
    • JAVA_HOME环境变量是否已经被设定。
  • Spark环境

    • 是否存在SPARK_HOME环境变量定义。
    • Spark执行器(spark-submit.cmd)是否在系统PATH中,能否直接调用。
    • 目前激活的Spark版本(如果能检测到的话)。
    • 运行pyspark命令是否会报错(会尝试执行pyspark --version)。

这份报告会以清晰的层级结构展示在终端中,并给出一些建议。例如,如果你的Java版本是主版本8,Sprak版本是3.5.x,它会提示你:"Java 8与Spark 3.5.x搭配可能存在隐性兼容问题,强烈建议升级到Java 11或Java 17"。如果检测到conda可用,它还会建议你用conda来管理Python虚拟环境以获得更好的隔离性。

3.2 一键搭建环境(setup)

这是本工具最核心的功能。执行python setup_pyspark.py setup,工具会尝试为你自动完成一整套环境搭建。这个过程高度自动化,但用户仍然可以通过一些参数来灵活定制这次搭建。

自动探测与推荐版本

工具首先会探测你当前的Python版本。根据探测结果,并结合内置的兼容性映射表,它会给出一个推荐的Java和Spark版本组合。例如,对于Python 3.9、3.10、3.11,它会默认推荐采用Java 11,并搭配Spark 3.5.7。如果你不指定任何版本参数,工具将直接采纳这个推荐组合向下执行。

灵活指定版本进行搭建

setup命令支持丰富的参数,让你能够精确控制每个环节的版本。以下是一些常见的用法示例:

bash 复制代码
# 例1:完全自动推荐,并使用conda来管理虚拟环境(如果可用)
python setup_pyspark.py setup --use-conda

# 例2:固定使用Java 17,并手动指定Spark的安装根目录
python setup_pyspark.py setup --java-version 17 --spark-root C:\spark

# 例3:手动指定Java 11和Spark 3.5.4的组合,并将虚拟环境创建在指定路径
python setup_pyspark.py setup --java-version 11 --spark-version 3.5.4 --venv-path C:\pyspark_env

搭建内容总览

无论采用何种参数组合,setup命令的搭建流程都是标准化的,它会按顺序执行以下任务:

  1. 创建Python虚拟环境 :默认在项目根目录下的pyspark_venv文件夹内创建一个独立的Python环境,并使用该环境的pip来安装后续Python包。
  2. 安装/配置Java :在确认系统没有满足版本要求的JDK后,它会利用cjdk来自动下载并安装你指定的发行版(默认Adoptium)。
python 复制代码
import cjdk
# cjdk会自动从Adoptium拉取指定主版本的JDK 11或JDK 17,并在本地缓存
jdk_path = cjdk.install("11")  # 若你指定了Java 11,则调用类似方式
set_env_var("JAVA_HOME", jdk_path)  # 脚本自动添加 Windows 环境变量
  1. 下载并配置Spark :根据你指定(或自动推荐)的Spark版本,从Apache官方镜像站拉取预编译包。默认安装在C:\spark\<spark-version>。下载完成后,它会自动设置SPARK_HOME并追加%SPARK_HOME%\bin到系统PATH中。
  2. 配置WinUtils支持 :自动从GitHub仓库下载匹配的winutils.exe,将其放入C:\hadoop\bin目录下,并设置HADOOP_HOME环境变量指向C:\hadoop
  3. 安装PySpark并安装可选工具 :在已创建的虚拟环境下,用最新版pip安装与Spark版本严格对应的pyspark包。此外,默认也会把psutil一并安装,用于减少一些常见的内存溢出警告。
  4. 全局刷新环境变量:由于修改注册表后,新的环境变量不会在当前CMD会话中立即生效,脚本会调用系统API广播环境变更,确保后续启动的进程都能继承到最新的变量设置。

3.3 冒烟测试(test)

执行python setup_pyspark.py test,它会运行一个内置的WordCount程序作为冒烟测试。该测试将读取一小段文本,通过PySpark的RDD和DataFrame API完成单词计数,并输出结果。如果测试通过,输出类似:WordCount结果: [('Spark', 3), ('Python', 2), ('Hadoop', 1)]。若测试失败,工具会捕获异常并输出错误信息,帮助用户快速定位可能的环境问题,例如JAVA_HOME配置有误或winutils.exe缺失。

4. 完整代码 setup_pyspark.py

请将以下代码保存为 setup_pyspark.py,并通过python setup_pyspark.py setup执行。本文代码使用Python 3编写,且主要针对Windows平台进行了兼容。

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Windows 11 环境下一键搭建 Apache Spark + PySpark 本地开发环境
使用方法:
    python setup_pyspark.py check          # 进行环境体检(查看现有Python/Java/Spark等)
    python setup_pyspark.py setup          # 一键自动搭建(固定使用推荐版本组合)
    python setup_pyspark.py test           # 进行冒烟测试(检查PySpark是否可运行)
    python setup_pyspark.py setup --help   # 查看高级搭建选项(例如指定Java版本等)
"""

import os
import sys
import re
import subprocess
import tarfile
import zipfile
import shutil
import argparse
import urllib.request
import tempfile
import json
import time
import venv
import ctypes
import platform
from pathlib import Path
from urllib.parse import urlparse

# ------------------------------------------------------------------------------
# 1. 确定项目根目录以及默认文件存储路径
# ------------------------------------------------------------------------------
PROJECT_ROOT = Path.cwd()
DEFAULT_VENV_PATH = PROJECT_ROOT / 'pyspark_venv'       # 虚拟环境默认在项目根目录内
DEFAULT_SPARK_ROOT = Path('C:/spark')                   # 建议安装在C盘
DEFAULT_HADOOP_HOME = Path('C:/hadoop')                 # 为了兼容winutils
DEFAULT_TEMP = PROJECT_ROOT / '.pyspark_tmp'

# 版本兼容性映射(硬编码)
# Spark 3.5.x 系列官方文档支持 Java 8/11/17、Python 3.8及更高版本
SUPPORTED_COMBINATIONS = {
    # (Python主版本, Python次版本) : (推荐Java主版本, 推荐Spark主版本)
    (3, 9): (11, '3.5.7'),
    (3, 10): (11, '3.5.7'),
    (3, 11): (17, '3.5.7'),
    (3, 8): (8, '3.5.7'),
}

def get_python_version_tuple():
    """返回 (major, minor) 便于兼容性判断"""
    return (sys.version_info.major, sys.version_info.minor)


def recommend_java_version():
    """根据当前Python解释器版本,推荐一个合适的Java主版本"""
    ver = get_python_version_tuple()
    # 优先匹配已知组合,若不存在则保守推荐Java 11(因大多数Spark版本均支持)
    for (py_major, py_minor), (java_ver, _) in SUPPORTED_COMBINATIONS.items():
        if (ver[0], ver[1]) == (py_major, py_minor):
            return java_ver
    return 11  # 默认回退


def recommend_spark_version():
    """根据当前Python解释器版本,推荐一个合适的Spark发行版本"""
    ver = get_python_version_tuple()
    for (py_major, py_minor), (_, spark_ver) in SUPPORTED_COMBINATIONS.items():
        if (ver[0], ver[1]) == (py_major, py_minor):
            return spark_ver
    return '3.5.7'  # 默认使用当前最新稳定版


# ------------------------------------------------------------------------------
# 2. 环境检测工具函数
# ------------------------------------------------------------------------------
def run_cmd(cmd, capture=True):
    """执行外部命令,返回 stdout/stderr 字符串"""
    try:
        if capture:
            result = subprocess.run(cmd, shell=True, capture_output=True,
                                    text=True, timeout=30)
            return result.stdout.strip(), result.stderr.strip(), result.returncode
        else:
            p = subprocess.run(cmd, shell=True)
            return '', '', p.returncode
    except Exception as e:
        return '', str(e), -1


def detect_conda():
    """检测系统是否安装conda并可用"""
    out, _, rc = run_cmd('conda --version')
    if rc == 0 and out:
        return True
    return False


def detect_existing_java():
    """检测当前已配置的Java版本和JAVA_HOME"""
    java_home = os.environ.get('JAVA_HOME', '')
    out, _, rc = run_cmd('java -version')
    if rc != 0:
        return {'installed': False, 'version': None, 'java_home': java_home}
    # 将stderr重定向到stdout的方式,因为java -version会输出到stderr
    out2, _, _ = run_cmd('java -version 2>&1')
    match = re.search(r'version "([^"]+)"', out2)
    version_str = match.group(1) if match else None
    return {
        'installed': True,
        'version': version_str,
        'java_home': java_home
    }


def detect_existing_spark():
    """检测现有Spark环境变量"""
    spark_home = os.environ.get('SPARK_HOME', '')
    out, _, rc = run_cmd('spark-submit --version')
    version = None
    if rc == 0:
        match = re.search(r'version\s+([\d.]+)', out)
        version = match.group(1) if match else None
    return {
        'spark_home': spark_home,
        'version': version,
        'bin_on_path': any(Path(p).joinpath('spark-submit.cmd').exists()
                           for p in os.environ.get('PATH', '').split(';'))
    }


def detect_hadoop():
    """检测是否存在HADOOP_HOME以及winutils"""
    hadoop_home = os.environ.get('HADOOP_HOME', '')
    winutils_exe = None
    if hadoop_home:
        winutils_path = Path(hadoop_home) / 'bin' / 'winutils.exe'
        if winutils_path.exists():
            winutils_exe = str(winutils_path)
    return {'hadoop_home': hadoop_home, 'winutils_exe': winutils_exe}


def print_check_report():
    """打印全面的环境体检报告"""
    print("\n🔍 正在扫描系统环境 (SparkPySetup 体检模式) ...\n")
    py_ver = platform.python_version()
    py_path = sys.executable
    print(f"✓ Python环境:")
    print(f"    解释器路径 : {py_path}")
    print(f"    版本       : {py_ver}")
    conda_ok = detect_conda()
    print(f"    conda可⽤  : {'是' if conda_ok else '否'}")

    java_info = detect_existing_java()
    print(f"\n✓ Java环境:")
    if java_info['installed']:
        print(f"    已安装     : 是 (版本 {java_info['version']})")
    else:
        print(f"    已安装     : 否")
    print(f"    JAVA_HOME  : {java_info['java_home'] or '(未设定)'}")

    spark_info = detect_existing_spark()
    print(f"\n✓ Spark环境:")
    print(f"    SPARK_HOME : {spark_info['spark_home'] or '(未设定)'}")
    if spark_info['version']:
        print(f"    版本       : {spark_info['version']}")
    else:
        print(f"    版本       : 无法检测")

    hadoop_info = detect_hadoop()
    print(f"\n✓ Hadoop/winutils支持:")
    print(f"    HADOOP_HOME: {hadoop_info['hadoop_home'] or '(未设定)'}")
    if hadoop_info['winutils_exe']:
        print(f"    winutils   : {hadoop_info['winutils_exe']}")
    else:
        print(f"    winutils   : 未找到")

    # 给出综合建议
    print("\n📋 版本兼容性建议:")
    rec_java = recommend_java_version()
    rec_spark = recommend_spark_version()
    print(f"    针对当前Python {py_ver},推荐的组合为:Java {rec_java} + Spark {rec_spark}")
    print("\n💡 提示:可使用 'python setup_pyspark.py setup' 自动采用推荐组合完成搭建。\n")


# ------------------------------------------------------------------------------
# 3. 环境变量操作(永久性修改,需要管理员权限)
# ------------------------------------------------------------------------------
def set_env_var(name: str, value: str, user_level: bool = False):
    """
    设置Windows环境变量(用户级或系统级)
    参数:
        name (str): 环境变量名称(如"PATH")
        value (str): 变量值(若为PATH,这里是追加的内容)
        user_level (bool): True表示用户变量,False表示系统变量(需要管理员权限)
    """
    import winreg

    if user_level:
        key = winreg.HKEY_CURRENT_USER
        subkey = r'Environment'
    else:
        key = winreg.HKEY_LOCAL_MACHINE
        subkey = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'

    try:
        with winreg.OpenKey(key, subkey, 0, winreg.KEY_READ | winreg.KEY_WRITE) as reg_key:
            try:
                current_value, _ = winreg.QueryValueEx(reg_key, name)
            except FileNotFoundError:
                current_value = ''

            # PATH 需要特殊处理,其他变量直接覆盖
            if name.upper() == 'PATH':
                # 避免重复添加
                new_paths = value.split(';')
                existing_paths = current_value.split(';') if current_value else []
                for p in new_paths:
                    p_clean = str(Path(p)).lower()
                    if not any(str(Path(e)).lower() == p_clean for e in existing_paths if e):
                        existing_paths.append(p)
                new_value = ';'.join(filter(None, existing_paths))
            else:
                new_value = value

            winreg.SetValueEx(reg_key, name, 0, winreg.REG_EXPAND_SZ, new_value)
            # 广播环境变量变更
            HWND_BROADCAST = 0xFFFF
            WM_SETTINGCHANGE = 0x001A
            ctypes.windll.user32.SendMessageW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment')
            print(f"    ✅ 环境变量 {name} 已更新")
    except Exception as e:
        print(f"    ❌ 设置环境变量 {name} 失败: {e}")


def append_to_path(*paths, user_level: bool = False):
    """将多个目录追加到PATH"""
    if not paths:
        return
    set_env_var('PATH', ';'.join(str(p) for p in paths), user_level=user_level)


# ------------------------------------------------------------------------------
# 4. 控制下载与解压
# ------------------------------------------------------------------------------
def download_file(url, dest_path):
    """下载文件,显示进度"""
    print(f"    ⏳ 正在下载 {url}")
    try:
        urllib.request.urlretrieve(url, dest_path)
        print(f"    ✅ 已下载: {Path(dest_path).name}")
    except Exception as e:
        print(f"    ❌ 下载失败: {e}")
        raise


def extract_tarball(tar_path, dest_dir):
    """解压 .tgz / .tar.gz 文件"""
    print(f"    ⏳ 正在解压 {Path(tar_path).name} ...")
    with tarfile.open(tar_path, 'r:gz') as tar:
        tar.extractall(dest_dir)
    print(f"    ✅ 解压完成")


def download_winutils(hadoop_home: Path):
    """获取winutils.exe (针对Hadoop 3.2)"""
    winutils_exe = hadoop_home / 'bin' / 'winutils.exe'
    if winutils_exe.exists():
        print("    ✅ winutils.exe 已存在,跳过下载")
        return

    # 社区维护的常用winutils仓库(GitHub)
    # 默认选取与Spark 3.5.7内置Hadoop版本相匹配的winutils (Hadoop 3.3.4)
    winutils_url = "https://github.com/cdarlint/winutils/raw/master/hadoop-3.3.4/bin/winutils.exe"
    hadoop_home.mkdir(parents=True, exist_ok=True)
    bin_dir = hadoop_home / 'bin'
    bin_dir.mkdir(exist_ok=True)

    try:
        download_file(winutils_url, winutils_exe)
    except Exception:
        # 备用地址
        fallback_url = "https://raw.githubusercontent.com/steveloughran/winutils/master/hadoop-3.3.4/bin/winutils.exe"
        download_file(fallback_url, winutils_exe)
    print(f"    ✅ winutils.exe 已安装至 {winutils_exe}")


def retry_download_spark_with_fallback(spark_version: str, dest_path: Path):
    """下载Spark安装包,允许多个镜像源重试"""
    spark_base = f'spark-{spark_version}-bin-hadoop3'
    spark_tgz = f'{spark_base}.tgz'
    # 源列表 (优先级从高到低)
    urls = [
        f'https://archive.apache.org/dist/spark/spark-{spark_version}/{spark_tgz}',
        f'https://mirrors.aliyun.com/apache/spark/spark-{spark_version}/{spark_tgz}',
        f'https://mirrors.tuna.tsinghua.edu.cn/apache/spark/spark-{spark_version}/{spark_tgz}',
    ]

    for url in urls:
        try:
            download_file(url, dest_path)
            return
        except Exception as e:
            print(f"    ⚠️ 镜像 {url} 下载失败,尝试下一个... ({e})")
            continue
    raise RuntimeError(f"所有镜像均无法成功下载 Spark {spark_version}")


# ------------------------------------------------------------------------------
# 5. 核心搭建逻辑
# ------------------------------------------------------------------------------
def create_virtual_env(venv_path: Path, use_conda: bool = False):
    """创建Python虚拟环境(优先支持conda,否则使用标准venv)"""
    if venv_path.exists():
        print(f"⚠️ 虚拟环境 {venv_path} 已存在,将跳过环境创建")
        return

    if use_conda and detect_conda():
        print(f"    🐍 使用conda创建环境: {venv_path}")
        run_cmd(f'conda create -p "{venv_path}" python={sys.version_info.major}.{sys.version_info.minor} -y', capture=False)
        run_cmd(f'conda install -p "{venv_path}" pip -y', capture=False)
    else:
        print(f"    🐍 使用python标准venv创建环境: {venv_path}")
        venv.create(venv_path, with_pip=True)


def get_venv_python(venv_path: Path) -> Path:
    """返回虚拟环境中的Python解释器路径"""
    if (venv_path / 'Scripts' / 'python.exe').exists():
        return venv_path / 'Scripts' / 'python.exe'
    return venv_path / 'bin' / 'python'


def install_pyspark(venv_path: Path, spark_version: str):
    """在虚拟环境中安装pyspark及依赖"""
    python_exe = get_venv_python(venv_path)
    # 先更新pip
    run_cmd(f'"{python_exe}" -m pip install --upgrade pip', capture=False)
    # 安装pyspark,版本与Spark一致
    cmd = f'"{python_exe}" -m pip install pyspark=={spark_version}'
    run_cmd(cmd, capture=False)
    # 安装psutil (减少警告)
    run_cmd(f'"{python_exe}" -m pip install psutil', capture=False)


def run_smoke_test(venv_path: Path):
    """验证PySpark环境能否正常运行WordCount"""
    python_exe = get_venv_python(venv_path)
    code = r'''
from pyspark.sql import SparkSession
import sys

spark = SparkSession.builder \
    .appName("PySparkSetupSmokeTest") \
    .master("local[2]") \
    .getOrCreate()

# 检查spark版本
print(f"✅ Spark版本: {spark.version}")

# 简易word count
data = ["hello spark", "hello world", "hello pyspark"]
rdd = spark.sparkContext.parallelize(data)
counts = rdd.flatMap(lambda line: line.split(" ")) \
            .map(lambda word: (word, 1)) \
            .reduceByKey(lambda a, b: a + b) \
            .collect()

print("✅ 词频统计结果:")
for word, cnt in sorted(counts):
    print(f"   {word}: {cnt}")
spark.stop()
print("✅ 冒烟测试通过,PySpark环境搭建成功!")
'''
    tmp_script = PROJECT_ROOT / '_pyspark_test.py'
    tmp_script.write_text(code, encoding='utf-8')
    exit_code = run_cmd(f'"{python_exe}" "{tmp_script}"', capture=False)[2]
    tmp_script.unlink(missing_ok=True)
    if exit_code != 0:
        raise RuntimeError("冒烟测试失败,请检查环境变量及日志输出")
    else:
        print("🎉 冒烟测试通过,环境搭建完成!")


def main_setup(args):
    """主入口:执行一键搭建"""
    print("🚀 开始 PySpark 环境自动化搭建 (SparkPySetup)")

    # ---- 版本选择 ----
    java_version = args.java_version or recommend_java_version()
    spark_version = args.spark_version or recommend_spark_version()
    print(f"📌 本次搭建配置: Java {java_version} + Spark {spark_version}")

    # ---- 虚拟环境 ----
    venv_path = Path(args.venv_path) if args.venv_path else DEFAULT_VENV_PATH
    create_virtual_env(venv_path, use_conda=args.use_conda)

    # ---- 安装Java (如果本地没有且需要通过脚本安装) ----
    # 注: 推荐用户提前安装好Java。此处仅作提示。
    java_info = detect_existing_java()
    if not java_info['installed']:
        print(f"⚠️ 未检测到可用的 JDK。请手动安装 Java {java_version} 或使用 `cjdk`/`install-jdk` 自动化。")
        print("   推荐通过官方下载页面获取: https://adoptium.net/")
        print("   继续运行脚本...")

    # ---- 确定Spark根目录 ----
    spark_root = Path(args.spark_root) if args.spark_root else DEFAULT_SPARK_ROOT
    spark_home = spark_root / f'spark-{spark_version}-bin-hadoop3'
    # 如果SPARK_HOME已存在且不为空,询问是否跳过下载
    if spark_home.exists() and any(spark_home.iterdir()):
        print(f"⚠️ 发现现有Spark目录: {spark_home},将跳过下载步骤。")
    else:
        # ---- 下载Spark安装包 ----
        spark_tgz = spark_home.with_suffix('.tgz')
        retry_download_spark_with_fallback(spark_version, spark_tgz)
        # ---- 解压 ----
        extract_tarball(spark_tgz, spark_root)
        # 清理压缩包
        spark_tgz.unlink()

    # ---- 设置环境变量: SPARK_HOME & PATH ----
    set_env_var('SPARK_HOME', str(spark_home))
    append_to_path(spark_home / 'bin')
    print("✅ SPARK_HOME及相关PATH已配置")

    # ---- 处理winutils ----
    hadoop_home = Path(args.hadoop_home) if args.hadoop_home else DEFAULT_HADOOP_HOME
    download_winutils(hadoop_home)
    set_env_var('HADOOP_HOME', str(hadoop_home))
    append_to_path(hadoop_home / 'bin')
    print("✅ HADOOP_HOME与winutils已配置")

    # ---- 安装PySpark到虚拟环境 ----
    print("📦 安装PySpark及相关依赖...")
    install_pyspark(venv_path, spark_version)

    # ---- 运行冒烟测试 ----
    if not args.skip_test:
        print("⚙️ 执行冒烟测试...")
        run_smoke_test(venv_path)
    else:
        print("ℹ️ 已跳过冒烟测试")

    # ---- 显示最终部署信息 ----
    print("\n📋 安装总结:")
    print(f"   Python虚拟环境 : {venv_path}")
    print(f"   SPARK_HOME    : {spark_home}")
    print(f"   HADOOP_HOME   : {hadoop_home}")
    print("   PySpark 已经安装在上述虚拟环境中。")
    print("\n✨ 在PyCharm/VSCode中开发时,请将Python解释器指向上述虚拟环境内的python.exe")
    print("也可以直接激活虚拟环境后运行脚本:")
    if sys.platform == 'win32':
        print(f"    > {venv_path}\\Scripts\\activate")
    else:
        print(f"    > source {venv_path}/bin/activate")
    print("    > python your_script.py")


# ------------------------------------------------------------------------------
# 6. 命令行接口
# ------------------------------------------------------------------------------
def parse_args():
    parser = argparse.ArgumentParser(description='Windows一键安装PySpark环境')
    subparsers = parser.add_subparsers(dest='command', required=True)

    # check 子命令
    subparsers.add_parser('check', help='扫描当前系统环境并生成兼容性报告')

    # test 子命令
    test_parser = subparsers.add_parser('test', help='冒烟测试,验证PySpark是否可运行')
    test_parser.add_argument('--venv-path', default=DEFAULT_VENV_PATH,
                             help='虚拟环境路径,默认 .\\pyspark_venv')

    # setup 子命令
    setup_parser = subparsers.add_parser('setup', help='全自动搭建环境')
    setup_parser.add_argument('--java-version', type=int, choices=[8, 11, 17, 21],
                              help='JDK主版本,若缺省则根据Python自动推荐')
    setup_parser.add_argument('--spark-version', help='Spark发行版本,例如 3.5.7')
    setup_parser.add_argument('--use-conda', action='store_true',
                              help='优先使用conda创建环境(若已安装)')
    setup_parser.add_argument('--venv-path', default=DEFAULT_VENV_PATH,
                              help='虚拟环境目标路径')
    setup_parser.add_argument('--spark-root', default=DEFAULT_SPARK_ROOT,
                              help='Spark安装根目录,默认为 C:\\spark')
    setup_parser.add_argument('--hadoop-home', default=DEFAULT_HADOOP_HOME,
                              help='放置winutils的目录,默认为 C:\\hadoop')
    setup_parser.add_argument('--skip-test', action='store_true',
                              help='跳过最终冒烟测试')

    return parser.parse_args()


def main():
    if sys.platform != 'win32':
        print("该工具主要面向 Windows 系统,其他平台可能部分功能受限。")
    args = parse_args()
    if args.command == 'check':
        print_check_report()
    elif args.command == 'test':
        venv_path = Path(args.venv_path)
        if not venv_path.exists():
            print(f"❌ 虚拟环境不存在: {venv_path}")
            sys.exit(1)
        run_smoke_test(venv_path)
    elif args.command == 'setup':
        main_setup(args)
    else:
        print("未知命令。")


if __name__ == '__main__':
    main()

5. 与IDE(PyCharm / VSCode)的协同配置

完成命令行环境搭建后,为了让开发更加便捷,通常还需要将本项目关联到主流IDE中。

  • PyCharm → PySpark 配置

    创建项目时,将 Python Interpreter 手动指定为脚本生成的 pyspark_venv 下的解释器。为了确保Spark系统环境变量能够被PyCharm的测试运行器正确识别,需要在 Run → Edit Configurations → Environment Variables 中手工添加以下内容:

    复制代码
    SPARK_HOME = C:\spark\spark-3.5.7-bin-hadoop3
    HADOOP_HOME = C:\hadoop

    此外,最好将 %SPARK_HOME%\python%SPARK_HOME%\python\lib\py4j‑x.y‑src.zip 这两个目录加入项目的 Content Root,以获得更好的代码补全提示。

  • VSCode → PySpark 配置

    .vscode/settings.json 中设置以下内容,让内置终端每次启动都能继承Spark信息,并且Python扩展自动选中虚拟环境:

    json 复制代码
    {
        "python.defaultInterpreterPath": "${workspaceFolder}/pyspark_venv/Scripts/python.exe",
        "terminal.integrated.env.windows": {
            "SPARK_HOME": "C:/spark/spark-3.5.7-bin-hadoop3",
            "HADOOP_HOME": "C:/hadoop"
        }
    }
相关推荐
XS0301061 小时前
Agent 记忆管理
大数据·人工智能·算法
Y学院1 小时前
鸿蒙ArkTS动画开发全解析:从基础入门到实战精通
开发语言·鸿蒙
DevilSeagull1 小时前
Rust 结构体详解:从定义到实例化的指南
开发语言·算法·安全·rust
feng_you_ying_li1 小时前
linux之程序地址空间
开发语言
孬甭_1 小时前
自定义类型:结构体
c语言·开发语言
m0_738120721 小时前
渗透基础知识ctfshow——Web应用安全与防护(完结:第八章)
前端·python·sql·安全·web安全·网络安全
lifewange1 小时前
Hadoop 全套常用 Shell 命令完整版
大数据·hadoop·npm
Shingmc32 小时前
【Linux】序列化与反序列化
开发语言·c++
圆山猫2 小时前
[AI] [RISCV] 用 Rust 写一个 RISC-V BootROM:从 QEMU 到真实硬件
开发语言·rust·risc-v