401ae6707840eb1fd69a98c7fe2c5304d52545d1
[migration-tools.git] / marc-cleanup
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 use Getopt::Long;
7 use Term::ReadLine;
8
9 $| = 1;
10
11 my $term = new Term::ReadLine 'yaz-cleanup';
12 my $OUT = $term->OUT || \*STDOUT;
13
14 my $count = 0;
15 my $reccount = 0;
16 my $line = '';
17
18 my @record = (); # current record storage
19 my %reccontext = ();
20 my @linecontext= (); # last 5 lines of file
21
22 my $input = shift || 'incoming.marc.xml';
23
24 open MARC, '<', $input;
25 open my $NUMARC, '>', 'incoming.clean.marc.xml';
26 print $NUMARC '<?xml version="1.0" encoding="UTF-8"?>',"\n";
27 open my $EXMARC, '>', 'incoming.exceptions.marc.xml';
28 print $EXMARC '<?xml version="1.0" encoding="UTF-8"?>',"\n";
29 open MARC2, '<', $input;
30 <MARC2>;
31
32 # this is the dispatch table which drives command selection in
33 # edit(), below
34 my %commands = ( c => \&print_context,
35                  C => \&print_linecontext,
36                  k => \&kill_line,
37                  o => \&show_original,
38                  m => \&merge_lines,
39                  t => \&commit_edit,
40                  x => \&dump_record,
41                  q => \&quit,
42                  '?' => \&help,
43                  h   => \&help,
44                  help => \&help,
45                );
46
47 my @spinner = qw(- / | \\);
48 my $sidx = 0;
49
50 while (my $line = getline()) {
51     unless ($count % 2000) {
52         print "\rWorking... ", $spinner[$sidx];
53         $sidx = ($sidx == $#spinner) ? 0 : $sidx + 1;
54     }
55     update_linecontext();
56
57     # catch empty datafield elements
58     if ($line =~ m|</datafield>|) {
59         if ($record[-2] =~ m/<datafield tag="..." ind1="." ind2=".">/) {
60             pop @record; pop @record;
61             print $OUT "\rEmpty datafield scrubbed at line $count\n";
62             next;
63         }
64     }
65
66     # clean misplaced dollarsigns
67     if ($line =~ m|<subfield code="\$">c?\d+\.\d{2}|) {
68         $line =~ s|"\$">c?(\d+\.\d{2})|"c">\$$1|;
69         print $OUT "\rDollar sign in subfield code corrected at line $count\n";
70     }
71
72     # clean up tags with spaces in them
73     $line =~ s/tag="  /tag="00/g;
74     $line =~ s/tag=" /tag="0/g;
75     $line =~ s/tag="-/tag="0/g;
76     $line =~ s/tag="(\d\d) /tag="0$1/g;
77
78     # stow tag data if we're looking at it
79     if ($line =~ m/<datafield tag="(.{3})" ind1="(.)" ind2="(.)">/) {
80         $reccontext{tag}  = $1;
81         $reccontext{ind1} = $2;
82         $reccontext{ind2} = $3;
83     }
84
85     # naked ampersands
86     if ($line =~ /&/ && $line !~ /&\w+?;/)
87       { edit("Naked ampersand", $line); next }
88
89     # tags must be numeric
90     if ($line =~ /<datafield tag="(.+?)"/) {
91         my $match = $1;
92         if ($match =~ /\D/) {
93             edit("Non-numerics in tag", $line);
94             next;
95         }
96     }
97
98     # subfields can't be non-alphanumeric
99     if ($line =~ /<subfield code="(.+?)"/) {
100         my $match = $1;
101         if ($match =~ /\P{IsAlnum}/) {
102             edit("Junk in subfield code", $line);
103             next;
104         }
105     }
106
107 }
108 print $NUMARC "</xml>\n";
109 print $EXMARC "</xml>\n";
110
111 =head2 edit
112
113 Handles the Term::ReadLine loop
114
115 =cut
116
117 sub edit {
118     my ($msg, $line_in) = @_;
119     print $OUT "\r".$msg, " at line $count:\n";
120     print_context();
121     while (1) {
122         my $line = $term->readline('marc-cleanup>');
123         if (length $line < 2)
124           { next unless (defined $commands{$line}) }
125         if (defined $commands{$line}) {
126             my $term = $commands{$line}->($line_in);
127             last if $term;
128         } else {
129             if ($linecontext[3] eq " [LINE KILLED]\n") {
130                 push @record, "$line\n"
131             } else {
132                 $record[-1] = "$line\n";
133             }
134             $linecontext[3] = "$line\n";
135             print_linecontext();
136         }
137     }
138 }
139
140 =head2 getline
141
142 Reads from the incoming MARC file; returns lines into the driver
143 loop. Batches records for output, and maintains the linecontext listing.
144
145 =cut
146
147 sub getline {
148     my $l = <MARC>;
149     $count++;
150     if (defined $l) {
151         if ($l =~ /<record>/) {
152             @record = ($l);
153             %reccontext = ();
154             $reccount++;
155         } elsif ($l =~ m|</record>|) {
156             push @record, $l;
157             write_record($NUMARC) if $reccount;
158         } else {
159             push @record, $l;
160         }
161     }
162     return $l;
163 }
164
165 sub write_record {
166     my ($FH) = @_;
167     print $FH '<collection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd" xmlns="http://www.loc.gov/MARC21/slim">',"\n";
168     print $FH @record;
169     print $FH "</collection>\n";
170 }
171
172 sub update_linecontext {
173     my $line2 = <MARC2>;
174     push @linecontext, $line2;
175     shift @linecontext if (@linecontext > 5);
176 }
177
178 #-----------------------------------------------------------------------------------
179 # command routines
180 #-----------------------------------------------------------------------------------
181
182 sub print_context {
183     print "\n Tag:",$reccontext{tag}, " Ind1:'",
184       $reccontext{ind1},"' Ind2:'", $reccontext{ind2}, "'";
185     print_linecontext();
186     return 0;
187 }
188
189 sub print_linecontext {
190     print $OUT "\n", join('    |','',@linecontext[0..2]);
191     print $OUT '==> |', $linecontext[3];
192     print $OUT '    |', $linecontext[4],"\n";
193     return 0;
194 }
195
196 sub show_original {
197     my ($line_in) = @_;
198     print $OUT "\n$line_in\n";
199     return 0;
200 }
201
202 sub merge_lines {
203     my $last = pop @record;
204     $last =~ s/^\s+//;
205     $record[-1] =~ s/\n//;
206     $record[-1] = join('', $record[-1], $last);
207     my @temp = ("\n");
208     push @temp, @linecontext[0..1];
209     $temp[3] = $record[-1];
210     $temp[4] = $linecontext[4];
211     @linecontext = @temp;
212     print_linecontext();
213     return 0;
214 }
215
216 sub kill_line {
217     pop @record;
218     $linecontext[3] = " [LINE KILLED]\n";
219     print_linecontext();
220     return 0;
221 }
222
223 sub dump_record {
224     my $line = <MARC>; $count++;
225     update_linecontext();
226     until ($line =~ m|</record>|) {
227         push @record, $line;
228         $line = <MARC>; $count++;
229         update_linecontext();
230     }
231     push @record, $line;
232     write_record($EXMARC);
233     return 1;
234 }
235
236 sub commit_edit { return 1 }
237
238 sub help {
239 print $OUT <<HELP;
240
241 Type a replacement for the indicated line, or enter a command.
242
243 Commands: c  Show record context ('C' for brief context)
244           k  Kill this line (remove from record)
245           m  Merge indicated line with previous line
246           o  Show original line
247           t  Commit changes and resume stream edit
248           x  Write this record to the exception file instead of output
249           q  Quit
250
251 HELP
252 return 0;
253 }
254
255 sub quit { exit }