aeb9b9e82dfe9b84fd3185c99bd1067373832148
[migration-tools.git] / fingerprinter
1 #!/usr/bin/perl
2 use strict;
3 use warnings;
4 use open ':utf8';
5
6 use Getopt::Long;
7 use MARC::Batch;
8 use MARC::File::XML ( BinaryEncoding => 'utf-8' );
9 use MARC::Field;
10 use Unicode::Normalize;
11
12 my $conf  = {}; # configuration hashref
13 my $count = 0;
14 $| = 1;
15
16 initialyze($conf);
17
18 open OF, '>', $conf->{output};
19 binmode(OF, ':utf8');
20 open XF, '>', $conf->{exception};
21 binmode(XF, ':utf8');
22
23 for my $file (@ARGV) {
24     print XF "Processing $file\n";
25     open my $records, '<:utf8', $file;
26
27     my $batch = MARC::Batch->new('XML', $records);
28     $batch->strict_off();
29     $batch->warnings_off();
30
31     while ( my $record = $batch->next ) {
32         $count++; progress_ticker();
33
34         my $id = $record->field($conf->{tag});
35         unless ($id) {
36             print XF "ERROR: Record $count in $file is missing a ",
37               $conf->{tag}, " field.\n", $record->as_formatted(), "\n=====\n";
38             next;
39         }
40
41         my $marc = populate_marc($record, $id);
42         $marc    = normalize_marc($marc);
43         unless (marc_isvalid($marc))
44           { dump_exception($marc); next; }
45         dump_fingerprints($marc);
46     }
47 }
48 print "\nProcessed $count records\n" unless $conf->{quiet};
49
50
51
52 =head2 populate_marc
53
54 Constructs a hash containing the relevant MARC data for a record and
55 returns a reference to it.
56
57 =cut
58
59 sub populate_marc {
60     my ($record, $id) = @_;
61     my %marc = (); $marc{isbns} = [];
62
63     # id, stringified
64     $marc{id} = $id->as_string($conf->{subfield});
65
66     # record_type, bib_lvl
67     $marc{record_type} = substr($record->leader, 6, 1);
68     $marc{bib_lvl}     = substr($record->leader, 7, 1);
69
70     # date1, date2
71     my $my_008 = $record->field('008');
72     $my_008 = $my_008->as_string() if ($my_008);
73     unless (length $my_008 == 40)
74       { print XF ">> Bad 008 field length in rec. $id\n"; return $marc }
75     $marc{date1} = substr($my_008,7,4) if ($my_008);
76     $marc{date2} = substr($my_008,11,4) if ($my_008); # UNUSED
77
78     # item_form
79     if ( $marc{record_type} =~ /[gkroef]/ ) { # MAP, VIS
80         $marc{item_form} = substr($my_008,29,1) if ($my_008);
81     } else {
82         $marc{item_form} = substr($my_008,23,1) if ($my_008);
83     }
84
85     # isbns
86     my @isbns = $record->field('020') if $record->field('020');
87     push @isbns, $record->field('024') if $record->field('024');
88     for my $f ( @isbns ) {
89         push @{ $marc{isbns} }, $1 if ( defined $f->subfield('a') and
90                                         $f->subfield('a')=~/(\S+)/ );
91     }
92
93     # author
94     for my $rec_field (100, 110, 111) {
95         if ($record->field($rec_field)) {
96             $marc{author} = $record->field($rec_field)->subfield('a');
97             last;
98         }
99     }
100
101     # issn, lccn, title, desc, pages, pub, pubyear, edition
102     $marc{lccn} = $record->field('010')->subfield('a') if $record->field('010');
103     $marc{issn} = $record->field('022')->subfield('a') if $record->field('022');
104     $marc{desc} = $record->field('300')->subfield('a') if $record->field('300');
105     $marc{pages} = $1 if (defined $marc{desc} and $marc{desc} =~ /(\d+)/);
106     $marc{title} = $record->field('245')->subfield('a')
107       if defined $record->field('245');
108     $marc{edition} = $record->field('250')->subfield('a')
109       if $record->field('250');
110     if ($record->field('260')) {
111         $marc{publisher} = $record->field('260')->subfield('b');
112         $marc{pubyear} = $record->field('260')->subfield('c');
113         $marc{pubyear} =
114           (defined $marc{pubyear} and $marc{pubyear} =~ /(\d{4})/) ? $1 : '';
115     }
116     return \%marc;
117 }
118
119
120
121 =head2 normalize_marc
122
123 Gently massages your data.
124
125 =cut
126
127 sub normalize_marc {
128     my ($marc) = @_;
129
130     $marc->{record_type }= 'a' if ($marc->{record_type} eq ' ');
131     if ($marc->{title}) {
132         $marc->{title} = NFD($marc->{title});
133         $marc->{title} =~ s/[\x{80}-\x{ffff}]//go;
134         $marc->{title} = lc($marc->{title});
135         $marc->{title} =~ s/\W+$//go;
136     }
137     if ($marc->{author}) {
138         $marc->{author} = NFD($marc->{author});
139         $marc->{author} =~ s/[\x{80}-\x{ffff}]//go;
140         $marc->{author} = lc($marc->{author});
141         $marc->{author} =~ s/\W+$//go;
142         if ($marc->{author} =~ /^(\w+)/) {
143             $marc->{author} = $1;
144         }
145     }
146     if ($marc->{publisher}) {
147         $marc->{publisher} = NFD($marc->{publisher});
148         $marc->{publisher} =~ s/[\x{80}-\x{ffff}]//go;
149         $marc->{publisher} = lc($marc->{publisher});
150         $marc->{publisher} =~ s/\W+$//go;
151         if ($marc->{publisher} =~ /^(\w+)/) {
152             $marc->{publisher} = $1;
153         }
154     }
155     return $marc;
156 }
157
158
159
160 =head2 marc_isvalid
161
162 Checks MARC record to see if neccessary fingerprinting data is
163 available
164
165 =cut
166
167 sub marc_isvalid {
168     my ($marc) = @_;
169     return 1 if ($marc->{item_form} and ($marc->{date1} =~ /\d{4}/) and
170                  $marc->{record_type} and $marc->{bib_lvl} and $marc->{title});
171     return 0;
172 }
173
174
175 =head2 dump_fingerprints
176
177 =cut
178
179 sub dump_fingerprints {
180     my ($marc) = @_;
181
182     if ($conf->{runtype} eq "primary") {
183         print OF join("\t",$marc->{id}, $marc->{item_form},
184                           $marc->{date1}, $marc->{record_type},
185                           $marc->{bib_lvl}, $marc->{title}), "\n";
186     } else {
187         if ((scalar @{ $marc->{isbns} } > 0) && $marc->{pages}) {
188             # case a : isbn and pages
189             foreach my $isbn ( @{ $marc->{isbns}} ) {
190                 print OF join("\t", $marc->{id}, "case a",
191                                   $marc->{item_form}, $marc->{date1},
192                                   $marc->{record_type},
193                                   $marc->{bib_lvl}, $marc->{title},
194                                   $isbn, $marc->{pages}), "\n";
195             }
196         }
197
198         if ($marc->{edition}) { # case b : edition
199             print OF join("\t", $marc->{id}, "case b",
200                               $marc->{item_form}, $marc->{date1},
201                               $marc->{record_type}, $marc->{bib_lvl},
202                               $marc->{title}, $marc->{edition}), "\n";
203         }
204
205         if ($marc->{issn}) { # case c : issn
206             print OF join("\t", $marc->{id}, "case c",
207                               $marc->{item_form}, $marc->{date1},
208                               $marc->{record_type}, $marc->{bib_lvl},
209                               $marc->{title}, $marc->{issn}), "\n";
210         }
211
212         if ($marc->{lccn}) { # case d : lccn
213             print OF join("\t", $marc->{id}, "case d",
214                               $marc->{item_form}, $marc->{date1},
215                               $marc->{record_type}, $marc->{bib_lvl},
216                               $marc->{title}, $marc->{lccn}) ,"\n";
217         }
218
219         # case e : author, publisher, pubyear, pages
220         if ($marc->{author} and $marc->{publisher} and $marc->{pubyear}
221             and $marc->{pages}) {
222             print OF join("\t", $marc->{id}, "case e",
223                               $marc->{item_form}, $marc->{date1},
224                               $marc->{record_type}, $marc->{bib_lvl},
225                               $marc->{title}, $marc->{author},
226                               $marc->{publisher}, $marc->{pubyear},
227                               $marc->{pages}), "\n";
228         }
229     }
230 }
231
232
233
234 =head2 dump_exception
235
236 Write line of exception report
237
238 =cut
239
240 sub dump_exception {
241     my ($marc) = @_;
242     print XF "Record ", $marc->{id}, " did not make the cut: ";
243     print XF "Missing item_form. " unless ($marc->{item_form});
244     print XF "Missing date1. " unless (defined $marc->{date1});
245     print XF "Invalid date1: ", $marc->{date1}
246       unless ($marc->{date1} =~ /\d{4}/);
247     print XF "Missing record_type. " unless ($marc->{record_type});
248     print XF "Missing bib_lvl. " unless ($marc->{bib_lvl});
249     print XF "Missing title. " unless ($marc->{title});
250     print XF "\n";
251 }
252
253
254 =head2 initialyze
255
256 Performs boring script initialization. Handles argument parsing,
257 mostly.
258
259 =cut
260
261 sub initialyze {
262     my ($c) = @_;
263     my @missing = ();
264
265     # set mode on existing filehandles
266     binmode(STDIN, ':utf8');
267
268     my $rc = GetOptions( $c,
269                          'exception|x=s',
270                          'output|o=s',
271                          'runtype|r=s',
272                          'subfield|s=s',
273                          'tag|t=s',
274                          'quiet|q',
275                          'help|h',
276                        );
277     show_help() unless $rc;
278
279     my @keys = keys %{$c};
280     show_help() unless (@ARGV and @keys);
281     for my $key ('runtype', 'tag', 'subfield', 'output', 'exception') {
282         push @missing, $key unless $c->{$key}
283     }
284     if (@missing) {
285         print "Required option: ", join(', ', @missing), " missing!\n";
286         show_help();
287     }
288
289     show_help() if ($c->{help});
290 }
291
292
293 =head2 progress_ticker
294
295 =cut
296
297 sub progress_ticker {
298     return if $conf->{quiet};
299
300     if ($count % 100 == 0) {
301         print '|';
302         print " $count \n" unless ($count % 1400);
303     } elsif ($count % 20 == 0) {
304         print '.';
305     }
306 }
307
308 =head2 show_help
309
310 Display usage message when things go wrong
311
312 =cut
313
314 sub show_help {
315 print <<HELP;
316 Usage is: fingerprinter [REQUIRED ARGS] [OPTIONS] <filelist>
317 Req'd Arguments
318   --runtype=(primary|full) -r  Do 'primary' or 'full' fingerprinting
319   --tag=N                  -t  Which tag to use
320   --subfield=X             -s  Which subfield to use
321   --output=<file>          -o  Output filename
322   --exceptions=<file>      -x  Exception report filename
323 Options
324   --quiet  -q  Don't write status messages to STDOUT
325 HELP
326 exit 1;
327 }