sed Faixa de caracteres de correspondência

2

Existe uma maneira de combinar exatamente alguns intervalos de Unicode.
Vamos usar o intervalo cirílico como exemplo: U + 400 a U + 52f

Toda a gama de caracteres pode ser impressa (de bash ou zsh) com:

$ echo -e $(printf '\U%x' $(seq 0x400 0x52f)) ЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ҂҃҄҇ҊҋҌҍҎҏҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽҾҿӀӁӂӃӄӅӆӇӈӉӊӋӌӍӎӏӐӑӒӓӔӕӖӗӘәӚӛӜӝӞӟӠӡӢӣӤӥӦӧӨөӪӫӬӭӮӯӰӱӲӳӴӵӶӷӸӹӺӻӼӽӾӿԀԁԂԃԄԅԆԇԈԉԊԋԌԍԎԏԐԑԒԓԔԕԖԗԘԙԚԛԜԝԞԟԠԡԢԣԤԥԦԧԨԩԪԫԬԭԮԯ

$ a=$(zsh -c 'echo -e $(printf '\''\U%x'\'' $(seq 0x400 0x52f))')

Para filtrar algum intervalo, vamos usar 0x452 para 0x490, essa é a saída esperada:

$ b=$(bash -c 'echo -e $(printf '\''\U%x'\'' $(seq 0x452 0x490))')
$ echo "$b"
ђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ҂҃҄҇ҊҋҌҍҎҏҐ
$ echo "$b" | xxd
00000000: d192 d193 d194 d195 d196 d197 d198 d199  ................
00000010: d19a d19b d19c d19d d19e d19f d1a0 d1a1  ................
00000020: d1a2 d1a3 d1a4 d1a5 d1a6 d1a7 d1a8 d1a9  ................
00000030: d1aa d1ab d1ac d1ad d1ae d1af d1b0 d1b1  ................
00000040: d1b2 d1b3 d1b4 d1b5 d1b6 d1b7 d1b8 d1b9  ................
00000050: d1ba d1bb d1bc d1bd d1be d1bf d280 d281  ................
00000060: d282 d283 d284 d285 d286 d287 d288 d289  ................
00000070: d28a d28b d28c d28d d28e d28f d290 0a    ...............

Mas parece impossível filtrar com sed. Isso não funciona:

$ echo "$a" | sed 's/[^\x452-\x490]//g'

Nem isso (o resultado corresponde a outros personagens (provavelmente um problema de classificação)):

$ echo "$a" | sed $'s/[^\u452-\u490]//g' АБВГжзийклмнопрстуфхцчшщъыьэюяёђєѕіїјљњћќѝўџҋҍҏҐҗҙқҝҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӂӄӆӈӊӌӎӐӒӔӝӟӡӣӥӧөӫӭӯӱӳӵӹԅԇԉԋԍԏ

Nem mesmo isso (o mesmo problema de classificação):

$ echo "$a" | sed 's/[^ђ-Ґ]//g'

Este trabalho com o awk:

$ echo "$a" | awk '{gsub(/[^ђ-Ґ]/,"")}1'

Mas a única maneira de usar um intervalo hexadecimal é usar o shell para converter hexadecimal em um caractere unicode

$ echo "$a" | awk $'{gsub(/[^\u452-\u490]/,"")}1'

ou (duas soluções):

$ c=$(bash -c 'printf "\u452-\u490"') 
$ echo "$a" | awk '{gsub(/[^'"$c"']/,"")}1'
$ echo $a | awk -v ra="[^$c]" '{gsub(ra,"")}1'

Perguntas:

  • Existe uma maneira de fazer isso com sed?
  • Poderia fazer isso em números hexadecimais sem um shell maior.

  • Se possível, qual é exatamente o intervalo correspondente à sequência de intercalação usada por sed com sed 's/[^ђ-Ґ]//g' .

P.S .: Eu sei que isso poderia ser feito em perl, obrigado.

    
por Isaac 07.05.2018 / 07:48

2 respostas

0

No sed básico, os intervalos nas expressões de colchetes seguem o Posix. No Posix, os intervalos nas expressões de colchetes seguem a ordem de agrupamento. A ordem de intercalação é definida para se basear no valor numérico do caractere apenas no idioma C. Mas apenas para valores unibyte. O resto dos locais são indefinidos em Posix.

Para fazer um intervalo funcionar na expressão de parênteses, precisamos usar uma ordem de agrupamento ordenada pelo ponto de código numérico Unicode, que é C.UTF-8. Mas isso cria o requisito secundário de caracteres de intervalo de codificação em utf8:

  • Obtenha a representação octal do caractere do intervalo de pontos de código unicode (se a localidade utilizada for utf-8):

    $ printf '\u452\u490' | od -An -to1
    

    Se não estiver em uma localidade utf-8, converta os valores para utf-8:

    $ printf '\u452\u490' | iconv -t utf-8 | od -An -to1
    321 222 322 220
    
  • Adicione um traço e um \ o para fazê-lo funcionar no sed mais antigo / presente:

    $ printf '\o%s\o%s-\o%s\o%s' $(printf '\u452\u490'|iconv -tutf-8|od -An -to1)
    \o321\o222-\o322\o220
    
  • Use esse intervalo para ser usado no sed:

    $ echo "$a" | LC_ALL=C.UTF-8 sed 's/[^\o321\o222-\o322\o220]//g'
    
  • Mas certifique-se de que a localidade seja C.UTF-8 e que a string dada esteja codificada em utf8 e convertida de volta para a localidade em uso:

    $ echo "$a" | iconv -t utf-8 |
                  LC_ALL=C.UTF-8 sed 's/[^\o321\o222-\o322\o220]//g' |
                                    iconv -f utf-8
    

    Observe que, acima, usamos um shell para converter \u452\u490 .

O GNU awk é capaz de gerar uma string de caracteres dado o ponto de código hexadecimal (desde que o locale permita tais caracteres):

<<<"$a" awk 'BEGIN{for(i=0x452;i<=0x490;i++){r=r sprintf("%c", i)}}
 {gsub("[^" range "]", "")}1'

Se a localidade atual não contiver esses pontos de código Unicode no número de ponto de código Unicode, será necessário converter em uma localidade conhecida por conter tais pontos de código e usar uma variável de ambiente locale correspondente, algo como:

<<<"$a" iconv -t utf8 |  
LC_ALL=en_US.UTF-8 awk '
        BEGIN{for(i=0x452;i<=0x490;i++){r=r sprintf("%c", i)}}
        {gsub("[^" r "]", "")}1
        ' | iconv -f utf8

Bottom line é necessário um shell maior (GNU bash ou zsh) ou awk (somente GNU).

Ou use uma linguagem de nível ainda mais alto como perl:

$ echo "$a" | perl -Mopen=locale -ane 's/[^\x{452}-\x{490}]//g; print'
    
por 08.05.2018 / 07:34
3

Por POSIX, os intervalos nas expressões de colchetes são especificados apenas para serem baseados no ponto de código no código do idioma C / POSIX. Em outras localidades, não é especificado e geralmente é baseado na ordem de agrupamento conforme você descobriu. Você verá que, em alguns locais, dependendo da ferramenta, [g-j] , por exemplo, inclui i , mas também ı , ǵ , às vezes até I ou mesmo ch , como em algumas localidades tchecas. / p>

zsh é um desses raros cujos intervalos [x-y] são baseados no ponto de código, independentemente da localidade. Para conjuntos de caracteres de byte único, isso será baseado no valor de byte, para os de múltiplos bytes no ponto de código Unicode ou qualquer que seja o sistema usado para representar caracteres largos internamente com mbstowc() e co. APIs (geralmente Unicode).

Então, em zsh ,

  • [[ $char = [$'\u452'-$'\u490'] ]]
  • [[ $char = [^ђ-Ґ] ]]
  • y=${x//[^ђ-Ґ]/}

funcionaria no seu caso para corresponder aos caracteres nesse intervalo de Unicode, desde que o conjunto de caracteres do código de idioma seja multi-byte e tenha esses dois caracteres. Existem conjuntos de caracteres de byte único que contêm alguns desses caracteres (como ISO8859-5 que tem a maioria dos que estão em U + 0401 ... U + 045F), mas em locais que os usam, os intervalos [ђ-Ґ] seriam baseados em o valor do byte (ponto de código no conjunto de caracteres correspondente, não o ponto de código Unicode).

Na localidade C, os intervalos são baseados no ponto de código, mas o conjunto de caracteres no idioma C só é garantido para incluir os caracteres no conjunto de caracteres portátil que é apenas os poucos caracteres necessários para gravar Código POSIX ou C (nenhum dos quais está no script cirílico). Também é garantido que seja byte único , portanto, não é possível incluir todos os caracteres especificados em Unicode. Na prática, é mais frequentemente ASCII.

Na prática, você não pode definir LC_COLLATE para C sem definir também LC_CTYPE para C (ou pelo menos um código de idioma com um conjunto de caracteres de um único byte). No entanto, muitos sistemas têm um C.UTF-8 locale que você pode usar aqui.

O UTF-8 é um desses conjuntos de caracteres que podem representar todos os caracteres Unicode e, portanto, todos aqueles em qualquer conjunto de caracteres. Então você poderia fazer:

< file iconv -t utf-8 |
  LC_ALL=C.UTF-8 sh -c 'sed "$(printf "s/[^12-20]//g")"' |
  iconv -f utf-8

A primeira iconv convertendo do charset de localidade do usuário para UTF-8, 12 e 20 sendo a codificação UTF-8 de U + 0452 e U + 0490 respectivamente, o segundo iconv convertendo de volta para o charset do locale.

Se a localidade atual já usa UTF-8 como o conjunto de caracteres (e file é escrito usando esse conjunto de caracteres), isso pode ser simplificado para:

<file LC_ALL=C.UTF-8 sed 's/[^ђ-Ґ]//g'

ou:

<file LC_ALL=C.UTF-8 sed "$(printf "s/[^12-20]//g")"

Com o GNU sed e desde que o $POSIXLY_CORRECT não esteja no ambiente, você pode especificar caracteres com base no valor de bytes de sua codificação.

<file LC_ALL=C.UTF-8 sed 's/[^12-20]//g'

Embora em versões mais antigas, você pode precisar de:

<file LC_ALL=C.UTF-8 sed 's/[^\o321\o222-\o322\o220]//g'

Ou a variante hexadecimal:

<file LC_ALL=C.UTF-8 sed 's/[^\xd1\x92-\xd2\x90]//g'

Outra opção, para locais usando um conjunto de caracteres de múltiplos bytes que inclui esses caracteres em sistemas nos quais a representação de caracteres largos é baseada em Unicode, é usar o GNU awk e:

awk 'BEGIN{for (i = 0x452; i<=0x490; i++) range = range sprintf("%c", i)}
     {gsub("[^" range "]", ""); print}'

(Inicialmente, eu acreditava que POSIX requeria que as implementações do awk se comportassem como o GNU awk, mas esse não é o caso, já que o POSIX deixa o comportamento de sprintf("%c", i) undefined para valores de i que não correspondem ao codificação (não codepoint) de um caractere na localidade, o que significa que ele não pode ser usado portualmente para caracteres de múltiplos bytes).

Em qualquer caso, observe que o intervalo U + 0400 .. U + 052F não são os únicos caracteres Unicode no script cirílico , e muito menos os idiomas que usam Cirílico como seu script. A lista de caracteres também varia com a versão do Unicode.

Em um sistema semelhante ao Debian, você pode obter uma lista deles com:

unicode --max 0 cyrillic

(que dá 435 diferentes no Ubuntu 16.04, 444 no Debian sid (provavelmente usando uma versão diferente do Unicode).

Em perl , consulte \p{Block: Cyrillic} , \p{Block: Cyrillic_Ext_A,B,C} , \p{Block: Cyrillic_Supplement} ... para corresponder nos blocos Unicode e \p{Cyrillic} para corresponder aos caracteres do script cirílico (atualmente atribuído na versão Unicode que sua versão de perl está usando (veja perl -MUnicode::UCD -le 'print Unicode::UCD::UnicodeVersion' , por exemplo)).

Então:

perl -Mopen=locale 's/\P{Cyrillic}//g'
    
por 07.05.2018 / 11:30

Tags