1
votes

Remplacer les guillemets dans la chaîne encapsulée par guillemets à l'aide d'expressions régulières Perl

J'essaie de remplacer les guillemets dans un fichier délimité par un tube et de citer un fichier encapsulé sans remplacer les guillemets fournissant l'encapsulation.

J'ai essayé d'utiliser la ligne Perl ci-dessous pour remplacer les guillemets par une coche arrière `mais je ne sais pas comment pour remplacer uniquement les guillemets et non le groupe entier 1.

Exemple de données (test.txt):

"1"|"Text"|"a"\n
"2"|"`Text in quotes`"|"ab"\n
"3"|"Text `around` quotes"|"abc"\n

Voici ce qui se passe: p>

"1"|"`"|"a"\n
"2"|"`"|"ab"\n
"3"|"`"|"abc"\n

Voici ce que j'essaye d'accomplir:

"1"|"Text"|"a"\n
"2"|""Text in quotes""|"ab"\n
"3"|"Text "around" quotes"|"abc"\n

perl -pi.bak -e 's/(?<=\|")(.*)(?="\|)/\1`/' test.txt


6 commentaires

Je suis surpris que vous ne vouliez pas de "3" | "Texte" "autour de" "guillemets" | "abc" \ n (CSV approprié pour 3 , Textez "autour" des guillemets et abc ) au lieu de clobber votre texte.


Vous n'avez pas défini comment gérer le texte contenant `


Grasshopper, une fois de retour, indiquez-nous comment vous souhaitez représenter les backticks littéraux dans vos champs. En bref, si vous avez "2" | "" Texte `entre` guillemets "" | "ab" , à quoi devrait-il ressembler? Est-ce que dans doit être entouré d'un double backticks? Ou échappé avec une barre oblique inverse? Ou d'une autre manière?


@ikegami Je charge ces données dans une base de données à l'aide de SQL * Loader et j'avais peur qu'un texte tel que "" around "" casse la charge. Si cela fonctionne, ce serait préférable.


Les backticks littéraux @ WiktorStribiżew et @ikegami dans les champs doivent être placés entre `` doubles backticks.


Voir ma réponse mise à jour avec un démo en ligne .


4 Réponses :


3
votes

Avec Perl 5.14 et plus récent, vous pouvez utiliser

perl -pi.bak -e 's/(?:^|\|)(")?\K(.*?)(?=\1(?:$|\|))/$2=~s#"|(`)#`$1#gr/ge' test.txt

Voir le démo regex et une démo en ligne .

Le point ici est que vous faites correspondre les champs avec le premier regex, puis vous traitez les guillemets doubles et les backticks en utilisant le deuxième regex exécuté sur la partie de correspondance.

Détails

  • (?: ^ | \ |) - correspond au début d'une chaîne ou |
  • (")? - un groupe 1 facultatif correspondant à un "
  • \ K - opérateur de réinitialisation de correspondance supprimant tout le texte du tampon de correspondance actuel
  • (. *?) - Groupe 2: 0+ caractères autres que les caractères de saut de ligne
  • (? = \ 1 (?: $ | \ |)) - une anticipation positive qui s'assure qu'il y a la même valeur que dans le groupe 1, puis la fin de la chaîne ou | immédiatement à droite de l'emplacement actuel.

Ainsi, le groupe 2 est le contenu de la cellule, sans guillemets doubles. $ 2 = ~ s # "| () # $ 1 # gr remplace tout " par ` et duplique tous ceux trouvés backticks littéraux dans la valeur du groupe 2 (voir cette démo regex ). Le motif "| (`) correspond à un " ou à un backtick (capturant ce dernier dans le groupe 1) et le ` $ 1 remplace la correspondance par un backtick et le contenu du groupe 1.


4 commentaires

Vous devez mentionner comment vous avez décidé de gérer le texte qui contient déjà `.


@ikegami Il n'y a aucun moyen d'ajouter cela à la réponse puisque OP n'a fourni aucune spécification concernant la façon de représenter les backticks littéraux à l'intérieur des champs (échappé avec un autre backtick? Un backslash?). Il peut être facilement corrigé une fois que les spécifications sont révélées.


C'est mon point. Vous avez fait une hypothèse mais ne l'avez pas documentée. Que pensez-vous de la spécification de l'OP à ce sujet?


@ikegami Maintenant, cela fonctionne comme prévu. Les backticks littéraux doivent être doublés < / a>.



2
votes

Mise à jour pour clarifier que les backticks déjà présents doivent être doublés


Une façon est de fractionner sur | et supprimez les guillemets pour simplifier l'expression régulière restante, puis assemblez la chaîne. Cela peut perdre un peu d'efficacité par rapport à une seule expression régulière, mais il est beaucoup plus simple à maintenir

use warnings;
use strict;
use feature 'say';

use Text::CSV;

my $file = shift || 'data.txt';
my $outfile = 'new_' . $file;

my $csv = Text::CSV->new( { binary => 1, sep_char => '|', 
    allow_loose_quotes => 1, escape_char => '',     # quotes inside fields
    always_quote => 1                               # output as desired
} ) or die "Can't do CSV: ", Text::CSV->error_diag;

open my $fh,     '<', $file    or die "Can't open $file: $!";
open my $out_fh, '>', $outfile or die "Can't open $outfile: $!";

while (my $row = $csv->getline($fh)) {
    s/`/``/g for @$row;
    tr/"/`/  for @$row;
    $csv->say($out_fh, $row);
}

L'option -a permet de "diviser automatiquement" chaque ligne Ainsi, dans le programme, les jetons de ligne sont disponibles dans @F , et le -F spécifie le modèle à fractionner (autre que celui par défaut). Le -l gère les nouvelles lignes. Voir Commutateurs de commande dans perlrun .

Dans le map les " englobants sont supprimés et tous les backticks existants doublés; puis " autour des motifs sont modifiés, globalement. Ensuite, les guillemets sont remis et la liste retournée join -ed. Le | dans la jointure est échappé afin de le glisser à travers le shell vers le programme Perl; si cela entre dans un script (au lieu d'un one-liner), ce que je recommande toujours, changez ce \ | en |.

Je ne connais pas les données typiques et les cas de bord possibles concernant les citations, mais s'il peut y avoir des citations lâches (simples, non appariées), ce qui précède aura des problèmes et peut produire une sortie erronée, et silencieusement; tout comme n'importe quelle procédure qui attend des devis par paires, sans une analyse extrêmement détaillée.

Il peut être globalement plus sûr de simplement remplacer tous les " (autres que ceux qui les entourent), par

map { s/^"|"$//g; s/`/``/g; s/"/`/g; qq("$_") }

(ou avec tr au lieu de regex s /// g ). Cela ajoute également une certaine efficacité.


Une autre façon d'accéder au "La viande" des données consiste à utiliser Text :: CSV , ce qui permet un délimiteur autre que la virgule (par défaut) et absorbe les guillemets. Avoir des guillemets à l'intérieur des champs est considéré comme un mauvais CSV, mais le module peut également analyser cela très bien, avec les choix ci-dessous.

perl -F"\|" -wlanE'
    say join "\|", 
        map { s/^"|"$//g; s/`/``/g; s/"([^"]+)"/`$1`/g; qq("$_") } @F
' data.txt

Pour travailler avec des guillemets à l'intérieur des champs, escape_char doit être différent de quote_char ; je l'ai simplement défini sur '' ici. La sortie est gérée par le module également, et l'attribut always_quote est pour cela (pour citer tous les champs, nécessaires ou non). Veuillez consulter la documentation.

Il y a beaucoup plus à faire avec ce module, bien sûr.

Si le but de la question est précisément de nettoyer un format de fichier où la même citation est utilisée à la fois pour les champs et à l'intérieur des champs, je suggérerais de tout faire avec le module. Cette approche permet de configurer proprement et de manière cohérente toutes sortes d'options, à la fois pour l'entrée et la sortie, et est maintenable.


Quelques questions

  • De quel type de données existe-t-il et est-il possible d'avoir une citation erronée? Alors quoi? Cela peut même affecter le choix de l'approche optimale car cela peut nécessiter une analyse détaillée.

  • Si la quête ici est de redresser les données de style CSV, alors pourquoi ne pas doubler les guillemets à l'intérieur des champs, comme cela est courant et approprié en CSV, au lieu de les remplacer (et potentiellement de nuire à leur signification textuelle)? Consultez la documentation du module, par exemple.


6 commentaires

Notez que les regex fractionnées et multiples ne perdent pas nécessairement en efficacité par rapport à une expression régulière plus compliquée. Si vous vous souciez vraiment, vous devriez les comparer.


@Grinnz Correct! Au début j'avais "peut-être perdre" ... mais ensuite, dans ce cas, contre une regex bien écrite , elle devrait en perdre "un peu" je pense. En effet, devrait se comparer. ... en fait, j'ai remis ce "peut". Merci


Split peut correspondre à un tube contenu dans une chaîne encapsulée entre guillemets.


@robartsd Eh bien ... il existe diverses possibilités avec des données qui torpilleraient tout traitement. Dans ce problème, un tube ne peut pas être là où il ne devrait pas être, ou nous devrions considérer le séparateur comme "|" - et cela est également possible (et pourrait être le modèle pour le séparateur dans split ). Cela dépend de la nature des données réelles et le PO peut nécessiter des améliorations pour toute approche que nous proposons.


Vous devez mentionner comment vous avez décidé de gérer le texte qui contient déjà `.


@ikegami heh, pas de décision mais vient de prendre la leur. Il faut une bonne mention, merci. Édité



1
votes

Perl utilise $ 1 comme espace réservé pour le premier groupe de capture dans la partie de remplacement de l'expression régulière au lieu de \ 1 (utilisé dans la partie correspondante de l'expression régulière). Votre expression régulière ne correspondait pas aux guillemets intérieurs et ne correspondrait pas au premier ou au dernier champ de vos données délimitées par un tube. Votre substitution n'a pas non plus inclus de caractère de guillemet avant le groupe capturé.

Essayez:

perl -pi.bak -e 's/(?<=(?:^|\|)")"([^"]*)"(?="(?:$|\|))/`$1´/' test.txt


0 commentaires

0
votes

Un autre Perl. Après la division par tableau @F, vérifiez "qui n'est pas au début / à la fin des éléments.

$ cat grasshopper.txt
"1"|"Text"|"a"
"2"|""Text in quotes""|"ab"
"3"|"Text "around" quotes"|"abc"
$  perl -F"\|"  -lane   ' for(@F) { s/(?<!^)"(?!$)/`/g }; print join("|",@F) ' grasshopper.txt
"1"|"Text"|"a"
"2"|"`Text in quotes`"|"ab"
"3"|"Text `around` quotes"|"abc"
$

avec les entrées données

 perl -F"\|"  -lane   ' for(@F) { s/(?<!^)"(?!$)/`/g }; print join("|",@F) ' 


0 commentaires