perl - regex - több egymásutáni szóköz kitölrése szövegben, kivéve ha az zárójelben van

Sziasztok,
a problémám, hogy normalizálni szeretnék egy szövegfájlt, a sok felesleges szóközt ki törölni. Ahol több a szóköz mint egy, ott egy üres szóköz legyen. A szövegfájlban viszont vannak olyan szövegek, amik zárójelbe vannak, azon belül nem szabad normalizálnom.

Az eredeti szövegre példa:


field analysis_version           datatype packed_decimal      default  '         1'

ezzel:


$line =~ s/\s+/ /g;

a következőt kapom:


field analysis_version datatype packed_decimal default  ' 1'

Amit viszont én szeretnék, az ez lenne:


field analysis_version datatype packed_decimal default '         1'

Már egy fél napot elidőztem vele, de eddig kollegák közül se tudott senki segíteni.

Valaki tudja a megoldást? Köszönöm a segítséget előre is.

Szerk: bár nem jelzi a példa, de az idézöjel utáni szövegnek is normalizálódni kellene, ha van mögötte.
Köszi/bocsi

Hozzászólások

phpban tudtam, de forgasd át. ez így gagyi?

$text = "field analysis_version datatype packed_decimal default ' 1'";
$text = preg_replace("/(\w)(\s+)(\w)/","$1 $3",$text);

Nincs energiám kipróbálni, de ilyesmi?

while( $file_file_content =~ s/([^']?+)('[^']?+')([^']?+)//gm )
{
my $pre=$1;
my $post=$3;
my $middle=$2;

$pre =~ s/\s+/ /g;
$post =~ s/\s+/ /g;

$result=$result.$pre.$middle.$post;
}

Én is lusta vagyok ellenőrizni :) , de így ránézésre furcsának tűnik. Egyrészt minek a substitution (s) mikor a match (m) is elég lenne (hiszen a lényeg csak a group-ok feltöltése), másrészt a ?+ is gyanúsnak tűnik - nem ++ vagy *+ akart az lenni?

(Persze ha lehet többfelé is aposztróf, akkor az egész bukik).

Nos akkor 2. nekifutás:

while( $file_content =~ s/([^']+?)('[^']*?')//sm )
{
my $pre=$1;
my $middle=$2;

$pre =~ s/\s+/ /g;

$result=$result.$pre.$middle;
}

A /s az elején azér kell, hogy 2x ne találjuk meg ugyan azt a részt.
A +? a "non greedy match" azaz nem a lehető leghoszabb részre illik, hanem a legkisebbre.

A cucc feltételezi, hogy a teljes inputot előzőleg beolvastuk $file_content -be. Az eredményt meg ki kell írni a végén valahová. Ha a bemenetben soronkénti rekordok vannak, akkor ezen lehet finomítani.
Szerintem ez alapján el lehet indulni.

Á, szóval minimal match akart lenni. Közben meg észre sem vettem, hogy az egész fájlt dolgozod fel egyszerre, így már világos miért alkalmaztad a substitution-t.

Viszont a minimal match-et még nem értem. Szerintem egyrészt itt lerontja itt a műveletigényt, hiszen először a subexpression a +? esetén egy karakterre illeszkedik, a +* pedig nullára, majd illesztené a '-t; ez nem sikerül, ezért mindkét részkifejezésre többször vissza kell lépnie. Ráadásul végeredmény szempontjából itt a minimal, a greedy és a possessive match ugyanazt adja, de pont csak a greedy és a possessive adja az opimális műveletigényt.

Ha feltesszük, hogy pont olyan minden sor, ahogy a példában szerepel, akkor lehet sokkal specifikusabban is feldolgozni, úgy hogy közben az eredeti fájl-változó értéke is megmarad:


my $regex = qr/^([^']*+)('[^']*+')\h*(\n?)/m;
my $result = "";
pos($file_content) = undef;
while ($file_content =~ m/$regex/g)
{
  my $normalized = $1;
  my $verbatim = $2;
  my $nl = $3;
  $normalized =~ s/\s+/ /g;
  $result .= $normalized . $verbatim . $nl;
}

(A te megoldásod viszont sokkal általánosabb).

És ez hogy hangzik (nem tudom kipróbálni, lehet hogy nem is fut)?


my $regex = qr/(?:([^']++)|('))/xs;
my $result = "";
pos($file_content) = undef;
my $in_quote = 0;
while ($file_content =~ m/$regex/g)
{
  my $fragment;
  if (defined $1)
  {
    $fragment = $1;
    $fragment =~ s/\h+/ /g unless ($in_quote);
  }
  else
  {
    $fragment = $2;
    $in_quote = 1 - $in_quote;
  }
  $result .= $fragment;
}

(előző bejegyzés törölve)

Szerk:

Most vettem észre, hogy ezen a honlapon lehet kódot futtatni. :)

Szóval az a helyzet, hogy a


  print $];

szerint ez 5.008-as Perl, ami nagyon régi. Ez még nem ismeri sem a '++'-t, sem a '\h'-t. Ennek megfelelően a következőket javaslom:

- A '++'-t '+'-szal, a '\h'-t '[\t ]'-vel helyettesítsd (utóbbiban van egy szóköz is).

- Használd a


  use strict;
  use warnings;

utasításokat a program elején, akkor megmondja az interpreter részletesen, hogy mi baja van. (Pl. a $file_content helyett $filecontent-et írtál).

Forkoltam a kódot itt.

Egy lehetséges megoldás, amit holnap reggel szerintem le fogok tagadni:

perl -F/\'/ -lane 'for $i (0..$#F){$F[$i]=~s/ +/ /g if !($i%2)} push @F,"" if $#F%2;print join chr(39), @F' szovegfajl.txt


#include <stdio.h>
#include <stdlib.h>

int
main(void)
{
  int quote,
      prev,
      c;

  quote = 0;
  prev = -1;

  while (EOF != (c = fgetc(stdin))) {
    switch (c) {
      case '\'':
        quote = !quote;
        break;

      case ' ':
        if (' ' == prev && !quote) {
          continue;
        }
    }

    if (EOF == fputc(c, stdout)) {
      perror("fputc(stdout)");
      return EXIT_FAILURE;
    }

    prev = c;
  }

  if (ferror(stdin)) {
    perror("fgetc(stdout)");
    return EXIT_FAILURE;
  }

  if (EOF == fflush(stdout)) {
    perror("fflush(stdout)");
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}

Nem foglalkozik különösebben újsor karakterrel; ha nincs lezárva az utolsó idézet a sor vége előtt, akkor az újsor karakter is az idézet része lesz (meg a következő sorok is, amíg el nem érünk egy idézőjelet). Ha ezt nem kívánjuk (vagyis az újsor zárja le a szökevény idézetet), akkor ennyi kell még bele:


--- squeeze.c	2012-05-09 22:39:26.000000000 +0200
+++ squeeze-nl.c	2012-05-09 22:42:32.000000000 +0200
@@ -17,6 +17,10 @@
         quote = !quote;
         break;
 
+      case '\n':
+        quote = 0;
+        break;
+
       case ' ':
         if (' ' == prev && !quote) {
           continue;

Igen, ez egy kiváló példa arra, hogy nem feltétlenül jó ágyúval (regex) verébre lőni. De ha már mindenképpen ágyú, akkor van egy Perles megfejtésem: Fel kell bontani a stringet idézőjelek mentén (split) és minden második tömbelemen kell csak elvégezni a szóközök átalakítását. Aztán összerakni (join) újból az egészet.

Ez egy zseniális ötlet. Csak az a gond, hogy a split-nek van egy furcsa tulajdonsága: ha a fájl elején vannak elválasztójelek, akkor rendben létrejönnek a nekik megfelelő üres tömbelemek; viszont ha a fájl végén vannak, akkor azokat simán eldobja. Emiatt a join-nál el fognak veszni aposztrófok. Nem tudom miért ilyen, lehet hogy bug.

Kicsit belenyúltam a kódodba: a beolvasott sort kiírom < és > jelek között, az eredményt [ és ] jelek között.

Lássunk két példafájlt:

1.) f végén van újsor:


jack@hypnos:~/Asztal$ cat f
''   sfdsfd     f 's   x'''
jack@hypnos:~/Asztal$

2.) g végén nincs:


jack@hypnos:~/Asztal$ cat f | tr -d '\n' >g
jack@hypnos:~/Asztal$ cat g
''   sfdsfd     f 's   x'''jack@hypnos:~/Asztal$

Futtassuk a kódot:

1.) f-re működik:


jack@hypnos:~/Asztal$ perl -w proba.pl f
<''   sfdsfd     f 's   x'''
>['' sfdsfd f 's   x'''
]

2.) g-re nem:


jack@hypnos:~/Asztal$ perl -w proba.pl g
<''   sfdsfd     f 's   x'''>['' sfdsfd f 's   x]
jack@hypnos:~/Asztal$

Neked azért működött, mert feltehetően a standard inputról adtál neki bemenetet. Ilyenkor a string végén ott a \n, tehát az aposztóf nem az utolsó karakter.

Egy kicsit pontositsuk: nem a standard input volt a problema gyokere, hanem hogy valoszinuleg echo-zta az inputot, vagy szovegszerkesztoben allitotta ossze, amely automatikusan rak egy ujsort is a fajl vegere.

Ez azert van igy, mert ha megnezed, a `cat g | perl -w proba.pl` ugyanugy a standard inputrol dolgozik, viszont ugyanazt a kimenetet fogja adni, mint a 2). A standard input lete sehol nem implikalja a fajlvegi sortorest.
--

Ki oda vagyik, hol szall a galamb, elszalasztja a kincset itt alant. | Gentoo Portal 

megoldás: Ctrl-D

Bővebben: EOF -- Special character on input, which is recognized if the ICANON flag is set. When received, all the bytes waiting to be read are immediately passed to the process without waiting for a <newline>, and the EOF is discarded. Thus, if there are no bytes waiting (that is, the EOF occurred at the beginning of a line), a byte count of zero shall be returned from the read(), representing an end-of-file indication. If ICANON is set, the EOF character shall be discarded when processed.

Hm, valóban a Vim odatett egy \n-t, pedig kérte a fene. Egyébként sosem értettem, miért olyan fontos a \n? Régi fordítók warningoltak is ilyesmire, de nem értem az okát.

És azt sem értem, hogy miért viselkedik így a Perl. Na mindegy, minden sor végére oda lehet kézzel pöttyinteni a soremelést:

while(<>) {
  tr/\n\r//d; # https://en.wikipedia.org/wiki/Newline
  $_ .= "\n";
...

Egyébként sosem értettem, miért olyan fontos a \n?

A történelmi okot nem tudom megadni.

Az ISO C99 7.19.2 p2 szerint A text stream is an ordered sequence of characters composed into lines, each line consisting of zero or more characters plus a terminating new-line character. Whether the last line requires a terminating new-line character is implementation-defined. [...]

A SUSv4 ugyanakkor szigorúbb:

Line: A sequence of zero or more non-<newline> characters plus a terminating <newline> character.

Incomplete Line: A sequence of one or more non-<newline> characters at the end of the file.

Text File: A file that contains characters organized into zero or more lines. The lines do not contain NUL characters and none can exceed {LINE_MAX} bytes in length, including the <newline> character. Although POSIX.1-2008 does not distinguish between text files and binary files (see the ISO C standard), many utilities only produce predictable or meaningful output when operating on text files. The standard utilities that have such restrictions always specify "text files" in their STDIN or INPUT FILES sections.

A Rationale ezt mondja: [...] The only difference between text and binary files is that text files have lines of less than {LINE_MAX} bytes, with no NUL characters, each terminated by a <newline>. The definition allows a file with a single <newline>, or a totally empty file, to be called a text file. If a file ends with an incomplete line it is not strictly a text file by this definition. [...]

A LINE_MAX-ról bővebben itt.