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

当我必须使用Python读取PowerPivot模型中的某些数据时,看起来像是一件简单的任务变成了一场真正的噩梦。 我相信在过去的几天里我已经研究得非常好,但现在我碰壁了,希望得到来自Python / SSAS / ADO社区的帮助。

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

0.一些背景信息

正如你所看到的,我尝试了几种不同的方法。 我将尽可能仔细地logging所有事情,以便那些在PowerPivotfunction中不熟悉的人能够很好地了解我想要做什么。

首先,对Analysis Services引擎进行编程式访问的一些背景知识(它说2005 SQL Server,但它们都应该仍然适用): 用于Analysis Services连接的 SQL Server数据挖掘可编程性和数据提供程序 。

下面的示例中将使用示例Excel / PowerPivot文件,可以在此处find: Microsoft PowerPivot for Excel 2010和PowerPivot Excel 2013示例 。

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

我在这个问题中使用的连接string是基于这里find的信息: 使用C#连接到PowerPivot引擎 。 此外,一些方法显然需要在数据检索之前对PowerPivot模型进行某种初始化。 请参阅此处: 从VBA自动执行PowerPivot刷新操作 。

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

  • 连接到PowerPivot DataModel,我怎样才能用它填充数据集?
  • 用C#连接PowerPivot
  • C#连接到PowerPivot DataModel
  • 连接Tableau和PowerPivot。 它只是工作。 (显示外部应用程序实际上可以读取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() 

这与使用python或ironpython访问mssql最简单的方法是什么? 。 不幸的是,这也不起作用:

 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) 

类似于Python和MS Access VBA之间的OLEDB / ODBC的相反工作方式 。 我得到的错误是:

 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的博客文章:使用VBA 将表或DAX查询从Power Pivot导出到CSV 。 请注意,此方法使用初始化模型(即“服务器”)的显式刷新命令。 这里是错误信息:

 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连接已经build立:

  • 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

看来问题在于创buildADODB.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) 

与从Python访问连接类似[ 在Win32平台(Python配方)中使用ADO查询访问) 。 不幸的是,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 plus模型刷新

 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模型,然后使用内部连接string创build一个连接Excel用于embeddedPowerPivot数据(类似于如何将powerpivot数据复制到excel工作簿中作为表请注意,连接string与我在其他地方使用的不同)。 不幸的是,这不起作用,我的猜测是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) 

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

我们在Dax Studio中通过在Excel中运行C#VSTO Excel加载项来执行此操作。 然而,这只是devise用于testing分析查询,而不是用于批量数据提取。 我们使用stringvariables将加载项中的数据编组到整个UI中,因此整个数据集必须小于2Gb,否则响应会被截断,您将看到“无法识别的响应”错误(数据被序列化为XMLA行集这是相当详细的,所以当仅提取几百Mb的数据时可能会看到它破裂)

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

你应该发现,你可以查询模型中的一些类似于“SELECT * FROM $ SYSTEM.DBSCHEMA_TABLES”的表格 – 然后你可以循环遍历每个表格,并在上面的链接中提取代码的变体。

瞧,我终于设法解决了这个问题 – 事实certificate,使用Python访问Power Pivot数据的确是可能的! 下面是我所做的一个简短的回顾 – 您可以在这里find更详细的说明: 分析服务(SSAS)上的小本 。 注意:代码已经被优化,既没有效率也没有优雅。

  • 安装Microsoft Power BI Desktop(随附免费的Analysis Services服务器,因此不需要昂贵的SQL Server许可证,但是,如果您拥有适当的许可证,相同的方法显然也适用)。
  • 通过首先创buildmsmdsrv.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) 

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

电子邮件#1

当你说Python时,是指将Python.NET作为独立的EXE运行? 如果是这样的话,那么您对Excel PP模型就不太好运了(Power BI桌面的不同之处)。 我已经从VBA和Python.NET(通过AMO)成功地访问了PP模型(2010+),使用类似于您的SO问题的代码。 不同的是(在VBA和.NET版本中)是我的代码在Excel中使用Excel的各种插件技术在进程中运行。 (可能Tableau也作为加载项运行,或者在其内部embedded了Excel以启用类似的行为)。 DAX Studio(一个用于学习PP访问方法的有用的C#代码库)既可以作为Excel插件使用,也可以作为独立的EXE使用,但只能作为附加模块访问基于Excel的PP模型。

电子邮件#2

你可能会发现使用Python.NET的过程有点困难。 您需要使用C#/ VB.NET Excel加载项代码embeddedPython引擎。 我曾经使用Excel-DNA(一个梦幻般的开源项目),而不是MS过去繁琐的“官方”方法来开发这样的.NET插件,但是我主要是坚持使用VBA。

使用VBA,您将无法访问仅限.NET的AMO(因此无法即时创build计算列),但通过将结果数据集加载到ADOlogging集中,您应该能够输出到工作表或公司数据库/ MS Access或平面文件/ CSV等

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

话虽如此,我从来没有试图提取一个非常大的数据集,使用模型作为数据集交换介质不是PP的“用例”之一。 所以,非常大的表格可能会碰到一些其他的bug /约束!