
次のような内容のテキスト ファイルがあります。
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
このテキスト ファイルから CSV ファイルを作成したいのですが、各行の = の前の値として列ヘッダー (識別子、状態、問題ステータスなど) が含まれ、次のすべての行には各列ヘッダーの下の値 (1130、終了、終了など) が含まれます。「=」の行以外は、CSV ファイルに何も抽出したくありません。
ここで私が直面したもう 1 つの複雑な問題は、一部のフィールドに「追加テキスト」のような改行文字があることです。「追加テキスト」のすべての値を「追加テキスト列」の下の 1 つの列で取得したいと考えています。
私は Linux/Unix を初めて使用するので、これを行う方法が見つかりません。これを行う最善の方法は何ですか?
答え1
そうですね、レコードごとに常に同じ数のフィールドがあり、レコード間に何もない場合 (これはあなたの投稿に基づいて私が立てた仮定ですが、正しいかどうかはわかりません)、awk ルートを利用できます。これにより、列の順序と埋め込まれた改行が保持されます。次の内容が にあると仮定します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
}
}
FieldsPerRecord
次に、コマンドラインで set を使用して呼び出すだけです。
$ awk -v FieldsPerRecord=26 -f parse.awk data.csv
これにより、LibreOffice Calc が問題なく受け入れると思われる次の CSV エンコード データが生成されます。
"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"
私がすべてを引用するこのアプローチは、少なくとも私にとっては、インポート時に驚くようなことが少なくなりますが、と の2行を設定することQ = ""
でこれを無効にすることができます。Sep = ","
gsub()
sanitise()
しかし、 私しないこれは正規表現の問題だと思います。これらのデータは固定幅なので、パールのunpack
おそらく、 が最善のアプローチでしょう。 私にはまだ理解できていませんが、 を使ってそれを実行する方法を誰かが示してくれるなら、これは私にとって学ぶ良い機会になるかもしれませんunpack
。
アップデート
私は Perl Hacker™ ではありませんが、次のコードはうまく機能しているようで、複数行フィールドの内容について何も想定しておらず、フィールドの順序とフィールド内の元のスペースがすべて保持され (ただし、ヘッダーから先頭のスペースは削除されます)、私の素人目には Perl 風に見えます。
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 = ();
}
次のように呼び出すだけです:
$ 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
を使用する方が良いかもしれませText::CSV
んが、私は がどのように機能するかを知りたいと思いましたunpack
。 固定幅データの場合、正規表現よりも読みやすく堅牢なようです。
答え2
または、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";
次のように呼び出します:
[user@host]$ /path/to/script.pl /path/to/file.txt
CSV形式の表を標準出力に出力します。
これは、複数行のステートメント"
には開始引用符と終了引用符を除いて二重引用符 ( ) が含まれないことを前提としています。
答え3
これはあまりきれいではありませんが、要求どおりです。私は Perl でスクリプトを書き、上記のファイルを取得して解析し、モジュールを使用してText::CSV
CSV 形式に変換しました。
スクリプト
#!/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";
例
次のように実行するだけです:
$ ./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"
ご意見や、実行中に問題が発生した場合にはお知らせください。必要な機能が動作する場合は、動作の詳細を記入します。