EDIFACT 转 XML 再转任何您想要的格式






4.97/5 (46投票s)
2005 年 8 月 11 日
16分钟阅读

367412

21208
一篇关于将 EDIFACT 消息转换为 XML 的文章,并将 XML 转换为 XSLT 可以处理的任何内容
引言
在本文中,我将讨论 EDIFACT 消息的基础知识,谈谈将 EDI 消息转换为 XML 的想法的演变,并用代码演示这些概念。最终目标是展示该框架如何用于接收 EDI 消息并使用 XML 转换将其输出为您想要的任何格式。我还会指出一些可能让您感兴趣的、最有意义的代码片段,并且(更确切地说,应该)在此基础上进行扩展。
背景
大约 10 个月前,我被要求帮助一家已经交付迟缓的公司组建一个 BizTalk 解决方案。我以前从未接触过 BizTalk,只知道它能自动化消息处理。这可能过于简化了该产品,但接收、处理和传递以业务为中心的消息,本质上就是它所做的一切。
那时,我刚接触业务消息框架,对它们了解不多。由于消息标准众多,我仍然可以谦虚地说我了解不多。尽管我被上述公司坑了,但了解 BizTalk 和 EDIFACT 标准是一次很好的经历。
就像许多刚开始接触 EDI 的人一样,我通过谷歌搜索来熟悉该标准,并阅读了前辈们的经验。我发现没有什么可供发现的。事实上,除了 BizTalk 和类似价格高昂的解决方案之外,关于这个主题的资料并不多。现有的解决方案看起来更像是带有天价标签的黑盒子,而不是可以构建的基础框架。因此,我开始了我尝试创建一些有用的东西的探索,这些东西可以为小型组织廉价地完成类似的操作。
EDIFACT 标准
与所有消息标准一样,EDIFACT 的创建是为了统一公司之间发送消息的方式。它是联合国创建和维护的标准,约有 3000 页,为贸易和运输领域的电子数据交换提供了丰富的语义。对许多普通人来说,语义很难理解和实现,但我会尽量证明,一旦你理解了它的结构,它其实很简单。该标准定义了诸如:定价目录、订单、订单响应、发票等。
不幸的是,并非每个国家都以相同的方式实现此标准。进一步的复杂性在于,不同的行业只使用与自身需求相关的某些段来开展电子商业活动。图 1 通过展示 INVOIC 消息的一种实现方式来证明这一点。白色框突出显示使用的段,而灰色框则被忽略。由于这些差异,它可能看起来比标准化的更灵活,但实际上它非常灵活。每条消息的文档都有助于使其可预测。
EDIFACT 消息包含许多称为“段”的部分。每个段包含许多称为“复合段”的子部分,每个复合段包含零个或多个称为“数据元素”的信息部分。参见图 2。
每个段都以三个字母的缩写开头,用于标识段的类型。EDIFACT 标准定义了哪些段允许出现在哪些类型的消息中。例如,无论处理的是订单还是发票,消息类型只能包含一定数量的预定义段,并按预定义顺序排列。当然,它是一个庞大的标准,包含的内容远超任何一家公司开展日常业务所需。因此,许多实现只使用每个相应消息标准中一小部分的段。
如图 2 所示,这是 D93A 版本消息的一个示例,EDIFACT 消息包含三个主要分隔符来分隔段的子部分。段本身由撇号分隔,而复合段由加号分隔。复合段中的数据元素由分号分隔。需要指出的是,在特殊情况下,分隔符需要被解释为普通字符而不是分隔符。在这种情况下,可以通过观察分隔符前面的问号来识别。例如,2?+2=4 实际上意味着 2+2=4。前导问号会恢复字符的含义。问号表示为 ??。
我们来谈谈分组。段以逻辑方式分组,以赋予其含义结构。如图 3 所示,分组包括:交换、功能以及实际的消息。服务字符串建议通常可以忽略。
为了简化起见,本示例中提出的框架没有结构化分组。但这并不是说段分组未被处理,实际上它们被处理了;尽管除了一个之外,其他都被忽略了。正如您将看到的,实现它们并不难,但对于演示将 EDI 转换为任何您想要的内容的想法来说,并不重要。
EDI 到 XML 框架
在继续之前,我认为值得注意的是,解析分隔文件的方法与“一千种杀猫的方法”一样多。我绝不是要告诉您哪种方法是正确的,但在接下来的示例中,Split
函数对于本次演示的目的来说绰绰有余。此外,如前所述,在某些情况下,分隔符不应被解释为分隔符。在代码中,可以通过正则表达式等工具轻松检测这些情况,但在代码中并未实现。我将指出在哪里以及何时是检测这种情况的好地方,但将其留给您来实现(您是否讨厌这样? ;-) 来吧,这会很有趣!)话虽如此,让我们继续。
从宏观角度看,该框架是一系列按顺序进行的步骤,它们相互级联,构建一个表示其包含信息的消息对象。
从图 4 可以看出,处理 EDI 文件的初始入口点是 EdiMessage
类。EdiMessage
在构造函数中方便地接受 EDI 文件的路径。这样处理时,构造函数会读取文件并实例化一个 Parser
类。解析器以 string
参数的形式接受读取的 EDI 消息。然后,解析器使用 Split
函数开始将 EDI 消息分解成 Segment
对象。由于分隔符字符不应被解释为分隔符的特殊情况最有可能出现在 segment string
中,因此我建议在将 segment
string
传递给 Segment
对象时实现正则表达式搜索。Segment object
只是一个方便的容器对象,用于存储段的名称(UNH、BGM 等)以及 FieldCollection
。FieldCollection
包含简单和复合数据元素。该框架本可以避免将段和字段包装在命名对象中,也许将来会这样做,但在这里和现在为了简洁起见使用了它们。
由于大多数 EDIFACT 消息实现是如何构造的,因此不总是需要显式分隔复合类型及其包含的数据元素。在解析数据元素以填充特定的 Segment
类时,行业规范可以轻松地通过明确说明消息规范中使用了什么来知道元素偏移量。明智地使用这些信息,可以演示如何使用偏移量来填充 IMessage
派生的对象,例如 D96A_INVOIC
。图 5 展示了来自此类规范的日期/时间/期间 (DTM) 段,这里展示它只是为了说明确定预期是多么容易。
Parser
类的 ParseDocument()
方法使用 UNB 和 UNH 分组段为每条消息填充一个 MessageProperties
结构。由于单个文件中可能有多条消息,因此 MessageProperties
用于确定其中包含的消息数量、消息标识符、版本等。
Parser
的 CreateMessageObject()
方法是真正有趣的地方(参见列表 1)。首先,创建一个 SegmentCollections
数组,其中有一个集合对应原始文件中的每条消息。每当检测到新的 UNH 段时,就会创建一个新集合。它以及每个后续段都会被添加到当前集合中,直到找到下一个 UNH 段。
列表 1
for (Int32 j = 0; j < arSegments .Length; j ++)
{
string name = arSegments[j] .Name;
if( name == "UNA" || name == "UNB" ||
name == "UNG" || name == "UNE" ||
name == "UNZ" ) continue;
if( name == "UNH" ) sc[scCount] =
new SegmentCollection();
if( name == "UNT" )
{
sc[scCount] .Add(segments[j]);
if (j == arSegments .Length - 1)
break;
scCount ++;
continue;
}
sc[scCount] .Add(segments[j]);
}
UNT 代表一个特殊情况,因为它标记自己为消息组中的最后一个。并且,它可能是一个段数组中的最后一个段。如果它是最后一个,它会退出循环,然后继续实例化与段集合数组中的 SegmentCollections
数量一样多的 IMessage
派生对象。
IMessage
派生类只是一个实现了 IMessage
接口的消息类。IMessage
定义了一个名为 PopulateMessage()
的方法。PopulateMessage()
接受一个段数组,并为每个段实例化等效的类。例如,如果当前的 Segment
对象名称是 UNH,PopulateMessage
会实例化一个 UNH 对象,用数据元素值填充相应的字段,然后将 UNH 对象添加到消息类中(ORDERS
、INVOICE
等)。
正如在列表 2 中所见,CreateMessageObject()
然后开始遍历 SegmentCollections
数组。它提取集合中的每个项目以构建一个临时的 Segment
数组。指向这个临时段数组的引用作为参数传递给 IMessage
派生对象,作为 PopulateMessage()
方法的参数。
列表 2
//Loop over segment collections and
//create as many edi messages.
for(Int32 i = 0; i < sc.Length; i++)
{
//Using release number (D96A) and
//Message Identifier (ORDERS),
//creates the appropriate class.
this.messageObject[i] = GetMessageType(mp.releaseNumber,
mp.identifier);
//Takes each segment from Segment
//Collection and creates a general
//Segment Array to pass into PopulateMessage(...)
Segment [] tempSegments = new Segment[sc[i].Count];
Int32 j=0;
foreach(Segment s in sc[i])
{tempSegments[j] = s;j++;}
//Now pass segment array into the
//message objects PopulateMessage
//routine. Each message object
//implements the interface method.
try
{
this.messageObject[i].PopulateMessage(
ref tempSegments);
}
catch(Exception e)
{
…
}
finally
{
tempSegments = null;
}
}
您可能会问,为什么不直接将 SegmentCollection
的索引传递给 PopulateMessage()
。事实是,正如您一会儿将看到的,我将如何最好地实现消息类的这一部分留给您决定。如前所述,每个 EDI 消息类都实现了 IMessage
接口,该接口强制要求一个方法,即 PopulateMessage()
。PopulateMethod()
接受构成完整(尽管是 UNH-UNT)EDIFACT 消息的 Segment
数组。但是,消息类(例如 D96A_INVOIC
类)充当单个消息对象 INVOIC
的包装器,该对象由各种组对象组成,而组对象又包含各种段类对象。可选地,如果您选择将 PopulateMessage()
传递 SegmentCollection
数组,D96A_INVOIC
随后可以实例化一个 INVOIC
对象数组并在内部填充每个对象。我选择不这样做,主要是因为我有其他要求。但是,以这种方式实现可以保持简单,以防将来需要重构。
最初,PopulateMessage()
会遍历每个 Segment
,填充相应的 Segment
类对象(例如 UNH
、BGM
、DTM
等),然后将该对象提交给 INVOIC
的 Add()
方法(列表 3)。Add()
接收 Segment
类对象,并使用字段标识符将其放置在 INVOIC
类中的适当段组中(列表 4)。
列表 3
case "UNH":
UNH unh = new UNH();
for(int j = 0;j < segmentArray[i].Fields.Count;j++)
{
switch(j)
{
case 0:
{
unh.referenceNumber =
segmentArray[i].Fields.Item(j).Value;
break;
}
case 1:
{
unh.typeIdentifier =
segmentArray[i].Fields.Item(j).Value;
break;
}
case 2:
{
unh.versionNumber =
segmentArray[i].Fields.Item(j).Value;
break;
}
case 3:
{
unh.releaseNumber =
segmentArray[i].Fields.Item(j).Value;
break;
}
case 4:
{
unh.controllingAgency =
segmentArray[i].Fields.Item(j).Value;
break;
}
case 5:
{
unh.associationAssignedCode =
segmentArray[i].Fields.Item(j).Value;
break;
}
}
}
INVOIC.Add(SegmentType.UNH,unh);
unh = null;
break;
列表 4
case SegmentType.DTM:
{
int qualifier =
Int32.Parse(((DTM)obj).dateTimePeriodQualifier);
if(qualifier == 171)
{
if((i= this.GRP1.Count) > 0)
this.GRP1[i-1].DTM = (DTM)obj;
}
else
{ //2, 63, 64, 137
this.DTMCollection.Add((DTM)obj);
}
break;
}
一旦我意识到我为每个消息对象(在列表 3 中可见)编写了相同的迭代代码,创建 SegmentProcessor
类来完成这项工作就显得很有意义了。PopulateMessage()
实例化一个 SegmentProcessor
实例,该实例在构造函数中接受一个委托类型。将委托分配给指向消息对象的 Add()
方法的属性(参见列表 5)。接下来,调用 SegmentProcessor
的 ProcessSegments()
方法,创建并填充 Segment
对象,如列表 3 所示。只是这次,它不是调用消息对象的 Add()
函数,而是调用委托属性 AddFunction()
。
列表 5
//------ The DELEGATE (Global)--------------
public delegate void AddSegmentDelegate(SegmentType segmentType,
object segmentObject);
//------POPULATEMESSAGE Method (D96A_INVOIC)---------------
public void PopulateMessage(ref Segment [] segments)
{
SegmentProcessor sp = new SegmentProcessor(
new AddSegmentDelegate(this.INVOIC.Add));
sp.ProcessSegments(segments);
}
//------SEGMENTPROCESSOR Constructor (SegmentProcessor)------
/// <summary>
/// Accepts an <see cref="AddSegmentDelegate"/> that poplulates the
/// <see cref="AddFunction"/> property.</summary>
/// <param name="myDelegate">An AddSegmentDelegate used to populate
/// the SegmentProcessors <see cref="AddFunction"/> property.</param>
public SegmentProcessor(AddSegmentDelegate myDelegate)
{
this.addFunction = myDelegate;
}
AddFunction()
将填充好的 Segment
对象传回调用它的消息对象,并继续添加 Segment
对象,如列表 4 所示。
最后,一旦原始 EDI 文件中的所有消息都被解析、消息对象被创建和填充,它们就可以通过 Parser
的 GetMessages()
方法获得。GetMessages()
简单地返回 Parser
现在包含的 IMessage
对象数组。
消息对象到 XML
此时,所有 EDI 消息处理都已完成。剩下的唯一一件事就是将消息对象转换为 XML。我不知道您怎么想,但我真的很喜欢 .NET 在 XML 处理方面带来的功能。在这些情况下,我特别喜欢 XML 属性,以及将它们与 XmlSerializer
类一起使用。例如,只需用 XML 属性修饰段和消息类(参见列表 6),就可以利用 XmlSerializer
类从我的类对象生成 XML 文档。太棒了!
列表 6
[XmlType(TypeName="DTM",Namespace=Declarations.SchemaVersion),
XmlRoot,Serializable]
public class DTM
{
[XmlAttribute(AttributeName="dateTimePeriodQualifier",
Form=XmlSchemaForm.Unqualified,
DataType="string",
Namespace=Declarations.SchemaVersion)]
public string __dateTimePeriodQualifier;
[XmlIgnore]
public bool __dateTimePeriodQualifierSpecified;
[XmlIgnore]
public string dateTimePeriodQualifier
{
get { return __dateTimePeriodQualifier; }
set {
__dateTimePeriodQualifier = value;
__dateTimePeriodQualifierSpecified = true;
}
}
… //Omitted for brevity, See Source...
public DTM()
{
}
}
现在我们所要做的就是将对象类型和我们想要转换为 XML 的实际对象传递给 XmlSerializer
对象,然后调用 Serialize()
方法(参见列表 7)。
列表 7
/// <summary>
/// This function converts the inner representation
/// of an EDI message into XML.</summary>
/// <returns>An array of XmlDocument that may
/// consist of zero or many
/// EDI messages in an XML representation.</returns>
public XmlDocument [] SerializeToXml()
{
XmlSerializer sr;
StringBuilder sb;
XmlTextWriter tr;
xDoc = new XmlDocument[messageArray.Length];
try {
for(Int32 i = 0; i < messageArray.Length; i++)
{
System.Type type = messageArray[i].GetType();
sr = new XmlSerializer(type);
sb = new StringBuilder();
tr = new XmlTextWriter(new
StringWriterWithEncoding(sb,
System.Text.Encoding.UTF8));
sr.Serialize(tr, messageArray[i]);
tr.Close();
xDoc[i] = new XmlDocument();
xDoc[i].LoadXml(sb.ToString());
}
return xDoc;
}
catch(Exception ex)
{
Console.WriteLine(string.Format("Exception: " +
"{0}. InnerException: {1}.",
ex.Message, ex.InnerException));
return null;
}
finally
{
sr = null;
sb = null;
tr = null;
}
}
SerializeToXml()
利用 XmlSerializer
类将消息对象转换为 XML。通过循环遍历 GetMessages()
返回的所有消息,SerializeToXml()
首先获取 IMessage
派生消息对象的类型。然后,它实例化一个 XmlSerializer
对象,将消息对象的类型和实际消息对象传递给构造函数。然后构造一个 XmlTextWriter
对象用于写入,它接受一个 StringBuilder
对象用于写入。最后,调用 XmlSerializer
的 Serialize()
方法将消息对象转换为 XML。结果被写入 StringBuilder
对象,然后该对象用于 XmlDocument
的 LoadXml()
方法。最后,XmlDocument
数组被返回给调用者。
现在的问题是,您想用 xDoc
数组中可能存在的许多 XmlDocument
做什么。
XML 转任何您想要的东西
EdiMessage
包含另外三个有趣的方法可供使用
SerializeToFile(string filename)
TransformToFile(string XslFilename, string filename)
Transform(string transformFile, ref XmlDocument[] transformedXml)
SerializeToFile()
顾名思义,它将 xDoc
中的每个 XmlDocument
写入磁盘。这里写入的文件是消息对象的镜像反映,只是 XML 格式,而不是二进制或 EDIFACT 格式。
TransformToFile()
接受 XSL/XSLT 文件名作为输入,使用它对每个 XmlDocument
对象执行转换,以及一个 string
,用于指定转换后的文件名。
Transform
接受 XSL/XSLT 文件名,用于对 xDoc
中的每个 XmlDocument
对象执行转换。它还通过引用接受一个 XmlDocument
对象数组的变量名。这个数组将成为转换的结果。传入的数组不必初始化,它将在需要时在方法内部完成。
同样重要的是要注意 AnalyseFileName()
函数。尽管它是一个 private
方法,但该函数接受通常提交给 SerializeToFile()
和 TransformToFile()
的文件名。AnalyseFileName()
接受一个带有嵌入分隔宏的 string
,这些宏有助于将文件名重命名为更有意义的内容。这有很多好处。首先,它有助于创建唯一的文件名。当 xDoc
中有一个以上的 XmlDocument
对象时,这非常方便。如果您只提供一个简单的 string
(例如“MyFile.xml”)来保存文件,当有一个以上的对象时会发生什么?很简单,每个对象都会覆盖上一个,直到最后一个对象被写入。这样,本应有许多文件,最终只剩下一个文件。但是,利用分隔宏,例如 %ID%,可以为每个对象创建唯一的文件名。
在文件名中使用 %ID%,例如 MyFile_%ID%.xml,将输出几个名称唯一的文件到磁盘。每个文件名都将以“MyFile_”开头,包含一个 GUID 值,并以“.xml”扩展名结尾。例如,“MyFile_ 400E0E5A-A319-4795-A9AE-79105EE834F1.xml”。
AnalyseFileName()
可以处理几个分隔宏。它们可以单独使用(如上所示),也可以组合使用以生成更有意义的内容。
表 1 文件名宏 | |
%D% | 当前日期 |
%ID% | GUID |
%I% | 增量 |
%MR% | 消息参考号 |
%MT% | 消息类型(ORDERS、INVOIC、DESADV 等) |
%T% | 当前时间 |
大多数宏都是不言自明的,但让我们花点时间来详细说明 Increment。默认情况下,Increment 是零基的;也就是说,计数器从 0
开始。但是,EdiMessage
有两个属性可以调整初始值和步长乘数。
IncrementSeed
设置 Increment 的初始值,而 IncrementStep
确定递增后续调用时使用的乘数。如果指定文件名中使用了默认值,Increment 将首先返回 0
,然后是 1
,然后是 2
,依此类推。如果通过将 IncrementSeed
设置为 5
,将 IncrementStep
设置为 5
来更改初始值,那么在文件名中使用 Increment
将首先返回 5
,然后是 10
,然后是 15
,依此类推。尝试使用所有宏来查看它们的工作方式。添加新的宏以生成更有趣的命名方案。
结论
本文提出的概念至少展示了一种处理 EDIFACT 消息的方法。本文涵盖了 EDIFACT 消息的基本知识,介绍了它们是如何定义的与它们的实现方式相比,以及提供了一种消费这些“恶魔”并驯服其价值的方法。这绝非一个完整的框架,但这里提出的概念和方法可能对那些正在寻找一种方法来创建通用 EDI 到 XML 实现的人有所帮助。该项目还可以定制并应用于自定义 Biztalk 管道解决方案。无论它在哪里被使用,我都希望它能帮助您将 EDIFACT 消息转换为 XML,并最终转换为任何您想要的内容。
使用文件
演示项目包括
WINEDIX.exe | 用于测试 EDI 到 XML 转换的 GUI 应用程序。 |
EDIX.exe | 用于测试 EDI 到 XML 转换的控制台应用程序。 |
EDIFACT.dll | 包含 EdiMessage 类以及进行转换的所有相关功能。 |
AxInterop.SHDocVw.dll | 用于使用浏览器控件的互操作程序集。 |
Interop.SHDocVw.dll | 用于使用浏览器控件的互操作程序集。 |
六个 EDI 测试文件 | D93A ORDERS, ORDRSP, INVOIC, DESADV, PRICAT。 D96A ORDERS。 |
四个 XSLT 文件 | XAL_ORDERS_D93A, EAN_ORDERS_D93A, XAL_ORDERS_D96A, EAN_ORDERS_D96A。 |
源项目包括
ConsoleEDIX | 用于测试 EDI 到 XML 转换的控制台应用程序。 |
EDIFACT | 构建 EDIFACT dll 所需的所有 C# 源代码。 |
WINEDIX | 用于测试 XML 到 EDI 转换的 GUI 应用程序。 |
使用 WINEDIX,打开一个 EDI 文件并单击 Convert。转换后的 EDI XML 会显示在浏览器控件中。
使用其中一个 ORDERS 的 EDI 文件,选择相应的 XSLT 文件并单击 Convert。
ORDERS_D93A.edi 搭配 EAN_ORDERS_D93A.xslt 或 XAL_ORDERS_D93A.xslt。
ORDERS_D96A.edi 搭配 EAN_ORDERS_D96A.xslt 或 XAL_ORDERS_D96A.xslt。
创建您自己的 XSLT 文件,将 EDI XML 转换为您想要的任何格式。请注意命名空间:http://www.default.com/D93A/orders, http://www.default.com/D96A/orders”。
历史
- 2005 年 8 月 11 日 - 文章提交
这是我第一次尝试这样做,我欢迎所有建议和意见。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。