Access 作为一款文件型数据库,与 SQLite 类似,可以作为简单应用解决方案的数据存储。根据 DB-Engines Ranking 的数据库排名数据,MS Access 在 2020年12月的排名为 11 位,不算太小众。

python 安装 ObsClient odbaccess怎么在python里面安装_bc


本篇讲解在 Python 语言环境中,如何操作 Access 数据库,代码中将用到 pyodbc, adodbapi 和 pywin32 三个第三方包,这三个包都用 pip 方式安装。

pyodbc

Python 没有专门针对 Access 数据库的驱动,但微软提供了 ODBC 方式,可以访问 Access ,python 有支持 odbc 访问数据库的第三方模块 pyodbc,所以可以通过 pyodbc 作为桥接的工具。

MS Office 有 32 位版本和 64 位版,如果操作系统版本与 Office 版本不一致,使用 pyodbc 的时候可能会遇到错误。假设操作系统是 Windows 64 位,但Office 版本是 32 位的,此时安装 ODBC for MS Access,应该是 32 位的,将出现 pyodbc 找不到 32 位驱动的情况,如果尝试安装 64 位 odbc 驱动,Windows 将提示已经安装 32 位驱动,不允许再安装 64 位驱动。

解决办法是使用微软提供的 Orca 工具(本文的后面的源代码提供了 Orca 工具),修改 64 位 odbc 安装文件的数据表,删除里面的 blockinstallation 限制。先通过下面的网址下载 64 位驱动 (https://www.microsoft.com/en-us/download/details.aspx?id=13255),将下载的 AccessDatabaseEngine_X64.exe 文件解压缩,用 Orca 工具打开里面的文件 AceRedist.msi,找到 launchcondition:

python 安装 ObsClient odbaccess怎么在python里面安装_bc_02


然后再运行 AceRedist.msi,就可以成功安装 64 位的 ODBC 驱动。

pyodbc 符合 python DB-API 2.0 规范。下面是 pyodbc 访问 MS Access 数据库的示例,包括 CRUD 操作,以及有参数 的 sql 语句操作方法。本篇的目的并不是详细讲解 pyodbc 的用法,只演示一个大概的模式。

import pyodbc
import unittest

conn = pyodbc.connect(DSN="msaccess_employees")
cursor = conn.cursor()


class TestPyOdbc(unittest.TestCase):

    def test_select(self):
        cursor.execute("select * from employees")

        result = cursor.fetchall()  # result为list类型
        for item in result:
            print(item)  # item为pyodbc.Row类型

    def test_insert(self):
        sql = """
            INSERT INTO employees ( EMP_ID, FIRST_NAME, LAST_NAME, GENDER, 
                                    AGE, EMAIL, PHONE_NR, EDUCATION, MARITAL_STAT,NR_OF_CHILDREN )
            VALUES ('9001', 'Stone', 'Wang', 'Male', 18, 'stone@126.com', '138xxx', 'Bachelor', 'Married', 2 ); 
        """
        cursor.execute(sql)
        conn.commit()

    def test_update(self):
        sql = "update employees set AGE=20 where EMP_ID=9001"
        cursor.execute(sql)
        conn.commit()

    def test_delete(self):
        cursor.execute("delete from employees where EMP_ID=9001")

    def test_sql_with_parameter(self):
        sql = "select * from employees where EMP_ID=?"
        cursor.execute(sql, ['1001'])

        print(cursor.fetchone())


if __name__ == "__main__":
    unittest.main()

提一下连接字符串 (connection string),如果 Windows 版本与 Office 版本一致,连接字符串可以这样写:

conn = pyodbc.connect("Driver={{Microsoft Access Driver (*.mdb, *.accdb)}};DBQ=<ms_access_db_path>")

版本不一致则只能通过 DSN 来访问,打开 ODBC 数据源 (odbcad32.exe),创建一个新的数据源,选择 MS Access 驱动:

python 安装 ObsClient odbaccess怎么在python里面安装_sql_03


完成数据源的配置界面如下:

python 安装 ObsClient odbaccess怎么在python里面安装_bc_04

adodbapi

adodbapi 库对 ADO 进行了封装,符合 DB-API 2.0 规范。项目在 pypi 的地址:https://pypi.org/project/adodbapi/,源代码托管在 SourceForge 上面。对 adodbapi 网络上没有太多文档,大家可以参考的主要是该项目提供的 quick reference,本文的源代码中附了 quick reference 文档。

以下是 adodbapi 的使用示例:

import adodbapi
import os
import unittest

def get_current_dir():
    """
    获取当前文件夹
    """
    return os.path.dirname(os.path.abspath(__file__))

db_file_path = get_current_dir() + r'\db\Employees.accdb'
conn_str = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=%s;" % db_file_path

conn = adodbapi.connect(conn_str)
cursor = conn.cursor()

class TestAdodbapi(unittest.TestCase):
    def test_select(self):
        cursor.execute("select * from employees;")

        result = cursor.fetchall()  # result为adodbpai.apibase.SQLRows类型
        for item in result:
            print(item)             # itemadodbpai.apibase.SQLRow类型

    def test_insert(self):
        sql = """
            INSERT INTO employees ( EMP_ID, FIRST_NAME, LAST_NAME, 
                                    GENDER, AGE, EMAIL, PHONE_NR, EDUCATION, MARITAL_STAT, NR_OF_CHILDREN)
            VALUES ('9001', 'Stone', 'Wang', 'Male', 18, 'stone@126.com', '138xxx', 'Bachelor', 'Married', 2); 
        """
        cursor.execute(sql)
        conn.commit()

    def test_update(self):
        sql = "update employees set AGE=20 where EMP_ID=9001"
        cursor.execute(sql)
        conn.commit()

    def test_delete(self):
        cursor.execute("delete from employees where EMP_ID=9001")

    def test_sql_with_paramter(self):
        sql = "select * from employees where EMP_ID=?"
        cursor.execute(sql, (1001,))

        print(cursor.fetchone())


if __name__ == "__main__":
    unittest.main()

原生 ADO 组件

ADO 是微软提供的 数据访问技术,我们也可以通过 pywin32 模块操作 ADO 组件,实现对 MS Access 数据库的访问。与 DB-API 2.0 规范相比,ADO 访问数据库的代码相对繁琐,但如果已经比较熟悉 ADO 编程模型,编写数据访问的代码也比较简单,而且我们可以利用 Python 面向对象的方法,对原生 ADO 按自己的需要进行封装,从而简化代码。

ADO 对象模型中,核心的是 Connection, Command 和 RecordSet 三个。Connection 代表与数据库的连接,Command 对象用于执行 SQL 语句,比如插入数据,修改数据等。RecordSet 对象代表从数据库获取的数据,可以用遍历的方式查看数据。RecordSet 对象本身也可以进行数据的插入、修改和删除等操作。

封装 Connection 对象

from win32com.client import Dispatch

class ConnectionWrapper(object):
    def __init__(self, conn_str):
        self.connection_string = conn_str

    def get_connection(self):
        conn = Dispatch("ADODB.Connection")
        conn.ConnectionString = self.connection_string
        return conn

封装 Command 对象

from win32com.client import Dispatch
from . import error_handler
from . import adoconstants

class CommandWrapper(object):

    @staticmethod
    def execute(conn, sql):
        cmd = Dispatch("ADODB.Command")

        try:
            conn.Open()
            cmd.ActiveConnection = conn
            cmd.CommandText = sql

            cmd.execute()
        except Exception as ex:
            print(ex)
            for err in conn.Errors:
                error_handler.print_error(err)
        finally:
            if conn.State == adoconstants.adStateOpen:
                conn.Close()

封装 RecordSet 对象

RecordSetWrapper 对象主要通过 query() 方法获取数据,参数可以是 sql 语句或者 table name。rst_to_list() 方法用于以 list 格式输出结果集,to_excel() 方法用于将 RecordSet 对象导出到 Excel 的工作表。

from win32com.client import Dispatch
from ADOWrapper.adoconstants import *

class RecordSetWrapper(object):
    def __init__(self):
        pass

    @staticmethod
    def rst_to_list(recordset):
        """
        Convert recordset to list
        """
        result = []

        if not (recordset.BOF and recordset.EOF):
            # header line
            header = []
            for idx in range(recordset.Fields.Count):
                header.append(recordset.Fields(idx).Name)
            result.append(header)

            # line items
            # Python对于数据库的NULL值自动转换成None
            recordset.MoveFirst()
            while not recordset.EOF:
                item = []
                for idx in range(recordset.Fields.Count):
                    item.append(str(recordset.Fields(idx)))
                result.append(item)
                recordset.MoveNext()
        return result

    @staticmethod
    def query(conn, sql):
        rst = Dispatch("ADODB.Recordset")
        result = []

        try:
            if conn.state != adStateOpen:
                conn.Open()

            rst.Open(sql, conn, adOpenKeyset, adLockReadOnly)

            result = RecordSetWrapper.rst_to_list(rst)
        except Exception as ex:
            print(ex)
            for err in conn.Errors:
                print(err.Description)
        finally:
            rst.Close()
            conn.Close()

        return result

    @staticmethod
    def get_recordset(conn, sql):
        rst = Dispatch("ADODB.Recordset")
        conn.Open()
        rst.Open(sql, conn, adOpenKeyset, adLockReadOnly)
        return rst

    @staticmethod
    def to_excel(recordset, excel_file, replace = False):
        # create excel file
        excel_app = Dispatch("Excel.Application")
        excel_app.Visible = True

        try:
            work_book = excel_app.Workbooks.Add()
            target_sheet = work_book.ActiveSheet

            # copy recordset header
            for idx in range(0, recordset.Fields.Count):
                target_sheet.cells(1, idx+1).Value = recordset.Fields(idx).Name

            # copy recordset lines
            target_sheet.Range("A2").CopyFromRecordSet(recordset)

            print("导出成功!")
        finally:
            recordset.Close()

以下是单元测试代码:

from ADOWrapper.ado_command import CommandWrapper
from ADOWrapper.ado_connection import ConnectionWrapper
from ADOWrapper.ado_recordset import RecordSetWrapper
from msaccess_db_file_path import get_access_db_file
import unittest

# 连接MS Access数据库
conn_str = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=%s;" % get_access_db_file()
conn = ConnectionWrapper(conn_str).get_connection()

class TestAdoWrapper(unittest.TestCase):
    def test_insert(self):
        sql = """
                INSERT INTO employees 
                       ( EMP_ID, FIRST_NAME, LAST_NAME, GENDER, AGE, EMAIL, PHONE_NR, 
                         EDUCATION, MARITAL_STAT, NR_OF_CHILDREN)
                VALUES ('9001', 'Stone', 'Wang', 'Male', 18, 'stone@126.com', '138xxx', 'Bachelor', 'Married', 2 ); 
              """
        CommandWrapper.execute(conn, sql)

    def test_update(self):
        sql = "UPDATE employees SET AGE=20 WHERE EMP_ID=9001"
        CommandWrapper.execute(conn, sql)

    def test_delete(self):
        CommandWrapper.execute(conn, "DELETE FROM employees WHERE EMP_ID=9001")

    def test_query(self):
        result = RecordSetWrapper.query(conn, "SELECT * FROM employees")
        for record in result:
            print(record)

    def test_query_table(self):
        result = RecordSetWrapper.query(conn, "employees")
        for record in result:
            print(record)

    def test_export_to_excel(self):
        rst = RecordSetWrapper.get_recordset(conn, "select * from employees where EMP_ID<1020;")
        RecordSetWrapper.to_excel(rst, "D:/employee_output.xlsx")

if __name__ == "__main__":
    unittest.main()

原生 RecordSet 对象

原生RecordSet 对象支持数据库的 CRUD 操作,以下代码演示了 Python 使用原生 RecordSet 的方法:

"""
原生的|RecordSet具备CRUD功能,本示例说明其用法
"""

from ADOWrapper.ado_connection import ConnectionWrapper
from ADOWrapper.ado_recordset import RecordSetWrapper
from ADOWrapper.adoconstants import *
from win32com.client import Dispatch
from msaccess_db_file_path import get_access_db_file
import unittest

# 连接MS Access数据库
conn_str = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=%s;" % get_access_db_file()
conn = ConnectionWrapper(conn_str).get_connection()

class TestRecordSet(unittest.TestCase):
    def test_list_employees(self):
        rst = RecordSetWrapper.get_recordset(conn, "select * from employees")
        if rst.BOF and rst.EOF:
            return

        rst.MoveFirst()
        while not rst.EOF:
            for idx in range(0, rst.Fields.Count):
                print(rst.Fields(idx), end=",")
            print()
            rst.MoveNext()

        rst.Close()
        conn.Close()

    def test_create_employee(self):
        rst = Dispatch("ADODB.Recordset")
        conn.Open()
        rst.Open('employees', conn, adOpenKeyset, adLockOptimistic)

        try:
            rst.AddNew()
            rst.Fields("EMP_ID").Value = '9002'
            rst.Fields("FIRST_NAME").Value = 'Stone'
            rst.Fields("LAST_NAME").Value = "Wang"
            rst.Update()

            print('新增记录成功!')
        except Exception as ex:
            print(ex)
            for err in conn.Errors:
                print(err)
        finally:
            rst.Close()
            conn.Close()

    def test_modify_employee(self):
        rst = Dispatch("ADODB.Recordset")
        try:
            conn.Open()
            sql = "SELECT * FROM employees WHERE EMP_ID=9002"

            rst.Open(sql, conn, adOpenKeyset, adLockOptimistic)
            if not rst.EOF:
                rst.Fields('AGE').Value = 18
                rst.Update()

                print('修改成功!')
        except Exception as ex:
            print(ex)
            for err in conn.Errors:
                print(err)
        finally:
            rst.Close()
            conn.Close()

    def test_delete_employee(self):
        rst = Dispatch("ADODB.Recordset")
        conn.Open()
        sql = "SELECT * FROM employees WHERE EMP_ID=9002"

        try:
            # IMPORTANT: client cursor should be used for deletion
            rst.CursorLocation = adUseClient
            rst.Open(sql, conn, adOpenKeyset, adLockOptimistic)

            if not rst.EOF:
                rst.Delete(1)  # deleter first row
                print('删除成功!')
            else:
                print('没有找到记录!')
        except Exception as ex:
            print(ex)
            for err in conn.Errors:
                print(err)
        finally:
            rst.Close()
            conn.Close()


if __name__ == '__main__':
    unittest.main()

sqlalchemy-access

sqlalchemy 并不直接支持 MS Access 数据库,但可以通过安装 sqlalchemy-access 模块来提供支持。sqlalchemy-access 在 pypi 的地址:https://pypi.org/project/sqlalchemy-access/。安装的方法:

pip install sqlalchemy-access

如果小伙伴对 sqlalchemy 的使用感兴趣,请自行寻找资源学习。本文也只是演示基本的使用。sqlalchemy 的优点是基于 ORM,不需要手工编写 sql 语句,但需要用代码定义 model,这个 model 可以用 sqlacodegen 基于数据库表来自动生成。以下是 model 定义的代码:

from sqlalchemy import Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Employee(Base):
    __tablename__ = "employees"

    emp_id = Column("EMP_ID", String(255), primary_key=True)
    first_name = Column("FIRST_NAME", String(255))
    last_name = Column("LAST_NAME", String(255))
    gender = Column("GENDER", String(255))
    age = Column("AGE", Integer)
    email = Column("EMAIL", String(255))
    phone = Column("PHONE_NR", String(255))
    education = Column("EDUCATION", String(255))
    marital_stat = Column("MARITAL_STAT", String(255))
    children = Column("NR_OF_CHILDREN", Integer)

    def __repr__(self):
        return "Employee <{emp_id},{first_name},{last_name},{gender},{age},{email},{phone},{education},{marital_stat},{children}>".format(
            emp_id=self.emp_id,
            first_name=self.first_name,
            last_name=self.last_name,
            gender=self.gender,
            age=self.age,
            email=self.email,
            phone=self.phone,
            education=self.education,
            marital_stat=self.marital_stat,
            children=self.children
        )

以下是基于 sqlalchemy 增删改查的代码示例:

import unittest
from employee_sqlalchemy.models import Employee
from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import sessionmaker

engine = create_engine("access+pyodbc://EMP_MSACCESS")
Session = sessionmaker(bind=engine)
session = Session()

class TestSqlalchemy(unittest.TestCase):
    def test_query(self):
        employees = session.query(Employee).all()
        for emp in employees:
            print(emp)

    def test_query_one(self):
        employees = session.query(Employee).filter(Employee.first_name=="Ted").all()
        for emp in employees:
            print(emp)

    def test_insert(self):
        new_emp = Employee(
            emp_id =9001,
            first_name = "Alice",
            last_name = "Brown"
        )
        session.add(new_emp)
        session.commit()
        session.close()

    def test_update(self):
        emp = session.query(Employee).get(9001)
        if emp is not None:
            emp.age = 20
            session.commit()
            session.close()

    def test_delete(self):
         emp = session.query(Employee).get(9001)
         if emp is not None:
            session.delete(emp)
            session.commit()
            session.close()

if __name__ == "__main__":
    unittest.main()

源代码

github - python-using-msaccess-db

参考

Python3.7 pyodbc完美配置访问access数据库