source: trunk/PERL_SCRIPTS/ARBTOOLS/import_from_table.pl

Last change on this file was 18658, checked in by westram, 3 years ago
  • accept quoted LFs while importing from CSV.
  • Property svn:executable set to *
File size: 11.7 KB
Line 
1#!/usr/bin/perl
2# =============================================================== #
3#                                                                 #
4#   File      : import_from_table.pl                              #
5#   Purpose   : import data from comma- or tab-separated tables   #
6#                                                                 #
7#   Coded by Ralf Westram (coder@reallysoft.de) in January 2011   #
8#   Institute of Microbiology (Technical University Munich)       #
9#   http://www.arb-home.de/                                       #
10#                                                                 #
11# =============================================================== #
12
13use strict;
14use warnings;
15
16BEGIN {
17  if (not exists $ENV{'ARBHOME'}) { die "Environment variable \$ARBHOME has to be defined"; }
18  my $arbhome = $ENV{'ARBHOME'};
19  push @INC, "$arbhome/lib";
20  push @INC, "$arbhome/PERL_SCRIPTS/lib";
21  1;
22}
23
24use ARB;
25use tools;
26
27sub usage($) {
28  my ($error) = @_;
29  print(
30        "Usage: perl import_from_table.pl --match CF --write CF [options] datafile [database outdb]\n".
31        "\n".
32        "Imports one column from the calc-sheet 'datafile' into an ARB database.\n".
33        "\n".
34        "'datafile'     should be a list of tab-separated values.\n".
35        "'database'     if a name is specified, the modified DB will be saved as 'outdb'.\n".
36        "               Otherwise the database running in ARB will be modified.\n".
37        "\n".
38        "--match CF     CF:=column,field\n".
39        "               Define a 'column' in the 'datafile' and a species-'field' in the database.\n".
40        "               For each row the content of the 'column' has to match the content of the\b".
41        "               'field' for exactly one species in the 'database'.\n".
42        "               Useful fields are 'acc' and 'name'.\n".
43        "--write CF     CF:=column,field\n".
44        "               For each row in 'datafile' write the content of 'column' into the\n".
45        "               'field' of the species matched via --match\n".
46        "\n".
47        "Available 'options':\n".
48        "--csv            expect 'datafile' is a list of comma-separated values (default: TAB-separated)\n".
49        "--overwrite      overwrite 'field' specified via --write (default: abort if 'field' exists)\n".
50        "--skip-unknown   silently skip rows that don't match any species (default: abort if no match found)\n".
51        "--marked-only    only write to marked species (default: all species)\n".
52        "--mark           mark species to which field has been imported (unmarks rest)\n".
53        "--as-integer     use INTEGER database-type for field (default: STRING)\n"
54       );
55
56  if (defined $error) {
57    die "\nError: $error";
58  }
59}
60
61sub max($$) { my ($a,$b) = @_; return $a<$b ? $b : $a; }
62
63sub parse_CF($$) {
64  my ($switch,$CF) = @_;
65  my ($column,$field);
66  eval {
67    if ($CF =~ /^([^,]+),(.*)$/o) {
68      ($column,$field) = ($1,$2);
69      my $int_column = int($column);
70      if ($int_column<1) { die "'$column' is not a valid column\n"; }
71      my $error = ARB::check_key($field);
72      if (defined $error) { die "'$field' is not a valid DB field name\n"; }
73    }
74    else { die "',' expected in '$CF'\n"; }
75  };
76  if ($@) { die "in '$switch $CF': $@\n"; }
77  return ($column,$field);
78}
79
80my $reg_plain_normal  = undef;
81my $reg_plain_lastCol = undef;
82my $reg_quoted_normal  = undef;
83my $reg_quoted_lastCol = undef;
84
85sub set_separator($) {
86  my ($sep) = @_;
87
88  my $plain_rex  = "^([^$sep\"\r\n]*)"; # plain, probably empty column
89  my $quoted_rex = "^\"([^\"]*)\""; # quoted column
90
91  $reg_plain_lastCol  = qr/$plain_rex\n/;
92  $reg_quoted_lastCol = qr/$quoted_rex\n/;
93
94  $reg_plain_normal  = qr/$plain_rex$sep/;
95  $reg_quoted_normal = qr/$quoted_rex$sep/;
96}
97
98sub parse_column(\$) {
99  my ($line_r) = @_;
100  if (($$line_r =~ $reg_plain_normal) or
101      ($$line_r =~ $reg_quoted_normal) or
102      ($$line_r =~ $reg_plain_lastCol) or
103      ($$line_r =~ $reg_quoted_lastCol))
104    {
105      my $col = $1;
106      $$line_r = $';
107      return $col;
108    }
109  return undef;
110}
111
112my $inform_ARB = 0;
113sub main() {
114  my $datafile;
115  my $database     = ':';
116  my $database_out;
117
118  my ($matchcolumn,$matchfield);
119  my ($writecolumn,$writefield);
120  my ($skip_unknown,$overwrite,$marked_only,$mark) = (0,0,0,0);
121  my $int_type = 0;
122
123  my @no_option = ();
124
125  set_separator("\t");
126
127  eval {
128    while (scalar(@ARGV)>0) {
129      my $arg = shift @ARGV;
130      if    ($arg eq '--match') { ($matchcolumn,$matchfield) = parse_CF($arg, shift @ARGV); }
131      elsif ($arg eq '--write') { ($writecolumn,$writefield) = parse_CF($arg, shift @ARGV); }
132      elsif ($arg eq '--csv') { set_separator(','); }
133      elsif ($arg eq '--overwrite') { $overwrite = 1; }
134      elsif ($arg eq '--skip-unknown') { $skip_unknown = 1; }
135      elsif ($arg eq '--marked-only') { $marked_only = 1; }
136      elsif ($arg eq '--mark') { $mark = 1; }
137      elsif ($arg eq '--as-integer') { $int_type = 1; }
138      else { push @no_option, $arg; }
139    }
140
141    $datafile = shift @no_option;
142    if (not defined $datafile) { die "Missing argument 'datafile'\n"; }
143    if ($datafile =~ /^--/) { die "Unknown switch '$datafile'\n"; }
144
145    if (scalar(@no_option)) {
146      $database     = shift @no_option;
147      $database_out = shift @no_option;
148      if (not defined $database_out) { die "Missing argument 'outdb'\n"; }
149    }
150
151    if (scalar(@no_option)) { die "Unexpected arguments: ".join(',', @no_option)."\n"; }
152
153    if (not defined $matchcolumn) { die "Mandatory option '--match CF' missing\n"; }
154    if (not defined $writecolumn) { die "Mandatory option '--write CF' missing\n"; }
155  };
156  if ($@) {
157    usage($@);
158  }
159
160  my $gb_main = ARB::open($database, "rw");
161  if ($database eq ':') {
162    if ($gb_main) { $inform_ARB = 1; }
163    else { expectError('db connect (no running ARB)'); }
164  }
165  else {
166    $gb_main || expectError('db connect (wrong \'database\' specified?)');
167  }
168
169  if (not -f $datafile) { die "No such file '$datafile'\n"; }
170  open(TABLE,'<'.$datafile) || die "can't open '$datafile' (Reason: $!)\n";
171
172  my %write_table = (); # key=matchvalue, value=writevalue
173  my %source_line = (); # key=matchvalue, value=source-linenumber (corrected by joined_lines)
174
175  my $joined_lines = 0;
176
177  eval {
178    my $min_elems = max($matchcolumn,$writecolumn);
179    my $line;
180    while (defined($line=<TABLE>)) {
181      eval {
182        my @row = ();
183
184        my $done = 0;
185        while ($done == 0) {
186          my $column = parse_column($line);
187          if (defined $column) {
188            push @row, $column;
189
190            if ($line =~ /^[\r\n]*$/o) { # only LF + CR or nothing left -> done with line
191              $done = 1;
192            }
193          }
194          else { # test whether line contains quoted LF -> join next line
195            if ($line =~ /\"/o) {
196              my $nextLine = <TABLE>;
197              if (not defined $nextLine) {
198                die "reached EOF while attempting to append multiline (quoted entry seems to contain LF/CR)\n";
199              }
200              $line .= $nextLine;
201              $joined_lines++;
202              # print "(detected quoted LF/CR -> appended next line)\n";
203            }
204            else {
205              die "cannot interpret rest of line: '$line'\n";
206            }
207          }
208        }
209
210        my $relems = scalar(@row);
211        if ($relems<$min_elems) {
212          die "need at least $min_elems columns per table-line\n".
213            "(seen only $relems column. Maybe wrong separator chosen?)\n";
214        }
215
216        my $matchvalue = $row[$matchcolumn-1];
217        my $writevalue = $row[$writecolumn-1];
218
219        if (exists $write_table{$matchvalue}) {
220          die "duplicated value '$matchvalue' in column $matchcolumn (first seen in row ".$source_line{$matchvalue}.")\n";
221        }
222        $write_table{$matchvalue} = $writevalue;
223        $source_line{$matchvalue} = $.-$joined_lines;
224      };
225      if ($@) { die "$@ (in row ".($.-$joined_lines)." of '$datafile')\n"; }
226    }
227
228    # match and write to species
229    dieOnError(ARB::begin_transaction($gb_main), 'begin_transaction');
230
231    my $report = '';
232
233    eval {
234      my $ambiguous_hits  = 0;
235      my %written         = (); # key=matchvalue, value: 1=written, 2=skipped cause not marked
236
237      for (my $gb_species = BIO::first_species($gb_main);
238           $gb_species;
239           $gb_species = BIO::next_species($gb_species)) {
240        eval {
241          my $species_value = BIO::read_as_string($gb_species, $matchfield);
242          my $wanted_mark = 0;
243          if ($species_value) {
244            if (exists $write_table{$species_value}) { # found species matching table entry
245              if ($marked_only==1 and ARB::read_flag($gb_species)==0) {
246                $written{$species_value} = 2;
247              }
248              else {
249                my $existing_entry = BIO::read_as_string($gb_species, $writefield);
250                if ($existing_entry and not $overwrite) {
251                  die "already has an existing field '$writefield'.\n".
252                    "Use --overwrite to allow replacement.\n";
253                }
254                my $error = undef;
255                if ($int_type==1) {
256                  $error = BIO::write_int($gb_species, $writefield, int($write_table{$species_value}));
257                }
258                else {
259                  $error = BIO::write_string($gb_species, $writefield, $write_table{$species_value});
260                }
261                if ($error) { die $error; }
262                $wanted_mark = 1;
263                my $prev_written = $written{$species_value};
264                if (defined $prev_written) {
265                  $ambiguous_hits++;
266                }
267                $written{$species_value} = 1;
268              }
269            }
270          }
271          if ($mark==1) {
272            my $error = ARB::write_flag($gb_species,$wanted_mark);
273            if ($error) { die $error; }
274          }
275        };
276        if ($@) {
277          my $name = BIO::read_name($gb_species);
278          die "species '$name': $@";
279        }
280      }
281      my $not_found  = 0;
282      my $not_marked = 0;
283      {
284        my %missing = ();
285        my $what = $skip_unknown ? 'Warning' : 'Error';
286        foreach (keys %write_table) {
287          my $wr = $written{$_};
288          if (not defined $wr) {
289            $missing{$_} = 1;
290            $not_found++;
291          }
292          elsif ($wr==2) { # was not marked
293            $not_marked++;
294          }
295        }
296        foreach (sort { $source_line{$a} <=> $source_line{$b}; } keys %missing) {
297          print "$what: Found no matching species for row ".$source_line{$_}." ($matchfield='$_')\n";
298        }
299      }
300      if ($not_found>0 and $skip_unknown==0) {
301        die "Failed to find $not_found species - aborting.\n".
302          "(Note: use --skip-unknown to allow unknown references)\n";
303      }
304      $report = "Entries imported: ".(scalar(keys %written)-$not_marked)."\n";
305      if ($ambiguous_hits>0) { $report .= "Ambiguous hits: $ambiguous_hits\n"; }
306      if ($not_found>0) { $report .= "Unmatched (skipped) entries: $not_found\n"; }
307      if ($not_marked>0) { $report .= "Entries not imported because species were not marked: $not_marked\n"; }
308
309      print "\n".$report;
310    };
311    if ($@) {
312      ARB::abort_transaction($gb_main);
313      die $@;
314    }
315    ARB::commit_transaction($gb_main);
316    if ($database ne ':') { # database has been loaded
317      print "Saving modified database to '$database_out'\n";
318      my $error = ARB::save_as($gb_main, $database_out, "b");
319      if ($error) { die $error; }
320    }
321    ARB::close($gb_main);
322
323    if ($inform_ARB==1) {
324      $report =~ s/\n$//;
325      `arb_message "$report"`;
326    }
327  };
328  if ($@) {
329    ARB::close($gb_main);
330    die $@;
331  }
332  close(TABLE);
333}
334
335eval {
336  set_inGlobalEvalState(1);
337  main();
338};
339set_inGlobalEvalState(0);
340if ($@) {
341  die $@; # this die message calls arb_message (see ARB.pm)
342  exit(-1);
343}
344exit(0);
Note: See TracBrowser for help on using the repository browser.