【问题标题】:Suds generates empty elements; how to remove them?Suds 生成空元素;如何删除它们?
【发布时间】:2012-02-22 02:04:03
【问题描述】:

[根据两天前第一篇文章以来的经验进行重大编辑。]

我正在使用 Suds 构建 Python SOAP/XML 脚本,但我正在努力获取生成服务器可接受的 SOAP/XML 的代码。我曾认为问题在于 Suds 没有为内部元素生成前缀,但后来发现缺少前缀(参见 Sh-Data 和内部元素)不是问题,因为 Sh-DataMetaSwitchData元素声明适当的命名空间(见下文)。

<SOAP-ENV:Envelope xmlns:ns3="http://www.metaswitch.com/ems/soap/sh" xmlns:ns0="http://www.metaswitch.com/ems/soap/sh/userdata" xmlns:ns1="http://www.metaswitch.com/ems/soap/sh/servicedata" xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <ns2:Body>
      <ns3:ShUpdate>
         <ns3:UserIdentity>Meribel/TD Test Sub Gateway 3</ns3:UserIdentity>
         <ns3:DataReference>0</ns3:DataReference>
         <ns3:UserData>
            <Sh-Data xmlns="http://www.metaswitch.com/ems/soap/sh/userdata">
               <RepositoryData>
                  <ServiceIndication>Meta_SubG_BaseInformation</ServiceIndication>
                  <SequenceNumber>0</SequenceNumber>
                  <ServiceData>
                     <MetaSwitchData xmlns="http://www.metaswitch.com/ems/soap/sh/servicedata" IgnoreSequenceNumber="False" MetaSwitchVersion="?">
                        <Meta_SubG_BaseInformation Action="apply">
                           <NetworkElementName>Meribel</NetworkElementName>
                           <Description>TD Test Sub Gateway 3</Description>
                           <DomainName>test.datcon.co.uk</DomainName>
                           <MediaGatewayModel>Cisco ATA</MediaGatewayModel>
                           <CallFeatureServerControlStatus/>
                           <CallAgentControlStatus/>
                           <UseStaticNATMapping/>
                           <AuthenticationRequired/>
                           <ProviderStatus/>
                           <DeactivationMode/>
                        </Meta_SubG_BaseInformation>
                     </MetaSwitchData>
                  </ServiceData>
               </RepositoryData>
            </Sh-Data>
         </ns3:UserData>
         <ns3:OriginHost>user@domain.com?clientVersion=7.3</ns3:OriginHost>
      </ns3:ShUpdate>
   </ns2:Body>
</SOAP-ENV:Envelope>

但这仍然失败。问题是 Suds 为可选元素生成空元素(在 WSDL 中标记为 Mandatory = No)。但是服务器要求可选元素要么存在且具有合理的值,要么不存在,并且我收到以下错误(因为&lt;CallFeatureServerControlStatus/&gt; 元素不是允许值之一。

提供的用户数据未针对用户数据的 MetaSwitch XML 架构进行验证。
详细信息:cvc-enumeration-valid:值''对于枚举'[控制,放弃,谨慎控制]'不是方面有效的。它必须是来自枚举的值。

如果我将生成的 SOAP/XML 放入 SOAPUI 并删除空元素,请求就可以正常工作。

有没有办法让 Suds 不为可选字段生成空元素,或者让我之后在代码中删除它们?

重大更新

我已经解决了这个问题(我在其他地方看到过),但是以一种非常不雅的方式。因此,我发布我当前的解决方案是希望 a) 它可以帮助他人和/或 b) 有人可以提出更好的解决方法。

事实证明,问题不在于 Suds 为可选元素生成空元素(在 WSDL 中标记为 Mandatory = No)。而是 Suds 为可选的 complex 元素生成空元素。例如,以下 Meta_SubG_BaseInformation 元素是简单元素,Suds 不会在 SOAP/XML 中为它们生成任何内容。

<xs:element name="CMTS" type="xs:string" minOccurs="0">
    <xs:annotation>
        <xs:documentation>
            <d:DisplayName firstVersion="5.0" lastVersion="7.4">CMTS</d:DisplayName>
            <d:ValidFrom>5.0</d:ValidFrom>
            <d:ValidTo>7.4</d:ValidTo>
            <d:Type firstVersion="5.0" lastVersion="7.4">String</d:Type>
            <d:BaseAccess firstVersion="5.0" lastVersion="7.4">RWRWRW</d:BaseAccess>
            <d:Mandatory firstVersion="5.0" lastVersion="7.4">No</d:Mandatory>
            <d:MaxLength firstVersion="5.0" lastVersion="7.4">1024</d:MaxLength>
        </xs:documentation>
    </xs:annotation>
</xs:element>

<xs:element name="TAGLocation" type="xs:string" minOccurs="0">
    <xs:annotation>
        <xs:documentation>
            <d:DisplayName>Preferred location of Trunk Gateway</d:DisplayName>
            <d:Type>String</d:Type>
            <d:BaseAccess>RWRWRW</d:BaseAccess>
            <d:Mandatory>No</d:Mandatory>
            <d:DefaultValue>None</d:DefaultValue>
            <d:MaxLength>1024</d:MaxLength>
        </xs:documentation>
    </xs:annotation>
</xs:element>

相比之下,下面的 Meta_SubG_BaseInformation 元素是一个复杂的元素,即使它是可选的并且我的代码没有为其赋值,它最终还是在生成的 SOAP/XML 中。

<xs:element name="ProviderStatus" type="tMeta_SubG_BaseInformation_ProviderStatus" minOccurs="0">
    <xs:annotation>
        <xs:documentation>
            <d:DisplayName>Provider status</d:DisplayName>
            <d:Type>Choice of values</d:Type>
            <d:BaseAccess>R-R-R-</d:BaseAccess>
            <d:Mandatory>No</d:Mandatory>
            <d:Values>
                <d:Value>Unavailable</d:Value>
                <d:Value>Available</d:Value>
                <d:Value>Inactive</d:Value>
                <d:Value>Active</d:Value>
                <d:Value>Out of service</d:Value>
                <d:Value>Quiescing</d:Value>
                <d:Value>Unconfigured</d:Value>
                <d:Value>Pending available</d:Value>
            </d:Values>
        </xs:documentation>
    </xs:annotation>
</xs:element>

Suds 为 ProviderStatus 生成以下内容,这(如上所述)扰乱了我的服务器。

<ProviderStatus/>

解决方法是在创建父元素之后、赋值之前将所有Meta_SubG_BaseInformation 元素设置为None,如下所示。这对于简单元素来说是多余的,但确实可以确保未分配的复杂元素不会导致生成 SOAP/XML。

subGatewayBaseInformation = client.factory.create('ns1:Meta_SubG_BaseInformation')
for (el) in subGatewayBaseInformation:
  subGatewayBaseInformation.__setitem__(el[0], None)
subGatewayBaseInformation._Action            = 'apply'
subGatewayBaseInformation.NetworkElementName = 'Meribel'
etc...

这会导致 Suds 生成没有空元素的 SOAP/XML,这对我的服务器来说是可以接受的。

但是有没有人知道一种更清洁的方法来达到同样的效果?

以下解决方案基于以下 dusan 和 Roland Smith 的答案/cmets。

此解决方案使用 Suds MessagePlugin 在 Suds 将请求发送到网络之前修剪 &lt;SubscriberType/&gt; 形式的“空”XML。我们只需要修剪 ShUpdates(我们正在更新服务器上的数据),并且逻辑(尤其是向下索引到子节点以获取服务指示元素列表)非常特定于 WSDL。它不适用于不同的 WSDL。

class MyPlugin(MessagePlugin):
  def marshalled(self, context):
    pruned = []
    req = context.envelope.children[1].children[0]
    if (req.name == 'ShUpdate'):
      si = req.children[2].children[0].children[0].children[2].children[0].children[0]
      for el in si.children:
        if re.match('<[a-zA-Z0-9]*/>', Element.plain(el)):
          pruned.append(el)
      for p in pruned:
        si.children.remove(p)

然后我们只需要在创建客户端时引用该插件即可。

client = Client(url, plugins=[MyPlugin()])

【问题讨论】:

标签: python soap suds


【解决方案1】:

您可以在发送到服务器之前使用插件来修改 XML(我的答案是基于 Ronald Smith 的解决方案):

from suds.plugin import MessagePlugin
from suds.client import Client
import re

class MyPlugin(MessagePlugin):
    def sending(self, context):
        context.envelope = re.sub('\s+<.*?/>', '', context.envelope)


client = Client(URL_WSDL, plugins=[MyPlugin()])

引用documentation

MessagePlugin 目前有 (5) 个钩子 ::
(...)
发送()
为插件提供在发送之前检查/修改消息文本的机会。

基本上 Suds 会在发送 XML 之前调用 sending,因此您可以修改生成的 XML(包含在 context.envelope 中)。您必须将插件类 MyPlugin 传递给 Client 构造函数才能使其工作。

编辑

另一种方法是使用marshalled修改XML结构,去掉空元素(未经测试的代码):

class MyPlugin(MessagePlugin):
    def marshalled(self, context):
        #remove empty tags inside the Body element
        #context.envelope[0] is the SOAP-ENV:Header element
        context.envelope[1].prune()

【讨论】:

  • 有趣。我不知道 MessagePlugins。所以......我已经测试了这种方法,并且在 MessagePlugin 中修改 XML 让我很接近 - 但正确使用正则表达式语法具有挑战性。特别是它必须在剥离(例如) 时单独留下
    。你们比我更擅长正则表达式吗?
  • 使用编组的 MessagePlugin 看起来很有希望,但我不能对要修剪的元素的位置做出假设。 [在 WSDL 中定义了数千个服务指示,具有几乎一样多的唯一格式/字段。] 我尝试对构成上下文的元素执行 tostring()(对于 context.envelope 中的 ct:ctStr = el.tostring( ct)) 将 xml.etree.ElementTree 导入为 el 后,出现错误 AttributeError: Element instance has no attribute 'getiterator'?
  • 试试这个:for child in context.envelope.children: print child
  • 我在做 for ct in context.envelope 而不是 for ct in context.envelope.children 但两者都返回 ct 作为一个元素,它不会转换为字符串(如上所述),因此我无法比较或做正则表达式替换单个元素。
  • 您可以从prune 实现中获得灵感:fedorahosted.org/suds/browser/trunk/suds/sax/element.py#L878。使用 children() 向下导航 XML,使用 remove 删除空元素。
【解决方案2】:

还有一种更简单的方法 - 不需要任何 Reg Ex 或令人兴奋的迭代器;)

首先,定义插件:

class PrunePlugin(MessagePlugin):
    def marshalled(self, context):
        context.envelope = context.envelope.prune()

然后在创建客户端的时候使用:

client = Client(url, plugins=[PrunePlugin()])

prune() 方法将删除所有空节点,如下所述:http://jortel.fedorapeople.org/suds/doc/suds.sax.element.Element-class.html

【讨论】:

  • 这是要走的路!谢谢
  • 这很好,很干净。如果您无法控制客户端创建但事后可以访问它,您可以像这样设置插件选项:client.set_options(plugins=[PrunePlugin()])
【解决方案3】:

Suds 工厂方法生成一个常规 Python 对象,该对象具有映射到 WSDL 类型定义的常规 Python 属性。

您可以使用 'del' 内置函数来删除属性。

>>> order_details = c.factory.create('ns2:OrderDetails')
>>> order_details
(OrderDetails){
   Amount = None
   CurrencyCode = None
   OrderChannelType =
      (OrderChannelType){
         value = None
      }
   OrderDeliveryType =
      (OrderDeliveryType){
         value = None
      }
   OrderLines =
      (ArrayOfOrderLine){
         OrderLine[] = <empty>
      }
   OrderNo = None
   TotalOrderValue = None
 }
>>> del order_details.OrderLines
>>> del order_details.OrderDeliveryType
>>> del order_details.OrderChannelType
>>> order_details
(OrderDetails){
   Amount = None
   CurrencyCode = None
   OrderNo = None
   TotalOrderValue = None
 }

【讨论】:

    【解决方案4】:

    您可以使用正则表达式过滤掉空元素。

    假设您的 XML 数据在字符串 xmltext 中;

    import re
    filteredtext = re.sub('\s+<.*?/>', '', xmltext)
    

    【讨论】:

    • 很好的尝试,但遗憾的是这不起作用。阅读 WSDL 后,Suds 不会直接公开 XML。因此 Python 代码永远无法访问 XML 的字符串表示形式。相反,Suds 提供对一系列对象类型的访问,Python 代码可以在其中设置值等。然后当 Python 代码调用 XML 中定义的方法时,Suds 在内部生成/发送 XML。
    • 如果我正确阅读了文档,则当前的 suds 版本支持插件。似乎从MessagePlugin 派生一个类并重新定义sending() 方法,您可以在发送之前检查/修改文本。覆盖receiving() 方法允许您在解析接收到的XML 之前对其进行检查/修改。结合正则表达式,这可能会奏效。
    • 看来发送 MessagePLugin 存在问题,因此即使我得到正确的正则表达式(脱机测试),当 SOAP 请求到达线路时,它包含原始 XML,而不是子 XML。见lists.fedoraproject.org/pipermail/suds/2010-November/…
    • 添加了一个基于dusanRoland Smith anserws/cmets 混合的解决方案。
    【解决方案5】:

    您如何看待以下 MonkeyPatch 跳过具有 None 值的复杂类型?

    from suds.mx.literal import Typed
    old_skip = Typed.skip
    def new_skip(self, content):
        x = old_skip(self, content)
        if not x and getattr(content.value, 'value', False) is None:
            x = True
        return x
    Typed.skip = new_skip
    

    【讨论】:

    • 你能调整这段代码,让它可以用作插件吗?
    【解决方案6】:

    我知道这个很久以前就关闭了,但是在亲自解决这个问题之后,我发现当前的答案缺乏。

    使用 MessagePlugin 上的发送方法将不起作用,因为尽管文档大量暗示,您实际上无法从那里更改消息字符串。您只能检索最终结果。

    如前所述,编组方法最适合此,因为它确实允许您影响 XML。我创建了以下插件来为自己解决这个问题:

    class ClearEmpty(MessagePlugin):
        def clear_empty_tags(self, tags):
            for tag in tags:
                children = tag.getChildren()[:]
                if children:
                    self.clear_empty_tags(children)
                if re.match(r'^<[^>]+?/>$', tag.plain()):
                    tag.parent.remove(tag)
    
        def marshalled(self, context):
            self.clear_empty_tags(context.envelope.getChildren()[:])
    

    这将消除所有空标签。如果您只需要从某个地方删除一些空标签,则可以根据需要对其进行定制,但是这个递归函数有效并且(除非您的 XML 模式非常糟糕以至于嵌套大于 Python 的调用深度),不应该造成问题。请注意,我们在此处复制列表,因为使用 remove() 会在我们迭代时破坏它们并导致问题。

    另外说明,其他答案给出的正则表达式很糟糕——&lt;test&gt; &lt;thingus/&gt; &lt;/test&gt; 上使用的\s+&lt;.*?/&gt; 将匹配&lt;test&gt; &lt;thingus/&gt;,而不仅仅是&lt;thingus/&gt;,如您所料。这是因为&gt;. 视为“任何字符”。如果您确实需要使用正则表达式在呈现的 XML 上解决此问题(注意:XML 是一种复杂的语法,最好由词法分析器处理),正确的语法应该是 &lt;[^&gt;]*/&gt;

    我们在这里使用它是因为我想不出最正确的方法来询问词法分析器“这是一个独立的空标签”,而不是检查该标签的渲染输出和正则表达式。在这种情况下,我还添加了 ^$ 标记,因为在此方法中渲染标签会渲染其整个上下文,因此这意味着特定标签下的任何空白标签都将匹配。我们只希望匹配一个特定的标签,以便我们可以告诉 API 将其从树中删除。

    最后,为了帮助那些搜索可能引发此问题的人,当我收到如下错误消息时,我遇到了这个问题:

    cvc-enumeration-valid: Value '' is not facet-valid with respect to enumeration
    

    这是因为空标签会导致服务器将该标签下的所有内容解释为空值/空字符串。

    【讨论】:

      【解决方案7】:

      我想我会在上面的解决方案上分享一个非常简单的更新,它应该适用于任何 WSDL: 请注意,发送方法不是必需的 - 这样您就可以审核您的更改,因为客户端的调试请求打印在 编组方法运行之前触发。

      class XMLBS_Plugin(MessagePlugin):
      def marshalled(self, context):
          def w(x):
              if x.isempty():
                  print "EMPTY: ", x
                  x.detach()
      
          context.envelope.walk(w)
      
      def sending(self,context):
          c = copy.deepcopy(context.envelope)
          c=c.replace('><','>\n<') # some sort of readability
          logging.info("SENDING: \n%s"%c)
      

      【讨论】:

      • walk(f) 是指walk(w)
      • 是的 - SO 上的标记在我预览时更改了该函数的名称。非常混乱 - 我会编辑。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-10
      • 1970-01-01
      • 2021-03-13
      • 2019-01-10
      相关资源
      最近更新 更多