news 2026/10/14 4:29:02

pymssql连接SQL Server排查指南:从安装报错到生产环境稳定连接

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pymssql连接SQL Server排查指南:从安装报错到生产环境稳定连接

用Python连SQL Server,真正的问题往往不在Python代码,而在pymssql这一层。最近我在做一个报表项目,需要按月把业务库的数据拉出来汇总,开发阶段直连测试环境一切正常,代码一到生产环境的服务器上就开始报(20009, 'DB-Lib error message 20009, severity 9: Unable to connect: Adaptive Server is unavailable or does not exist')。如果你也撞上这类SQLServer连接问题,网上能搜到一堆“改这个参数就好了”的碎片方案,但很少有人把底层逻辑串起来讲。这篇文章就把pymssql连接sqlserver的安装、报错、网络链路、加密认证、真实案例一次讲透,适合所有用Python读写SQL Server的人。

1. 先读懂报错:FreeTDS、DB-Lib和pymssql的三层关系

1.1 一条OperationalError里藏着几层信息

很多人第一次看到pymssql报错会懵:错误信息里全是“DB-Lib error message”这种看起来跟Python毫无关系的文字,而且错误本身还是以元组形式出现的,比如(20009, 'DB-Lib error message 20009, severity 9: ...')。要理解这个格式,得先弄清楚pymssql在Python和SQL Server之间扮演的角色。

pymssql并不是纯Python实现的数据库驱动。它底层调用的是FreeTDS这个C语言库,而FreeTDS实现了SQL Server使用的TDS协议——Tabular Data Stream,一种专门用于SQL Server和Sybase数据库的表格数据流协议。pymssql做的只是把Python调用翻译成FreeTDS的函数调用,再把FreeTDS返回的数据结果转成Python对象。所以当你看到报错前缀是“DB-Lib error message”时,这条错误信息根本不是Python生成的,而是FreeTDS最底层的那一层,也就是DB-Lib层给出的。

一条完整的pymssql连接报错通常长这样:

pymssql.OperationalError: (20009, b'DB-Lib error message 20009, severity 9:\\nUnable to connect: Adaptive Server is unavailable or does not exist\\n')

拆开来看,第一个数字是错误码,20009代表“服务器不可达”;后面跟着的一长串字符串包含了严重级别和具体描述。其中的severity 9在TDS协议里表示一个非致命错误,但连接已经被中断或无法建立。理解这个结构的意义在于:你可以根据错误码快速判断问题出在链路哪一段,而不是盲目尝试各种网上看来的参数。

1.2 从Python调用到SQL Server的完整链路

一次pymssql连接请求,在成功拿到数据之前,要依次经过下面这些环节:

  1. pymssql读取server、port、user、password等连接参数;
  2. FreeTDS根据地址发起TCP连接;
  3. 双方通过TDS prelogin阶段协商版本、加密方式、字符集;
  4. SQL Server验证账号密码,返回登录结果;
  5. 登录成功后切换到指定数据库;
  6. 之后才能执行查询语句。

每一个环节出问题,报错的表现都不一样。第2步失败,通常就是20009;第3、4步失败,容易看到TDS版本相关错误、加密错误或登录失败;第5步失败,往往是数据库不存在或没有权限;第6步之后的报错才跟SQL语句本身有关。

所以当你拿到一条连接失败信息时,第一件事不是改代码,而是判断它属于哪个环节。我见过太多人在第6步的SQL问题上反复纠结,结果其实是第2步的防火墙把端口挡了,SQL语句背了黑锅。理解了这条链路,后面的排查才有方向。

2. 安装环节的两类典型失败:缺依赖源码编译与Python版本不匹配

2.1 Linux下源码编译报错与FreeTDS开发包依赖

pymssql在Windows和macOS上通常有现成的预编译wheel包,但在部分Linux环境和较新的Python版本上,pip会尝试从源码编译。源码编译最常见的一个报错是这样:

fatal error: freetds.h: No such file or directory

这个报错的意思很简单:编译pymssql需要FreeTDS的头文件,但你的系统里只有FreeTDS的运行时库(或者连库都没有),缺少开发包。Debian/Ubuntu系统需要安装freetds-dev,CentOS/RHEL系统对应的是freetds-devel。

以Debian/Ubuntu为例:

sudo apt-get update sudo apt-get install -y freetds-dev build-essential python3-dev

CentOS/RHEL则这样处理:

sudo yum install -y freetds-devel gcc python3-devel openssl-devel

如果你看到的报错是Cython相关的问题,或者提示找不到cythonize,同样需要先安装构建依赖。pymssql从源码构建时要依赖Cython把模板代码生成C源文件,版本不满足就会出现这类错误。较新的pymssql源码包通常会在pyproject.toml里声明构建依赖,但你本地的pip版本如果太老,可能不会自动安装这些构建依赖,这时候升级pip能解决大半问题:

pip install --upgrade pip setuptools wheel

2.2 Windows/Mac的wheel安装与版本选型

Windows用户在这个环节踩坑相对少,多数情况下直接pip install pymssql就能装上。但如果你用的是较新的Python版本,比如3.12或3.13,而当前pymssql版本还没有提供对应的预编译wheel,pip会尝试现场编译。Windows现场编译pymssql非常痛苦,你没有Visual Studio的合适工具链,没有FreeTDS开发头文件,基本寸步难行。

我的建议是,遇到这种情况先升级pymssql到最新版本,很多新版本就是为适配新Python而发布的。如果升级之后还是提示No matching distribution found for pymssql,再考虑退回一个更成熟的Python小版本,或者用Docker跑一个官方Python镜像,镜像里的环境通常能顺利装好。

macOS用户则先安装FreeTDS再装pymssql会顺很多:

brew install freetds pip install pymssql

装完之后一定记得验证一下安装结果,这个操作很多人忽略:

import pymssql print(pymssql.__version__) print(pymssql.freetds_version)

pymssql.freetds_version能显示编译进去的FreeTDS版本号。这个信息在排查加密和TDS版本兼容问题时非常有用,我后面会提到。

3. 连不上服务器的三大根因:网络端口、实例解析与TCP/IP协议

3.1 错误码20009和20002的定位分界线

pymssql连接失败时最常出现的两个错误码是20009和20002。很多人把这两个错误混为一谈,觉得反正都是连不上。但它们代表的故障环节完全不同,分清楚能省掉大量无效排查。

错误码典型表现故障环节排查重点
20009Unable to connect: Adaptive Server is unavailable or does not existTCP连接阶段,找不到目标服务器地址写没写错、端口通不通、服务启没启动、实例名能否解析
20002Adaptive Server connection failedTCP连接已建立,但连接过程中断网络稳定性、TDS/加密协商失败、服务端主动断开、负载过高

我自己的经验是:如果报错是20009,先把网络层和服务层查个底朝天,先别碰代码;如果是20002,除了查网络,还要重点关注SQL Server实例的配置和客户端FreeTDS版本。

举例来说,我在某台服务器上排查过一次20009,ping主机能通,telnet 1433端口也通,但pymssql就是报20009。后来发现该服务器上装了多个SQL Server实例,默认实例根本没启用TCP/IP,网络是通的,但那个端口后面没有服务在听。这就是典型的“端口通不等于数据库可达”。

3.2 三步验证:ping通不等于端口通,端口通不等于实例可达

遇到20009,按下面三个步骤排查,基本能覆盖90%的原因。

第一步,验证网络和端口。用telnet或系统自带的工具直接测目标端口:

telnet 192.168.1.100 1433

在Windows PowerShell里可以这样:

Test-NetConnection 192.168.1.100 -Port 1433

如果端口不通,先看防火墙入站规则有没有放行SQL Server端口,再看SQL Server服务有没有启动,最后看SQL Server配置管理器里TCP/IP协议是不是处于启用状态。SQL Server的默认协议设置里,Shared Memory和Named Pipes可能开着,但TCP/IP未必开了,而pymssql走的恰恰是TCP协议。

第二步,确认实例监听端口和连接写法。SQL Server实例分两种:默认实例监听1433,命名实例则可能监听一个动态端口。命名实例的标准写法是主机名加反斜杠加实例名,比如192.168.1.100\REPORT。如果在Python里把它写成'192.168.1.100/REPORT',或者逗号风格'192.168.1.100,1433',解析就会出问题。

第三步,检查SQL Server Browser服务。命名实例要能被客户端用实例名解析到动态端口,靠的是SQL Server Browser服务,它监听UDP 1434。这个服务没启动,或者UDP 1434被防火墙拦截,pymssql解析不了实例名,只能报20009。最省心的办法是把命名实例改成固定端口,或者干脆在连接字符串里直接用IP加端口,跳过实例名解析。

4. 新版SQL Server最容易踩的暗坑:强制加密与TDS版本

4.1 SQL Server强制加密时客户端连接失败的常见表现

这几年遇到越来越多人问:pymssql连SQL Server 2019或2022时,在旧实例上好好的连接代码突然就报错了。问题大概率出在加密设置上。SQL Server较新的版本默认启用了更严格的加密行为,如果客户端使用的FreeTDS版本过老,不支持服务器要求的加密方式,连接会在TDS prelogin阶段被服务器掐断。

这种故障的表现很有迷惑性:错误码可能是20009,也可能是20002,而且不会在报错信息里明确写“加密失败”几个字。你检查网络、端口、账号密码都没问题,但连接就是不稳定,或者时好时坏。

按我的实践经验,处理这类问题要分两头看。服务器端,最推荐的做法是给实例配置一张受信任的证书,让客户端能完成证书校验。如果只是临时调试,在SQL Server配置管理器里把“强制加密”调整为非强制也可以,但生产环境不建议这么做,因为那相当于把数据传输暴露在明文风险下。

客户端这边,FreeTDS 1.2及以上版本支持通过freetds.conf配置加密行为。在配置文件里加上:

[global] encryption = request

encryption = request表示如果服务器支持加密就使用加密,服务器不要求时也不强制。如果你确认服务器开启了强制加密,可以改成require,表示必须加密连接,不加密就连接失败。改完freetds.conf之后,重启Python进程重新连接。

还有一点要特别提醒:pymssql本身没有直接在连接参数里暴露“信任服务器证书”这种开关,所以大多数加密相关的调整最终都落在FreeTDS配置上。排查前先确认一下pymssql.freetds_version输出的版本号,如果版本低于1.2,建议先升级pymssql,很多老版本连加密协商都做不完整。

4.2 TDS版本选不对:什么时候需要显式指定tds_version

TDS版本是另一个容易被忽略的因素。SQL Server多年迭代,每个阶段引入的新特性需要对应的TDS协议版本支持。pymssql允许在连接时指定tds_version参数,如果这个值设置得低于SQL Server实际所需的最低版本,连接同样会出问题。

常见的TDS版本和SQL Server大致对应关系是这样的:

TDS版本对应SQL Server年代说明
7.0SQL Server 2000老协议,不支持很多新类型
7.1SQL Server 2005支持基础新特性
7.2SQL Server 2008增强了一些数据类型支持
7.3SQL Server 2008 R2 / 2012目前大部分pymssql默认区间
7.4SQL Server 2012及更新版本需要较新的FreeTDS
8.0SQL Server 2022及之后新协议,需要FreeTDS 1.4+

如果你连的是SQL Server 2016以上,而pymssql默认用的还是7.0或7.1,可能会遇到两类问题:一是某些数据类型在查询时行为异常,比如新版datetime2、date类型返回的结果不对;二是连接建立后执行某些SQL时报错。这类错误不太容易让人联想到是TDS版本问题,因为它的表现五花八门。

我的建议很直接:连接SQL Server 2012以上,显式指定tds_version='7.3';SQL Server 2016以上,尝试tds_version='7.4'。如果tds_version='7.4'报版本不支持,说明你的FreeTDS版本太老,需要升级pymssql或单独升级FreeTDS。

import pymssql conn = pymssql.connect( server='192.168.1.100', user='sa', password='your_password', database='your_db', tds_version='7.3', )

5. 18456登录失败与中文乱码的定式解法

5.1 18456背后的认证模式问题与排查顺序

错误码18456是SQL Server层返回的登录失败错误,跟网络和端口无关。它的错误信息里通常包含Login failed for user 'xxx',后面跟着一个状态码。很多人只看到18456就开始改密码,但真正的问题可能出在认证模式上。

SQL Server有两种认证模式:Windows认证模式,只允许Windows系统账户通过集成认证登录;混合认证模式,允许Windows认证和SQL Server账号密码认证同时存在。如果你的服务端处于纯Windows认证模式,那不管sa密码多正确,连接时都会收到18456。

检查认证模式最直观的方式是在SSMS里右键服务器属性,找到“安全性”页,看“服务器身份验证”选的是哪种。命令行环境也可以查注册表:

HKLM\SOFTWARE\Microsoft\Microsoft SQL Server\<实例ID>\MSSQLServer\LoginMode

这个键值如果是0,表示Windows认证模式;如果是1,表示混合认证模式。修改后需要重启SQL Server服务才能生效。

确认了认证模式正确,还要检查账号本身的状态。SQL Server登录账号在数据库里可能有这些状态:禁用、密码过期、要求首次登录必须修改密码,或者账号有效但未被授予目标数据库的访问权限。特别是“要求首次登录必须修改密码”这种设置,你如果用密码去连,照样会报18456。

排查顺序我建议固定为:认证模式确认、账号不存在检查、密码正确性检查、密码过期或强制修改检查、数据库用户权限检查。这个顺序能把绝大部分18456问题快速定位,不用一次次瞎试。

5.2 charset与数据库排序规则的配合:不再看到问号

连上数据库之后,第二个让人头疼的问题是中文乱码。pymssql查询出来的中文字段显示成???,或者一堆乱字符,问题通常出在charset参数和SQL Server端排序规则的不匹配上。

pymssql的charset参数有两个作用:一是指定客户端发送查询请求时使用的编码,二是指定客户端期望服务器返回结果时使用的编码。SQL Server端每个数据库都有一个排序规则,比如Chinese_PRC_CI_AS就是常见的中文不区分大小写排序规则,底层编码是GBK/CP936;而Latin1_General_BIN2之类的规则则是基于Latin1。

如果数据库排序规则是中文GBK体系,连接时应该用charset='cp936':

import pymssql conn = pymssql.connect( server='192.168.1.100', user='sa', password='your_password', database='your_db', charset='cp936', )

如果数据库用的是SQL Server 2019以上才支持的UTF-8排序规则,则把charset设为utf8:

conn = pymssql.connect( server='192.168.1.100', user='sa', password='your_password', database='your_db', charset='utf8', )

我自己处理乱码问题的经验是:先把Python脚本里打印结果的终端编码固定为UTF-8,再排查pymssql的charset。有时候数据本身是对的,只是终端显示编码不对,就会误判成数据库乱码。如果脚本和charset都对了还乱,再用DBCC CHECKDB一类工具确认数据库里的实际字节内容。新建项目的话,我建议直接在当前SQL Server版本支持的前提下,把表和数据库排序规则统一成UTF-8相关规则,连接配charset='utf8',Python侧不用再做任何编码转换,整套链路最干净。

6. 一次生产环境连接故障的完整排查链路:命名实例与动态端口

6.1 把20009当网络问题处理,差点带偏方向

某次遇到一个报表服务(模拟项目X),部署到生产服务器后,日志里频繁出现pymssql抛出20009错误,而且不是一直失败,是间歇性的:有时一小时内失败几次,有时整个上午都正常。开发环境完全复现不了,因为开发环境直连的是一台测试库,测试库用的是默认实例加固定端口1433。

按照常规思路,我第一轮排查把重心放在网络上。ping生产数据库主机,延迟正常;telnet1433端口,每次都通;防火墙规则也确认过,TCP入站规则对1433端口是放行的。到这里,网络层看起来干净得不能再干净了。既然端口能通,为什么pymssql还说服务器不可达?

后来仔细看了生产环境的连接日志,发现连接字符串里写的是server=r'prod-server\REPORT'这样的命名实例写法。问题一下子变得清晰:命名实例的端口不一定固定是1433,它可能由SQL Server在启动时动态分配一个端口,然后用SQL Server Browser服务在UDP 1434上广播实例名到端口的映射关系。换句话说,网络层测试的1433端口可能压根不是目标实例正在监听的端口。

6.2 根因确认:SQL Server Browser服务与实例名解析

我在生产数据库服务器上运行netstat -ano | findstr 1434和netstat -ano | findstr LISTENING查看SQL Server的实际监听状态,结果发现目标实例确实监听着一个动态端口,而且还开着UDP 1434的广播端口。但应用服务器上测试UDP 1434到生产服务器不通,进一步检查发现生产服务器的防火墙入站规则只放行了TCP端口,UDP 1434没有放行。

这就解释了整个故障链路:应用服务器每次发起连接,需要先把实例名REPORT解析成实际端口,这个解析过程依赖UDP 1434的通畅。UDP被防火墙拦截后,实例名解析失败,pymssql拿不到真实端口,于是报20009“服务器不可达”。开发环境之所以正常,是因为开发机器到测试库的网络路径上没有拦截UDP。

这个坑最隐蔽的地方在于:你会下意识觉得“端口都通了,问题肯定在数据库本身”,结果在数据库配置上反复折腾,完全没想到是UDP 1434被防火墙掐断了。

修复方案有两个方向。第一个方向是在SQL Server配置管理器里把命名实例的端口固定下来,强制它监听1433,这样连接字符串改成IP加端口的形式,彻底绕开UDP依赖;第二个方向是放行防火墙的UDP 1434,让SQL Server Browser服务能正常应答实例名解析请求。

我当时选了第一个方案,因为改动最小,而且后续所有连接代码可以统一成server='192.168.1.100,1433'这种直连格式,少一层动态解析就少一个故障点。

6.3 修复后的验证方法

修复完并不是立刻收工,要验证三件事。第一,确认实例实际监听的端口已经变成固定端口,netstat里能看到监听状态;第二,从应用服务器重新执行telnet 192.168.1.100 1433,确认TCP连通;第三,实际跑一次pymssql连接脚本,连续执行多轮,确认不再复现20009。

import pymssql conn = pymssql.connect( server=('192.168.1.100', 1433), user='x_report_user', password='your_password', database='ReportDB', tds_version='7.3', login_timeout=10, ) cursor = conn.cursor() cursor.execute("SELECT 1") print(cursor.fetchone()) conn.close()

这次经验之后,我每次接触新的SQL Server环境,都会先问一句:这是默认实例还是命名实例?如果是命名实例,网络层放行TCP 1433其实不能保证连接成功,必须把UDP 1434或者固定端口一起纳入规划。这个教训在文档里很难看到,踩过一次就记得特别牢。

7. 稳定连接的配置模板和运维习惯

7.1 一份能直接复用的pymssql连接参数模板

把上面所有经验总结成一份连接参数模板。我给一个适合大多数生产环境的配置示例:

import pymssql conn_params = { "server": ("192.168.1.100", 1433), "user": "report_user", "password": "your_password", "database": "ReportDB", "charset": "utf8", "tds_version": "7.3", "login_timeout": 10, "timeout": 30, "as_dict": True, "appname": "report-service", } conn = pymssql.connect(**conn_params)
参数推荐值说明
server元组形式(主机,端口)用IP加固定端口最稳,避免实例名解析的UDP依赖
charsetutf8或cp936根据数据库排序规则选择
tds_version7.3或7.4根据SQL Server版本调整
login_timeout10到15秒不要用默认60秒,故障时要快速暴露问题
timeout30秒左右查询超时保护,防止SQL卡死拖垮服务
as_dictTrue返回字典格式,代码可读性好
appname服务名称方便SQL Server侧监控哪个应用在连接

如果你要用SQLAlchemy,连接串这样写:

from sqlalchemy import create_engine engine = create_engine( "mssql+pymssql://report_user:your_password@192.168.1.100:1433/ReportDB?charset=utf8" )

SQLAlchemy的pymssql方言会自动把URL参数转成pymssql.connect的参数,目前用下来兼容性不错。

7.2 连接池、超时和重试的设计建议

生产环境的服务不能每次请求都新建连接,一定要用连接池。我常用DBUtils的PooledDB配合pymssql,配置一个针对数据库连接池的包装层:

from dbutils.pooled_db import PooledDB import pymssql pool = PooledDB( creator=pymssql, maxconnections=10, mincached=2, blocking=True, server=("192.168.1.100", 1433), user="report_user", password="your_password", database="ReportDB", charset="utf8", tds_version="7.3", login_timeout=10, as_dict=True, )

连接池里我建议把login_timeout设得比单次连接短一点,因为连接池创建连接时如果等太久,请求会堆积。blocking=True保证连接数达到上限时,请求是排队而不是直接抛错。

重试策略要区分错误类型。20009和20002这类瞬时网络类错误,值得重试两到三次;18456这类认证错误,重试一百次结果都一样,直接抛给上层处理就好。我写过一个简单的重试封装:

import time import pymssql def connect_with_retry(conn_params, retries=3, delay=2): for attempt in range(retries): try: return pymssql.connect(**conn_params) except pymssql.OperationalError as exc: code = exc.args[0] if exc.args else None if code == 18456 or attempt == retries - 1: raise print(f"连接失败(错误码{code}),{delay}秒后重试") time.sleep(delay)

最后分享一个我自己的排查习惯:遇到pymssql连接问题,我从不先去改代码,而是先开三个终端分别做三件事——ping服务器、telnet端口、看SQL Server的ERRORLOG。三分钟定位到网络层还是服务层,就能省下一整天瞎试参数的时间。pymssql本身的重点从来不是Python语法,而是它背后那条从TDS协议到SQL Server实例的网络链路,把这条链路摸透了,绝大多数连接报错都能在五分钟内找到方向。

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

AIoT科技黑市:野路子方案如何低成本搞定智慧园区

干这行时间久了&#xff0c;你就会发现一个特别有意思的现象&#xff1a;凡是挂着“智慧园区”、“AIoT”、“万物互联”招牌的项目&#xff0c;表面上的方案书都长得差不多——云平台、数字孪生、AI分析、统一运维&#xff0c;讲得天花乱坠。但真正落到现场&#xff0c;尤其是…

作者头像 李华
网站建设 2026/10/14 4:28:28

用 Roslyn 与 AvalonEdit 从零构建跨平台 C# 编辑器:架构与踩坑全解析

最近我花了大约两周的业余时间&#xff0c;把标题里这口“大话”真给落地了&#xff1a;用 Roslyn 和 AvalonEdit 这两大引擎&#xff0c;从 0 到 1 搭了一个跨平台 C# 编辑器。它不是 Visual Studio&#xff0c;也不是 VS Code 里的某个插件&#xff0c;而是一个自己掌控所有细…

作者头像 李华
网站建设 2026/10/14 4:28:26

阿里云代理公司有哪些:按采购、迁移和降本需求分组比较

问“阿里云代理公司有哪些”&#xff0c;很难有一个通用答案&#xff0c;因为阿里云合作伙伴主页本身就区分分销、咨询、集成、产品生态、服务伙伴等类别&#xff0c;不同类别对应的服务范围并不相同。更实用的做法是先把本次需求归到采购、迁移、降本三类中的一类或几类&#…

作者头像 李华
网站建设 2026/10/14 4:28:26

数据分析与科学计算实战:从数据清洗到统计建模的完整流程

“数据分析”这四个字&#xff0c;大家听得太多了&#xff0c;但真正落到“科学计算”这个层面&#xff0c;很多人其实没搞明白它到底在解决什么问题。我接手的很多项目&#xff0c;名义上叫数据分析&#xff0c;可一旦数据量上来、逻辑复杂起来&#xff0c;Excel 卡死、SQL 嵌…

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

Claude Code 安装与配置全指南:从环境准备到权限调优

很多人看到“Claude Code 编程安装”这个标题&#xff0c;第一反应是复制粘贴一条npm install命令就完事。我以前也这么干&#xff0c;直到第一次启动时被一连串权限弹窗和莫名其妙的报错打得措手不及&#xff0c;才意识到&#xff1a;Claude Code 这类终端 agent 工具的“安装…

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

GridView AJAX化实战:告别Postback,实现无刷新分页与排序

简介&#xff1a;面向ASP.NET开发者的GridView实用示例包&#xff0c;针对VS自带的GridView在数据增删改操作上不灵活、页面刷新频繁、交互体验差等痛点&#xff0c;提供了一套基于ASP.NET 4.0与SQL Server 2008的轻量级解决方案&#xff0c;通过Ajax实现无刷新操作&#xff0c…

作者头像 李华