web-dev-qa-db-fra.com

Importer des fichiers XML dans PostgreSQL

J'ai beaucoup de fichiers XML à importer dans la table xml_data:

create table xml_data(result xml);

Pour ce faire, j'ai un script bash simple avec loop:

#!/bin/sh
FILES=/folder/with/xml/files/*.xml
for f in $FILES
do
  psql psql -d mydb -h myhost -U usr -c \'\copy xml_data from $f \'
done

Cependant, cela tentera d'importer chaque ligne de chaque fichier en tant que ligne distincte. Cela conduit à une erreur:

ERROR:  invalid XML content
CONTEXT:  COPY address_results, line 1, column result: "<?xml version="1.0" encoding="UTF-8"?>"

Je comprends pourquoi cela échoue, mais je ne vois pas comment faire \copy pour importer tout le fichier en une fois sur une seule ligne.

10
Tomas Greif

Je voudrais essayer une approche différente: lire le fichier XML directement dans variable dans une fonction plpgsql et procéder à partir de là. Devrait être beaucoup plus rapide et beaucoup plus robuste. Vous avez cependant besoin des privilèges de superutilisateur.

CREATE OR REPLACE FUNCTION f_sync_from_xml()
  RETURNS boolean AS
$BODY$
DECLARE
    myxml    xml;
    datafile text := 'path/to/my_file.xml';
BEGIN

myxml := pg_read_file(datafile, 0, 100000000);  -- arbitrary 100 MB max.

CREATE TEMP TABLE tmp AS
SELECT (xpath('//some_id/text()', x))[1]::text AS id
FROM   unnest(xpath('/xml/path/to/datum', myxml)) x;
...

Trouvez un exemple de code complet avec explication et liens dans cette réponse étroitement liée:

10
Erwin Brandstetter

Nécromancien: Pour ceux qui ont besoin d'un exemple de travail:

DO $$
   DECLARE myxml xml;
BEGIN

myxml := XMLPARSE(DOCUMENT convert_from(pg_read_binary_file('MyData.xml'), 'UTF8'));

DROP TABLE IF EXISTS mytable;
CREATE TEMP TABLE mytable AS 

SELECT 
     (xpath('//ID/text()', x))[1]::text AS id
    ,(xpath('//Name/text()', x))[1]::text AS Name 
    ,(xpath('//RFC/text()', x))[1]::text AS RFC
    ,(xpath('//Text/text()', x))[1]::text AS Text
    ,(xpath('//Desc/text()', x))[1]::text AS Desc
FROM unnest(xpath('//record', myxml)) x
;

END$$;


SELECT * FROM mytable;

Ou avec moins de bruit

SELECT 
     (xpath('//ID/text()', myTempTable.myXmlColumn))[1]::text AS id
    ,(xpath('//Name/text()', myTempTable.myXmlColumn))[1]::text AS Name 
    ,(xpath('//RFC/text()', myTempTable.myXmlColumn))[1]::text AS RFC
    ,(xpath('//Text/text()', myTempTable.myXmlColumn))[1]::text AS Text
    ,(xpath('//Desc/text()', myTempTable.myXmlColumn))[1]::text AS Desc
    ,myTempTable.myXmlColumn as myXmlElement
FROM unnest(
    xpath
    (    '//record'
        ,XMLPARSE(DOCUMENT convert_from(pg_read_binary_file('MyData.xml'), 'UTF8'))
    )
) AS myTempTable(myXmlColumn)
;

Avec cet exemple de fichier XML (MyData.xml):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<data-set>
    <record>
        <ID>1</ID>
        <Name>A</Name>
        <RFC>RFC 1035[1]</RFC>
        <Text>Address record</Text>
        <Desc>Returns a 32-bit IPv4 address, most commonly used to map hostnames to an IP address of the Host, but it is also used for DNSBLs, storing subnet masks in RFC 1101, etc.</Desc>
    </record>
    <record>
        <ID>2</ID>
        <Name>NS</Name>
        <RFC>RFC 1035[1]</RFC>
        <Text>Name server record</Text>
        <Desc>Delegates a DNS zone to use the given authoritative name servers</Desc>
    </record>
</data-set>

Remarque: 
MyData.xml doit se trouver dans le répertoire PG_Data (le répertoire parent du répertoire pg_stat). 
par exemple. /var/lib/postgresql/9.3/main/MyData.xml
Cela nécessite PostGreSQL 9.1+

Globalement, vous pouvez y arriver sans fichier, comme ceci:

SELECT 
     (xpath('//ID/text()', myTempTable.myXmlColumn))[1]::text AS id
    ,(xpath('//Name/text()', myTempTable.myXmlColumn))[1]::text AS Name 
    ,(xpath('//RFC/text()', myTempTable.myXmlColumn))[1]::text AS RFC
    ,(xpath('//Text/text()', myTempTable.myXmlColumn))[1]::text AS Text
    ,(xpath('//Desc/text()', myTempTable.myXmlColumn))[1]::text AS Desc
    ,myTempTable.myXmlColumn as myXmlElement
FROM unnest(xpath('//record', 
 CAST('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<data-set>
    <record>
        <ID>1</ID>
        <Name>A</Name>
        <RFC>RFC 1035[1]</RFC>
        <Text>Address record</Text>
        <Desc>Returns a 32-bit IPv4 address, most commonly used to map hostnames to an IP address of the Host, but it is also used for DNSBLs, storing subnet masks in RFC 1101, etc.</Desc>
    </record>
    <record>
        <ID>2</ID>
        <Name>NS</Name>
        <RFC>RFC 1035[1]</RFC>
        <Text>Name server record</Text>
        <Desc>Delegates a DNS zone to use the given authoritative name servers</Desc>
    </record>
</data-set>
' AS xml)   
)) AS myTempTable(myXmlColumn)
;
12
Stefan Steiger

Pour étendre l'excellente réponse de @ stefan-steiger, voici un exemple qui extrait des éléments XML de nœuds enfants contenant plusieurs frères et soeurs (par exemple, plusieurs éléments <synonym>, pour un nœud parent <synomyms> particulier).

J'ai rencontré ce problème avec mes données et j'ai beaucoup cherché une solution; sa réponse a été la plus utile pour moi.

Exemple de fichier de données, hmdb_metabolites_test.xml:

<?xml version="1.0" encoding="UTF-8"?>
<hmdb>
<metabolite>
  <accession>HMDB0000001</accession>
  <name>1-Methylhistidine</name>
  <synonyms>
    <synonym>(2S)-2-amino-3-(1-Methyl-1H-imidazol-4-yl)propanoic acid</synonym>
    <synonym>1-Methylhistidine</synonym>
    <synonym>Pi-methylhistidine</synonym>
    <synonym>(2S)-2-amino-3-(1-Methyl-1H-imidazol-4-yl)propanoate</synonym>
  </synonyms>
</metabolite>
<metabolite>
  <accession>HMDB0000002</accession>
  <name>1,3-Diaminopropane</name>
  <synonyms>
    <synonym>1,3-Propanediamine</synonym>
    <synonym>1,3-Propylenediamine</synonym>
    <synonym>Propane-1,3-diamine</synonym>
    <synonym>1,3-diamino-N-Propane</synonym>
  </synonyms>
</metabolite>
<metabolite>
  <accession>HMDB0000005</accession>
  <name>2-Ketobutyric acid</name>
  <synonyms>
    <synonym>2-Ketobutanoic acid</synonym>
    <synonym>2-Oxobutyric acid</synonym>
    <synonym>3-Methyl pyruvic acid</synonym>
    <synonym>alpha-Ketobutyrate</synonym>
  </synonyms>
</metabolite>
</hmdb>

A part: le fichier XML d'origine avait une URL dans l'élément de document

<hmdb xmlns="http://www.hmdb.ca">

cela empêchait xpath d'analyser les données. Il volonté s'exécutera (sans message d'erreur), mais la relation/table est vide:

[hmdb_test]# \i /mnt/Vancouver/Programming/data/hmdb/sql/hmdb_test.sql
DO
 accession | name | synonym 
-----------+------+---------

Le fichier source étant de 3,4 Go, j'ai décidé de modifier cette ligne à l'aide de sed:

sed -i '2s/.*hmdb xmlns.*/<hmdb>/' hmdb_metabolites.xml

[ L'ajout du 2 (indique à sed de modifier la "ligne 2") également, ce qui est une coïncidence, doublant ainsi la vitesse d'exécution de la commande sed. ]


Mon dossier de données postgres (PSQL: SHOW data_directory;) est

/mnt/Vancouver/Programming/RDB/postgres/postgres/data

donc, comme Sudo, je devais y copier mon fichier de données XML et le chown pour une utilisation dans PostgreSQL:

Sudo chown postgres:postgres /mnt/Vancouver/Programming/RDB/postgres/postgres/data/hmdb_metabolites_test.xml

Script (hmdb_test.sql):

DO $$DECLARE myxml xml;

BEGIN

myxml := XMLPARSE(DOCUMENT convert_from(pg_read_binary_file('hmdb_metabolites_test.xml'), 'UTF8'));

DROP TABLE IF EXISTS mytable;

-- CREATE TEMP TABLE mytable AS 
CREATE TABLE mytable AS 
SELECT 
    (xpath('//accession/text()', x))[1]::text AS accession
    ,(xpath('//name/text()', x))[1]::text AS name 
    -- The "synonym" child/subnode has many sibling elements, so we need to
    -- "unnest" them,otherwise we only retrieve the first synonym per record:
    ,unnest(xpath('//synonym/text()', x))::text AS synonym
FROM unnest(xpath('//metabolite', myxml)) x
;

END$$;

-- select * from mytable limit 5;
SELECT * FROM mytable;

Exécution, sortie (dans PSQL):

[hmdb_test]# \i /mnt/Vancouver/Programming/data/hmdb/hmdb_test.sql

accession  |        name        |                         synonym                          
-------------+--------------------+----------------------------------------------------------
HMDB0000001 | 1-Methylhistidine  | (2S)-2-amino-3-(1-Methyl-1H-imidazol-4-yl)propanoic acid
HMDB0000001 | 1-Methylhistidine  | 1-Methylhistidine
HMDB0000001 | 1-Methylhistidine  | Pi-methylhistidine
HMDB0000001 | 1-Methylhistidine  | (2S)-2-amino-3-(1-Methyl-1H-imidazol-4-yl)propanoate
HMDB0000002 | 1,3-Diaminopropane | 1,3-Propanediamine
HMDB0000002 | 1,3-Diaminopropane | 1,3-Propylenediamine
HMDB0000002 | 1,3-Diaminopropane | Propane-1,3-diamine
HMDB0000002 | 1,3-Diaminopropane | 1,3-diamino-N-Propane
HMDB0000005 | 2-Ketobutyric acid | 2-Ketobutanoic acid
HMDB0000005 | 2-Ketobutyric acid | 2-Oxobutyric acid
HMDB0000005 | 2-Ketobutyric acid | 3-Methyl pyruvic acid
HMDB0000005 | 2-Ketobutyric acid | alpha-Ketobutyrate

[hmdb_test]#
2
Victoria Stuart

J'ai utilisé tr pour remplacer toutes les nouvelles lignes par des espaces. Cela créera un fichier XML avec une seule ligne. Ce fichier que je peux importer facilement dans une ligne en utilisant \copy.

Évidemment, ce n'est pas une bonne idée dans le cas où vous avez des valeurs multilignes en XML. Heureusement, ce n'est pas mon cas.

Pour importer tous les fichiers XML dans un dossier, vous pouvez utiliser ce script bash:

#!/bin/sh
FILES=/folder/with/xml/files/*.xml
for f in $FILES
do
  tr '\n' ' ' < $f > temp.xml
  psql -d database -h localhost -U usr -c '\copy xml_data from temp.xml'
done
0
Tomas Greif