news 2026/10/19 14:59:14

PyInstaller打包Python脚本:从依赖收集到发布避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyInstaller打包Python脚本:从依赖收集到发布避坑指南

很多人问过一个很实在的问题:Python 脚本写完了,为什么不直接把.py文件发给对方?等你真的经历过目标机器没装 Python、装了 Python 但版本对不上、依赖包装了几次都失败、对方还明确要求"给我一个双击就能跑的程序"这些场景之后,就会明白打包这一步根本躲不开。PyInstaller 是我这些年用下来最顺手的打包工具,它能把 Python 解释器、依赖库和资源文件统一组织成可执行程序,让目标机器不需要预装任何环境。

这篇文章不打算写成 PyInstaller 官方文档的翻译,而是想把实际操作中反复踩过的坑、总结出的参数习惯、排查报错的完整链路,以及发布前必须处理的体积和安全软件误报问题,一次性写清楚。无论你是刚写完第一个脚本想打包给同事用,还是在维护一个积累了较多第三方依赖的工具,这篇文章都适合你。

1. 为什么"打包"总是躲不开:从一次交付说起

1.1 目标机器没环境,问题永远比想象的多

先说一个很典型的场景。某次项目里,我用 Python 写了一个数据处理小工具,功能很简单:读取 Excel,做清洗,输出统计报告。开发阶段一切顺利,把脚本发给隔壁团队后,问题接踵而来——对方机器上没有 Python 环境,虽然有 IT 部门的安装包,但需要审批流程,等审批下来项目都结束了。

更麻烦的是,就算装了 Python,也往往是 3.8 或者 3.11 之类和开发版本不一致的解释器,一跑就报语法错误或者依赖缺失。尝试让对方装依赖包,对方回一句"pip 是什么?"那一刻我就确定了,这类工具型脚本必须打包成独立可执行程序,否则永远无法真正交付。

打包的本质,就是把运行这段 Python 程序所需要的全部东西——解释器核心、标准库、第三方依赖、C 扩展库、资源文件——整理到一个目标机器能直接运行的文件集合里。目标机器有没有 Python 无所谓,有没有网也无所谓,只要你给它的文件是完整的,它就能跑。这也是 PyInstaller 这类工具存在的核心价值。

1.2 PyInstaller 为什么是默认选项

打包 Python 程序的工具不止 PyInstaller 一个,我也试过其他方案。py2exe 起步早,但主要面向 Windows,维护节奏较慢;cx_Freeze 的配置方式更偏向传统的 setup.py 风格,对新手不算友好;Nuitka 的思路是先把 Python 代码编译成 C,再编译成机器码,启动性能和反编译难度确实有优势,但打包流程更复杂,对纯工具型项目来说投入产出比不高。

PyInstaller 能成为默认选择,原因很实际:它对现代 Python 版本的支持及时,内置了大量第三方库的 hook,多数常用库不需要额外配置就能收集完整;它的跨平台特性也不错,Windows、macOS、Linux 都有对应版本,在哪个平台上打包就产出哪个平台的可执行程序,逻辑很直接。

通常我的判断标准是:如果目标程序以纯 Python 为主,或者虽然带着 numpy、pandas 这类 C 扩展但不需要极端性能,那么 PyInstaller 就是最省事的选择。如果你确实需要极高的启动性能,或者要交付给安全要求极严格的场景,再考虑 Nuitka 也不迟。

2. PyInstaller 的打包机制:onefile 不是"压缩包"那么简单

2.1 分析、收集、启动:打包过程中到底发生了什么

很多人把 PyInstaller 想象成一个"把文件塞进压缩包"的工具,这个理解过于简单,会导致排查问题时走弯路。实际上,PyInstaller 打包一个脚本大致经过三个阶段:分析依赖、收集文件、生成可执行程序。

分析阶段,PyInstaller 会解析你指定入口脚本的 import 语句,建立一个依赖图。它会沿着模块的导入关系往下走,把用到的纯 Python 模块、扩展模块都找出来。这个静态分析过程并不能覆盖所有情况,比如字符串形式的动态导入就无法被识别,这就是后面要讲的 hidden import 问题的根源。

收集阶段,根据分析结果把对应的模块文件、二进制动态库、数据文件复制到构建目录。每种第三方库还可能需要执行 hook 逻辑——hook 是 PyInstaller 提供的一种扩展机制,本质是一段 Python 脚本,用于告诉打包器"这个库除了主模块之外,还需要额外收集哪些文件"。例如某些库会在运行时动态加载自己的插件,只有 hook 才能把这些插件找全。

最后,生成阶段会写出一个启动程序和必要的运行时文件。启动程序负责初始化 Python 解释器环境,导入主模块,然后执行你的业务逻辑。理解了这个过程,你再去排查"为什么缺某个模块""为什么少了某个 dll",思路就会清晰得多。

2.2 onefile 与 onedir 的取舍

PyInstaller 的-F参数会生成单一可执行文件,很多人习惯一上来就-F,但这未必是正确选择。单文件模式(onefile)的原理是,把一个完整的运行目录整体打包成一个可执行文件,运行时再自动解压到系统临时目录。这意味着程序启动时有额外的解压时间,文件越大启动越慢,而且杀毒软件可能对这个"自己解压自己"的行为比较敏感。

目录模式(onedir)则是生成一个文件夹,里面包含主程序和各依赖文件。启动时不需要解压,速度明显更快,排查问题也方便——缺了什么文件一眼就能看到,甚至可以手动补一个 dll 进去试试。缺点也很直观:交付时是一堆文件,必须打包成 zip 或者安装程序才能发给别人,显得不那么"干净"。

对比维度onefile(单文件)onedir(目录)
启动速度慢,需要解压到临时目录快,直接加载
交付方式一个文件,方便传输多个文件,通常要打包
排查难度困难,运行信息不透明容易,目录结构一目了然
杀软误报概率相对较高相对较低
资源占用运行时会复制全部文件,内存和临时目录占用大直接从原位置加载

我的建议是:开发调试阶段用 onedir,每次修改后快速打包、快速运行、快速看报错;确认稳定之后,再根据交付形式决定是否换成 onefile。如果对方只接受"一个文件",就做 onefile;如果可以接受一个压缩包,onedir 加一个压缩壳其实是更稳妥的选择。

3. 从命令行到 spec:我常用的打包参数与工作流

3.1 命令行参数按组记忆

PyInstaller 的参数不少,但按用途分组后很好记。最基础的一组是入口和输出相关:-F(单文件)、-D(目录,默认)、-n(指定程序名)、--icon(指定图标)。第一组参数决定程序长什么样、叫什么名字。

第二组是运行形态相关:-w表示窗口程序,不弹出控制台;如果不加,启动时会带一个黑色的命令行窗口。写 GUI 程序时记得加-w,否则用户会看到一个莫名其妙的黑窗。如果你需要看程序的 print 输出,调试阶段先不要加-w,这样报错信息能直接显示在控制台里。

第三组是依赖相关:--add-data用于添加资源文件,--hidden-import用于声明那些无法被静态分析到的模块,--exclude-module用于排除绝对用不到的模块以减小体积。这三组是打包成败的关键。

一个比较完整的命令行示例:

pyinstaller -F -w -n DataTool --icon=assets/app.ico \ --add-data "assets;assets" \ --hidden-import plugins.autoloader \ --exclude-module tkinter \ main.py

3.2 spec 文件:把打包配置变成项目的一部分

命令行每次输入一大串容易出错,而且团队协作时无法统一。PyInstaller 在第一次执行后会生成一个与程序同名的.spec文件,这个文件本质是 Python 代码,记录了你本次打包的所有配置。后续直接用pyinstaller DataTool.spec就能按照固定配置重新打包,不需要再敲一遍参数。

我习惯把 spec 文件当作项目的一部分来维护。特别是datas和hiddenimports这两个字段,直接对应命令行的--add-data和--hidden-import,在 spec 里用 Python 列表写起来比命令行更清晰,也支持写路径处理逻辑。一个典型的 spec 文件核心部分长这样:

# DataTool.spec a = Analysis( ['main.py'], pathex=[], binaries=[], datas=[ ('assets', 'assets'), ('models/model.pth', 'models'), ], hiddenimports=[ 'plugins.autoloader', 'xml.etree.ElementTree', ], hookspath=[], runtime_hooks=[], excludes=['tkinter'], )

修改 spec 之后直接执行pyinstaller DataTool.spec即可。官方命令行也能生成 spec,但只要你手动改动过 spec,就不要再用pyinstaller -F main.py那种方式重新生成,否则会覆盖你的手工配置。spec 文件还能集成版本信息、图标甚至自定义 manifest,是大型打包项目的推荐做法。

还有一个很容易忽略的点:打包前务必使用干净的虚拟环境。不要在全局环境打包,因为全局环境里可能安装了几百个包,PyInstaller 虽然不会全部打进去,但依赖分析时容易把不该引入的东西引入进来,导致包体积膨胀,甚至出现奇怪的冲突。我的习惯是每次针对项目创建独立的 venv,只安装pyinstaller和requirements.txt里列出的依赖。

4. Hook、动态导入与第三方库的"暗坑"

4.1 动态导入:静态分析看不见的依赖

PyInstaller 的依赖分析是静态的,它会去读代码里的import和from ... import ...语句,但拿字符串形式的动态导入没有办法。最常见的写法是:

import importlib module_name = "plugins." + plugin_name module = importlib.import_module(module_name)

这种代码在本地运行完全正常,打包后一旦真正走到这行,程序就会直接抛ModuleNotFoundError。因为打包时 PyInstaller 根本不知道plugins包下面还有哪些模块,自然也就不会把它们收进去。

解决思路分两层。如果插件模块是项目内部自己写的、模块名固定,直接在命令行或者 spec 的hiddenimports里把可能出现的模块全部列出来即可。如果插件数量很多,或者将来随时会增加新插件,手动列不是长久之计,可以在代码里用一个显式的加载清单,程序启动时用pkgutil.iter_modules遍历插件目录,同时把遍历结果转换成字符串列表,再在打包配置里通过 hook 收集。

4.2 Hook 机制与第三方库的常见问题

PyInstaller 对大量主流第三方库都准备了 hook。例如pandas、numpy、Pillow、requests这类库,通常能一次打包成功,因为 hook 已经帮你处理了大部分动态加载和二进制文件收集问题。但总有例外,尤其是 GUI 框架和科学计算库。

以 PySide2/PyQt 为例,除了主包之外,还需要 Qt 的插件目录、翻译文件、样式文件等,如果 hook 漏掉某个插件,程序可能在初始化窗口时报Could not find the Qt platform plugin "windows"。这类问题通常需要检查 hook 是否生效,或者手动用--add-data补充插件目录。

另一个常见问题是openpyxl与lxml的组合,某些版本在打包后会因为找不到lxml.etree而崩溃,解决办法是在hiddenimports中显式加入lxml.etree和lxml._elementpath。这类黑暗角落非常多,但思路是一致的:先看报错缺什么,再确认这个模块是否真的被打包进来,最后用 hidden import 或 hook 补上。

如果某个库比较复杂,且官方没有合适 hook,可以自己写一个 hook 文件。PyInstaller 的 hook 就是一个 Python 文件,放在hooks目录下,执行打包时用--additional-hooks-dir指向它。例如你希望把所有 plugins 子模块都收集进去,可以写:

# hook-plugins.py from PyInstaller.utils.hooks import collect_submodules hiddenimports = collect_submodules('plugins')

collect_submodules会遍历plugins包下面的所有子模块并返回列表,这样新增插件后只要保证它位于包目录中,打包时就能自动收集,不用再手动改配置。

5. 资源文件路径与 sys._MEIPASS:写对路径比写对代码更重要

5.1 开发环境路径与打包环境路径的差异

很多程序在本地跑得好好的,一打包就报"找不到文件",原因很简单:开发时用的是相对路径data/config.json,这个路径相对于当前工作目录,也就是终端启动的目录;打包后,工作目录可能是用户的桌面、快捷方式指定的目录,甚至程序双击启动时的系统默认目录,相对路径自然就不成立了。

更关键的是 onefile 模式下,程序运行时会先解压到临时目录,资源文件会被放到这个临时目录内部,与当前工作目录是两个完全不同的地方。PyInstaller 提供了一个运行期变量sys._MEIPASS,指向的就是这些资源文件实际所在的根目录。

处理资源路径的标准写法是在入口脚本最前面加一个路径基点的计算:

import sys import os if getattr(sys, 'frozen', False): BASE_DIR = sys._MEIPASS else: BASE_DIR = os.path.dirname(os.path.abspath(__file__)) asset_path = os.path.join(BASE_DIR, 'assets', 'app.ico')

用sys.frozen判断当前是否处于打包状态,是则使用sys._MEIPASS,否则使用脚本文件自身目录。这样开发时和打包后都能正确定位资源文件。

5.2 用户数据与运行期文件应该放哪里

需要特别区分两类文件:一类是程序自带的资源,例如图标、默认配置模板、模型权重,这类文件应该打包进可执行程序内部,通过--add-data添加;另一类是运行期生成的用户数据,例如日志、用户自定义配置、输出结果,这类文件绝不应该写入sys._MEIPASS目录,因为它是临时目录,程序退出后可能被清理,而且多实例运行时还会互相干扰。

运行期文件的合理位置通常是可执行文件同级的某个子目录,或者当前用户的文档目录。我的习惯是做一个目录选择函数,优先级从可执行文件所在目录开始,找不到再退回用户目录,以保证程序在只读环境下也不会崩溃。

关于大资源文件还有一个提醒:如果你把几百 MB 的模型文件通过 onefile 方式打包,程序启动时不仅要把文件解压到临时目录,还要在内存中维护一份完整数据,启动时间和临时目录占用都会明显上升。对于这种情况,我通常会选择 onedir 模式,或者把大文件放在可执行目录旁边,用运行时路径加载,而不是强行塞进单文件。

6. 一次"No module named xxx"报错的完整排查链路

6.1 先看 warn 文件,再开 DEBUG 日志

打包后运行报ModuleNotFoundError是最常见的问题,但千万不要直接在代码里加打印、重新打包、再运行,那样效率太低。PyInstaller 每次打包会在build目录下生成一个名为warn-程序名.txt的文本文件,里面记录了依赖分析阶段所有无法识别的导入。

我第一次排查这类问题时,发现warn文件里其实早就写着missing module named plugins.calc,只是因为打包本身没有报错,所以一直没看这个文件。之后我的习惯是:所有打包完成后,第一步就打开warn-程序名.txt,用搜索功能查一下有没有和你项目相关的 missing module。

如果需要更详细的日志,可以在打包命令中加上:

pyinstaller main.py --log-level=DEBUG > build.log 2>&1

DEBUG 级别的日志会打印依赖分析的每个模块判定过程,对于定位"为什么某个模块被排除"很有帮助。虽然信息量很大,但搜索关键字比两眼一抹黑强得多。

6.2 从 ModuleNotFoundError 追到系统 DLL

有一类报错表面上也是模块找不到,但根因完全不同。某个同事打包 PySide2 程序后,启动时报ModuleNotFoundError: No module named 'shiboken2',但检查 hidden imports 时已经加了shiboken2,重新打包仍然报错。后来在 DEBUG 日志中发现真正的问题不是模块缺失,而是shiboken2.pyd导入时需要依赖某个 Visual C++ 运行库动态库,运行库里缺少它,导入过程失败,最终错误被包装成了模块找不到。

这类问题尤其在 Windows 上发生率高。C 扩展模块(.pyd文件)本质是动态链接库,它依赖系统运行库,例如vcruntime140.dll、msvcp140.dll。目标机器如果没有安装对应版本的运行库,模块就会加载失败。

处理方式有两种。一种是把运行库动态库也作为二进制文件加入打包:

pyinstaller -F -w --add-binary "C:/Windows/System32/vcruntime140.dll;." main.py

另一种是让目标机器安装官方分发的运行库安装包。两种方法都不复杂,但要注意的是,即使本地运行正常,也不代表目标机器一定正常——开发机往往安装了各种开发工具,系统运行库很全,而普通用户机器可能就是最小环境。

完整的排查链路通常是这样:先复现问题,再打开warn文件和DEBUG日志,查看缺失模块;确认模块确实被打包后,检查是否属于 C 扩展,进一步查看动态库依赖;如果依赖运行库,再决定是随包带上还是要求目标机器安装。整个过程最忌讳的是不做分析、盲目重复打包。

7. 体积、启动速度与安全软件误报:发布前要处理的事

7.1 体积优化的三个方向

用 PyInstaller 打包出来的程序体积往往远超预期,一个几十 KB 的脚本打包后动辄几十 MB,因为其中包含了 Python 解释器和大量标准库。体积优化的方向通常有三个。

第一是使用干净的虚拟环境打包,避免把全局环境里无关的包分析进来。这个前面说过,但值得再强调一次:很多人包体积膨胀,根源就是全局环境太"脏",PyInstaller 在分析依赖时附带了一堆不需要的模块。

第二是使用--exclude-module排除确定用不到的标准库。最典型的是tkinter,很多命令行工具根本不用 GUI,但 PyInstaller 可能因为某些间接引用把它纳入依赖。排除后体积能减少几 MB。操作时需谨慎,确认程序里确实没有使用再排除,否则会出现运行期导入错误。

第三是 UPX 压缩。UPX 是一个可执行文件压缩工具,PyInstaller 支持通过--upx-dir指定 UPX 路径并在打包时使用。压缩后体积能减少不少,实测某些项目可以从 40 MB 压到 25 MB 左右。但 UPX 并不总是安全的,部分动态库或电子证书格式的 DLL 被 UPX 压缩后可能加载失败,某些安全软件也会因为程序被压缩壳包裹而判定为可疑文件。我现在的大多数项目已经不再启用 UPX,优先靠虚拟环境和排除模块来控制体积。

依赖项太多时,还可以把纯 Python 模块打包进标准库压缩格式,这个属于进阶操作,普通项目不需要为省几 MB 增加复杂度。

7.2 启动速度与安全软件误报的处理思路

onefile 的启动速度慢是一个绕不开的缺点。程序体积越大,解压耗时越长,在机械硬盘上尤其明显,甚至会出现双击后好几秒才出现窗口的情况。如果产品对启动速度有要求,推荐用 onedir 模式加一个安装包壳子,安装后生成的目录结构就是运行环境,启动时无需解压。

安全软件误报是另一个容易被低估的问题。PyInstaller 生成的程序普遍包含 Python 解释器特征,加上 onefile 的"自解压"行为,很容易触发某些安全软件的启发式判断。这不是程序有问题,而是行为特征恰好和恶意程序有重合。

我的处理思路主要有几点:一是尽量使用 onedir 模式,减少自解压行为;二是不启用 UPX,避免压缩壳特征;三是为程序申请代码签名证书,签名后的程序信誉度更高,触发误报的概率会明显下降;四是如果误报来自某个安全软件,通常可以联系厂商提交误报申诉,正规流程完善后一般能解决。

这里特别提醒一点:不要为了绕过安全软件去研究对抗手段。遇到误报的正确做法是分析原因、调整打包方式、申请签名、走申诉渠道,而不是继续添加反检测逻辑。后者不仅不合规,也会给你的用户在安全软件侧制造更多弹窗。

8. 一套可复用的自动化打包流程

8.1 目录结构与构建脚本

当项目逐渐稳定,打包就变成一件需要反复执行的事情。每次都手敲命令很容易遗漏,我倾向于把打包流程固化到脚本里,配合 spec 文件形成一套半自动化的构建流程。

推荐的项目目录结构是这样的:

MyTool/ ├── main.py ├── plugins/ │ ├── __init__.py │ └── autoloader.py ├── assets/ │ ├── icon.ico │ └── config.json ├── build.bat ├── requirements.txt ├── MyTool.spec └── .gitignore

build.bat里通常做这几件事:创建并激活虚拟环境、安装依赖、清理旧构建产物、解析资源文件、执行 PyInstaller、最后把产物复制到发布目录。注意清理步骤很重要,因为 PyInstaller 的增量构建偶尔会保留旧文件,导致你测试时运行的是旧版本。

一个简单的 Windows 构建脚本示例:

@echo off set PYTHON_VERSION=3.11 if not exist venv ( py -%PYTHON_VERSION% -m venv venv ) call venv\Scripts\activate.bat pip install -r requirements.txt pip install pyinstaller rd /s /q build dist pyinstaller MyTool.spec rd /s /q publish mkdir publish copy dist\MyTool\* publish\

每次发布前运行一次build.bat,就能保证整个流程是干净、可复现的。

8.2 版本信息写入与最终验证

可执行文件不带版本信息的话,用户在 Windows 文件属性里看到的是一片空白,不利于版本管理。PyInstaller 支持通过--version-file指定一个版本资源文件,用工具生成初始文件后可以手动修改其中的版本号、公司名和文件描述。

所谓公司名和产品名可以填写你的团队项目名,注意不要使用虚构真实机构信息,保持通用描述即可。版本文件的基本格式如下:

VSVersionInfo( ffi=FixedFileInfo( filevers=(1, 0, 0), prodvers=(1, 0, 0), mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0, date=(0, 0) ), kids=[ StringFileInfo([ StringTable('040904B0', [ StringStruct('FileDescription', 'MyTool 数据处理工具'), StringStruct('FileVersion', '1.0.0'), StringStruct('ProductName', 'MyTool'), ]) ]) ] )

最后一步是验证。我会在每次打包完成后,把产物复制到一个没有 Python 也没有任何开发工具的环境(通常用虚拟机)里测试一遍核心流程,确认资源文件路径正常、依赖完整、退出无残留。这个环节比在开发机上反复运行要有价值得多,因为开发机上的环境太齐全,很多问题根本暴露不出来。

我自己的习惯是:把"warn 文件检查—干净环境运行—目录文件完整性确认—杀软首次扫描"这四项做成一份简单的发布检查单,每完成一项打一个勾。有了这套流程,PyInstaller 打包就不再是每次交付前的手忙脚乱,而是一个稳定、可预期、能快速复现的工程环节。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 14:59:08

SpringBoot+Vue健康管理系统全栈项目实战指南

1. 项目整体设计:健康管理系统到底在做什么每年到了毕设季,被问到最多的一个问题就是:“能不能给我一个全栈项目?”这个基于SpringBoot与Vue.js的健康管理系统,就是这类问题的标准答案。技术栈主流、业务逻辑清晰、前后…

作者头像 李华
网站建设 2026/10/19 14:59:04

超快爆炸法合成高熵合金纳米催化剂,高效OER新思路

电催化方向这几年最受关注的素材,绝对绕不开高熵合金。它的典型特征是多主元、高位点、可调性强,理论上可以通过元素组合把催化性能“堆”出来。但高熵合金催化剂真正卡脖子的地方,不是设计,而是制备——常规的高温熔炼和球磨&…

作者头像 李华
网站建设 2026/10/19 14:58:58

配电房在线监测:如何精准捕捉电压不稳与故障苗头

供电这摊事,平时没人觉得它存在,可只要小区里有人喊了一句“我家电压怎么老是不稳”,或者半夜跳闸两次,大家才会突然意识到:原来我们离“用电自由”还有段距离。而真正决定你家灯会不会闪、冰箱会不会无辜停机、电脑能…

作者头像 李华
网站建设 2026/10/19 14:57:27

DeepSeek高效使用指南:9个提示词技巧让AI输出质量翻倍

最近好几个朋友问我同一个问题:“为什么我用DeepSeek写出来的东西总是干巴巴的,你写的却能直接用?”老实说,不是模型差别大,是按下回车之前的那段话差别大。同一个AI,有人用它写周报、写方案、做竞品分析、…

作者头像 李华
网站建设 2026/10/19 14:56:04

医药冷链数据治理实践:从数据使能到AI使能的平台化升级

先讲清楚一件事:医药冷链是我见过对数据最较真的行业之一。温度、湿度、时间、位置、批号,每一环都要对得上,少一秒、偏一度都可能引发连锁后果。而要把这盘散沙变成可信资产,靠传统的数据治理手段已经不太够用了。这也是我今天想…

作者头像 李华
网站建设 2026/10/19 14:56:00

REA建模实战:资源事件代理模型如何搭稳进销存与对账体系

如果你在系统设计笔记里看到“REA”三个字母,很可能是在某个进销存、财务或供应链相关的讨论里。我最近恰好帮一个传统制造业客户梳理数据模型,业务方被月底对账折磨得不轻:采购系统、库存系统、销售系统各有一套台账,合并之后账实…

作者头像 李华