web-dev-qa-db-fra.com

Convertir une matrice d'octet de XML en Varbinary

Je reçois des fichiers image sous forme de données XML, chaque octet de l'image étant un nœud avec sa valeur décimale, par ex. pour cet exemple .png Fichier1 , le XML que je reçois est:

DECLARE @xml XML = N'<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<XmlData>
    <Element>
        <id>Test</id>
        <image>
            <Element>137</Element><Element>80</Element><Element>78</Element><Element>71</Element><Element>13</Element><Element>10</Element><Element>26</Element><Element>10</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>13</Element><Element>73</Element><Element>72</Element><Element>68</Element><Element>82</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>20</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>20</Element><Element>8</Element><Element>6</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>141</Element><Element>137</Element><Element>29</Element><Element>13</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>4</Element><Element>103</Element><Element>65</Element><Element>77</Element><Element>65</Element><Element>0</Element><Element>0</Element><Element>177</Element><Element>143</Element><Element>11</Element><Element>252</Element><Element>97</Element><Element>5</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>9</Element><Element>112</Element><Element>72</Element><Element>89</Element><Element>115</Element><Element>0</Element><Element>0</Element><Element>14</Element><Element>193</Element><Element>0</Element><Element>0</Element><Element>14</Element><Element>193</Element><Element>1</Element><Element>184</Element><Element>145</Element><Element>107</Element><Element>237</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>24</Element><Element>116</Element><Element>69</Element><Element>88</Element><Element>116</Element><Element>83</Element><Element>111</Element><Element>102</Element><Element>116</Element><Element>119</Element><Element>97</Element><Element>114</Element><Element>101</Element><Element>0</Element><Element>112</Element><Element>97</Element><Element>105</Element><Element>110</Element><Element>116</Element><Element>46</Element><Element>110</Element><Element>101</Element><Element>116</Element><Element>32</Element><Element>52</Element><Element>46</Element><Element>48</Element><Element>46</Element><Element>54</Element><Element>252</Element><Element>140</Element><Element>99</Element><Element>223</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>108</Element><Element>73</Element><Element>68</Element><Element>65</Element><Element>84</Element><Element>56</Element><Element>79</Element><Element>99</Element><Element>24</Element><Element>5</Element><Element>184</Element><Element>192</Element><Element>114</Element><Element>32</Element><Element>254</Element><Element>14</Element><Element>196</Element><Element>30</Element><Element>96</Element><Element>30</Element><Element>21</Element><Element>192</Element><Element>126</Element><Element>32</Element><Element>254</Element><Element>15</Element><Element>196</Element><Element>9</Element><Element>96</Element><Element>30</Element><Element>21</Element><Element>192</Element><Element>48</Element><Element>55</Element><Element>80</Element><Element>7</Element><Element>136</Element><Element>29</Element><Element>208</Element><Element>240</Element><Element>121</Element><Element>32</Element><Element>6</Element><Element>25</Element><Element>216</Element><Element>142</Element><Element>36</Element><Element>6</Element><Element>195</Element><Element>34</Element><Element>64</Element><Element>140</Element><Element>23</Element><Element>128</Element><Element>98</Element><Element>19</Element><Element>164</Element><Element>153</Element><Element>88</Element><Element>60</Element><Element>27</Element><Element>136</Element><Element>241</Element><Element>130</Element><Element>213</Element><Element>64</Element><Element>12</Element><Element>242</Element><Element>34</Element><Element>50</Element><Element>126</Element><Element>15</Element><Element>196</Element><Element>32</Element><Element>205</Element><Element>215</Element><Element>145</Element><Element>196</Element><Element>96</Element><Element>56</Element><Element>3</Element><Element>136</Element><Element>73</Element><Element>6</Element><Element>32</Element><Element>141</Element><Element>32</Element><Element>3</Element><Element>71</Element><Element>147</Element><Element>13</Element><Element>249</Element><Element>128</Element><Element>234</Element><Element>6</Element><Element>250</Element><Element>0</Element><Element>113</Element><Element>5</Element><Element>16</Element><Element>43</Element><Element>128</Element><Element>121</Element><Element>163</Element><Element>0</Element><Element>1</Element><Element>24</Element><Element>24</Element><Element>0</Element><Element>127</Element><Element>60</Element><Element>48</Element><Element>197</Element><Element>152</Element><Element>102</Element><Element>243</Element><Element>130</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>0</Element><Element>73</Element><Element>69</Element><Element>78</Element><Element>68</Element><Element>174</Element><Element>66</Element><Element>96</Element><Element>130</Element>
        </image>
    </Element>
</XmlData>'

En binaire:

SELECT * FROM OPENROWSET(BULK 'C:\test.png', SINGLE_BLOB) AS q;
==========
BulkColumn
----------
0x89504E470D0A1A0A0000000D49484452000000140000001408060000008D891D0D0000000467414D410000B18F0BFC6105000000097048597300000EC100000EC101B8916BED0000001874455874536F667477617265007061696E742E6E657420342E302E36FC8C63DF0000006C49444154384F631805B8C07220FE0EC41E601E15C07E20FE0FC409601E15C030375007881DD0F079200619D88E2406C322408C17806213A499583C1B88F182D5400CF222327E0FC420CDD791C4603803884906208D200347930DF980EA06FA007105102B8079A300011818007F3C30C59866F3820000000049454E44AE426082

Comment calculer le fichier image du XML comme varbinaire?

J'ai demandé une question similaire il y a longtemps , j'ai donc essayé la requête suivante, mais les données binaires résultantes sont incorrectes:

SELECT r.c.value('id[1]', 'varchar(50)') AS id,
CONVERT(VARBINARY(MAX), (SELECT (t.u.value('.','tinyint')) FROM r.c.nodes('image/Element') AS t(u) FOR XML PATH(''))) AS image
FROM @xml.nodes('/XmlData/Element') AS r(c);
=============
id      image
-------------
Test    0x31003300370038003000370038003700310031003300310030003200360031003000300030003000310033003700330037003200360038003800320030003000300032003000300030003000320030003800360030003000300031003400310031003300370032003900310033003000300030003400310030003300360035003700370036003500300030003100370037003100340033003100310032003500320039003700350030003000300039003100310032003700320038003900310031003500300030003100340031003900330030003000310034003100390033003100310038003400310034003500310030003700320033003700300030003000320034003100310036003600390038003800310031003600380033003100310031003100300032003100310036003100310039003900370031003100340031003000310030003100310032003900370031003000350031003100300031003100360034003600310031003000310030003100310031003600330032003500320034003600340038003400360035003400320035003200310034003000390039003200320033003000300030003100300038003700330036003800360035003800340035003600370039003900390032003400350031003800340031003900320031003100340033003200320035003400310034003100390036003300300039003600330030003200310031003900320031003200360033003200320035003400310035003100390036003900390036003300300032003100310039003200340038003500350038003000370031003300360032003900320030003800320034003000310032003100330032003600320035003200310036003100340032003300360036003100390035003300340036003400310034003000320033003100320038003900380031003900310036003400310035003300380038003600300032003700310033003600320034003100310033003000320031003300360034003100320032003400320033003400350030003100320036003100350031003900360033003200320030003500320031003500310034003500310039003600390036003500360033003100330036003700330036003300320031003400310033003200330037003100310034003700310033003200340039003100320038003200330034003600320035003000300031003100330035003100360034003300310032003800310032003100310036003300300031003200340032003400300031003200370036003000340038003100390037003100350032003100300032003200340033003100330030003000300030003000370033003600390037003800360038003100370034003600360039003600310033003000
4
user1952162

Ceci est proche mais manque quelques pièces. Vous extrait dans des lignes de TINYINT la valeur décimale de chaque <Element> dans le XML (par exemple 137, 80, 78, etc.), mais la fonction FOR XML PATH('') les convertit dans des cordes et les concaténe, en laissant Vous avec une chaîne codée UTF-16 de "1378078 ...". Convertir cela en VARBINARY allume chaque chaîne chiffre - "1", "3", "7", "8", etc - dans son point de code binaire/hexagonal :

SELECT CONVERT(VARBINARY(20), N'1378078');
-- 0x3100330037003800300037003800
-- 0x 3100 3300 3700 3800 3000 3700 3800 -- each character separated for readability

-- XML in SQL Server is encoded as UTF-16, same as NCHAR / NVARCHAR.
-- Each of these characters in UTF-16 is two bytes: 0x31 + 0x00, 0x33 + 0x00, etc.
-- Each pair of bytes is in reverse order due to "endianness". 0x3100 is really 0x0031.

SELECT NCHAR(0x0031), NCHAR(0x0033), NCHAR(0x0037), NCHAR(0x0038), NCHAR(0x0030),
       NCHAR(0x0037), NCHAR(0x0038);
-- 1    3   7   8   0   7   8

Au lieu de cela, vous devez faire ce qui suit:

  1. Convertir la décimale/TINYINT "137" en hex/BINARY "0x89"
  2. Convertir le hex/binaire "0x89" en une chaîne/VARCHAR, mais sans le principal "0x" (cela nécessite d'utiliser la fonction CONVERT, pas CAST, afin que vous puissiez spécifier le "Style" de 2)
  3. Une fois que la fonction FOR XML PATH('') met tout ensemble dans une chaîne sous la forme de 89504E470D0A1A0A0000..., alors vous devez appliquer le "style" de 2 à nouveau dans la fonction CONVERT(VARBINARY(MAX), ... afin qu'il sait que 89504E470D0A1A0A0000... est simplement 0x89504E470D0A1A0A0000... sans le "0x" leader.

Mettre ces morceaux dans votre requête, nous obtenons ce qui suit:

SELECT r.c.value('id[1]', 'varchar(50)') AS [id],
       CONVERT(VARBINARY(MAX),
               (SELECT CONVERT(VARCHAR(3),
                               CONVERT(BINARY(1),
                                       t.u.value('.', 'tinyint')
                                      ),
                               2 -- style creates binary string without the leading "0x"
                              )
                FROM r.c.nodes('image/Element') AS t(u)
                FOR XML PATH('')
               ),
               2 -- style creates binary string without the leading "0x"
              ) AS [image]
FROM @xml.nodes('/XmlData/Element') AS r(c);

Et cela renvoie ce qui suit pour le champ image:

0x89504E470D0A1A0A0000000D49484452000000140000001408060000008D891D0D0000000467414D410000B18F0BFC6105000000097048597300000EC100000EC101B8916BED0000001874455874536F667477617265007061696E742E6E657420342E302E36FC8C63DF0000006C49444154384F631805B8C07220FE0EC41E601E15C07E20FE0FC409601E15C030375007881DD0F079200619D88E2406C322408C17806213A499583C1B88F182D5400CF222327E0FC420CDD791C4603803884906208D200347930DF980EA06FA007105102B8079A300011818007F3C30C59866F3820000000049454E44AE426082


quelque chose à considérer :

La méthode d'envoi de données binaires via

<Element>137</Element><Element>80</Element>...

est probablement la méthode pire/la moins efficace possible. Je me rends compte que vous avez dit que vous recevez ces informations dans ce format et que vous ne devez probablement pas blâmer pour cela et N'avez aucun contrôle. Cependant, tout le monde comprend ce qui se passe (S'il vous plaît voir METTRE À JOUR section ci-dessous), chaque octet des données binaires est:

  1. d'abord transformé en son équivalent décimal (valeurs 0 - 255)
  2. puis converti en une chaîne (prendre 1 à 3 caractères)
  3. une chaîne qui est stockée comme UTF-16 qui est 2 octets par caractères pour ces caractères (2 à 6 octets)
  4. et enveloppé dans <Element> et </Element> tags {pourquoi pas <Byte>? } (19 caractères)
  5. encore une fois, est stocké dans UTF-16, qui est 2 octets par caractère pour ces caractères (38 octets)
  6. finalement prendre 40 - 46 octets (38 + 2 sur le bas de gamme, 38 + 6 sur le haut de gamme) par chaque octet de l'original valeur binaire.

Comment cela fonctionne-t-il? Eh bien, seules 10 valeurs décimales (0 à 9) sont 1 caractères/2 octets. Les autres 90 (10 à 99) sont 2 caractères/4 octets, tandis que les 156 valeurs restantes (100 à 255) sont de 3 caractères/6 octets. Donc, la majorité des valeurs possibles occupent toutes les 6 octets, et seule une petite minorité occupe le minimum de 2 octets. Cela signifie que l'espace moyen repris par chaque octet d'origine est probablement compris entre 2 et 3 caractères/4 à 6 octets (je suppose qu'ils appellent cela "5" à certains endroits ;-)? ).

Pour vos exemples de données particuliers, vous pouvez exécuter la requête suivante pour voir la panne:

;WITH cte AS
(  SELECT LEN(CONVERT(VARCHAR(3),  r.c.value('.', 'tinyint'))) AS [Length]
   FROM @xml.nodes('/XmlData/Element/image/Element') AS r(c)
)
SELECT cte.[Length] AS [ElementLength], COUNT(*) AS [ElementCount]
FROM   cte
GROUP BY cte.[Length];

Qui retourne:

ElementLength    ElementCount
-------------    ------------
1                55
2                98
3                85

Maintenant, nous pouvons multiplier chaque ElementCount par (ElementLength * 2) pour obtenir le nombre d'octets. Et nous devons prendre en compte les tags <Element>__, qui est à nouveau 38 octets par chacun des 238 octets originaux:

SELECT (55 * 2) + (98 * 4) + (85 * 6) + (238 * 38)
-- 10,056 bytes !!!

Et de mettre cela en termes plus concrets, nous devrions comparer que la taille binaire d'origine pour avoir un sens du gonflement:

SELECT 10056 / 238.0 -- 42.25 times larger !!!

Signification, si vous êtes envoyé une image de 1 Mo (pas déraisonnable), cela sera représenté par 42,25 Mo de XML. Yikes! (S'il vous plaît voir METTRE À JOUR section ci-dessous)

Mais avant que quiconque ne commence à se plaindre de XML, ce n'est pas la faute de XML, qui, tout en étant un format ballonné indéniablement, peut faire beaucoup mieux que cela. XML (au moins Nativement dans SQL Server) prend en charge la possibilité de gérer les données binaires à l'aide du codage/décodage de base64. Par exemple, à l'aide de la même valeur binaire de test PNG, nous pouvons convertir cela en une chaîne de XML à l'aide de la clause FOR XML PATH:

DECLARE @PngImage VARBINARY(MAX);
SET @PngImage = 0x89504E470D0A1A0A0000000D49484452000000140000001408060000008D891D0D0\
000000467414D410000B18F0BFC6105000000097048597300000EC100000EC101B8916BED000000187445\
5874536F667477617265007061696E742E6E657420342E302E36FC8C63DF0000006C49444154384F63180\
5B8C07220FE0EC41E601E15C07E20FE0FC409601E15C030375007881DD0F079200619D88E2406C322408C\
17806213A499583C1B88F182D5400CF222327E0FC420CDD791C4603803884906208D200347930DF980EA0\
6FA007105102B8079A300011818007F3C30C59866F3820000000049454E44AE426082;

SELECT @PngImage AS [PngImage]
FOR XML PATH('Test'), BINARY BASE64;

qui nous donne:

<Test>
  <PngImage>iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwQAADsEBuJFr7QAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC42/Ixj3wAAAGxJREFUOE9jGAW4wHIg/g7EHmAeFcB+IP4PxAlgHhXAMDdQB4Gd0PB5IAYZ2I4kBsMiQIwXgGITpJlYPBuI8YLVQAzyIjJ+D8QgzdeRxGA4A4hJBiCNIANHkw35gOoG+gBxBRArgHmjAAEYGAB/PDDFmGbzggAAAABJRU5ErkJggg==</PngImage>
</Test>

Et faire une DATALENGTH(N'iVBORw0KGg...') donne 640 octets et y compris la balise <PngImage> (DATALENGTH(N'<PngImage>iVBORw0KGg...</PngImage>')) nous donne un total de 682 octets, pas de caractères, Mais total d'octets (seulement 2,87 fois plus grand que l'original) par rapport aux octets totaux de 10 056 de la méthode actuelle.

Et combien d'effort faut-il pour obtenir la valeur VARBINARY(MAX) valeur hors de cette chaîne codée de base64? Absolument aucun:

DECLARE @PngImage XML = N'<Test>
  <PngImage>iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwQAADsEBuJFr7QAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC42/Ixj3wAAAGxJREFUOE9jGAW4wHIg/g7EHmAeFcB+IP4PxAlgHhXAMDdQB4Gd0PB5IAYZ2I4kBsMiQIwXgGITpJlYPBuI8YLVQAzyIjJ+D8QgzdeRxGA4A4hJBiCNIANHkw35gOoG+gBxBRArgHmjAAEYGAB/PDDFmGbzggAAAABJRU5ErkJggg==</PngImage>
</Test>';
SELECT @PngImage.value('(/Test/PngImage)[1]', 'VARBINARY(MAX)');

Retour:

0x89504E470D0...

Ainsi, vous voudrez peut-être mentionner cela à qui que ce soit vous envoie ces données. Et le codage de base64 est assez standardisé et facile à encoder/décoder dans la plupart des langues.


MISE À JOUR

En recherche quelque chose d'autre, j'ai trouvé que le type XML DataType dans SQL Server est optimisé assez bien. C'est en fait non Stockez le texte intégral du document XML. À tout le moins, il crée un dictionnaire (c'est-à-dire un dictionnaire de noms d'éléments et d'attributs uniques, attribue à chacun un numéro et utilise ce numéro pour les référencer dans le document. Cela permet d'économiser une quantité incroyable d'espace. Donc, dans ce document XML particulier, non seulement les 19 caractères de chaque <Element></Element> elcode__ dans le noeud <image> nœud non répété (qui correspond à 38 octets dans l'UTF-16 coding), mais dans le dictionnaire principal, le dictionnaire principal), mais dans le dictionnaire principal le Nom Element ne montre qu'une fois, même s'il est utilisé à deux niveaux différents de la structure.

La structure optimisée ne peut être vue que lors de la sauvegarde du document sur un champ XML d'une table, puis de la visualisation de la page de données via DBCC PAGE. La visualisation montre une certaine quantité d'octets utilisés pour les frais généraux de document, le dictionnaire des noms d'élément (contenant "element" une seule fois) et les 238 éléments (représentant les octets du PNG), chaque élément ayant 5 octets de surcharge.

J'ai ré-vérifié la taille de chaque variation, ce temps en vérifiant la taille directement pour la variable d'origine @xml (comme indiqué dans la question), puis convertissant cela à la fois NVARCHAR et à VARCHAR, puis faire Des chèques similaires à la base64 codé XML via ce qui suit modifier le 2e au dernier test ci-dessus:

DECLARE @ConvertedXml XML;
SET @ConvertedXml = (
  SELECT @PngImage AS [PngImage]
  FOR XML PATH('Test'), BINARY BASE64
);

SELECT DATALENGTH(@PngImage) AS [PNG],
       DATALENGTH(@ConvertedXml) AS [XmlBytes],
       DATALENGTH(CONVERT(NVARCHAR(4000), @ConvertedXml)) AS [NVarCharBytes],
       DATALENGTH(CONVERT(VARCHAR(4000), @ConvertedXml)) AS [VarCharBytes];

Et les résultats:

Format                           Size (in bytes)

PNG (original file)                         238

XML in VARCHAR (or ASCII text file)        5094
XML in NVARCHAR (or UTF-16 text file)     10188
XML datatype                               2295

Base64-XML datatype                         690
Base64-NVARCHAR                             708
Base64-VARCHAR                              354

Comme vous pouvez le constater, la taille varie en fonction de sa stockage. Le XML de la question est venu d'un fichier. De sorte que ce fichier, probablement régulier ASCII/ANSI, était de 5094 octets. Et s'il a été stocké dans un champ VARCHAR, il s'agirait de la même taille. Si le fichier a été enregistré. Si le fichier a été enregistré Avec un codage UTF-16, il s'agissait en réalité de 10 188 octets et seraient la même taille si elles sont stockées dans un champ NVARCHAR. Mais ce même document XML, stocké dans SQL Server dans un champ XML ou variable , est seulement 2295 octets! C'est un peu cool :-). Bien que 2295 octets soient encore environ 10 fois plus grands que le fichier PNG d'origine.

Mais le codage de base64 est toujours debout comme le meilleur moyen de stocker et de transmettre la valeur binaire dans XML. Le même fichier PNG n'est que 690 octets lorsqu'il est stocké dans un champ XML ou une variable. Il ne s'agit également que de 708 octets si ce XML était stocké dans un champ de champ NVARCHAR ou de fichier texte UTF-16, et seulement 354 octets si stockés dans un champ VARCHAR ou ASCII = fichier texte.

15
Solomon Rutzky