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