在Excel Power Query文件中,数据连接可以来自SQL Server。我们有大量文件通过名称指定SQL Server,并且该服务器将被停用。我们需要更新连接,以将旧服务器名称替换为新服务器名称。这可以通过打开Excel文件,浏览到查询并手动编辑服务器名称来实现。由于文件数量众多,因此希望使用C#进行此操作。下图显示了输入字段(已删除名称),您可以在其中手动更新此字段。
首先,通过解压缩Excel文件并浏览文件夹xl > connections.xml
下的内容,我希望它可以指定那里的连接,但是只显示$Workbook$
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<connections xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<connection id="1" keepAlive="1" name="Query" description="Connection to the query in the workbook." type="5" refreshedVersion="6" background="1" saveData="1">
<dbPr connection="Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location="table"" command="SELECT * FROM [table]"/>
</connection>
</connections>
在MDSN forms上有一个关于此主题的参考,Will Gregg提供的答案是:
外部数据源连接信息存储在XLSX包的自定义部分中。您可以在包的customXML文件夹下找到自定义部件。例如:customXml \ iem1.xml。
item1.xml中包含一个元素。元素的定义可以在[MS-QDEFF]:查询定义文件格式文档(https://msdn.microsoft.com/en-us/library/mt577220(v=office.12).aspx)中找到。
为了使用元素的数据,您需要按照[MS-QDEFF]:查询定义文件格式文档中所述对内容进行解码。
数据解码后,您将需要检查PackagePart的内容。在该软件包中,您可以在Forumlas \ Section1.m部分中找到外部数据连接信息。
这有助于将我指向item.xml
文件夹中的customXml
文件,但没有提供有关如何解码DataMashup
对象中信息的任何详细信息。答案确实提到link上main article的[MS-QDEFF]: Query Definition File Format
文档中有关查询定义格式的信息。乍看之下,本文档中的信息可能看起来很复杂。
在堆栈溢出中,有6个问题提到DataMashup
,其中有4个问题与Power BI有关,尽管与此问题相似,但并不相同。下面列出了每个问题的链接:
其他两个问题更相关,因为它们询问的是Excel,而不是Power BI,我将在下面讨论:
$Workbook$
。在Axel Richter的评论中,他说In *.xlsx/customXml/ you will find a item1.xml which contains a DataMashup element which contains a base64Binary which is the binary query definition file. I have no clue how to work with that. That's why only a comment and not a answer.
一年后,汤姆·杰博(Tom Jebo)添加了一个答案,指向我也发现的开放规范详细信息,但未提供有关如何操作DataMashup
的解决方案宾语。我将此添加为新问题,因为该问题旨在解决与我不同的问题,并且它也在寻找JavaScript中的解决方案。解码DataMashup
对象,更改服务器名称,然后将更新后的连接保存回Excel文件的最佳方法是什么?
在Jeff Atwood在2011年7月1日发布的blog post中,鼓励提出和回答自己的问题。此外,this page表格的Stack Overflow帮助中心也解决了同一问题。我决定在C#中发布一个完整的工作解决方案,以供其他人修改和使用,希望可以节省他们完成我所做的所有工作所需的时间。
答案 0 :(得分:0)
如问题中所述,最有用的文档是[MS-QDEFF]: Query Definition File Format
。我将在此处包括本文档中最相关的部分,但如果需要,请参考原始文档。下面显示了Microsoft提供的带有DataMashup
的示例XML。这是一个简短的查询,但是如果您打开customXml > item1.xml
文件,期望会有类似的结果。
<DataMashup sqmid="7690c5d6-5698-463c-a560-a0093d4f6332"
xmlns="http://schemas.microsoft.com/DataMashup">
AAAAAEUDAABQSwMEFAACAAgAta0pR62KRJynAAAA+QAAABIAHABDb25maWcvUGFja2FnZS54bWwgohgA
KKAUAAAAAAAAAAAAAAAAAAAAAAAAAAAhY9NDoIwGESvQrqnP4jGkI+ycCuJCdG4bUqFRiiGFsvdXHgkr
yCJYti5nMmb5M3r8YRsbJvgrnqrO5MihikKlJFdqU2VosFdwi3KOByEvIpKBRNsbDJanaLauVtCiPce+
xXu+opElDJyzveFrFUrQm2sE0Yq9FuV/1eIw+kjwyMcxTimmzVmMWVA5h5ybRbMpIwpkEUJu6FxQ6+4M
uGxADJHIN8b/A1QSwMEFAACAAgAta0pRw/K6aukAAAA6QAAABMAHABbQ29udGVudF9UeXBlc10ueG1sI
KIYACigFAAAAAAAAAAAAAAAAAAAAAAAAAAAAG2OSw7CMAxErxJ5n7qwQAg1ZQHcgAtEwf2I5qPGReFsL
DgSVyBtd4ilZ+Z55vN6V8dkB/GgMfbeKdgUJQhyxt961yqYuJF7ONbV9Rkoihx1UUHHHA6I0XRkdSx8I
Jedxo9Wcz7HFoM2d90Sbstyh8Y7JseS5x9QV2dq9DSwuKQsr7UZB3Fac3OVAqbEuMj4l7A/eR3C0BvN2
cQkbZR2IXEZXn8BUEsDBBQAAgAIALWtKUdi3rmEPAAAAEsAAAATABwARm9ybXVsYXMvU2VjdGlvbjEub
SCiGAAooBQAAAAAAAAAAAAAAAAAAAAAAAAAAAArTk0uyczPUwiG0IbWvFy8XMUZiUWpKQqBpalFlYYKt
go5qSW8XApAEJxfWpScChQx1Dbk5crMQxa1BgBQSwECLQAUAAIACAC1rSlHrYpEnKcAAAD5AAAAEgAAA
AAAAAAAAAAAAAAAAAAAQ29uZmlnL1BhY2thZ2UueG1sUEsBAi0AFAACAAgAta0pRw/K6aukAAAA6QAAA
BMAAAAAAAAAAAAAAAAA8wAAAFtDb250ZW50X1R5cGVzXS54bWxQSwECLQAUAAIACAC1rSlHYt65hDwAA
ABLAAAAEwAAAAAAAAAAAAAAAADkAQAARm9ybXVsYXMvU2VjdGlvbjEubVBLBQYAAAAAAwADAMIAAABtA
gAAAAA0AQAA77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48UGVybWlzc2lvb
kxpc3QgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIge
G1sbnM6eHNkPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PENhbkV2YWx1YXRlRnV0d
XJlUGFja2FnZXM+ZmFsc2U8L0NhbkV2YWx1YXRlRnV0dXJlUGFja2FnZXM+PEZpcmV3YWxsRW5hYmxlZ
D50cnVlPC9GaXJld2FsbEVuYWJsZWQ+PFdvcmtib29rR3JvdXBUeXBlIHhzaTpuaWw9InRydWUiIC8+P
C9QZXJtaXNzaW9uTGlzdD7LBwAAAAAAAKkHAADvu788P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nP
SJ1dGYtOCI/PjxMb2NhbFBhY2thZ2VNZXRhZGF0YUZpbGUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczL
m9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHNkPSJodHRwOi8vd3d3LnczLm9yZy8yM
DAxL1hNTFNjaGVtYSI+PEl0ZW1zPjxJdGVtPjxJdGVtTG9jYXRpb24+PEl0ZW1UeXBlPkFsbEZvcm11b
GFzPC9JdGVtVHlwZT48SXRlbVBhdGggLz48L0l0ZW1Mb2NhdGlvbj48U3RhYmxlRW50cmllcyAvPjwvS
XRlbT48SXRlbT48SXRlbUxvY2F0aW9uPjxJdGVtVHlwZT5Gb3JtdWxhPC9JdGVtVHlwZT48SXRlbVBhd
Gg+U2VjdGlvbjEvUXVlcnkxPC9JdGVtUGF0aD48L0l0ZW1Mb2NhdGlvbj48U3RhYmxlRW50cmllcz48R
W50cnkgVHlwZT0iSXNQcml2YXRlIiBWYWx1ZT0ibDAiIC8+PEVudHJ5IFR5cGU9IlJlc3VsdFR5cGUiI
FZhbHVlPSJzTnVtYmVyIiAvPjxFbnRyeSBUeXBlPSJGaWxsRW5hYmxlZCIgVmFsdWU9ImwxIiAvPjxFb
nRyeSBUeXBlPSJGaWxsVG9EYXRhTW9kZWxFbmFibGVkIiBWYWx1ZT0ibDAiIC8+PEVudHJ5IFR5cGU9I
kZpbGxDb3VudCIgVmFsdWU9ImwxIiAvPjxFbnRyeSBUeXBlPSJGaWxsRXJyb3JDb3VudCIgVmFsdWU9I
mwwIiAvPjxFbnRyeSBUeXBlPSJGaWxsQ29sdW1uVHlwZXMiIFZhbHVlPSJzQlE9PSIgLz48RW50cnkgV
HlwZT0iRmlsbENvbHVtbk5hbWVzIiBWYWx1ZT0ic1smcXVvdDtRdWVyeTEmcXVvdDtdIiAvPjxFbnRye
SBUeXBlPSJGaWxsRXJyb3JDb2RlIiBWYWx1ZT0ic1Vua25vd24iIC8+PEVudHJ5IFR5cGU9IkZpbGxMY
XN0VXBkYXRlZCIgVmFsdWU9ImQyMDE1LTA5LTEwVDA0OjQ1OjQxLjkyNzU5MDBaIiAvPjxFbnRyeSBUe
XBlPSJSZWxhdGlvbnNoaXBJbmZvQ29udGFpbmVyIiBWYWx1ZT0ic3smcXVvdDtjb2x1bW5Db3VudCZxd
W90OzoxLCZxdW90O2tleUNvbHVtbk5hbWVzJnF1b3Q7OltdLCZxdW90O3F1ZXJ5UmVsYXRpb25zaGlwc
yZxdW90OzpbXSwmcXVvdDtjb2x1bW5JZGVudGl0aWVzJnF1b3Q7OlsmcXVvdDtTZWN0aW9uMS9RdWVye
TEvQXV0b1JlbW92ZWRDb2x1bW5zMS57UXVlcnkxLDB9JnF1b3Q7XSwmcXVvdDtDb2x1bW5Db3VudCZxd
W90OzoxLCZxdW90O0tleUNvbHVtbk5hbWVzJnF1b3Q7OltdLCZxdW90O0NvbHVtbklkZW50aXRpZXMmc
XVvdDs6WyZxdW90O1NlY3Rpb24xL1F1ZXJ5MS9BdXRvUmVtb3ZlZENvbHVtbnMxLntRdWVyeTEsMH0mc
XVvdDtdLCZxdW90O1JlbGF0aW9uc2hpcEluZm8mcXVvdDs6W119IiAvPjxFbnRyeSBUeXBlPSJGaWxsZ
WRDb21wbGV0ZVJlc3VsdFRvV29ya3NoZWV0IiBWYWx1ZT0ibDEiIC8+PEVudHJ5IFR5cGU9IkFkZGVkV
G9EYXRhTW9kZWwiIFZhbHVlPSJsMCIgLz48RW50cnkgVHlwZT0iUmVjb3ZlcnlUYXJnZXRTaGVldCIgV
mFsdWU9InNTaGVldDIiIC8+PEVudHJ5IFR5cGU9IlJlY292ZXJ5VGFyZ2V0Q29sdW1uIiBWYWx1ZT0ib
DEiIC8+PEVudHJ5IFR5cGU9IlJlY292ZXJ5VGFyZ2V0Um93IiBWYWx1ZT0ibDEiIC8+PEVudHJ5IFR5c
GU9Ik5hbWVVcGRhdGVkQWZ0ZXJGaWxsIiBWYWx1ZT0ibDAiIC8+PEVudHJ5IFR5cGU9IkZpbGxUYXJnZ
XQiIFZhbHVlPSJzUXVlcnkxIiAvPjxFbnRyeSBUeXBlPSJCdWZmZXJOZXh0UmVmcmVzaCIgVmFsdWU9I
mwxIiAvPjxFbnRyeSBUeXBlPSJGaWxsU3RhdHVzIiBWYWx1ZT0ic0NvbXBsZXRlIiAvPjxFbnRyeSBUe
XBlPSJRdWVyeUlEIiBWYWx1ZT0iczdlMDQzNjJlLTkyZjUtNGQ4Mi04YjA3LTI3NjFlYWY2OGFlNSIgL
z48L1N0YWJsZUVudHJpZXM+PC9JdGVtPjxJdGVtPjxJdGVtTG9jYXRpb24+PEl0ZW1UeXBlPkZvcm11b
GE8L0l0ZW1UeXBlPjxJdGVtUGF0aD5TZWN0aW9uMS9RdWVyeTEvU291cmNlPC9JdGVtUGF0aD48L0l0Z
W1Mb2NhdGlvbj48U3RhYmxlRW50cmllcyAvPjwvSXRlbT48L0l0ZW1zPjwvTG9jYWxQYWNrYWdlTWV0Y
WRhdGFGaWxlPhYAAABQSwUGAAAAAAAAAAAAAAAAAAAAAAAA2gAAAAEAAADQjJ3fARXREYx6AMBPwpfrA
QAAACLWGAG5O6FHjkAGtB+m5EQAAAAAAgAAAAAAA2YAAMAAAAAQAAAAaH8KNe2ciHwfVosIvSCr6gAAA
AAEgAAAoAAAABAAAAA40fOKWe6kmTAWJSBXs4cYUAAAAPNy7uF6Dtr9PvADu+eZdeV7JutpIQTh41qqT
3QnFoWPwE0Xyrur5N6Q2s2TEzjlBDfkEmNaGtr3htemOjWZYXKQHP+R5u/90zHWiwOwjjowFAAAAF2UC
6Jm8C98hVmJBo638e4Qk65V
</DataMashup>
此对象的值编码在Base64
字符串中。如果您不熟悉Base 64,那么this Wikipedia文章将是一个不错的起点。解决方案的第一步是打开XML文档,并将其转换为其byte
表示形式。可以按照以下步骤进行操作:
string file = @"\customXml\item1.xml"; // or wherever your xml file is
XDocument doc = XDocument.Load(file);
byte[] dataMashup = Convert.FromBase64String(doc.Root.Value);
注意:在此答案底部提供的完整示例中,所有操作都在内存中完成。
来自Microsoft定义文档:
版本(4个字节):必须设置为0的无符号整数。
包装件长度(4个字节):无符号整数,用于指定“包装件”字段的长度。
包装件(可变):可变长度的二进制流(第2.3节)。
权限长度(4个字节):无符号整数,用于指定“权限”字段的长度。
权限(可变)::可变长度的二进制流(第2.4节)。
元数据长度(4个字节):无符号整数,用于指定元数据字段的长度。
元数据(可变):可变长度的二进制流(第2.5节)。
权限绑定长度(4个字节):无符号整数,用于指定“权限绑定”字段的长度。
权限绑定(可变):可变长度的二进制流(第2.6节)。
由于每个定义其内容长度的字段均为4个字节,因此我定义了一个常量
private const int FIELDS_LENGTH = 4;
然后可以在本节中定义的每个值(从Microsoft引用)如下所示:
int version = BitConverter.ToUInt16(dataMashup.Take(FIELDS_LENGTH).ToArray(), 0);
int packagePartsLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] packageParts = dataMashup.Skip(FIELDS_LENGTH * 2).Take(packagePartsLength).ToArray();
int permissionsLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH * 2 + packagePartsLength).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] permissions = dataMashup.Skip(FIELDS_LENGTH * 3).Take(permissionsLength).ToArray();
int metadataLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH * 3 + packagePartsLength + permissionsLength).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] metadata = dataMashup.Skip(FIELDS_LENGTH * 4 + packagePartsLength + permissionsLength).Take(metadataLength).ToArray();
int permissionsBindingLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH * 4 + packagePartsLength + permissionsLength + metadataLength).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] permissionsBinding = dataMashup.Skip(FIELDS_LENGTH * 5 + packagePartsLength + permissionsLength + metadataLength).Take(permissionsBindingLength).ToArray();
使用byte[]
作为包装零件,它代表Package
名称空间中的System.IO.Packaging
对象。
using (MemoryStream ms = new MemoryStream(packageParts)) {
using (Package package = Package.Open(ms, FileMode.Open, FileAccess.ReadWrite)) {
PackagePart section = package.GetParts().Where(x => x.Uri.OriginalString == "/Formulas/Section1.m").FirstOrDefault();
string query;
using (StreamReader reader = new StreamReader(section.GetStream())) {
query = reader.ReadToEnd();
// do other replacing, removing of query here
}
using (BinaryWriter writer = new BinaryWriter(section.GetStream())) {
// write updated query back to package part
writer.Write(Encoding.ASCII.GetBytes(query));
}
}
packageParts = ms.ToArray();
}
最后,我需要使用来自更新包中的新信息来更新原始byte[]
。
bytes = BitConverter.GetBytes(version)
.Concat(BitConverter.GetBytes(packageParts.Length))
.Concat(packageParts)
.Concat(BitConverter.GetBytes(permissionsLength))
.Concat(permissions)
.Concat(BitConverter.GetBytes(metadataLength))
.Concat(metadata)
.Concat(BitConverter.GetBytes(permissionsBindingLength))
.Concat(permissionsBinding);
doc.Root.Value = Convert.ToBase64String(bytes.ToArray());
entryStream.SetLength(0);
doc.Save(entryStream);
下面是完整的完整示例。它是一个控制台应用程序,它接收文件目录以作为命令行参数进行更新,然后用新的服务器名替换旧的服务器名。
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.IO.Compression;
using System.Xml.Linq;
using System.IO.Packaging;
using System.Text;
namespace MyApp {
class Program {
private const int FIELDS_LENGTH = 4;
static void Main(string[] args) {
if (args.Length != 1) {
Console.WriteLine("specify one directory to update");
}
if (!Directory.Exists(args[0])) {
Console.WriteLine("directory does not exist");
}
IEnumerable<FileInfo> files = Directory.GetFiles(args[0]).Where(x => Path.GetExtension(x) == ".xlsx").Select(x => new FileInfo(x));
foreach (FileInfo file in files) {
using (FileStream fileStream = File.Open(file.FullName, FileMode.OpenOrCreate)) {
using (ZipArchive archive = new ZipArchive(fileStream, ZipArchiveMode.Update)) {
ZipArchiveEntry entry = archive.GetEntry("customXml/item1.xml");
IEnumerable<byte> bytes;
using (Stream entryStream = entry.Open()) {
XDocument doc = XDocument.Load(entryStream);
byte[] dataMashup = Convert.FromBase64String(doc.Root.Value);
int version = BitConverter.ToUInt16(dataMashup.Take(FIELDS_LENGTH).ToArray(), 0);
int packagePartsLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] packageParts = dataMashup.Skip(FIELDS_LENGTH * 2).Take(packagePartsLength).ToArray();
int permissionsLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH * 2 + packagePartsLength).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] permissions = dataMashup.Skip(FIELDS_LENGTH * 3).Take(permissionsLength).ToArray();
int metadataLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH * 3 + packagePartsLength + permissionsLength).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] metadata = dataMashup.Skip(FIELDS_LENGTH * 4 + packagePartsLength + permissionsLength).Take(metadataLength).ToArray();
int permissionsBindingLength = BitConverter.ToUInt16(dataMashup.Skip(FIELDS_LENGTH * 4 + packagePartsLength + permissionsLength + metadataLength).Take(FIELDS_LENGTH).ToArray(), 0);
byte[] permissionsBinding = dataMashup.Skip(FIELDS_LENGTH * 5 + packagePartsLength + permissionsLength + metadataLength).Take(permissionsBindingLength).ToArray();
// use double memory stream to solve issue as memory stream will change
// size when re-saving the data mashup object
using (MemoryStream packagePartsStream = new MemoryStream(packageParts)) {
using (MemoryStream ms = new MemoryStream()) {
packagePartsStream.CopyTo(ms);
using (Package package = Package.Open(ms, FileMode.Open, FileAccess.ReadWrite)) {
PackagePart section = package.GetParts().Where(x => x.Uri.OriginalString == "/Formulas/Section1.m").FirstOrDefault();
string query;
using (StreamReader reader = new StreamReader(section.GetStream())) {
query = reader.ReadToEnd();
// do other replacing, removing of query here
query = query.Replace("old-server", "new-server");
}
using (BinaryWriter writer = new BinaryWriter(section.GetStream())) {
writer.Write(Encoding.ASCII.GetBytes(query));
}
}
packageParts = ms.ToArray();
}
bytes = BitConverter.GetBytes(version)
.Concat(BitConverter.GetBytes(packageParts.Length))
.Concat(packageParts)
.Concat(BitConverter.GetBytes(permissionsLength))
.Concat(permissions)
.Concat(BitConverter.GetBytes(metadataLength))
.Concat(metadata)
.Concat(BitConverter.GetBytes(permissionsBindingLength))
.Concat(permissionsBinding);
doc.Root.Value = Convert.ToBase64String(bytes.ToArray());
entryStream.SetLength(0);
doc.Save(entryStream);
}
}
}
}
}
}
}
}
注意::由于我只需要更新Package Parts
部分,因此可以确认此解码/编码有效,但是我没有测试{{1}的解码/编码},Permissions
或Metadata
。如果您需要使用这些功能,那么至少应该可以开始使用。
注意::此代码不会捕获错误或处理所有情况。这旨在成为如何更新Power Query文件中的连接的有效示例。随时根据需要进行调整。