Como converter números persas em UTF-8 para números europeus em ASCII?

16

Em números persas, ۰۱۲۳۴۵۶۷۸۹é equivalente a 0123456789dígitos europeus.

Como posso converter o número persa (pol UTF-8) para ASCII?

Por exemplo, eu quero ۲۱tornar-se 21.

بارپابابا
fonte
11
Interessante, parece echo "۰۱۲۳۴۵۶۷۸۹" | iconv -f UTF-8 -t ascii//TRANSLITque não lida com isso ...
Kusalananda
@Kusalananda NÃO funcionou
#
3
@Kusalananda: É realmente tão inesperado? Como eu entendi, iconvestá aqui apenas para mapear caracteres em codificações diferentes, mas esses são caracteres (números do árabe oriental) que não têm equivalente em ASCII, você pode convertê-los em algo semelhante o suficiente, mas é apenas de sentido único.
Phd #
3
Bem, eu não tinha certeza do que iconvera capaz ou não. Eu esperava que o uso //TRANSLITajudasse, mas não ajudou.
Kusalananda
11
Você também precisa reverter a ordem? Sei que os algarismos arábicos são escritos como little endian da direita para a esquerda, e os latinos são big endian da esquerda para a direita (parecidos na impressão ou na tela, mas invertidos na memória). Persa é o mesmo?
Toby Speight

Respostas:

6

Podemos tirar vantagem do fato de que o ponto de código UNICODE dos números persas é consecutivo e ordenado de 0 a 9 :

$ printf '%b' '\U06F'{0..9}
۰۱۲۳۴۵۶۷۸۹

Isso significa que o último dígito hexadecimal É o valor decimal:

$ echo $(( $(printf '%d' "'۲") & 0xF ))
2

Isso torna esse loop simples uma ferramenta de conversão:

#!/bin/bash
(   ### Use a locale that use UTF-8 to make the script more reliable.
    ### Maybe something like LC_ALL=fa_IR.UTF-8 for you?.
    LC_ALL=en_US.UTF-8
    a="$1"
    while (( ${#a} > 0 )); do
        # extract the last hex digit from the UNICODE code point
        # of the first character in the string "$a":
        printf '%d' $(( $(printf '%d' "'$a") & 15 ))
        a=${a#?}    ## Remove one character from $a
    done
)
echo

Utilizando-o como:

$ sefr.sh ۰۱۲۳۴۵۶۷۸۹
0123456789

$ sefr.sh ۲۰۱
201

$ sefr.sh ۲۱
21

Observe que esse código também pode converter números arábicos e latinos (mesmo se misturados):

$ sefr.sh ۴4٤۵5٥۶6٦۷7٧۸8٨۹9٩
444555666777888999

$ sefr.sh ٤٧0٠٦7١٣3٥۶٦۷
4700671335667

fonte
muito, muito graças, isso é muito bom solução ,, e eu tenho pergunta ,, neste comando printf '% d' '"0' por que usar double-citação?
بارپابابا
@Babyy Não é uma citação dupla, é uma maneira de dar printf um argumento que começar com uma única citação: . Poderia ter sido escrito também como '"۰'. O motivo é que printf fornecerá o ponto de código UNICODE se o argumento iniciar com uma aspas simples 'ou duplas ". Pesquisar um pouco antes de este link para o texto "se o personagem principal é uma aspa simples ou de aspas duplas"
@Babyy O código foi estendido para converter persa, árabe e latim (mesmo se misturado).
27

Como é um conjunto fixo de números, você pode fazer isso manualmente:

$ echo ۲۱ | LC_ALL=en_US.UTF-8 sed -e 'y/۰۱۲۳۴۵۶۷۸۹/0123456789/'
21

(ou usando tr , mas ainda não o GNU tr )

É necessário definir seu código de idioma como en_US.utf8(ou melhor, o código de idioma ao qual o conjunto de caracteres pertence) sedpara reconhecer seu conjunto de caracteres.

Com perl:

$ echo "۲۱" |
  perl -CS -MUnicode::UCD=num -MUnicode::Normalize -lne 'print num(NFKD($_))'
21
cuonglm
fonte
LC_ALLÉ necessário definir o que é necessário para que todos os caracteres unicode sejam considerados como tal sed, certo?
Php
@phk: Sim, veja a atualização.
cuonglm
Por que tudo deve ser um script sed? Nós não inventamos trpara esse propósito exato?
21716 Kevin
3
@ Kevin Veja a outra resposta que envolve trcomo ele não funciona em todos os lugares. Lembre-se também de que algumas ferramentas são otimizadas para lidar com bytes, enquanto outras são para lidar com caracteres. No Unicode (especialmente UTF-8), isso faz uma enorme diferença.
Php
Isso não funciona para mim no OS X 10.10.5 / GNU bash 4.3. Curiosamente, eu preciso remover a configuração explícita de LC_ALL. LC_ALLtambém não está definido no meu ambiente (mas LANGestá definido como en_GB.UTF-8). Com o código acima, recebo o erro "sed: 1:" y / ۰۱۲۳۴۵۶۷۸۹ / ... ": as seqüências de transformação não têm o mesmo comprimento".
Konrad Rudolph
15

Para Python, existe a unidecodebiblioteca que lida com essas conversões em geral: https://pypi.python.org/pypi/Unidecode .

No Python 2:

>>> from unidecode import unidecode
>>> unidecode(u"۰۱۲۳۴۵۶۷۸۹")
'0123456789'

No Python 3:

>>> from unidecode import unidecode
>>> unidecode("۰۱۲۳۴۵۶۷۸۹")
'0123456789'

O encadeamento SO em /programming//q/8087381/2261442 pode estar relacionado.

/ edit: Como Wander Nauta apontou nos comentários e como mencionado na página Unidecode, também há uma versão do shell unidecode(em /usr/local/bin/se instalado sobre pip):

$ echo '۰۱۲۳۴۵۶۷۸۹' | unidecode
0123456789
phk
fonte
2
A biblioteca unidecode também envia um utilitário chamado (sem surpresa), unidecodeque faz o mesmo que o seu trecho de código Python 3. Só echo '۰۱۲۳۴۵۶۷۸۹' | unidecodedeveria funcionar.
Wander Nauta
@Wander - o pacote Debian de python-unidecode não envia o programa utilitário, portanto a forma longa pode ser necessária nessas plataformas (não encontrei um no tarball de origem do upstream, talvez o programa seja algo adicionado por sua distribuição?)
Toby Speight
@TobySpeight Se você o instalar usando pipele está lá.
Php
@TobySpeight O utilitário está no tarball upstream como unidecode/util.py- estranho que o Debian não o inclua. (Edit: Ah, mistério resolvido O pacote Debian está desatualizado e mais velhos do que a utilidade..)
Wander Nauta
7

Uma versão pura do bash:

#!/bin/bash

number="$1"

number=${number//۱/1}
number=${number//۲/2}
number=${number//۳/3}
number=${number//۴/4}
number=${number//۵/5}
number=${number//۶/6}
number=${number//۷/7}
number=${number//۸/8}
number=${number//۹/9}
number=${number//۰/0}

echo "Result is $number"

Já testei na minha máquina Gentoo e funciona.

./convert ۱۳۲
Result is 132

Feito como um loop, dada a lista de caracteres (de 0 a 9) para converter:

#!/bin/bash
conv() ( LC_ALL=en_US.UTF-8
         local n="$2"
         for ((i=0;i<${#1};i++)); do
              n=${n//"${1:i:1}"/"$i"}
         done
         printf '%s\n' "$n"
       )

conv "۰۱۲۳۴۵۶۷۸۹" "$1"

E usado como:

$ convert ۱۳۲
132

Outra maneira (um exagero) de usar grep:

#!/bin/bash

nums=$(echo "$1" | grep -o .)
result=()

for i in $nums
do
    case $i in
        ۱)
            result+=1
            ;;
        ۲)
            result+=2
            ;;
        ۳)
            result+=3
            ;;
        ۴)
            result+=4
            ;;
        ۵)
            result+=5
            ;;
        ۶)
            result+=6
            ;;
        ۷)
            result+=7
            ;;
        ۸)
            result+=8
            ;;
        ۹)
            result+=9
            ;;
        ۰)
            result+=0
            ;;
    esac
done
echo "Result is $result"
coffeMug
fonte
11
Pure Bash, exceto o grep. Na verdade, eu não entendo essa linha, nem por que você não define result=0. Você está sendo excessivamente cauteloso caso $1contenha outras coisas além dos dígitos farsi?
Kusalananda
@Kusalananda essa linha lê os dígitos do farsi em números. Torna o loop capaz.
coffemug
11
Dez substituições simples seriam mais rápidas ... number=${number//۱/1}etc. e evitariam o echoe grep.
Kusalananda
11
@Kusalananda Nice. Mudou isso. Agora é Bash puro! ;-)
coffemug
@coffeMug: 132 132 é não 123: D
بارپابابا
3

Como iconvnão consigo entender isso, a próxima porta de chamada seria usar o trutilitário:

$ echo "۲۱" | tr '۰۱۲۳۴۵۶۷۸۹' '0123456789'
21

tr converte um conjunto de caracteres para outro, então simplesmente dizemos para traduzir o conjunto de dígitos farsi para o conjunto de dígitos latinos.

EDIT : Como o usuário @cuonglm aponta. Isso requer não-GNU tr, por exemplo, trem um Mac, e também exige que $LC_CTYPEesteja definido como en_US.UTF-8.

Kusalananda
fonte
2
Observe que ele não funcionará com o GNU tr, que não suporta caracteres de vários bytes.
cuonglm
11
Oh meu. GNU parvo. ;-)
Kusalananda
E também é necessário definir seu código de idioma para o que suporta unicode, como en_US.utf8.
cuonglm