Initial commit.

Signed-off-by: Thomas Hochstein <thh@inter.net>
This commit is contained in:
Thomas Hochstein 2010-01-15 08:55:38 +01:00
commit 2ea2972072
4 changed files with 1181 additions and 0 deletions

115
usestats.cfg Normal file
View file

@ -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.

40
usestats.changes Normal file
View file

@ -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

708
usestats.pl Normal file
View file

@ -0,0 +1,708 @@
#!/usr/bin/perl
#
# usestats.pl
#############
# (c) 10/2003-10/2004 Thomas Hochstein <thh@inter.net>
#
# 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 <savefile>] [-c <configfile>]\n";
exit(0);
};
print STDERR "$0 v $ver [" . scalar(gmtime) . "]\n(c) 10/2003-10/2004 Thomas Hochstein * <thh\@inter.net>\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 * <thh\@inter.net>\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(<POSTING>) {
# 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(<CONFIG>) {
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 (<DATEI>) {
my @fields = split /^([^:]+):\s*/m;
shift @fields;
push(@postings, {map/(.*)/, @fields});
};
close DATEI;
return \@postings;
};

318
usestats.readme Normal file
View file

@ -0,0 +1,318 @@
NAME
usestats - newsgroup statistic generator
SYNOPSIS
usestats.pl [-hq] [-c <CONFFILE>] [-r|w <SAVEFILE>]
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
<user>@<domain> <dow> <mon> <day> <hour>:<minute>:<second>
<year>". 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: <undef>
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: <undef>
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: <undef>
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: <undef>
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 <thh@inter.net> gerne entgegen.
TODO
- HMTL-Templates als weitere Ausgabemöglichkeit sind angedacht.
AUTHOR
Thomas Hochstein <thh@inter.net>
VERSION
V 0.17 [beta]
COPYRIGHT
© 10/2003-10/2004 Thomas Hochstein.
See source for license und warranty.