Hapus tanda diakritik (ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ) dari karakter Unicode


88

Saya melihat algoritma yang dapat memetakan antara karakter dengan diakritik ( tilde , sirkumfleksa , tanda sisipan , umlaut , caron ) dan karakter "sederhana" mereka.

Sebagai contoh:

ń  ǹ  ň  ñ  ṅ  ņ  ṇ  ṋ  ṉ  ̈  ɲ  ƞ ᶇ ɳ ȵ  --> n
á --> a
ä --> a
ấ --> a
ṏ --> o

Dll

  1. Saya ingin melakukan ini di Java, meskipun saya curiga itu harus menjadi sesuatu Unicode-y dan harus dapat dilakukan dengan cukup mudah dalam bahasa apa pun.

  2. Tujuan: untuk memudahkan pencarian kata-kata dengan tanda diakritik. Misalnya, jika saya memiliki database pemain tenis, dan Björn_Borg dimasukkan, saya juga akan menyimpan Bjorn_Borg sehingga saya dapat menemukannya jika seseorang memasuki Bjorn dan bukan Björn.


Itu tergantung pada lingkungan tempat Anda memprogram, meskipun Anda mungkin harus mempertahankan semacam tabel pemetaan secara manual. Jadi, bahasa apa yang kamu gunakan?
Thorarin

15
Harap berhati-hati bahwa beberapa huruf seperti ñ en.wikipedia.org/wiki/%C3%91 tidak boleh dicabut diakritiknya untuk tujuan penelusuran. Google dengan tepat membedakan antara bahasa Spanyol "ano" (anus) dan "año" (tahun). Jadi jika Anda benar-benar menginginkan mesin pencari yang baik, Anda tidak dapat mengandalkan penghapusan tanda diakritik dasar.
Eduardo

@Eduardo: Dalam konteks tertentu yang mungkin tidak penting. Menggunakan contoh yang diberikan OP, mencari nama seseorang dalam konteks multinasional, Anda sebenarnya ingin pencarian tidak terlalu akurat.
Amir Abiri

(Dikirim secara tidak sengaja sebelumnya) Namun demikian, masih ada ruang untuk memetakan diakritik ke padanan fonetiknya guna menyempurnakan penelusuran fonetik. yaitu ñ => ni akan memberikan hasil yang lebih baik jika mesin pencari yang mendasari mendukung pencarian berbasis fonetik (misalnya soundex)
Amir Abiri

Kasus penggunaan di mana mengubah año menjadi ano dll. Adalah menghapus karakter non-base64 untuk URL, ID, dll.
Ondra Žižka

Jawaban:


83

Saya telah melakukan ini baru-baru ini di Jawa:

public static final Pattern DIACRITICS_AND_FRIENDS
    = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");

private static String stripDiacritics(String str) {
    str = Normalizer.normalize(str, Normalizer.Form.NFD);
    str = DIACRITICS_AND_FRIENDS.matcher(str).replaceAll("");
    return str;
}

Ini akan dilakukan seperti yang Anda tentukan:

stripDiacritics("Björn")  = Bjorn

tetapi akan gagal misalnya Białystok, karena łkarakternya bukan diakritik.

Jika Anda ingin memiliki penyederhanaan string yang lengkap, Anda memerlukan putaran pembersihan kedua, untuk beberapa karakter khusus lainnya yang bukan diakritik. Apakah peta ini, saya telah menyertakan karakter khusus paling umum yang muncul di nama pelanggan kami. Ini bukan daftar lengkap, tetapi ini akan memberi Anda gambaran bagaimana cara memperpanjangnya. The immutableMap hanyalah kelas sederhana dari koleksi-google.

public class StringSimplifier {
    public static final char DEFAULT_REPLACE_CHAR = '-';
    public static final String DEFAULT_REPLACE = String.valueOf(DEFAULT_REPLACE_CHAR);
    private static final ImmutableMap<String, String> NONDIACRITICS = ImmutableMap.<String, String>builder()

        //Remove crap strings with no sematics
        .put(".", "")
        .put("\"", "")
        .put("'", "")

        //Keep relevant characters as seperation
        .put(" ", DEFAULT_REPLACE)
        .put("]", DEFAULT_REPLACE)
        .put("[", DEFAULT_REPLACE)
        .put(")", DEFAULT_REPLACE)
        .put("(", DEFAULT_REPLACE)
        .put("=", DEFAULT_REPLACE)
        .put("!", DEFAULT_REPLACE)
        .put("/", DEFAULT_REPLACE)
        .put("\\", DEFAULT_REPLACE)
        .put("&", DEFAULT_REPLACE)
        .put(",", DEFAULT_REPLACE)
        .put("?", DEFAULT_REPLACE)
        .put("°", DEFAULT_REPLACE) //Remove ?? is diacritic?
        .put("|", DEFAULT_REPLACE)
        .put("<", DEFAULT_REPLACE)
        .put(">", DEFAULT_REPLACE)
        .put(";", DEFAULT_REPLACE)
        .put(":", DEFAULT_REPLACE)
        .put("_", DEFAULT_REPLACE)
        .put("#", DEFAULT_REPLACE)
        .put("~", DEFAULT_REPLACE)
        .put("+", DEFAULT_REPLACE)
        .put("*", DEFAULT_REPLACE)

        //Replace non-diacritics as their equivalent characters
        .put("\u0141", "l") // BiaLystock
        .put("\u0142", "l") // Bialystock
        .put("ß", "ss")
        .put("æ", "ae")
        .put("ø", "o")
        .put("©", "c")
        .put("\u00D0", "d") // All Ð ð from http://de.wikipedia.org/wiki/%C3%90
        .put("\u00F0", "d")
        .put("\u0110", "d")
        .put("\u0111", "d")
        .put("\u0189", "d")
        .put("\u0256", "d")
        .put("\u00DE", "th") // thorn Þ
        .put("\u00FE", "th") // thorn þ
        .build();


    public static String simplifiedString(String orig) {
        String str = orig;
        if (str == null) {
            return null;
        }
        str = stripDiacritics(str);
        str = stripNonDiacritics(str);
        if (str.length() == 0) {
            // Ugly special case to work around non-existing empty strings
            // in Oracle. Store original crapstring as simplified.
            // It would return an empty string if Oracle could store it.
            return orig;
        }
        return str.toLowerCase();
    }

    private static String stripNonDiacritics(String orig) {
        StringBuffer ret = new StringBuffer();
        String lastchar = null;
        for (int i = 0; i < orig.length(); i++) {
            String source = orig.substring(i, i + 1);
            String replace = NONDIACRITICS.get(source);
            String toReplace = replace == null ? String.valueOf(source) : replace;
            if (DEFAULT_REPLACE.equals(lastchar) && DEFAULT_REPLACE.equals(toReplace)) {
                toReplace = "";
            } else {
                lastchar = toReplace;
            }
            ret.append(toReplace);
        }
        if (ret.length() > 0 && DEFAULT_REPLACE_CHAR == ret.charAt(ret.length() - 1)) {
            ret.deleteCharAt(ret.length() - 1);
        }
        return ret.toString();
    }

    /*
    Special regular expression character ranges relevant for simplification -> see http://docstore.mik.ua/orelly/perl/prog3/ch05_04.htm
    InCombiningDiacriticalMarks: special marks that are part of "normal" ä, ö, î etc..
        IsSk: Symbol, Modifier see http://www.fileformat.info/info/unicode/category/Sk/list.htm
        IsLm: Letter, Modifier see http://www.fileformat.info/info/unicode/category/Lm/list.htm
     */
    public static final Pattern DIACRITICS_AND_FRIENDS
        = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");


    private static String stripDiacritics(String str) {
        str = Normalizer.normalize(str, Normalizer.Form.NFD);
        str = DIACRITICS_AND_FRIENDS.matcher(str).replaceAll("");
        return str;
    }
}

bagaimana dengan karakter seperti ╨?
mickthompson

mereka akan dilewati. juga semua karakter jepang dll.
Andreas Petersson

terima kasih Andreas. Apakah ada cara untuk menghapusnya? Karakter seperti ら が な を 覚 男 (atau lainnya) akan dimasukkan ke dalam string yang dihasilkan dan ini pada dasarnya akan merusak keluaran. Saya mencoba menggunakan output simplifiedString sebagai generator URL seperti yang dilakukan StackOverflow untuk URL Pertanyaannya.
mickthompson

2
Seperti yang saya katakan di komentar pertanyaan. Anda tidak dapat mengandalkan penghapusan tanda diakritik dasar jika Anda menginginkan mesin pencari yang baik.
Eduardo

3
Terima kasih Andreas, bekerja dengan sangat baik! (diuji pada rrrr̈r'ŕřttẗţỳỹẙy'yýÿŷpp̈sss̈s̊s's̸śŝŞşšddd̈ďd'ḑf̈f̸ggg̈g'ģqĝǧḧĥj̈j'ḱkk̈k̸ǩlll̈Łłẅẍcc̈c̊c'c̸Çççćĉčvv̈v'v̸bb̧ǹnn̈n̊n'ńņňñmmmm̈m̊m̌ǵß) :-)
Fortega

25

Paket inti java.text dirancang untuk menangani kasus penggunaan ini (mencocokkan string tanpa mempedulikan diakritik, kasus, dll.).

Konfigurasi a Collatoruntuk mengurutkan PRIMARYperbedaan karakter. Dengan itu, buat a CollationKeyuntuk setiap string. Jika semua kode Anda ada di Java, Anda dapat menggunakan CollationKeysecara langsung. Jika Anda perlu menyimpan kunci dalam database atau jenis indeks lainnya, Anda dapat mengonversinya menjadi array byte .

Kelas-kelas ini menggunakan data lipat kasus standar Unicode untuk menentukan karakter mana yang setara, dan mendukung berbagai strategi dekomposisi .

Collator c = Collator.getInstance();
c.setStrength(Collator.PRIMARY);
Map<CollationKey, String> dictionary = new TreeMap<CollationKey, String>();
dictionary.put(c.getCollationKey("Björn"), "Björn");
...
CollationKey query = c.getCollationKey("bjorn");
System.out.println(dictionary.get(query)); // --> "Björn"

Perhatikan bahwa kolator bersifat spesifik-lokal. Ini karena "urutan abjad" berbeda di antara bahasa lokal (dan bahkan seiring waktu, seperti yang terjadi pada bahasa Spanyol). The Collatorkelas mengurangi Anda dari keharusan untuk melacak semua aturan ini dan menjaga mereka up to date.


terdengar menarik, tetapi dapatkah Anda mencari kunci collation Anda di database dengan pilih * dari orang di mana collated_name seperti 'bjo%' ??
Andreas Petersson

sangat bagus, tidak tahu tentang itu. akan mencobanya.
Andreas Petersson

Di Android, CollationKeys tidak bisa digunakan sebagai prefiks untuk pencarian database. Kunci collation string aberubah menjadi byte 41, 1, 5, 1, 5, 0, namun string abberubah menjadi byte 41, 43, 1, 6, 1, 6, 0. Urutan byte ini tidak muncul sebagaimana mestinya dalam kata-kata penuh (array byte untuk kunci pemeriksaan atidak muncul dalam array byte untuk kunci pemeriksaan untuk ab)
Grzegorz Adam Hankiewicz

1
@GrzegorzAdamHankiewicz Setelah beberapa pengujian, saya melihat bahwa array byte dapat dibandingkan, tetapi tidak membentuk prefiks, seperti yang Anda catat. Jadi, untuk melakukan kueri awalan seperti bjo%, Anda perlu melakukan kueri rentang di mana kolatornya adalah> = bjodan < bjp(atau apa pun simbol berikutnya akan berada di lokal itu, dan tidak ada cara terprogram untuk menentukannya).
erickson

16

Itu bagian dari Apache Commons Lang sejak ver. 3.1.

org.apache.commons.lang3.StringUtils.stripAccents("Añ");

kembali An


1
Untuk Ø memberikan lagi Ø
Mike Argyriou

2
Terima kasih, Mike karena telah menunjukkannya. Metode ini hanya menangani aksen. Hasil dari "ń ǹ ň ñ ṅ ņ ṇ ṋ ṉ ̈ ɲ ƞ ᶇ ɳ ȵ" adalah "nnnnnnnnn ɲ ƞ ᶇ ɳ ȵ"
Kenston Choi

12

Anda dapat menggunakan kelas Normalizer dari java.text:

System.out.println(new String(Normalizer.normalize("ń ǹ ň ñ ṅ ņ ṇ ṋ", Normalizer.Form.NFKD).getBytes("ascii"), "ascii"));

Tetapi masih ada beberapa pekerjaan yang harus dilakukan, karena Java membuat hal-hal aneh dengan karakter Unicode yang tidak dapat diubah (tidak mengabaikannya, dan tidak mengeluarkan pengecualian). Tapi saya pikir Anda bisa menggunakannya sebagai titik awal.


3
ini tidak akan bekerja untuk diakritik non-ascii, seperti dalam bahasa Rusia, mereka memiliki diakritik, juga, dan selanjutnya memotong semua string Asia. jangan gunakan. alih-alih mengonversi ke ascii, gunakan regexp \\ p {InCombiningDiacriticalMarks} seperti dalam jawaban stackoverflow.com/questions/1453171/…
Andreas Petersson


5

Harap diperhatikan bahwa tidak semua tanda ini hanyalah "tanda" pada beberapa karakter "normal", yang dapat Anda hapus tanpa mengubah artinya.

Dalam bahasa Swedia, å ä dan ö adalah karakter kelas satu yang benar dan tepat, bukan beberapa "varian" dari beberapa karakter lain. Mereka terdengar berbeda dari semua karakter lainnya, mereka mengurutkan berbeda, dan membuat kata-kata berubah arti ("mätt" dan "matt" adalah dua kata yang berbeda).


4
Meskipun benar, ini lebih merupakan komentar daripada jawaban atas pertanyaan.
Simon Forsberg

2

Unicode memiliki karakter diatrik tertentu (yang merupakan karakter komposit) dan sebuah string dapat diubah sehingga karakter dan diatrik tersebut terpisah. Kemudian, Anda bisa menghapus diatrict dari string dan pada dasarnya Anda sudah selesai.

Untuk informasi lebih lanjut tentang normalisasi, dekomposisi dan kesetaraan, lihat Standar Unicode di beranda Unicode .

Namun, bagaimana Anda benar-benar dapat mencapai ini tergantung pada kerangka / OS / ... yang Anda kerjakan. Jika Anda menggunakan .NET, Anda dapat menggunakan metode String.Normalize yang menerima enumerasi System.Text.NormalizationForm .


2
Ini adalah metode yang saya gunakan di .NET, meskipun saya masih harus memetakan beberapa karakter secara manual. Mereka bukan diakritik, tapi digraf. Masalah serupa sekalipun.
Thorarin

1
Ubah ke bentuk normalisasi "D" (yaitu terdekomposisi) dan ambil karakter dasar.
Richard

2

Cara termudah (bagi saya) adalah dengan mempertahankan array pemetaan yang jarang yang hanya mengubah poin kode Unicode Anda menjadi string yang dapat ditampilkan.

Seperti:

start    = 0x00C0
size     = 23
mappings = {
    "A","A","A","A","A","A","AE","C",
    "E","E","E","E","I","I","I", "I",
    "D","N","O","O","O","O","O"
}
start    = 0x00D8
size     = 6
mappings = {
    "O","U","U","U","U","Y"
}
start    = 0x00E0
size     = 23
mappings = {
    "a","a","a","a","a","a","ae","c",
    "e","e","e","e","i","i","i", "i",
    "d","n","o","o","o","o","o"
}
start    = 0x00F8
size     = 6
mappings = {
    "o","u","u","u","u","y"
}
: : :

Penggunaan larik jarang akan memungkinkan Anda merepresentasikan penggantian secara efisien bahkan saat penggantian berada di bagian tabel Unicode yang berjarak lebar. Penggantian string akan memungkinkan urutan arbitrer menggantikan diakritik Anda (seperti ægrafem menjadi ae).

Ini adalah jawaban tanpa bahasa jadi, jika Anda memiliki bahasa tertentu dalam pikiran, akan ada cara yang lebih baik (meskipun mereka semua kemungkinan besar akan sampai pada ini di tingkat yang paling rendah).


Menambahkan semua kemungkinan karakter aneh bukanlah tugas yang mudah. Saat melakukan ini hanya untuk beberapa karakter, ini adalah solusi yang bagus.
Simon Forsberg

2

Sesuatu yang perlu dipertimbangkan: jika Anda berusaha mendapatkan satu "terjemahan" untuk setiap kata, Anda mungkin kehilangan beberapa kemungkinan alternatif.

Misalnya, dalam bahasa Jerman, saat mengganti "s-set", beberapa orang mungkin menggunakan "B", sementara yang lain mungkin menggunakan "ss". Atau, mengganti umlauted o dengan "o" atau "oe". Solusi apa pun yang Anda hasilkan, idealnya, menurut saya harus menyertakan keduanya.


2

Di Windows dan .NET, saya hanya mengonversi menggunakan pengkodean string. Dengan cara itu saya menghindari pemetaan dan pengkodean manual.

Cobalah bermain dengan pengkodean string.


3
Bisakah Anda menjelaskan tentang pengkodean string? Misalnya dengan contoh kode.
Peter Mortensen

2

Dalam kasus Jerman, tidak ingin menghapus diakritik dari Umlauts (ä, ö, ü). Sebaliknya diganti dengan kombinasi dua huruf (ae, oe, ue). Misalnya, Björn harus ditulis Bjoern (bukan Bjorn) agar pengucapannya benar.

Untuk itu, saya lebih memilih pemetaan hardcode, di mana Anda dapat menentukan aturan penggantian secara individual untuk setiap grup karakter khusus.


0

Untuk referensi di masa mendatang, berikut adalah metode ekstensi C # yang menghilangkan aksen.

public static class StringExtensions
{
    public static string RemoveDiacritics(this string str)
    {
        return new string(
            str.Normalize(NormalizationForm.FormD)
                .Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != 
                            UnicodeCategory.NonSpacingMark)
                .ToArray());
    }
}
static void Main()
{
    var input = "ŃŅŇ ÀÁÂÃÄÅ ŢŤţť Ĥĥ àáâãäå ńņň";
    var output = input.RemoveDiacritics();
    Debug.Assert(output == "NNN AAAAAA TTtt Hh aaaaaa nnn");
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.