HIS系统数据导出:Python连接Oracle实战,我踩过的7个坑
医院里最常听到的一句话是:"能不能帮我导一下上个月的数据?"
HIS系统自带的导出功能要么是固定模板改不了,要么导出来是加密的。我在医院做了20年数据,最后还是自己写脚本连数据库最省事。
这篇把我用Python连Oracle导数据的完整流程写出来,重点讲7个真实踩过的坑——这些坑文档的坑,不写清楚你迟早要撞一遍。
环境准备
pip install cx_Oracle oracledb
注意:cx_Oracle 已经改名叫 python-oracledb 了,新项目直接用新版:
import oracledb # 新版
# import cx_Oracle # 旧版,写法几乎一样
新版的好处是不需要装Oracle客户端(thin模式),这点省了很大的事。老版本装Instant Client能把人折腾疯。
最基础的:连上去查一条
import oracledb
conn = oracledb.connect(
user="his_query",
password="your_password",
host="192.168.1.100",
port=1521,
service_name="ORCL" # 不是 SID!见坑1
)
cursor = conn.cursor()
cursor.execute("SELECT * FROM inp_bill_detail WHERE ROWNUM <= 10")
for row in cursor:
print(row)
cursor.close()
conn.close()
能跑通这段,剩下都是细节问题。
坑1:service_name 和 SID 分不清
这是新手第一个绊脚石。Oracle有两种标识数据库的方式:
# 方式A:service_name(现在主流)
conn = oracledb.connect(user=..., password=..., host=..., port=1521,
service_name="ORCL")
# 方式B:SID(老系统常见)
dsn = oracledb.makedsn("192.168.1.100", 1521, sid="ORCL")
conn = oracledb.connect(user=..., password=..., dsn=dsn)
怎么知道该用哪个? 问DBA,或者自己试:
-- 查 service_name
SELECT value FROM v$parameter WHERE name = 'service_names';
-- 查 SID
SELECT instance_name FROM v$instance;
我见过有人卡在这三天,最后发现是填错了这两个之一。报错信息是 ORA-12505 或 ORA-12514,看到这俩基本就是这个问题。
坑2:连接池不关,数据库被你拖垮
导出一次数据开一个连接看起来没问题,但如果你在循环里反复连:
# ❌ 错误写法:循环里反复开关连接
for dept in dept_list:
conn = oracledb.connect(...) # 每次都新建
cursor = conn.cursor()
cursor.execute("SELECT ... WHERE dept = :1", [dept])
...
conn.close()
医院HIS的连接数是有限制的。你这么跑,DBA会直接打电话过来。
正确做法:一个连接复用,或者用连接池:
# ✅ 方式一:一个连接复用到底
conn = oracledb.connect(...)
cursor = conn.cursor()
for dept in dept_list:
cursor.execute("SELECT ... WHERE dept = :1", [dept])
rows = cursor.fetchall()
...
conn.close()
# ✅ 方式二:连接池(并发场景)
pool = oracledb.create_pool(
user="his_query", password="...", dsn="...",
min=2, max=10, increment=1
)
conn = pool.acquire()
...
pool.release(conn)
pool.close()
坑3:SQL注入 —— 拼接字符串的代价
我知道你想这么写:
# ❌ 千万别这么干
dept = input("输入科室: ")
sql = f"SELECT * FROM inp_bill_detail WHERE dept_name = '{dept}'"
cursor.execute(sql)
如果有人输入 ' OR '1'='1,整张表就出来了;输入带分号的,可能直接删库。医院数据出事是要担责任的。
必须用绑定变量:
# ✅ 正确:用 :1 :2 占位
sql = "SELECT * FROM inp_bill_detail WHERE dept_name = :1 AND bill_date >= :2"
cursor.execute(sql, [dept, start_date])
不只是安全问题,绑定变量还能让Oracle复用执行计划,速度也更快。没有理由不用。
坑4:日期字段的坑
Oracle的日期处理和其他数据库不太一样,我在这栽过跟头。
from datetime import datetime
# ❌ 错误:直接传字符串,依赖数据库的 NLS_DATE_FORMAT 设置
sql = "SELECT * FROM t WHERE bill_date >= '2026-01-01'"
# ✅ 正确:传 datetime 对象
sql = "SELECT * FROM t WHERE bill_date >= :1"
cursor.execute(sql, [datetime(2026, 1, 1)])
# ✅ 或者在SQL里显式转换,最稳妥
sql = "SELECT * FROM t WHERE bill_date >= TO_DATE(:1, 'YYYY-MM-DD')"
cursor.execute(sql, ["2026-01-01"])
还有个更隐蔽的:日期范围查询的边界问题
-- ❌ 这样会漏掉 1月31日 这天中 00:00 之后的所有数据
WHERE bill_date BETWEEN TO_DATE('2026-01-01','YYYY-MM-DD')
AND TO_DATE('2026-01-31','YYYY-MM-DD')
-- ✅ 正确写法:上界用第二天
WHERE bill_date >= TO_DATE('2026-01-01','YYYY-MM-DD')
AND bill_date < TO_DATE('2026-02-01','YYYY-MM-DD')
因为 2026-01-31 会被当成 2026-01-31 00:00:00,当天其余23小时的数据全丢了。这个bug我查了两天才反应过来。
坑5:CLOB字段读不出来
HIS里有些字段是CLOB类型(比如病历文本),直接读会拿到一个对象而不是字符串:
cursor.execute("SELECT patient_id, diagnosis FROM medical_record")
for pid, diag in cursor:
print(diag) # 输出: <oracledb.LOB object at 0x...>
解决:让驱动自动转换
# 方式一:建连接时指定(推荐)
def output_type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_CLOB:
return cursor.var(oracledb.DB_TYPE_LONG, arraysize=cursor.arraysize)
if default_type == oracledb.DB_TYPE_NCLOB:
return cursor.var(oracledb.DB_TYPE_LONG_NVARCHAR, arraysize=cursor.arraysize)
if default_type == oracledb.DB_TYPE_BLOB:
return cursor.var(oracledb.DB_TYPE_LONG_RAW, arraysize=cursor.arraysize)
conn.outputtypehandler = output_type_handler
加上这段,CLOB直接当字符串用,省事多了。
坑6:中文乱码
这个在Windows上几乎必然会遇到。
# 连接时指定编码
conn = oracledb.connect(
user=..., password=..., dsn=...,
encoding="UTF-8",
nencoding="UTF-8"
)
如果还是乱码,先确认数据库的字符集:
SELECT * FROM nls_database_parameters
WHERE parameter IN ('NLS_CHARACTERSET', 'NLS_NCHAR_CHARACTERSET');
如果是 ZHS16GBK 而你的客户端用UTF-8,就会乱。两边对齐就行。
还有个Windows特有的坑:Python脚本输出中文到控制台报错。加这句:
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
坑7:数据量大,一次性fetchall撑爆内存
# ❌ 几十万行数据这么干,内存直接爆
cursor.execute("SELECT * FROM inp_bill_detail") # 假设50万行
rows = cursor.fetchall() # 全部载入内存
正确做法:分批取
# ✅ 方式一:fetchmany 循环
cursor.execute("SELECT * FROM inp_bill_detail")
cursor.arraysize = 1000 # 每次网络往返取多少行
while True:
batch = cursor.fetchmany(1000)
if not batch:
break
process(batch) # 处理这一批
# ✅ 方式二:直接迭代游标(最简单,驱动自动分批)
cursor.execute("SELECT * FROM inp_bill_detail")
for row in cursor: # 底层自动分批,内存友好
process(row)
arraysize 调大能减少网络往返次数,我一般设1000。太小了慢,太大了内存涨。
完整实战脚本:导出住院明细到Excel
把上面所有坑都避开的完整版本,这是我在用的简化版:
# -*- coding: utf-8 -*-
"""
HIS住院明细导出工具
导出指定时间范围的住院费用明细到Excel
"""
import sys, io
import oracledb
import openpyxl
from datetime import datetime
from openpyxl.styles import Font, PatternFill, Alignment
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
# ---------- 配置 ----------
DB_CONFIG = {
"user": "his_query",
"password": "your_password",
"host": "192.168.1.100",
"port": 1521,
"service_name": "ORCL",
"encoding": "UTF-8",
"nencoding": "UTF-8",
}
SQL = """
SELECT
b.patient_id AS 病案号,
p.patient_name AS 姓名,
b.dept_name AS 科室,
b.item_name AS 项目名称,
b.item_price AS 单价,
b.item_qty AS 数量,
b.total_cost AS 金额,
b.bill_date AS 记账日期
FROM inp_bill_detail b
LEFT JOIN patient_info p ON b.patient_id = p.patient_id
WHERE b.bill_date >= TO_DATE(:1, 'YYYY-MM-DD')
AND b.bill_date < TO_DATE(:2, 'YYYY-MM-DD')
AND (:3 IS NULL OR b.dept_name = :3)
ORDER BY b.bill_date, b.patient_id
"""
def clob_handler(cursor, name, default_type, size, precision, scale):
"""CLOB自动转字符串"""
if default_type in (oracledb.DB_TYPE_CLOB, oracledb.DB_TYPE_NCLOB):
return cursor.var(oracledb.DB_TYPE_LONG, arraysize=cursor.arraysize)
def export_to_excel(start_date, end_date, dept, output_file):
conn = oracledb.connect(**DB_CONFIG)
conn.outputtypehandler = clob_handler # 坑5
cursor = conn.cursor()
cursor.arraysize = 1000 # 坑7
try:
# 坑3:绑定变量,绝不拼接
cursor.execute(SQL, [start_date, end_date, dept])
# 拿列名
columns = [d[0] for d in cursor.description]
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "住院明细"
ws.append(columns)
total = 0
# 坑7:迭代游标,不一次性载入
for row in cursor:
ws.append(list(row))
total += 1
if total % 10000 == 0:
print(f" 已读取 {total} 行...")
# 美化
fill = PatternFill("solid", start_color="4472C4")
for cell in ws[1]:
cell.fill = fill
cell.font = Font(bold=True, color="FFFFFF")
cell.alignment = Alignment(horizontal="center")
ws.freeze_panes = "A2"
ws.auto_filter.ref = ws.dimensions
wb.save(output_file)
print(f"\n✓ 导出完成:{total} 行 → {output_file}")
finally:
cursor.close()
conn.close() # 坑2:务必关闭
if __name__ == "__main__":
export_to_excel(
start_date="2026-09-01",
end_date="2026-10-01", # 坑4:用上界,不用BETWEEN
dept=None, # 传None查全部科室
output_file=r"D:\导出\住院明细_202609.xlsx"
)
几个补充说明
只读账号:一定要申请一个只读权限的查询账号,别用业务账号跑查询。万一写错SQL,只读账号改不了数据,这是保命的。
查之前先EXPLAIN:数据量大的表,先看看执行计划:
EXPLAIN PLAN FOR SELECT ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
看到 TABLE ACCESS FULL(全表扫描)且表很大,就该考虑加索引或者加WHERE条件了。
生产库和测试库分开:脚本先在测试库跑通,确认无误再上生产。
写在最后
连Oracle导出数据这件事,代码本身不复杂,复杂的是这些环境细节和问题排查。我把7个坑都写出来了,你照着做基本能一次跑通。
真正值钱的不是这段代码,而是知道哪里会出问题、出问题怎么查——这是做了20年数据才积累出来的。
代码都在上面了,改改连接配置和SQL就能用。跑不通的欢迎来问。
作者:雲澤 | 医院IT运维,20年医院数据经验
关注我,每周分享Python实战技巧和服务器运维经验。

浙公网安备 33010602011771号