
Tengo un archivo de texto con el siguiente tipo de contenido:
OPERATION_CONTEXT VMD1HTE1A71_ns:.oc.GJ_OAD2 alarm_object 1130 On director: VMD1HTE1A71_ns:.temip.VMD1HTE1A71_director AT Fri 18 Oct 2013 06:56:39 All Attributes
Identifier = 1130
State = Terminated
Problem Status = Closed
Clearance Report Flag = True
Escalated Alarm = False
Close User Identifier = "Auto-Clear"
Termination User Identifier = "Auto-Clear"
Close Time Stamp = Fri 18 Oct 2013 05:01:46
Termination Time Stamp = Fri 18 Oct 2013 05:01:46
Creation Timestamp = Fri 18 Oct 2013 04:37:29
Clearance Time Stamp = Fri 18 Oct 2013 05:01:40
Last Modification Timestamp = Fri 18 Oct 2013 05:01:46
Previous State = Outstanding
Managed Object = Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD "AMS" Node "INGJJMGRJMTSNB0001AG2OLT001"
Target Entities = { Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD "AMS" Node "INGJJMGRJMTSNB0001AG2OLT001" }
Alarm Type = CommunicationsAlarm
Event Time = Fri 18 Oct 2013 05:01:40
Probable Cause = Unknown
Specific Problems = { }
Notification Identifier = 160315
Domain = Domain VMD1HTE1A71_ns:.dm.GJ_OAD2
Alarm Origin = IncomingAlarm
Perceived Severity = Major
Additional Text = "
nativeProbableCause: Attempt Threshold Crossed
osTime: 20131018163727.250+0530
neTime: 20131011174021.0+0530
notificationId: AMS:160315
portNumber:
ftpNumber:
meNm: INGJJMGRJMTSNB0001AG2OLT001
mdNm: AMS
objectType: OT_MANAGED_ELEMENT
aliasValue: MGMT Security
Access:INGJJMGRJMTSNB0001AG2OLT001:IP10.70.6.6.T0.S841 "
Original Severity = Major
Original Event Time = Fri 11 Oct 2013 05:40:21
Quiero crear un archivo CSV a partir de este archivo de texto con encabezados de columna como los valores antes de = en cada línea como Identificador, Estado, Estado del problema y todas las líneas siguientes que contienen los valores debajo de cada encabezado de columna como 1130, Terminado, Cerrado, etc. Aparte de las líneas con "=" no quiero que se extraiga nada más en el archivo CSV.
Otra complicación que enfrenté en esto es que algunos campos tienen un carácter de nueva línea como Texto adicional. Quiero obtener todo el valor de Texto adicional en una columna en Columna de texto adicional.
Como soy nuevo en Linux/Unix, no puedo encontrar una manera de hacer esto. ¿Cuál es la mejor manera de hacer esto?
Respuesta1
Bueno, si siempre tienes la misma cantidad de campos por registro y no tienes nada entre los registros (suposiciones que hago en base a tu publicación y que pueden ser correctas o no), puedes seguir la ruta awk. Esto preservará el orden de las columnas y las nuevas líneas incrustadas. Supongamos que lo siguiente está en parse.awk
:
BEGIN {
RS = "( = |\n\\s+)";
isHeader = 0;
Sep = "\",\"";
Q = "\"";
# WinEOL = "\r"; # enable this if your CSV will be used on Windows
Headers = Fields = Q;
}
function sanitise (Entry) {
gsub(/(^[ "]*|[" \n]*$)/, "", Entry); # Trim leading/trailing double quotes and white space
gsub(/"/, "\"\"", Entry); # Escape double quotes
return Entry;
}
function addField (Field) {
Fields = Fields FieldsSep sanitise(Field);
isHeader = 1;
FieldsSep = Sep;
FieldCounter++
}
function addHeader (Header) {
Headers = Headers HeadersSep sanitise($0);
isHeader = 0;
HeadersSep = Sep;
}
1 == NR { # Special case of first header
addHeader($1);
next;
}
$0 == "\"" { # Fields with newlines
LongField = $0;
LongFieldSep = "";
while (getline > 0) {
LongField = LongField LongFieldSep $0;
LongFieldSep = "\n";
if ($NF ~ /"$/) {
addField(LongField);
next;
}
}
}
{
if (isHeader) {
addHeader($0);
}
else {
addField($0);
}
if (FieldsPerRecord == FieldCounter) {
if (!HeadersPrinted) {
print Headers Q WinEOL;
HeadersPrinted = 1
}
print Fields Q WinEOL;
Fields = FieldsSep = "";
FieldCounter = 0
}
}
Entonces podrías invocarlo con FieldsPerRecord
set en la línea de comando:
$ awk -v FieldsPerRecord=26 -f parse.awk data.csv
Lo que producirá los siguientes datos codificados en CSV que LibreOffice Calc parece aceptar sin problemas:
"Identifier","State","Problem Status","Clearance Report Flag","Escalated Alarm","Close User Identifier","Termination User Identifier","Close Time Stamp","Termination Time Stamp","Creation Timestamp","Clearance Time Stamp","Last Modification Timestamp","Previous State","Managed Object","Target Entities","Alarm Type","Event Time","Probable Cause","Specific Problems","Notification Identifier","Domain","Alarm Origin","Perceived Severity","Additional Text","Original Severity","Original Event Time"
"1130","Terminated","Closed","True","False","Auto-Clear","Auto-Clear","Fri 18 Oct 2013 05:01:46","Fri 18 Oct 2013 05:01:46","Fri 18 Oct 2013 04:37:29","Fri 18 Oct 2013 05:01:40","Fri 18 Oct 2013 05:01:46","Outstanding","Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD ""AMS"" Node ""INGJJMGRJMTSNB0001AG2OLT001","{ Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD ""AMS"" Node ""INGJJMGRJMTSNB0001AG2OLT001"" }","CommunicationsAlarm","Fri 18 Oct 2013 05:01:40","Unknown","{ }","160315","Domain VMD1HTE1A71_ns:.dm.GJ_OAD2","IncomingAlarm","Major","nativeProbableCause: Attempt Threshold Crossed
osTime: 20131018163727.250+0530
neTime: 20131011174021.0+0530
notificationId: AMS:160315
portNumber:
ftpNumber:
meNm: INGJJMGRJMTSNB0001AG2OLT001
mdNm: AMS
objectType: OT_MANAGED_ELEMENT
aliasValue: MGMT Security
Access:INGJJMGRJMTSNB0001AG2OLT001:IP10.70.6.6.T0.S841","Major","Fri 11 Oct 2013 05:40:21"
Tenga en cuenta que tomo elcita todoenfoque que, al menos para mí, produce menos sorpresas al importar, pero puedes desactivarlo configurando Q = ""
y Sep = ","
las dos gsub()
líneas en sanitise()
.
sin embargo, InoCreo que esto es un problema de expresiones regulares. Estos datos son de ancho fijo, por lo que parece quePerlunpack
es probablemente el mejor enfoque a seguir. Nunca he podido entenderlo, pero esta podría ser una buena oportunidad para aprender si alguien quiere mostrarme una manera de hacerlo unpack
.
actualizar
No soy un Perl Hacker™, pero lo siguiente parece funcionar bien, no hace suposiciones sobre el contenido de los campos de varias líneas, conserva el orden de los campos y todo el espaciado original dentro de los campos (pero elimina el espacio inicial de los encabezados) y aparece como Perl. -esqueish para mis ojos inexpertos:
BEGIN{
our (@headers, @fields);
our $headers_printed = 0;
}
my ($header, $field) = unpack("A36x2A*", $_); # magic!
if ("" eq $header) { # Fields with newlines
$fields[$#fields] .= "\n" . $field;
next;
}
push(@headers, $header =~ s/^\s*//gr);
push(@fields, $field);
if (26 == $#headers + 1) { # Print complete record
printf "%s\n", join ",", @headers unless $headers_printed;
$headers_printed = 1;
printf "%s\n", join ",", @fields;
@fields = @headers = ();
}
Simplemente invoca con:
$ perl -nf /tmp/parse.pl /tmp/data.txt
Identifier,State,Problem Status,Clearance Report Flag,Escalated Alarm,Close User Identifier,Termination User Identifier,Close Time Stamp,Termination Time Stamp,Creation Timestamp,Clearance Time Stamp,Last Modification Timestamp,Previous State,Managed Object,Target Entities,Alarm Type,Event Time,Probable Cause,Specific Problems,Notification Identifier,Domain,Alarm Origin,Perceived Severity,Additional Text,Original Severity,Original Event Time
1130,Terminated,Closed,True,False,"Auto-Clear","Auto-Clear",Fri 18 Oct 2013 05:01:46,Fri 18 Oct 2013 05:01:46,Fri 18 Oct 2013 04:37:29,Fri 18 Oct 2013 05:01:40,Fri 18 Oct 2013 05:01:46,Outstanding,Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD "AMS" Node "INGJJMGRJMTSNB0001AG2OLT001",{ Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD "AMS" Node "INGJJMGRJMTSNB0001AG2OLT001" },CommunicationsAlarm,Fri 18 Oct 2013 05:01:40,Unknown,{ },160315,Domain VMD1HTE1A71_ns:.dm.GJ_OAD2,IncomingAlarm,Major,"
nativeProbableCause: Attempt Threshold Crossed
osTime: 20131018163727.250+0530
neTime: 20131011174021.0+0530
notificationId: AMS:160315
portNumber:
ftpNumber:
meNm: INGJJMGRJMTSNB0001AG2OLT001
mdNm: AMS
objectType: OT_MANAGED_ELEMENT
aliasValue: MGMT Security
Access:INGJJMGRJMTSNB0001AG2OLT001:IP10.70.6.6.T0.S841 ",Major,Fri 11 Oct 2013 05:40:21
Probablemente sería mejor usarlo Text::CSV
, pero estaba más interesado en aprender cómo unpack
funciona. Parece ser mucho más legible y robusto para datos de ancho fijo que las expresiones regulares.
Respuesta2
O puede utilizar las subrutinas de expresiones regulares de Perl:
my $grammar = qr!
( ?(DEFINE)
(?<Identifier> [^=\n]+ )
(?<Statement>
(?: # Begin alternation
" #Opening quotes
[^"]+? # Any non-quotes (including a new line)
" # Closing quotes
| [^\n]+ # Or a single line
) # End alternation
)
)
!x;
my $file = do { local $/; <> }; #Slurp file named on command line
my %columns;
while( $file =~
m{ ((?&Identifier))[\t ]*=[ \t]*((?&Statement)) $grammar}xgc )
{
my ($header,$value) = ($1,$2);
# Remove leading spaces and quote variable if it contains commas:
for($header,$value) { s/^\s+//mg; /,/ and s/^|$/"/g }
# Substitute \n with \\n to make multi-line values single-line:
for($value) { chomp; s/\n/\\n/g }
$columns{$header}=$value
}
print join "," => sort keys %columns; # Print column headers
print "\n";
print join "," => map { $columns{$_} } sort keys %columns; # Column content
print "\n";
Invocarlo así:
[user@host]$ /path/to/script.pl /path/to/file.txt
Imprimirá en la salida estándar la tabla con formato CSV.
Esto supone que una declaración de varias líneas no contendrá comillas dobles ( "
) excepto las de apertura y cierre.
Respuesta3
OK, esto no es bonito, pero hace lo que pides. Escribí un script en Perl que tomará el archivo anterior, lo analizará y luego usará el módulo Text::CSV
para convertirlo a formato CSV.
La secuencia de comandos
#!/usr/bin/env perl
use Text::CSV;
open(my $fh, "<data.txt");
@lines = <$fh>;
close ($fh);
my (%csv, $name, $val);
foreach my $line (@lines) {
if ($line =~ m/=/) {
chomp($line);
$line =~ s/^\s+//g;
($name, $val) = split(/ = /, $line);
$val =~ s/^"$//;
$csv{$name} = $val;
} else {
$line =~ s/^\s+//g;
$line =~ s/\s+$/\\n/g;
$line =~ s/ "\\n$//;
$csv{$name} .= $line;
}
}
my @vals;
foreach my $i (sort keys %csv) {
push(@vals, $csv{$i});
}
my $ccsv = Text::CSV->new();
$ccsv->combine(sort keys %csv);
$ccsv->parse($ccsv->string());
print $ccsv->string() . "\n";
$ccsv->combine(@vals);
$ccsv->parse($ccsv->string());
print $ccsv->string() . "\n";
Ejemplo
Simplemente ejecútelo así:
$ ./csv.pl
"Additional Text","Alarm Origin","Alarm Type","Clearance Time Stamp","Close Time Stamp","Creation Timestamp",Domain,"Event Time","Last Modification Timestamp","Managed Object","Notification Identifier","Original Event Time","Original Severity","Perceived Severity","Previous State","Probable Cause","Specific Problems","Target Entities","Termination Time Stamp"
"nativeProbableCause: Attempt Threshold Crossed\nosTime: 20131018163727.250+0530\nneTime: 20131011174021.0+0530\nnotificationId: AMS:160315\nportNumber:\nftpNumber:\nmeNm: INGJJMGRJMTSNB0001AG2OLT001\nmdNm: AMS\nobjectType: OT_MANAGED_ELEMENT\naliasValue: MGMT Security\nAccess:INGJJMGRJMTSNB0001AG2OLT001:IP10.70.6.6.T0.S841",IncomingAlarm,CommunicationsAlarm,"Fri 18 Oct 2013 05:01:40","Fri 18 Oct 2013 05:01:46","Fri 18 Oct 2013 04:37:29","Domain VMD1HTE1A71_ns:.dm.GJ_OAD2","Fri 18 Oct 2013 05:01:40","Fri 18 Oct 2013 05:01:46","Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD ""AMS"" Node ""INGJJMGRJMTSNB0001AG2OLT001""",160315,"Fri 11 Oct 2013 05:40:21",Major,Major,Outstanding,Unknown,"{ }","{ Alcatel_5529OAD VMD1HTE1A71_ns:.OAD2 MD ""AMS"" Node ""INGJJMGRJMTSNB0001AG2OLT001"" }","Fri 18 Oct 2013 05:01:46"
Déjame saber lo que piensas o si tienes problemas para ejecutarlo. Si hace lo que necesitas, completaré los detalles de cómo funciona.