From 2ea29720725a775ce4ca44b9c19ff80b76b0b2bf Mon Sep 17 00:00:00 2001 From: Thomas Hochstein Date: Fri, 15 Jan 2010 08:55:38 +0100 Subject: [PATCH] Initial commit. Signed-off-by: Thomas Hochstein --- usestats.cfg | 115 ++++++++ usestats.changes | 40 +++ usestats.pl | 708 +++++++++++++++++++++++++++++++++++++++++++++++ usestats.readme | 318 +++++++++++++++++++++ 4 files changed, 1181 insertions(+) create mode 100644 usestats.cfg create mode 100644 usestats.changes create mode 100644 usestats.pl create mode 100644 usestats.readme diff --git a/usestats.cfg b/usestats.cfg new file mode 100644 index 0000000..8d028b2 --- /dev/null +++ b/usestats.cfg @@ -0,0 +1,115 @@ +# Beispiel-Konfiguration fuer usestats.pl +# v 0.17 + +# spooldir = +# Das Spoolverzeichnis, falls die Postings von dort gelesen werden +# sollen. "spooldir" darf nicht gesetzt sein, wenn man eine MBOX in +# usestats pipen möchte. + +# recursive = 0 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob das Spoolverzeichnis rekursiv verarbeitet werden soll, +# d.h. alle darunterliegenden Unterverzeichnisse auch. Ermöglicht die +# Verarbeitung nicht nur einzelner Newsgroups, sondern auch ganzer +# Hierarchien oder des kompletten Newsspools des Servers. +# +# Noch nicht intensiv getestet, Fehler bitte mitteilen! + +# pipeformat = mbox +# Das Format der Datei, die in usestats gepiped wird. +# Mögliche Werte sind 'mbox' fuer eine Datei in MBOX-Format oder +# 'hamster' fuer eine .dat-Datei des Hamsters. + +# charset = ISO-8859-1 +# Zeichensatz, in dem die Ausgabe erfolgen soll + +# tz = +0200 +# Die lokale Zeitzone. Empfohlen wird die Angabe als Abweichung von +# GMT, also bspw. "+0200". + +# newsgroup = +# Wenn dieser Parameter angegeben wird, werden nur Postings +# erfasst, die -auch- in diese Newsgroup gegangen sind. + +# start = +# Bei Angabe dieses Parameters werden nur Postings nach diesem +# Datum erfasst. Die Angabe sollte im ISO-Format erfolgen, also +# bspw. "2003-10-01" für den 1. Oktober 2003. Die Verwendung von +# "start"/"stop" verlängert die Programmlaufzeit. + +# stop = +# Bei Angabe dieses Parameters werden nur Postings vor diesem Datum +# erfasst. Die Angabe sollte im ISO-Format erfolgen, also bspw. +# "2003-10-31" für den 31. Oktober 2003. Die Verwendung von +# "start"/"stop" verlängert die Programmlaufzeit. + +# day = 1 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob der Report "Postings pro Tag" ausgegeben werden soll. + +# newsgroups = 0 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob der Report "Newsgroupnutzung" ausgegeben werden +# soll. + +# poster = 1 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob der Report "Poster (strukturiert)" ausgegeben werden +# soll. + +# posterraw = 0 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob der Report "Poster (unstrukturiert)" ausgegeben +# werden soll. + +# subject = 1 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob der Report "Subjects (strukturiert)" ausgegeben +# werden soll. + +# newsreader = 1 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob der Report "Newsreader (strukturiert)" ausgegeben +# werden soll. + +# nruser = 1 +# Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet +# darüber, ob der Report "Nutzer pro Newsreader (strukturiert)" +# ausgegeben werden soll. + +# unknownreader = show +# Dieser Parameter kann den Wert 'show' oder 'noshow' annehmen und +# entscheidet darüber, ob unbekannte Newsreader mit dem vollen +# Eintrag der Headerzeile User-Agent/X-Newsreader/X-Mailer +# angezeigt oder unter "Sonstige" summiert werden sollen. + +# poster_width = 50 +# Dieser Parameter gibt die Spaltenbreite an, die maximal für die +# Namen der Poster zur Verfügung steht. Analog kann der Wert auch +# für die anderen Reports - ausser day - gesetzt werden. + +# poster_indent = 2 +# Dieser Parameter gibt die Einrückung der Tabelle an. Analog kann +# der Wert auch für die anderen Reports - ausser day - gesetzt +# werden. + +# poster_lines = 0 +# Dieser Parameter gibt an, wie viele Zeilen die Tabelle haben +# soll, ob also nur die häufigsten Poster genannt werden sollen. 0 +# gibt die komplette Tabelle aus. Analog kann der Wert auch für die +# anderen Reports - ausser day - gesetzt werden. + +# poster_cutoff = 0 +# Dieser Parameter gibt an, wie viele Postings jemand mindestens +# erreicht haben muss, um in die Tabelle aufgenommen zu werden. 0 +# gibt die komplette Tabelle aus. Analog kann der Wert auch für die +# anderen Reports - ausser day - gesetzt werden. + +# poster_graph = 20 +# Dieser Parameter gibt an, wie breit die "grafische" Darstellung der +# Prozentzahlen werden darf. 0 schaltet die Balkengrafik aus. Analog +# kann der Wert auch für die anderen Reports - ausser day - gesetzt +# werden. + +# graphchar = # +# Das Zeichen, das für die Balkengraphik verwendet werden soll. diff --git a/usestats.changes b/usestats.changes new file mode 100644 index 0000000..6f9816b --- /dev/null +++ b/usestats.changes @@ -0,0 +1,40 @@ +v 0.17: [20041003] +Neu: eingelesene Daten können gespeichert werden +Neu: Anzahl der Postings wird auch pro Monat ausgegeben +Fix: Fehler in Ermittlung der durchschnittlichen Postingzahl behoben +v 0.16: [20041002] +Neu: Option "-h" gibt Hilfetext aus +Chg: Optionen/Parameter werden jetzt mittels Getopt::Std ausgewertet; + dieses Modul wird dementsprechend auch benötigt +Fix: Anzeige der Postings pro Tag und Ermittlung der durchschnittlichen + Postingzahl waren fehlerhaft implementiert und führten zu + willkürlichen Ausgaben, sobald Tage völlig ohne Postings + "zwischendrin" vorkommen +v 0.15: [20040404] +Neu: neuer Report 'Newsgroupnutzung' +v 0.14: [20040215] +Neu: Spoolverzeichnisse können rekursiv verarbeitet werden +Fix: Zusammenführen gefoldeter Header mit MIME-words verbessert (?) +v 0.13: +Neu: Ausgabe der Prozentwerte als "Balkengraphik" +v 0.12: +Neu: MIME-words im Header werden korrekt in einen variablen + Ausgabezeichensatz konvertiert +Chg: Report "Poster (unstrukturiert)" wird defaultmäßig nicht mehr + ausgegeben +Fix: (nicht MBOX-konformer) Postingtrenner von exportierten Agent- + Dateien wird jetzt richtig(er) erkannt +v 0.11: +Neu: Verarbeitung von data.dat-Dateien des Hamsters (siehe Readme) +Neu: Summenzeile unter jedem Report +Neu: neuer Report 'Nutzer pro Newsreader' (siehe Readme) +Neu: neue Option 'unknownreader' (siehe Readme) +Neu: weitere Newsreader werden erkannt +Fix: fuer 'stop' wird als Uhrzeit 23:59:59 des entsprechenden Tages + angenommen +Fix: nur die Analysen werden durchlaufen, fuer die auch Reports + generiert werden sollen +Fix: nicht zu parsendes Date: wird ignoriert +Fix: nicht zu parsendes From: wird ignoriert +Fix: User-Agent: wird nur dann ignoriert, wenn Hamster am Anfang der + Zeile steht \ No newline at end of file diff --git a/usestats.pl b/usestats.pl new file mode 100644 index 0000000..ab0ec33 --- /dev/null +++ b/usestats.pl @@ -0,0 +1,708 @@ +#!/usr/bin/perl +# +# usestats.pl +############# + +# (c) 10/2003-10/2004 Thomas Hochstein +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. + +# Modules ############################# +# use Data::Dumper; +use Date::Manip qw(Date_Init ParseDate Date_Cmp DateCalc UnixDate Delta_Format Date_DaysInMonth); +use File::Find; +use Getopt::Std; +use Locale::Recode; +use Mail::Address; +use MIME::Words qw(decode_mimewords); + +# Versionsnummer ###################### +$ver = '0.17 beta (20041003)'; + +# Konstanten ######################### + +$hex_nibb = '[0-9a-fA-F]'; +$gt_hex_nibb = '[0-9A-F]'; +$lt_hex_nibb = '[0-9a-f]'; +$alpha_num = '[0-9a-zA-Z]'; +$lt_alpha_num = '[0-9a-z]'; +$gt_alpha_num = '[0-9A-Z]'; +%reporttitel = ('newsgroups' => 'Newsgroupnutzung', + 'poster' => 'Poster (strukturiert)', + 'posterraw' => 'Poster (unstrukturiert)', + 'subject' => 'Subjects (strukturiert)', + 'newsreader' => 'Newsreader (strukturiert)', + 'nruser' => 'Nutzer pro Newsreader (strukturiert)'); + +# Variablen ########################### + +# Defaults +%config=(); + +# $config{'newsgroup'} = ''; +# $config{'start'} = ''; +# $config{'stop'} = ''; +$config{'recursive'} = 0; +$config{'pipeformat'} = 'mbox'; +$config{'charset'} = 'ISO-8859-1'; +$config{'tz'} = '+0200'; +$config{'graphchar'} = '#'; + +$config{'day'} = 1; +@reports = ('newsgroups','poster','posterraw','subject','newsreader','nruser'); +foreach $report (@reports) { + $config{$report} = 1; + $config{$report.'_width'} = 30; + $config{$report.'_indent'} = 2; + $config{$report.'_lines'} = 0; + $config{$report.'_cutoff'} = 0; + $config{$report.'_graph'} = 20; +}; +$config{'newsgroups'} = 0; +$config{'posterraw'} = 0; +$config{'unknownreader'} = 'show'; + +# Hauptprogramm ####################### +# Konfiguration einlesen +my %options; +getopts('hqc:r:w:', \%options); +if ($options{'h'}) { + print "$0 v $ver\nUsage: $0 [-hq] [-w|r ] [-c ]\n"; + exit(0); +}; +print STDERR "$0 v $ver [" . scalar(gmtime) . "]\n(c) 10/2003-10/2004 Thomas Hochstein * \n" if (!$options{'q'}); +if ($options{'c'}) { + &readconfig($options{'c'}); +}; +if (length($config{'graphchar'}) > 1 or $config{'graphchar'} eq '') { + $config{'graphchar'} = '#'; +}; +# Charset prüfen +$config{'charset'} = uc($config{'charset'}); +$supported = Locale::Recode->getSupported; +if (!scalar(grep /$config{'charset'}/, @{$supported})) { + print STDERR "ERROR: unknown charset $config{'charset'}\n"; + $config{'charset'} = 'ISO-8859-1'; +}; + +&Date_Init("TZ = $config{'tz'}"); + +if ($options{'r'}) { + @postings = @{&readdata($options{'r'})}; + print STDERR "\n>>>>> Data loaded.\n\n" if (!$options{'q'}); +} else { + # Postings parsen + if (exists($config{'spooldir'})) { + if ($config{'recursive'}) { + @postings = @{&get_spool_recursive($config{'spooldir'})}; + } else { + @postings = @{&get_spool($config{'spooldir'})}; + }; + } else { + @postings = @{&get_stdin($config{'pipeformat'})}; + }; + print STDERR "\n>>>>> Data parsed.\n\n" if (!$options{'q'}); + + # Daten speichern, falls verlangt + if ($options{'w'}) { + &writedata($options{'w'},\@postings); + } +}; + +# Postings auswerten +$newsgrouplgth = 0; +$posterrawlgth = 0; +$posterlgth = 0; +$subjectlgth = 0; +$newsreaderlgth = 0; + +foreach $posting (@postings) { + %header = %{$posting}; + &day($header{'date'}) if $config{'day'}; + &newsgroups($header{'newsgroups'}) if $config{'newsgroups'}; + &poster($header{'from'}) if ($config{'poster'} or $config{'posterraw'}); + &subject($header{'subject'}) if $config{'subject'}; + &client($posting) if ($config{'newsreader'} or $config{'nruser'}); +}; + +# Nutzer pro Newsreader ermitteln +if ($config{'nruser'}) { + foreach $user (keys %userreader) { + $nruser{$userreader{$user}}++; + if (length($userreader{$user}) > $nruserlgth) { $nruserlgth = length($userreader{$user}); }; + $nrusersum++; + #D print "!> $user: $userreader{$user}\n"; + }; +}; + +print STDERR "\n>>>>> Data analyzed.\n\n" if (!$options{'q'}); + +# Ausgabe generieren +$output = "Auswertung"; +if ($config{'newsgroup'}) { $output .= " für $config{'newsgroup'}"; }; +print center($output); +if ($config{'start'} || $config{'stop'}) { + $output = ''; + if ($config{'start'}) { $output .= "von $config{'start'} "; }; + if ($config{'stop'}) { $output .= "bis $config{'stop'}"; }; + print center($output); +}; +print center("=" x length($output))."\n"; + +# Postings pro Tag ermitteln und ausgeben +if ($config{'day'}) { + print center("Postings pro Tag"); + print center("----------------"); + &Date_Init("Language = German"); + $postcount = 0; $firstday = 0; $lastday = 0; $lastkw = 0; $lastmonat = 0; + foreach $day (sort keys %datecount) { + $firstday = $day if ($lastday == 0); + $monat = UnixDate($day,"%B %Y"); + if ($monat ne $lastmonat) { + if ($postcount > 0) { + printf "\n\nPostings %-14s: %d (%.2f pro Tag)\n",$lastmonat,$postcount,($postcount / Date_DaysInMonth(UnixDate($lastday,"%m"),UnixDate($lastday,"%Y"))); + }; + $postcount = 0; + print "\n\n".center($monat,50)."\n"; + print " KW : Mo : Di : Mi : Do : Fr : Sa : So :\n"; + print "------------------------------------------------"; + }; + $kw = UnixDate($day,"%W"); + if ($kw != $lastkw && $lastkw > 0 && $monat eq $lastmonat) { + for ($i=$lastkw+1; $i < $kw; $i++) { + printf "\n %2.2d :",$i; + }; + }; + if ($kw != $lastkw || $monat ne $lastmonat) { + printf "\n %2.2d :",$kw; + print " " x (6*(UnixDate($day,"%w")-1)); + } elsif ($lastday != 0 && $day-$lastday > 1) { + print " " x (6*($day-$lastday-1)); + }; + printf " %5.5s",$datecount{$day}; + $postcount += $datecount{$day}; + $lastday = $day; + $lastkw = $kw; + $lastmonat = $monat; + }; + if ($postcount > 0) { + printf "\n\nPostings %-14s: %d (%.2f pro Tag)\n",$lastmonat,$postcount,($postcount / Date_DaysInMonth(UnixDate($lastday,"%m"),UnixDate($lastday,"%Y"))); + }; + $daycount = Delta_Format(DateCalc(ParseDate($firstday),ParseDate($lastday)),0,'%dh'); + #D print STDERR "$daycount = $lastday - $firstday\n"; + print "\n\n"; + if (scalar(@postings)) { + printf "Postings insgesamt: %d (%.2f pro Tag)\n\n\n",scalar(@postings),scalar(@postings)/$daycount; + } else { + print "Keine Postings erfasst.\n\n\n"; + }; +}; +#D print &format_table(\%datecount,scalar(@postings),8,50,2,0,0); + +# restliche Reports ausgeben +foreach $report (@reports) { + if ($config{$report}) { + print center($reporttitel{$report}); + print center("-" x length($reporttitel{$report}))."\n"; + $reportlgth = $report.'lgth'; + $reportsum = $report.'sum'; + $$reportsum = scalar(@postings) unless defined($$reportsum); + print &format_table(\%$report,$$reportsum,$$reportlgth,$config{$report.'_width'},$config{$report.'_indent'},$config{$report.'_lines'},$config{$report.'_cutoff'},$config{$report.'_graph'}); + print "\n\n"; + }; +}; + +print "-- \n$0 v $ver [" . scalar(gmtime) . "]\n(c) 10/2003-10/2004 Thomas Hochstein * \n"; +exit; + +# Subroutinen ######################### + +sub get_stdin { + # Header aus Postings auslesen (gepipete Datei) + # IN: $format + # (MBOX-File oder data.dat, gepiped in STDIN) + # OUT: Referenz auf @postings, das jeweils + # Referenzen auf %header (die Header) enthält + my $format = shift; # Format der zu verarbeitenden Datei + my ($trigger,$hname,$hcontents,%header,@postings,%midcache,$exit); + if ($format eq 'hamster') { + $trigger = '^.{4}X-Hamster-Info: '; + } else { + # Default: mbox + $trigger = '^From \S+\@\S+ '; + }; + while (<>) { + # Neues Posting beginnt? + if (/$trigger/o) { + $exit = 0; + PARSELOOP: + while (<>) { + # Leerzeile = Header beendet + if (/^$/) {last PARSELOOP; } + chomp; + # Gefoldete Header an letzten anhängen + if (/^\s+/) { + s/^\s*(.+)\s*$/$1/; + # $header{lc($hname)} .= "\n\t$_"; + if (/^=\?/) { + $header{lc($hname)} .= $_; + } else { + $header{lc($hname)} .= " $_"; + }; + # Normale Header + } else { + ($hname,$hcontents) = split /:/,$_,2; + $hcontents =~ s/^\s*(.+)/$1/; + # richtige Newsgroup, falls gesetzt? + if (exists($config{'newsgroup'}) && $config{'newsgroup'} ne '' && lc($hname) eq 'newsgroups' && lc($hcontents) !~ /$config{'newsgroup'}/) { + #D print STDERR "ERROR: not $config{'newsgroup'}\n"; + $exit = 1; + last PARSELOOP; + # richtiger Datumsbereich, falls gesetzt? + } elsif (lc($hname) eq 'date' && ((exists($config{'start'}) && $config{'start'} ne '' && Date_Cmp($hcontents,$config{'start'}) < 0) or (exists($config{'stop'}) && $config{'stop'} ne '' && Date_Cmp($hcontents,$config{'stop'}.' 23:59:59') > 0))) { + #D print STDERR "ERROR: $hcontents not between $config{'start'} and $config{'stop'}\n"; + $exit = 1; + last PARSELOOP; + # Posting schonmal erfasst? MID-cache + } elsif (lc($hname) eq 'message-id' && exists($midcache{$hcontents})) { + #D print STDERR "ERROR: $hcontents was already cached\n"; + $exit = 1; + last PARSELOOP; + }; + $header{lc($hname)} = $hcontents; + }; + }; + #D $count++; print "--$count--".$header{'message-id'}."\n"; + # Referenz auf Kopie von %header in @postings einhängen & MID cachen + if (!$exit) { + #D print "! $header{'message-id'}\n"; + #D print "> $header{'from'}\n"; + #D print "> $header{'newsgroups'}\n"; + #D print "> $header{'date'}\n"; + #D print "-----\n"; + $midcache{$header{'message-id'}}++; + push @postings, { %header }; + }; + undef %header; + }; + }; + return \@postings; +}; + +sub get_spool { + # Header aus Postings auslesen (Dateien im Spool) + # IN: $verzeichnis + # OUT: Referenz auf @postings, das jeweils + # Referenzen auf %header (die Header) enthält + # Globale Variablen, die gesetzt werden: + # %midcache; + my $verzeichnis = shift; + my ($file,$hname,$hcontents,%header,@postings,$exit); + opendir(SPOOL,$verzeichnis) || print STDERR "ERROR: Could not open spool directory $verzeichnis: $!"; + FILELOOP: + while(defined($file = readdir(SPOOL))) { + if($file =~/^\./) { next FILELOOP; }; + #D print "!-> $file\n"; + open POSTING, "<$verzeichnis/$file" || print STDERR "ERROR: Could not open article file $verzeichnis/$file for reading: $!"; + $exit = 0; + PARSELOOP: + while() { + # Leerzeile = Header beendet + if (/^$/) {last PARSELOOP; } + chomp; + # Gefoldete Header an letzten anhängen + if (/^\s+/) { + s/^\s*(.+)\s*$/$1/; + # $header{lc($hname)} .= "\n\t$_"; + if (/^=\?/) { + $header{lc($hname)} .= $_; + } else { + $header{lc($hname)} .= " $_"; + }; + # Normale Header + } else { + ($hname,$hcontents) = split /:/,$_,2; + $hcontents =~ s/^\s*(.+)/$1/; + # richtige Newsgroup, falls gesetzt? + if (exists($config{'newsgroup'}) && $config{'newsgroup'} ne '' && lc($hname) eq 'newsgroups' && lc($hcontents) !~ /$config{'newsgroup'}/) { + #D print STDERR "ERROR: not $config{'newsgroup'}\n"; + $exit = 1; + last PARSELOOP; + # richtiger Datumsbereich, falls gesetzt? + } elsif (lc($hname) eq 'date' && ((exists($config{'start'}) && $config{'start'} ne '' && Date_Cmp($hcontents,$config{'start'}) < 0) or (exists($config{'stop'}) && $config{'stop'} ne '' && Date_Cmp($hcontents,$config{'stop'}).' 23:59:59' > 0))) { + #D print STDERR "ERROR: $hcontents not between $config{'start'} and $config{'stop'}\n"; + $exit = 1; + last PARSELOOP; + # Posting schonmal erfasst? MID-cache + } elsif (lc($hname) eq 'message-id' && exists($midcache{$hcontents})) { + #D print STDERR "ERROR: $hcontents was already cached\n"; + $exit = 1; + last PARSELOOP; + }; + $header{lc($hname)} = $hcontents; + }; + }; + # Referenz auf Kopie von %header in @postings einhängen & MID cachen + if (!$exit) { + $midcache{$header{'message-id'}} = 1 ; + push @postings, { %header }; + }; + undef %header; + # print "! $header{'message-id'}\n"; + # print "> $header{'from'}\n"; + close(POSTING); + }; + closedir(DIR); + return \@postings; +}; + +sub get_spool_recursive { + # Header aus Postings auslesen (Dateien im Spool, mehrere Spoolverzeichnisse) + # ruft &get_spool rekursiv auf + # IN: $wurzelverzeichnis + # OUT: Referenz auf @postings, das jeweils + # Referenzen auf %header (die Header) enthält + my $wurzelverzeichnis = shift; + my (@verzeichnisse, $verzeichnis); + find sub { -d $File::Find::name && push @verzeichnisse, $File::Find::name }, $wurzelverzeichnis; + foreach $verzeichnis (@verzeichnisse) { + push @postings, @{&get_spool($verzeichnis)}; + }; + return \@postings; +}; + +sub day { + # Postings pro Tag ermitteln + # IN: Date:-Header des jeweiligen Postings + # OUT: --- + # Globale Variablen, die gesetzt werden: + # %datecount; + my $datum = shift; + if ($datum = UnixDate($datum,"%Y%m%d")) { + $datecount{$datum}++; + }; + #D print ">> $datum\n"; + return; +}; + +sub poster { + # Poster (mit vollem From + nur mit Namen) auswerten + # IN: From:-Header des jeweiligen Postings + # OUT: --- + # Globale Variablen, die gesetzt werden: + # %posterraw; + # %poster; + # $posterrawlgth; + # $posterlgth; + my $from = shift; + my $name; + ($from) = Mail::Address->parse($from); + if (defined($from)) { + # Postings pro Poster (nach Namen im From:) + $name = $from->format; + } else { + $name = '(unbekannt)'; + }; + if ($name eq '') { $name = '(unbekannt)'; }; + $posterraw{$name}++; + if (length($name) > $posterrawlgth) { $posterrawlgth = length($name); }; + if (defined($from)) { + # Postings pro Poster (nach From:) + $name = &mime_decode($from->name); + }; + #D print "::>> $name\n"; + $poster{$name}++; + if (length($name) > $posterlgth) { $posterlgth = length($name); }; + #D print "!> $address : $name\n"; + return; +}; + +sub subject { + # Subject auswerten + # IN: Subject:-Header des jeweiligen Postings + # OUT: --- + # Globale Variablen, die gesetzt werden: + # %subject; + # $subjectlgth; + # Subject decodieren + my $subject = shift; + $subject = &mime_decode($subject); + $subject =~ s/^(re|aw): (.+)/$2/i; + # Postings mit entsprechendem Subject + $subject{$subject}++; + if (length($subject) > $subjectlgth) { $subjectlgth = length($subject); }; + #D print "!> $subject\n"; + return; +}; + +sub newsgroups { + # Newsgroup auswerten + # IN: Newsgroup:-Header des jeweiligen Postings + # OUT: --- + # Globale Variablen, die gesetzt werden: + # %newsgroups; + # $newsgroupslgth; + # $newsgroupssum: + # Newsgroup decodieren - wohl nicht erforderlich? + my $newsgroup; + my $newsgroups = shift; + #D print "!> $newsgroup\n"; + # $newsgroups = &mime_decode($newsgroups); + my @newsgroups = split /,/,$newsgroups; + # Postings in entsprechende Newsgroups + foreach $newsgroup (@newsgroups) { + # auch Crossposts als Inkarnation zählen + $newsgroupssum++; + chomp($newsgroup); + $newsgroups{$newsgroup}++; + if (length($newsgroup) > $newsgroupslgth) { $$newsgroupslgth = length($newsgroup); }; + #D print "--> $newsgroup\n"; + } + return; +}; + +sub client { + # Newsreader zu ermitteln versuchen + # IN: Referenz auf %header des jeweiligen Postings + # OUT: --- + # Globale Variablen, die gesetzt werden: + # %newsreader; + # $newsreaderlgth; + # %userreader; + my $posting = shift; + my %header = %{$posting}; + my ($newsreader); + # zutreffenden Header ermitteln, mit Präzedenz User-Agent -> X-Newsreader -> X-Mailer + if (defined($header{'user-agent'}) && $header{'user-agent'} !~ /^hamster/i) { + $newsreader = &mime_decode($header{'user-agent'}); + } elsif(defined($header{'x-newsreader'})) { + $newsreader = &mime_decode($header{'x-newsreader'}); + } elsif(defined($header{'x-mailer'})) { + $newsreader = &mime_decode($header{'x-mailer'}); + }; + # Newsreader zu ermitteln versuchen + if ((defined($newsreader)) and ($newsreader ne '')) { + #D print "! $newsreader\n"; + KNOWN: { + $newsreader = 'Outlook Express', last KNOWN if $newsreader=~/Outlook Express/i; + $newsreader = 'Mozilla', last KNOWN if ($newsreader=~/Mozilla/i and $newsreader!~/StarOffice/i); + $newsreader = 'Star Office', last KNOWN if ($newsreader=~/Mozilla/i and $newsreader =~/StarOffice/i); + $newsreader = 'Forté Agent', last KNOWN if ($newsreader=~/Forte.*Agent/i or $header{'message-id'}=~/^[a-zA-Z0-9=+]{28,34}\@/ or $header{'message-id'}=~/^$lt_alpha_num{8}\.\d{7,9}\@/o or $header{'message-id'}=~/^$lt_alpha_num{7}\.\d{2,3}\.\d\@/o); + $newsreader = 'Xnews', last KNOWN if ($newsreader=~/Xnews/i or $header{'message-id'}=~/^$lt_alpha_num{6}\.$lt_alpha_num{2}\.\d\@/o); + $newsreader = 'Gnus', last KNOWN if ($newsreader=~/Gnus/i or $header{'message-id'}=~/^$lt_alpha_num{10,11}\.fsf\@/o); + $newsreader = 'slrn', last KNOWN if ($newsreader=~/slrn/i or $header{'message-id'}=~/^slrn$lt_alpha_num{6}\.$lt_alpha_num{2,3}\.\w+\@/o); + $newsreader = 'MacSOUP', last KNOWN if ($newsreader=~/MacSOUP/i or $header{'message-id'}=~/^$lt_alpha_num{7}\.$lt_alpha_num{13,14}[A-Z]\%[a-zA-Z\.]+\@/o); + $newsreader = 'Gravity', last KNOWN if ($newsreader=~/Gravity/i or $header{'message-id'}=~/^MPG\.$lt_hex_nibb{22}\@/o); + $newsreader = 'Pine', last KNOWN if ($newsreader=~/Pine/i or $header{'message-id'}=~/^Pine\.$gt_alpha_num{3}\.\d\.\d{2}\.\d{14}\.\d{4,5}-\d{6}\@/o); + $newsreader = 'Crosspoint', last KNOWN if ($newsreader=~/Crosspoint/i or $header{'message-id'}=~/^[a-zA-Z0-9\$\-]{11}\@/o); + $newsreader = 'Crosspoint/OpenXP', last KNOWN if ($newsreader=~/OpenXP/); + $newsreader = 'Pan', last KNOWN if ($newsreader=~/Pan/); + $newsreader = 'Fortitude Dialog', last KNOWN if ($newsreader=~/Dialog/i); + $newsreader = 'tin', last KNOWN if ($newsreader=~/tin\//); + $newsreader = 'KNode', last KNOWN if ($newsreader=~/KNode/); + $newsreader = 'Opera', last KNOWN if ($newsreader=~/Opera/); + $newsreader = 'Freenet-Webnews', last KNOWN if ($newsreader=~/Freenet-Webnews/i); + $newsreader = 'NewsFleX', last KNOWN if ($newsreader=~/NewsFleX/i); + $newsreader = 'knews', last KNOWN if ($newsreader=~/knews/i); + $newsreader = 'WinVN', last KNOWN if ($newsreader=~/WinVN/i); + $newsreader = 'nn', last KNOWN if ($newsreader=~/nn\//); + $newsreader = 'Lotus Notes', last KNOWN if ($newsreader=~/Lotus.*Notes/); + $newsreader = 'NewsPortal', last KNOWN if ($newsreader=~/NewsPortal/i); + $newsreader = 'ProNews', last KNOWN if ($newsreader=~/ProNews/i); + $newsreader = 'Sylpheed', last KNOWN if ($newsreader=~/Sylpheed/i); + print STDERR "Unknown Newsreader: $newsreader\n" if (!$options{'q'}); + if($config{'unknownreader'} eq 'show') { + # $newsreader ist ja schon gesetzt + } else { + $newsreader = '(Sonstiger)'; + }; + } + } else { + $newsreader = '(keine Angabe)' + }; + # Usernamen ermitteln und Newsreader dieses Nutzers setzen + # (bei versch. Newsreadern zählt also der letzte) + ($from) = Mail::Address->parse($header{'from'}); + if (defined($from)) { + $name = $from->format; + $userreader{$name} = $newsreader; + #D print "> $userreader{$name}\n"; + }; + #D print "> $newsreader\n"; + if (length($newsreader) > $newsreaderlgth) { $newsreaderlgth = length($newsreader); }; + $newsreader{$newsreader}++; + return; +} + +sub center { + # Text mittig formatieren + # IN: $text, $width + # OUT: (zentrierter Text) + my $text = shift; # Text + my $width = shift; # Breite der Spalte + chomp($text); + $width ||= 70; # Defaultbreite: 70 + return ' 'x(($width-length($text))/2).$text."\n"; +}; + +sub format_table { + # Tabelle formatieren + # IN: \%values, $sum, $longest, $maxwidth, $indent, $lines, $cutoff, $graph + # OUT: $output (formatierte Tabelle) + my $values = shift; # Referenz auf %values + my $sum = shift; # Gesamtsumme (fuer Prozentwert) + my $longest = shift; # Längster Index von %values + my $maxwidth = shift; # Maximale Spaltenbreite für den Index + my $indent = shift; # Einzug links + my $lines = shift; # Maximale Zeilenzahl der Tabelle + my $cutoff = shift; # Minimaler anzuzeigender Wert + my $graphwidth = shift; # Spaltenbreite für Graph + my %values = %{$values}; + my ($output,$value,$outputvalue,$valuelongest,$place,$outputplace,$lastvalue,$notdisplayed,$percent,$maxpercent,$graph,$graphlength); + # $maxwidth auf minimal 10 setzen und $longest an $maxwidth anpassen + if ($maxwidth > 0) { + if ($maxwidth < 10) { $maxwidth = 10; }; + if ($longest > $maxwidth) { $longest = $maxwidth; }; + }; + # $lines auf minimal 3 setzen + if ($lines > 0 && $lines < 3) { $lines = 3; }; + # Tabelle ausgeben + $lastvalue = 0; + foreach $value (sort {$values{$b} <=> $values{$a}} keys %values) { + # Prozentzahl berechnen + $percent = 100*$values{$value}/$sum; + # Platzziffer setzen, ggf. unterdrücken, falls selber Wert wie zuvor + $place++; $outputplace = ''; + if ($lastvalue != $values{$value}) { $outputplace = $place.'.'; }; + $outputvalue = $value; + # Platz 1 hat größten Wert -> höchste Breite für Wertespalte und höchsten Prozentwert ermitteln + if ($place == 1) { + $valuelongest = length($values{$value}); + $maxpercent = $percent; + }; + # falls Index zu breit: kürzen und "[...]" anhängen + if ($maxwidth > 0 && length($value) > $maxwidth) { + $outputvalue = substr($value,0,$maxwidth-5)."[...]"; + }; + # Balkengrafik generieren + if ($graphwidth > 0) { + $graphlength = int($percent*$graphwidth/$maxpercent); + $graph = '|'.$config{'graphchar'}x$graphlength.' 'x($graphwidth-$graphlength).'|'; + } else { + $graph = '' + }; + # Zeilen ausgeben, bis maximale Zeilenzahl oder Grenzwert erreicht (in letzterem Fall aber mind. 1 Zeile) + if (($lines < 1 || $place <= $lines) && ($cutoff < 1 || $place < 2 || $values{$value} >= $cutoff)) { + $output .= ' 'x$indent.sprintf "%4s %-${longest}s: %${valuelongest}s (%5.2f%%) %${graphwide}s\n", $outputplace, $outputvalue, $values{$value}, $percent, $graph; + # falls Zeile nicht mehr ausgegeben: mitzählen + } else { + $notdisplayed++; + }; + $lastvalue = $values{$value}; + }; + # "(+ ... weitere)" + if (($lines > 0 && $place > $lines) or ($cutoff > 0 && $lastvalue < $cutoff)) { + $output .= ' 'x($indent+4)."(+ $notdisplayed weitere"; + if ($cutoff > 0 || $values{$value} < $cutoff) { + $output .= " mit < $cutoff"; + }; + $output .= ")\n"; + }; + $output .= ' 'x$indent.'-'x(16+$longest+$valuelongest)."\n"; + $output .= ' 'x($indent+6+$longest).sprintf "%${valuelongest}s ( 100% )\n", $sum; + return $output; +}; + +sub readconfig { + # Konfigurationsdatei auslesen + # IN: Name der Konfigurationsdatei + # OUT: --- + # Globale Variablen, die gesetzt werden: + # %config; + my $conffile = shift; + if (-e $conffile) { + open CONFIG, "<$conffile" || print STDERR "ERROR: Could not open config file $conffile for reading: $!"; + } else { + print STDERR "ERROR: Config file $conffile does not exist!\n"; + return; + }; + while() { + chomp; + s/#.*//; + s/^\s+//; + s/\s+$//; + next unless length; + my($option,$value) = split(/\s*=\s*/, $_, 2); + $config{$option} = $value; + #D print "$option: $value\n;" + }; + close CONFIG; + return; +}; + +sub mime_decode { + # Header mit MIME-Words decodieren + # IN: $mimeheader - potentiell codierter Header + # OUT: $decoded - decodierte Zeile + my $mimeheader = shift; + my ($mimeword, $decoded); + foreach $mimeword (decode_mimewords($mimeheader)) { + my ($data, $encoding) = @{$mimeword}; + #D printf "Enc: %12s %s \n", $encoding, $data; + if ($encoding ne '') { + $encoding = uc($encoding); + my $convert = Locale::Recode->new (from => $encoding, + to => $config{'charset'}); + print STDERR 'Recode: '.$convert->getError."\n" if $convert->getError; + $convert->recode ($data) or print STDERR 'Recode: '.$convert->getError."\n"; + #D printf " >>> %12s %s \n", $encoding, $data; + }; + $decoded .= $data; + }; + return $decoded; +}; + +sub writedata { + # --> Perl-Kochbuch, Rezept 11.10 + # Datendatei (Header) schreiben + # IN: $datei - Name der Datendatei + # Referenz auf @postings + # OUT: --- + my $datei = shift; + my $postingr = shift; + my @postings = @{$postingr}; + open DATEI, ">$datei" || print STDERR "ERROR: Could not open file $datei for writing: $!"; + foreach $posting (@postings) { + for $header (sort keys %{$posting}) { + print DATEI "$header: $posting->{$header}\n"; + } + print DATEI "\n"; + }; + close DATEI; + return; +}; + +sub readdata { + # --> Perl-Kochbuch, Rezept 11.10 + # Datendatei (Header) einlesen + # IN: $datei - Name der Datendatei + # OUT: Referenz auf @postings, das jeweils + # Referenzen auf %header (die Header) enthält + my $datei = shift; + open DATEI, "<$datei" || print STDERR "ERROR: Could not open file $datei for reading: $!"; + $/ = ''; + while () { + my @fields = split /^([^:]+):\s*/m; + shift @fields; + push(@postings, {map/(.*)/, @fields}); + }; + close DATEI; + return \@postings; +}; \ No newline at end of file diff --git a/usestats.readme b/usestats.readme new file mode 100644 index 0000000..cfd7cd8 --- /dev/null +++ b/usestats.readme @@ -0,0 +1,318 @@ +NAME + usestats - newsgroup statistic generator + +SYNOPSIS + usestats.pl [-hq] [-c ] [-r|w ] + +DESCRIPTION + usestats erzeugt Statistiken über Newsgroups, indem vorhandene + Postings analysiert und daraus verschiedene Reports erstellt werden. + + Argumente: + + -h + Mit dem Argument -h wird eine kurze Hilfe ausgegeben. + + -q + Mit dem Argument -q werden alle Statusausgaben auf STDERR + unterdrückt. + + -c + Mit dem Argument -c, gefolgt nach Leerzeichen von einem + Dateinamen, kann eine Konfigurationsdatei übergeben werden. + + Beispiel: usestats.pl -c usestats.cfg + + -w + Mit dem Argument -w, gefolgt nach Leerzeichen von einem + Dateinamen, können die Daten der eingelesenen Postings + gespeichert werden. Spätere Durchläufe von usestats.pl + können dann auf diese gespeicherten Daten zurückgreifen. + + Beispiel: usestats.pl -w de-admin-misc.txt + + -r + Mit dem Argument -r, gefolgt nach Leerzeichen von einem + Dateinamen, können gespeicherte Daten zuvor eingelesener + Postings aus einem früheren Durchlauf von usestats.pl + wieder geladen werden. + + Beispiel: usestats.pl -r de-admin-misc.txt + + Eingabeformate: + + usestats verarbeitet verschiedene Eingabeformate: + + (1) MBOX-Datei + + Die Postings müssen in diesem Fall als Unix-MBOX-Datei vorliegen, + also fortlaufen, getrennt jeweils durch eine Zeile der Form "From + @ :: + ". In dieses Format können etliche Clients die Postings + exportieren; abgesehen davon gibt es eine ganze Reihe + Konvertierungsprogramme, die MBOX-Dateien ausgeben. + + Die MBOX-Datei wird einfach in usestats gepiped: + + usestats.pl < mypostings.txt + usestats.pl -cusestats.cfg < mypostings.txt + + (2) data.dat-Datei (Hamster) + + Eine in dem Mail-und Newsserver "Hamster" fuer Windows-Systeme + vorliegende Newsgroup kann auf dieselbe Weise in usestats + gepiped werden. Zu diesem Zweck wird die entsprechende + data.dat-Datei, die die Postings enthält, aus dem jeweiligen + Spoolverzeichnis (...\Groups\news.grup.pe\data.dat) in + usestats gepiped: + + usestats.pl -cusestats.cfg < Groups\hamster.de.talk\data.dat + + Zu diesem Zweck muss das zu parsende Format in einer + Konfigurationsdatei auf "hamster" umgestellt werden. + + (3) Spoolverzeichnis + + usestats kann auch ein Spoolverzeichnis verarbeiten, in dem jedes + Postings als einzelne Datei vorliegt, also bspw. im "traditional + spool" von INN. Für diese Art der Verarbeitung muss in der + Konfigurationsdatei die Variable "spooldir" gesetzt sein. + + Beispiel: /var/spool/news/de/test + + Reporte: + + (1) Postings pro Tag + + Dieser Report gibt aus, wie viele Postings pro Tag angefallen + sind, aufsummiert nach Monaten. + + (2) Newsgroupnutzung + + Dieser Report gibt an, in wecher Newsgroup im Statistikzeitraum + wie viele Postings abgesetzt wurden. Crossposts in mehrere Gruppen + zugleich werden in jeder Gruppe als einzelnes Posting gezählt. + + (3) Poster (unstrukturiert) + + Dieser Report gibt an, wer im Statistikzeitraum wie viele + Postings abgesetzt hat. + + (4) Poster (strukturiert) + + Für diesen Report werden alle Poster mit identischem Realnamen + zusammengefasst, unabhängig von ggf. wechselnden Mailadressen. + + (5) Subjects + + Dieser Report fasst zusammen, wie viele Postings zum jeweiligen + Subject angefallen sind. + + (6) Newsreader + + Schließlich werden die verwendeten Newsreader statistisch + erfassrt. + + (7) Nutzer pro Newsreader + + Auch der Nutzeranteil pro Newsreader kann ausgegeben werden. + + Konfigurationsdatei / Variablen: + + Die Konfigurationsdatei besteht aus Zuweisungen von Werten zu + Parametern in der Form + + parameter = wert + + Mit "#" eingeleitete Zeilen sind Kommentare. + + Es können folgende Parameter gesetzt werden: + + spooldir: + Das Spoolverzeichnis, falls die Postings von dort gelesen werden + sollen. "spooldir" darf nicht gesetzt sein, wenn man eine MBOX in + usestats pipen möchte. + + Default: + + recursive: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + entscheidet darüber, ob das Spoolverzeichnis rekursiv verarbeitet + werden soll, d.h. alle darunterliegenden Unterverzeichnisse auch. + Ermöglicht die Verarbeitung nicht nur einzelner Newsgroups, + sondern auch ganzer Hierarchien oder des kompletten Newsspools des + Servers. + + Noch nicht intensiv getestet, Fehler bitte mitteilen! + + Default: 0 + + pipeformat: + Das Format der Datei, die in usestats gepiped wird. + Mögliche Werte sind 'mbox' fuer eine Datei in MBOX-Format oder + 'hamster' fuer eine .dat-Datei des Hamsters. + + Default: mbox + + charset + Der Zeichensatz, in dem die Ausgabe erfolgen soll. In diesen + Zeichensatz werden From:- und Subject:-Header für die Ausgabe + konvertiert. (Die Ausgaben des Programms selbst umfassen nur + 7bit-ASCII-Zeichen.) + + Default: ISO-8859-1 + + tz: + Die lokale Zeitzone. Empfohlen wird die Angabe als Abweichung von + GMT, also bspw. "+0200". + + Default: +0200 + + newsgroup: + Wenn dieser Parameter angegeben wird, werden nur Postings + erfasst, die -auch- in diese Newsgroup gegangen sind. + + Default: + + start: + Bei Angabe dieses Parameters werden nur Postings nach diesem + Datum erfasst. Die Angabe sollte im ISO-Format erfolgen, also + bspw. "2003-10-01" für den 1. Oktober 2003. Die Verwendung von + "start"/"stop" verlängert die Programmlaufzeit. + + Default: + + stop: + Bei Angabe dieses Parameters werden nur Postings vor diesem Datum + erfasst. Die Angabe sollte im ISO-Format erfolgen, also bspw. + "2003-10-31" für den 31. Oktober 2003. Die Verwendung von + "start"/"stop" verlängert die Programmlaufzeit. + + Default: + + day: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + darüber, ob der Report "Postings pro Tag" ausgegeben werden soll. + + Default: 1 + + newsgroups: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + darüber, ob der Report "Newsgroupnutzung" ausgegeben werden + soll. + + Default: 0 + + poster: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + darüber, ob der Report "Poster (strukturiert)" ausgegeben werden + soll. + + Default: 1 + + posterraw: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + darüber, ob der Report "Poster (unstrukturiert)" ausgegeben + werden soll. + + Default: 0 + + subject: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + darüber, ob der Report "Subjects (strukturiert)" ausgegeben + werden soll. + + Default: 1 + + newsreader: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + darüber, ob der Report "Newsreader (strukturiert)" ausgegeben + werden soll. + + Default: 1 + + nruser: + Dieser Parameter kann den Wert 0 oder 1 annehmen und entscheidet + darüber, ob der Report "Nutzer pro Newsreader (strukturiert)" + ausgegeben werden soll. + + Default: 1 + + unknownreader: + Dieser Parameter kann den Wert 'show' oder 'noshow' annehmen und + entscheidet darüber, ob unbekannte Newsreader mit dem vollen + Eintrag der Headerzeile User-Agent/X-Newsreader/X-Mailer + angezeigt oder unter "Sonstige" summiert werden sollen. + + Default: show + + poster_width: + Dieser Parameter gibt die Spaltenbreite an, die maximal für die + Namen der Poster zur Verfügung steht. Analog kann der Wert auch + für die anderen Reports - ausser day - gesetzt werden. + Der Wert sollte entsprechend der Angabe von poster_graph und der + erwünschten maximalen Zeilenlänge gewählt werden. + + Default: 30 + + poster_indent: + Dieser Parameter gibt die Einrückung der Tabelle an. Analog kann + der Wert auch für die anderen Reports - ausser day - gesetzt + werden. + + Default: 2 + + poster_lines: + Dieser Parameter gibt an, wie viele Zeilen die Tabelle haben + soll, ob also nur die häufigsten Poster genannt werden sollen. 0 + gibt die komplette Tabelle aus. Analog kann der Wert auch für die + anderen Reports - ausser day - gesetzt werden. + + Default: 0 + + poster_cutoff: + Dieser Parameter gibt an, wie viele Postings jemand mindestens + erreicht haben muss, um in die Tabelle aufgenommen zu werden. 0 + gibt die komplette Tabelle aus. Analog kann der Wert auch für die + anderen Reports - ausser day - gesetzt werden. + + Default: 0 + + poster_graph: + Dieser Parameter gibt an, wie breit die "grafische" Darstellung + der Prozentzahlen werden darf. 0 schaltet die Balkengrafik aus. + Analog kann der Wert auch für die anderen Reports - ausser day - + gesetzt werden. + + Default: 20 + + graphchar: + Das Zeichen, das für die Balkengraphik verwendet werden soll. + + Default: # + +DEPENDANCIES + Die folgenden CPAN-Module werden neben Perl 5.6.1 oder höher benötigt: + + Locale::Recode (und die übrigen Module aus diesem Paket) + Date::Manip + Mail::Address + MIME::Words + +BUGS + - Fehler und Fehleingaben werden größtenteils nicht abgefangen. + + Weitere Bugs nimmt gerne entgegen. + +TODO + - HMTL-Templates als weitere Ausgabemöglichkeit sind angedacht. + +AUTHOR + Thomas Hochstein + +VERSION + V 0.17 [beta] + +COPYRIGHT + © 10/2003-10/2004 Thomas Hochstein. + See source for license und warranty. -- 2.20.1