使用 Python 从 PowerPivot 模型中提取原始数据

Posted

技术标签:

【中文标题】使用 Python 从 PowerPivot 模型中提取原始数据【英文标题】:Extracting raw data from a PowerPivot model using Python 【发布时间】:2016-04-23 02:44:27 【问题描述】:

当我不得不使用 Python 从 PowerPivot 模型中读取一些数据时,看似微不足道的任务变成了一场真正的噩梦。我相信在过去的几天里我已经对此进行了很好的研究,但现在我碰壁了,希望 Python/SSAS/ADO 社区提供一些帮助。

基本上,我要做的就是以编程方式访问存储在 PowerPivot 模型中的原始数据 - 我的想法是通过下面列出的方法之一连接到底层 PowerPivot(即 MS Analysis Services)引擎,列出包含在模型,然后使用简单的 DAX 查询(类似于EVALUATE (table_name))从每个表中提取原始数据。很容易,对吧?好吧,也许不是。

0。一些背景资料

如您所见,我尝试了几种不同的方法。我会尽可能仔细地记录所有内容,以便那些不熟悉 PowerPivot 功能的人能够很好地了解我想要做什么。

首先,一些关于以编程方式访问 Analysis Services 引擎的背景知识(上面写着 2005 SQL Server,但所有这些都应该仍然适用):SQL Server Data Mining Programmability 和 Data providers used for Analysis Services connections。

我将在下面的示例中使用的示例 Excel/PowerPivot 文件可在此处找到:Microsoft PowerPivot for Excel 2010 and PowerPivot in Excel 2013 Samples。

另外,请注意,我使用的是 Excel 2010,因此我的一些代码是特定于版本的。例如。如果您使用的是 Excel 2013,wb.Connections["PowerPivot Data"].OLEDBConnection.ADOConnection 应该是 wb.Model.DataModelConnection.ModelConnection.ADOConnection

我将在整个问题中使用的连接字符串基于此处找到的信息:Connect to PowerPivot engine with C#。此外,某些方法显然需要在数据检索之前对 PowerPivot 模型进行某种初始化。见这里:Automating PowerPivot Refresh operation from VBA。

最后,这里有几个链接表明这应该是可以实现的(但是请注意,这些链接主要是指 C#,而不是 Python):

Made connection to PowerPivot DataModel, how can I fill a dataset with it? Connecting to PowerPivot with C# 2013 C# connection to PowerPivot DataModel Connecting Tableau and PowerPivot. It just works.(表明外部应用程序实际上可以读取 PowerPivot 模型数据 - 请注意,Tableau 加载项安装了 Interop.ADODB.dll 程序集,我猜它是用来访问 PowerPivot 数据的)

1。使用 ADOMD

import clr
clr.AddReference("Microsoft.AnalysisServices.AdomdClient")
import Microsoft.AnalysisServices.AdomdClient as ADOMD
ConnString = "Provider=MSOLAP;Data Source=$Embedded$;Locale Identifier=1033;
             Location=H:\\PowerPivotTutorialSample.xlsx;SQLQueryMode=DataKeys"

Connection = ADOMD.AdomdConnection(ConnString)
Connection.Open()

在这里,问题似乎是 PowerPivot 模型尚未初始化:

AdomdConnectionException: A connection cannot be made. Ensure that the server is running.

2。使用 AMO

import clr
clr.AddReference("Microsoft.AnalysisServices")
import Microsoft.AnalysisServices as AMO
ConnString = "Provider=MSOLAP;Data Source=$Embedded$;Locale Identifier=1033;
             Location=H:\\PowerPivotTutorialSample.xlsx;SQLQueryMode=DataKeys"

Connection = AMO.Server()
Connection.Connect(ConnString)

同样的故事,“服务器没有运行”:

ConnectionException: A connection cannot be made. Ensure that the server is running.

请注意,AMO 在技术上不用于查询数据,但我将其作为连接到 PowerPivot 模型的潜在方式之一。

3。使用 ADO.NET

import clr
clr.AddReference("System.Data")
import System.Data.OleDb as ADONET
ConnString = "Provider=MSOLAP;Data Source=$Embedded$;Locale Identifier=1033;
             Location=H:\\PowerPivotTutorialSample.xlsx;SQLQueryMode=DataKeys"

Connection = ADONET.OleDbConnection()
Connection.ConnectionString = ConnString
Connection.Open()

这类似于What's the simplest way to access mssql with python or ironpython?。不幸的是,这也不起作用:

OleDbException: OLE DB error: OLE DB or ODBC error: The following system error occurred:
The requested name is valid, but no data of the requested type was found.

4。通过 adodbapi 模块使用 ADO

import adodbapi
ConnString = "Provider=MSOLAP;Data Source=$Embedded$;Locale Identifier=1033;
             Location=H:\\PowerPivotTutorialSample.xlsx;SQLQueryMode=DataKeys"

Connection = adodbapi.connect(ConnString)

类似于Opposite Workings of OLEDB/ODBC between Python and MS Access VBA。我得到的错误是:

OperationalError: (com_error(-2147352567, 'Exception occurred.', (0, u'Microsoft OLE DB
Provider for SQL Server 2012 Analysis Services.', u'OLE DB error: OLE DB or ODBC error: The
following system error occurred:  The requested name is valid, but no data of the requested
type was found...

这与上面的 ADO.NET 基本相同。

5。通过 Excel/win32com 模块使用 ADO

from win32com.client import Dispatch
Xlfile = "H:\\PowerPivotTutorialSample.xlsx"
XlApp = Dispatch("Excel.Application")
Workbook = XlApp.Workbooks.Open(Xlfile)
Workbook.Connections["PowerPivot Data"].Refresh()
Connection = Workbook.Connections["PowerPivot Data"].OLEDBConnection.ADOConnection
Recordset = Dispatch('ADODB.Recordset')

Query = "EVALUATE(dbo_DimDate)" #sample DAX query
Recordset.Open(Query, Connection)

这种方法的想法来自这篇使用 VBA 的博客文章:Export a table or DAX query from Power Pivot to CSV using VBA。请注意,此方法使用初始化模型(即“服务器”)的显式刷新命令。这是错误消息:

com_error: (-2147352567, 'Exception occurred.', (0, u'ADODB.Recordset', u'Arguments are of
the wrong type, are out of acceptable range, or are in conflict with one another.',
u'C:\\Windows\\HELP\\ADO270.CHM', 1240641, -2146825287), None)

但是,ADO 连接似乎已经建立:

type(Connection) 返回instance print(Connection) 返回Provider=MSOLAP.5;Persist Security Info=True;Initial Catalog=Microsoft_SQLServer_AnalysisServices;Data Source=$Embedded$;MDX Compatibility=1;Safety Options=2;ConnectTo=11.0;MDX Missing Member Mode=Error;Subqueries=2;Optimize Response=3;Cell Error Mode=TextValue

似乎问题在于 ADODB.Recordset 对象的创建。

6。通过Excel/win32com使用ADO,直接使用ADODB.Connection

from win32com.client import Dispatch
ConnString = "Provider=MSOLAP;Data Source=$Embedded$;Locale Identifier=1033;
             Location=H:\\PowerPivotTutorialSample.xlsx;SQLQueryMode=DataKeys"

Connection = Dispatch('ADODB.Connection')
Connection.Open(ConnString)

类似于Connection to Access from Python [duplicate] 和Query access using ADO in Win32 platform (Python recipe)。不幸的是,Python 吐出的错误与上面两个示例中的相同:

com_error: (-2147352567, 'Exception occurred.', (0, u'Microsoft OLE DB Provider for SQL
Server 2012 Analysis Services.', u'OLE DB error: OLE DB or ODBC error: The following system
error occurred:  The requested name is valid, but no data of the requested type was found.
..', None, 0, -2147467259), None)

7.通过Excel/win32com使用ADO,直接使用ADODB.Connection加模型刷新

from win32com.client import Dispatch
Xlfile = "H:\\PowerPivotTutorialSample.xlsx"
XlApp = Dispatch("Excel.Application")
Workbook = XlApp.Workbooks.Open(Xlfile)
Workbook.Connections["PowerPivot Data"].Refresh()
ConnStringInternal = "Provider=MSOLAP.5;Persist Security Info=True;Initial Catalog=
                     Microsoft_SQLServer_AnalysisServices;Data Source=$Embedded$;MDX
                     Compatibility=1;Safety Options=2;ConnectTo=11.0;MDX Missing Member
                     Mode=Error;Optimize Response=3;Cell Error Mode=TextValue"

Connection = Dispatch('ADODB.Connection')
Connection.Open(ConnStringInternal)

我希望我可以初始化 Excel 的一个实例,然后初始化 PowerPivot 模型,然后使用 Excel 用于嵌入式 PowerPivot 数据的内部连接字符串创建一个连接(类似于How do you copy the powerpivot data into the excel workbook as a table? - 请注意,连接字符串是不同的来自我在其他地方使用过的那个)。不幸的是,这不起作用,我的猜测是 Python 在单独的实例中启动 ADODB.Connection 进程(因为当我在没有首先初始化 Excel 的情况下执行最后三行时收到相同的错误消息等):

com_error: (-2147352567, 'Exception occurred.', (0, u'Microsoft OLE DB Provider for SQL
Server 2012 Analysis Services.', u'Either the user, ****** (masked), does not have access
to the Microsoft_SQLServer_AnalysisServices database, or the database does not exist.',
None, 0, -2147467259), None)

【问题讨论】:

请问为什么需要直接访问PP模型中的数据? Power Pivot 数据必须从其他来源加载,无论是公开更强大的 ODBC 接口(或其他编程连接)的数据源还是可以在 Pyhon 中本地使用的平面文件。 我在一个行业工作,我们经常从第三方获取 PowerPivot 数据。他们不是我们的客户,所以我们无法以更合适的格式请求原始数据等。 明白。 DAX Studio 能够针对打开的工作簿运行任意 DAX 查询。您或许可以深入了解其内部结构或联系维护者 Darren Gosbell,以获得更多信息帮助:daxstudio.codeplex.com 感谢您的建议,@greggyb!我已经看过 DAX Studio,但由于 PowerPivot/Vertipaq 数据库引擎中没有行和行索引的概念,我认为没有办法将 DAX 查询限制为记录的子集(我已经完成对此进行了相当多的研究,我认为这实际上值得提出一个全新的问题)。无论如何,这种方式可以绕过 Excel 的 1m 行限制,例如使用 VBA 脚本将 1m 块数据输出到独立工作簿中(类似于此处建议的内容:***.com/a/33580647)。 我应该注意,只有当记录超过 1m 时才需要将表拆分为更小的块(例如,上面提到的示例文件中的 dbo_FactSales 表),而不是当行数为小于那个值(例如,dbo_DimProduct 表)。在这种情况下,该 SO 链接中建议的解决方案非常简单。不过,我会联系达伦,看看是否有什么我可能错过的。再次感谢! 【参考方案1】:

瞧,我终于设法解决了这个问题 - 事实证明,使用 Python 访问 Power Pivot 数据确实是可能的!下面是我所做的简短回顾 - 你可以在这里找到更详细的描述:Analysis Services (SSAS) on a shoestring。注意:代码没有针对效率和优雅进行优化。

安装 Microsoft Power BI Desktop(随附免费的 Analysis Services 服务器,因此无需昂贵的 SQL Server 许可证 - 但是,如果您拥有适当的许可证,同样的方法显然也适用)。 首先创建 msmdsrv.ini 设置文件启动 AS 引擎,然后从 ABF 文件恢复数据库(使用 AMO.NET),然后使用 ADOMD.NET 提取数据。

这是说明 AS 引擎 + AMO.NET 部分的 Python 代码:

import psutil, subprocess, random, os, zipfile, shutil, clr, sys, pandas

def initialSetup(pathPowerBI):
    sys.path.append(pathPowerBI)

    #required Analysis Services assemblies
    clr.AddReference("Microsoft.PowerBI.Amo.Core")
    clr.AddReference("Microsoft.PowerBI.Amo")     
    clr.AddReference("Microsoft.PowerBI.AdomdClient")

    global AMO, ADOMD
    import Microsoft.AnalysisServices as AMO
    import Microsoft.AnalysisServices.AdomdClient as ADOMD

def restorePowerPivot(excelName, pathTarget, port, pathPowerBI):   
    #create random folder
    os.chdir(pathTarget)
    folder = os.getcwd()+str(random.randrange(10**6, 10**7))
    os.mkdir(folder)

    #extract PowerPivot model (abf backup)
    archive = zipfile.ZipFile(excelName)
    for member in archive.namelist():
        if ".data" in member:
            filename = os.path.basename(member)
            abfname = os.path.join(folder, filename) + ".abf"
            source = archive.open(member)
            target = file(os.path.join(folder, abfname), 'wb')
            shutil.copyfileobj(source, target)
            del target
    archive.close()

    #start the cmd.exe process to get its PID
    listPIDpre = [proc for proc in psutil.process_iter()]
    process = subprocess.Popen('cmd.exe /k', stdin=subprocess.PIPE)
    listPIDpost = [proc for proc in psutil.process_iter()]
    pid = [proc for proc in listPIDpost if proc not in listPIDpre if "cmd.exe" in str(proc)][0]
    pid = str(pid).split("=")[1].split(",")[0]

    #msmdsrv.ini
    msmdsrvText = '''<ConfigurationSettings>
       <DataDir>0</DataDir>
       <TempDir>0</TempDir>
       <LogDir>0</LogDir>
       <BackupDir>0</BackupDir>
       <DeploymentMode>2</DeploymentMode>
       <RecoveryModel>1</RecoveryModel>
       <DisklessModeRequested>0</DisklessModeRequested>
       <CleanDataFolderOnStartup>1</CleanDataFolderOnStartup>
       <AutoSetDefaultInitialCatalog>1</AutoSetDefaultInitialCatalog>
       <Network>
          <Requests>
             <EnableBinaryXML>1</EnableBinaryXML>
             <EnableCompression>1</EnableCompression>
          </Requests>
          <Responses>
             <EnableBinaryXML>1</EnableBinaryXML>
             <EnableCompression>1</EnableCompression>
             <CompressionLevel>9</CompressionLevel>
          </Responses>
          <ListenOnlyOnLocalConnections>1</ListenOnlyOnLocalConnections>
       </Network>
       <Port>1</Port>
       <PrivateProcess>2</PrivateProcess>
       <InstanceVisible>0</InstanceVisible>
       <Language>1033</Language>
       <Debug>
          <CallStackInError>0</CallStackInError>
       </Debug>
       <Log>
          <Exception>
             <CrashReportsFolder>0</CrashReportsFolder>
          </Exception>
          <FlightRecorder>
             <Enabled>0</Enabled>
          </FlightRecorder>
       </Log>
       <AllowedBrowsingFolders>0</AllowedBrowsingFolders>
       <ResourceGovernance>
          <GovernIMBIScheduler>0</GovernIMBIScheduler>
       </ResourceGovernance>
       <Feature>
          <ManagedCodeEnabled>1</ManagedCodeEnabled>
       </Feature>
       <VertiPaq>
          <EnableDisklessTMImageSave>0</EnableDisklessTMImageSave>
          <EnableProcessingSimplifiedLocks>1</EnableProcessingSimplifiedLocks>
       </VertiPaq>
    </ConfigurationSettings>'''

    #save ini file to disk, fill it with required parameters
    msmdsrvini = open(folder+"\\msmdsrv.ini", "w")
    msmdsrvText = msmdsrvText.format(folder, port, pid) #0,1,2
    msmdsrvini.write(msmdsrvText)
    msmdsrvini.close()

    #run AS engine inside the cmd.exe process
    initString = "\"0\\msmdsrv.exe\" -c -s \"1\""
    initString = initString.format(pathPowerBI.replace("/","\\"),folder)
    process.stdin.write(initString + " \n")

    #connect to the AS instance from Python
    AMOServer = AMO.Server()
    AMOServer.Connect("localhost:0".format(port))

    #restore database from PowerPivot abf backup, disconnect
    AMORestoreInfo = AMO.RestoreInfo(os.path.join(folder, abfname))
    AMOServer.Restore(AMORestoreInfo)
    AMOServer.Disconnect()

    return process

还有数据提取部分:

def runQuery(query, port, flag):
    #ADOMD assembly
    ADOMDConn = ADOMD.AdomdConnection("Data Source=localhost:0".format(port))
    ADOMDConn.Open()
    ADOMDCommand = ADOMDConn.CreateCommand() 
    ADOMDCommand.CommandText = query

    #read data in via AdomdDataReader object
    DataReader = ADOMDCommand.ExecuteReader()

    #get metadata, number of columns
    SchemaTable = DataReader.GetSchemaTable()
    numCol = SchemaTable.Rows.Count #same as DataReader.FieldCount

    #get column names
    columnNames = []
    for i in range(numCol):
        columnNames.append(str(SchemaTable.Rows[i][0]))

    #fill with data
    data = []
    while DataReader.Read()==True:
        row = []
        for j in range(numCol):
            try:
                row.append(DataReader[j].ToString())
            except:
                row.append(DataReader[j])
        data.append(row)
    df = pandas.DataFrame(data)
    df.columns = columnNames 

    if flag==0:
        DataReader.Close()
        ADOMDConn.Close()

        return df     
    else:   
        #metadata table
        metadataColumnNames = []
        for j in range(SchemaTable.Columns.Count):
            metadataColumnNames.append(SchemaTable.Columns[j].ToString())
        metadata = []
        for i in range(numCol):
            row = []
            for j in range(SchemaTable.Columns.Count):
                try:
                    row.append(SchemaTable.Rows[i][j].ToString())
                except:
                    row.append(SchemaTable.Rows[i][j])
            metadata.append(row)
        metadf = pandas.DataFrame(metadata)
        metadf.columns = metadataColumnNames

        DataReader.Close()
        ADOMDConn.Close()

        return df, metadf

然后通过以下方式提取原始数据:

pathPowerBI = "C:/Program Files/Microsoft Power BI Desktop/bin"
initialSetup(pathPowerBI)
session = restorePowerPivot("D:/Downloads/PowerPivotTutorialSample.xlsx", "D:/", 60000, pathPowerBI)
df, metadf = runQuery("EVALUATE dbo_DimProduct", 60000, 1)
endSession(session)

【讨论】:

【参考方案2】:

从 PowerPivot 中获取数据的问题在于,PowerPivot 中的表格引擎在 Excel 中的进程内运行,而连接到该引擎的唯一方法是让您的代码也在 Excel 中运行。 (我怀疑它可能使用共享内存或其他一些传输,但它绝对没有监听 TCP 端口或命名管道或任何允许外部进程连接的东西)

我们在 Dax Studio 中通过在 Excel 中运行 C# VSTO Excel 加载项来执行此操作。然而,这只设计用于测试分析查询,而不是用于进行批量数据提取。我们使用字符串变量将数据从加载项编组到 UI,因此整个数据集必须小于 2Gb,否则响应会被截断,您将看到“无法识别的响应”错误(数据被序列化为 XMLA 行集这非常冗长,因此在仅提取几百 Mb 数据时可能会看到它中断)

如果您想构建一个脚本来自动从模型中提取所有原始数据,我认为您无法使用 Python 来完成,因为我不相信您可以让 Python 解释器在进程内运行Excel里面。我会考虑使用像这样的 vba 宏 http://www.powerpivotblog.nl/export-a-table-or-dax-query-from-power-pivot-to-csv-using-vba/

您应该发现您可以使用“SELECT * FROM $SYSTEM.DBSCHEMA_TABLES”之类的内容查询模型以获取表列表 - 然后您可以遍历每个表并使用上述链接中的代码变体进行提取。

【讨论】:

非常感谢您的意见,达伦! Darren,在处理大型 PowerPivot 模型时,您不能自动将数据子集成块,然后通过管道传输到 Dax Studio 以绕过 2Gb 限制吗?假设您一次拉出 100 万行,然后最后将所有内容重新组装在一起? @akavalar - 可能有一种方法可以对数据进行分块,但这需要比我们目前拥有的更低级别的 XMLA 协议实现。目前,这对我们来说并不是真正的优先事项。如果有人需要详细的数据转储,最好从原始源系统中获取,而不是从 PowerPivot 模型中提取。【参考方案3】:

我与 Tom Gleeson(又名 Gobán Saor)取得了联系,他很友好地让我在这里发布了他的电子邮件。其中有一些有趣的掘金,所以希望其他人也会发现它们有用。

电子邮件 #1

当您说 Python 时,您的意思是将 Python.NET 作为独立的 exe 运行? 如果是这种情况,您对 Excel PP 模型不走运(不同的 Power BI 桌面的故事)。我访问过 PP 模型 (2010+) 成功地从 VBA 和 Python.NET(通过 AMO)使用 与您的 SO 问题中的代码类似。区别在于(在 VBA 和 .NET 版本)是我的代码在进程内运行 Excel 使用 Excel 的各种加载项技术。 (可能 Tableau 是 也作为加载项运行或在其自身中嵌入了 Excel 类似的行为)。 DAX Studio(一个有用的 C# 代码库,用于学习 PP 访问指南)作为 Excel 加载项和独立运行 EXE,但只有作为插件才能访问基于 Excel 的 PP 模型。

电子邮件 #2

您可能会发现使用 Python.NET 的过程有点 具有挑战性的。您需要使用 C#/VB.NET 嵌入 Python 引擎 Excel 加载项代码。我用过 Excel-DNA(一个很棒的开源 项目),而不是 MS 非常繁琐的“官方”方法 过去开发过这样的.NET插件,但我主要坚持VBA 在所有可能的地方。

使用 VBA,您将无法访问 .NET-only AMO(因此无法动态创建计算列), 但是通过将结果数据集加载到 ADO 记录集中,您应该 能够输出到工作表或企业数据库/MS Access 或到平面文件/CSV 等。

与 1M 工作表限制不同,对于 平面文件或数据库输出内存 (RAM) 将是限制因素, 但是,假设您使用的是 64 位 Excel 并且有足够的内存来保存 压缩模型和最大模型的工作空间 未压缩形式的表(即基于行而不是基于列 DAX 查询产生的格式)乘以 2ish(一 PP 工作区中的一个实例,另一个 VBA 的 ADO 工作区中的实例)你 应该没问题。

话虽如此,我从未尝试过提取 非常大的数据集,并且使用模型作为数据集交换媒介是 不是 PP 的“用例”之一;所以,非常大的桌子可能会碰到一些 其他错误/约束!

【讨论】:

以上是关于使用 Python 从 PowerPivot 模型中提取原始数据的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用 Power Pivot 在 Excel 中删除数据模型?

PowerPivot无法添加到数据模型的解决方法

什么是PowerPivot?和PowerBI什么关系?

PowerPivot:如何识别计算列中每组的最大值

在 PowerPivot 中导入自定义 Atom 提要

Excel、VBA、PowerPivot、DataFeed 连接 - 更新文件路径