2013-07-17 25 views
7

Sto provando a sostituire 600 stringhe diverse in un file di testo molto grande 30Mb +. Sto costruendo una sceneggiatura che fa questo; seguendo questo Question:Efficienza di sostituzione stringa multipla PowerShell

Script:

$string = gc $filePath 
$string | % { 
    $_ -replace 'something0','somethingelse0' ` 
     -replace 'something1','somethingelse1' ` 
     -replace 'something2','somethingelse2' ` 
     -replace 'something3','somethingelse3' ` 
     -replace 'something4','somethingelse4' ` 
     -replace 'something5','somethingelse5' ` 
     ... 
     (600 More Lines...) 
     ... 
} 
$string | ac "C:\log.txt" 

Ma poiché questo controllerà ogni linea 600 volte e ci sono ben più di oltre 150.000 membri righe nel file di testo questo significa che c'è un sacco di tempo di elaborazione.

Esiste un'alternativa migliore per fare ciò che è più efficiente?

Qualsiasi consiglio su questo sarebbe apprezzato, Saluti.

risposta

4

Quindi, quello che stai dicendo è che vuoi sostituire una qualsiasi di 600 stringhe in ciascuna delle 150.000 linee e vuoi eseguire un'operazione di sostituzione per riga?

Sì, c'è un modo per farlo, ma non in PowerShell, almeno non riesco a pensarne uno. Può essere fatto in Perl.


Il Metodo:

  1. costruire un hash in cui le chiavi sono i quarantina ed i valori sono le somethingelses.
  2. Unire le chiavi dell'hash con il | Simbolo e usarlo come gruppo di corrispondenza nella regex.
  3. In sostituzione, interpolare un'espressione che recupera un valore dalla hash utilizzando la variabile partita per il gruppo Capture

Il problema:

Frustratingly, PowerShell non espone la partita le variabili al di fuori della regex sostituiscono la chiamata. Non funziona con l'operatore -replace e non funziona con [regex] :: replace.

In Perl, si può fare questo, per esempio:

$string =~ s/(1|2|3)/@{[$1 + 5]}/g; 

Questo aggiungerà 5 alle cifre 1, 2, e 3 per tutta la stringa, quindi se la stringa è "1.224.526,123 mila [2] [ 6] ", diventa" 6774576678 [7] [6] ".

Tuttavia, in PowerShell, entrambi questi falliscono:

$string -replace '(1|2|3)',"$($1 + 5)" 

[regex]::replace($string,'(1|2|3)',"$($1 + 5)") 

In entrambi i casi, $ 1 vale NULL, e l'espressione restituisce vecchi 5. Le variabili partita di pianura in sostituzioni sono significativi solo in la stringa risultante, vale a dire una stringa con quotatura singola o qualunque sia la stringa a virgolette valutata. Sono fondamentalmente solo backreferenze che assomigliano a variabili di corrispondenza. Certo, puoi citare il $ prima del numero in una stringa con doppia citazione, quindi valuterà il corrispondente gruppo di corrispondenza, ma ciò vanifica lo scopo - non può partecipare a un'espressione.


La soluzione:

[Questa risposta è stata modificata rispetto all'originale. È stato formattato per adattarsi alle stringhe di corrispondenza con i metacaratteri regex. . E il vostro schermo piatto, ovviamente]

Se si utilizza un'altra lingua è accettabile per voi, il seguente script Perl funziona come un fascino:

$filePath = $ARGV[0]; # Or hard-code it or whatever 
open INPUT, "< $filePath"; 
open OUTPUT, '> C:\log.txt'; 
%replacements = (
    'something0' => 'somethingelse0', 
    'something1' => 'somethingelse1', 
    'something2' => 'somethingelse2', 
    'something3' => 'somethingelse3', 
    'something4' => 'somethingelse4', 
    'something5' => 'somethingelse5', 
    'X:\Group_14\DACU' => '\\DACU$', 
    '.*[^xyz]' => 'oO{xyz}', 
    'moresomethings' => 'moresomethingelses' 
); 
foreach (keys %replacements) { 
    push @strings, qr/\Q$_\E/; 
    $replacements{$_} =~ s/\\/\\\\/g; 
} 
$pattern = join '|', @strings; 
while (<INPUT>) { 
    s/($pattern)/$replacements{$1}/g; 
    print OUTPUT; 
} 
close INPUT; 
close OUTPUT; 

Cerca le chiavi del cancelletto (a sinistra il =>) e li sostituisce con i valori corrispondenti. Ecco cosa sta succedendo:

  • Il foreach ciclo passa attraverso tutti gli elementi del hash e di creare un array chiamato @strings che contiene le chiavi dei% sostituzioni hash, con metacaratteri citato utilizzando \ Q e \ E e il risultato di quello indicato per l'uso come modello regex (qr = quote regex). Nello stesso passaggio, sfugge a tutti i backslash presenti nelle stringhe sostitutive raddoppiandoli.
  • Successivamente, gli elementi dell'array vengono uniti con | per formare il modello di ricerca. Potresti includere le parentesi di raggruppamento nel modello $ se lo desideri, ma penso che in questo modo sia più chiaro cosa sta succedendo.
  • Il ciclo mentre legge ogni riga dal file di input, sostituisce qualsiasi stringa nel modello di ricerca con le stringhe di sostituzione corrispondenti nell'hash e scrive la riga nel file di output.

BTW, potreste aver notato diverse altre modifiche dalla sceneggiatura originale. Il mio Perl ha raccolto un po 'di polvere durante il mio recente calcio PowerShell, e in un secondo sguardo ho notato diverse cose che potevano essere fatte meglio.

  • while (<INPUT>) legge il file una riga alla volta. Molto più ragionevole della lettura di tutte le 150.000 linee in un array, specialmente quando il tuo obiettivo è l'efficienza.
  • I semplificato @{[$replacements{$1}]} a $replacements{$1}. Perl non ha un modo incorporato di interpolazione di espressioni come PowerShell $(), quindi @ {[]} viene utilizzato come soluzione alternativa: crea un array letterale di un elemento contenente l'espressione. Ma mi sono reso conto che non è necessario se l'espressione è solo una singola variabile scalare (l'ho avuta lì come un residuo del mio test iniziale, dove stavo applicando i calcoli alla variabile di partita $ 1).
  • Le istruzioni close non sono strettamente necessarie, ma è buona norma chiudere i filehandle in modo esplicito.
  • Ho modificato per abbreviazione in foreach, per renderlo più chiaro e più familiare ai programmatori PowerShell.
+0

Grazie per la risposta, sono nuovo di Perl e ho cercato di implementare la soluzione, ma sono venuto su w con un problema con la sintassi. Voglio sostituire una stringa che assomigli a '' X: \ Group_14 \ DACU "' con la seguente stringa '" \\ DACU $ "'. Ho provato qualcosa del tipo ''X: \ Group_14 \ DACU' => '[\\ DACU $]'' Ma sembra che non funzioni, la mia sintassi è corretta? – Richard

+0

Ah, ho trascurato la gestione di personaggi speciali, come sciatta. Il problema è che il backslash è il carattere di escape in Perl (equivalente a un backtick in Powershell). Anche se hai citato una sola stringa, i problemi si verificano quando è interpolato in un'espressione regolare, in cui la barra indica varie sequenze di escape con significati speciali. Ad esempio, ** \ D ** indica qualcosa di diverso da una cifra e ** \ G ** indica l'offset dell'ultima corrispondenza globale sulla stringa (non preoccuparti di ciò ...). Modificheremo la risposta per citare le chiavi dell'hash da usare in un'espressione regolare. –

2

Inoltre non ho idea di come risolvere questo in PowerShell, ma so come risolverlo in Bash e cioè utilizzando uno strumento chiamato sed. Fortunatamente, c'è anche Sed for Windows. Se tutto quello che vogliamo fare è sostituire "qualcosa #" con "somethingelse #" in tutto il mondo, allora questo comando farà il trucco per voi

sed -i "s/something([0-9]+)/somethingelse\1/g" c:\log.txt 

In Bash si sarebbe in realtà bisogno di fuggire un paio di quei personaggi con backslash , ma non sono sicuro che sia necessario in Windows. Se il primo comando si lamenta si può provare

sed -i "s/something\([0-9]\+\)/somethingelse\1/g" c:\log.txt 
6

Combinando la tecnica di hash da Adi Inbar's answer, e la partita valutatore da Keith Hill's answer ad un altro recente interrogazione, ecco come è possibile eseguire la sostituzione in PowerShell:

# Build hashtable of search and replace values. 
$replacements = @{ 
    'something0' = 'somethingelse0' 
    'something1' = 'somethingelse1' 
    'something2' = 'somethingelse2' 
    'something3' = 'somethingelse3' 
    'something4' = 'somethingelse4' 
    'something5' = 'somethingelse5' 
    'X:\Group_14\DACU' = '\\DACU$' 
    '.*[^xyz]' = 'oO{xyz}' 
    'moresomethings' = 'moresomethingelses' 
} 

# Join all (escaped) keys from the hashtable into one regular expression. 
[regex]$r = @($replacements.Keys | foreach { [regex]::Escape($_) }) -join '|' 

[scriptblock]$matchEval = { param([Text.RegularExpressions.Match]$matchInfo) 
    # Return replacement value for each matched value. 
    $matchedValue = $matchInfo.Groups[0].Value 
    $replacements[$matchedValue] 
} 

# Perform replace over every line in the file and append to log. 
Get-Content $filePath | 
    foreach { $r.Replace($_, $matchEval) } | 
    Add-Content 'C:\log.txt' 
+0

Penso che sia necessario 'ForEach-Object' per le istruzioni 'foreach' nel blocco sopra. Inoltre, digitando $ matchEval si chiarirebbe cosa sta succedendo. – MonaLisaOverdrive

+0

@MonaLisaOverdrive 'foreach' è un alias per' ForEach-Object' (prova 'Get-Alias ​​foreach'). Ho aggiornato l'esempio per indicare che '$ matchEval' è un blocco di script. –

+0

Interessante ... e confuso ... circa l'alias 'foreach'. Grazie per la FYI. $ R.Replace funziona con $ matchEval come [scriptblock] quando prende [System.Text.RegularExpressions.MatchEvaluator] come secondo parametro? Anche se così fosse, quest'ultimo potrebbe essere più utile. – MonaLisaOverdrive

1

vorrei utilizzare l'istruzione switch PowerShell:

$string = gc $filePath 
$string | % { 
    switch -regex ($_) { 
     'something0' { 'somethingelse0' } 
     'something1' { 'somethingelse1' } 
     'something2' { 'somethingelse2' } 
     'something3' { 'somethingelse3' } 
     'something4' { 'somethingelse4' } 
     'something5' { 'somethingelse5' } 
     'pattern(?<a>\d+)' { $matches['a'] } # sample of more complex logic 
    ... 
    (600 More Lines...) 
    ... 
     default { $_ } 
    } 
} | ac "C:\log.txt" 
Problemi correlati