ลบเครื่องหมายกำกับเสียง (ńǹňñṅņṇṋṉ̈ɲƞᶇɳȵ) จากตัวอักษร Unicode


88

ฉันกำลังมองหาที่อัลกอริทึมที่สามารถ map ระหว่างตัวอักษรกำกับ ( ตัวหนอน , หมวก , ลูกศร , เครื่องหมาย , รอน ) และ "ง่าย" ของพวกเขาตัวอักษร

ตัวอย่างเช่น:

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

ฯลฯ

  1. ฉันต้องการทำสิ่งนี้ใน Java แม้ว่าฉันจะสงสัยว่ามันควรจะเป็น Unicode-y และควรจะทำได้ง่ายพอสมควรในทุกภาษา

  2. วัตถุประสงค์: เพื่อให้สามารถค้นหาคำที่มีเครื่องหมายกำกับเสียงได้อย่างง่ายดาย ตัวอย่างเช่นหากฉันมีฐานข้อมูลของนักเทนนิสและป้อนBjörn_Borgฉันจะเก็บ Bjorn_Borg ไว้ด้วยเพื่อที่ฉันจะได้พบว่ามีคนเข้า Bjorn ไม่ใช่Björn


ขึ้นอยู่กับสภาพแวดล้อมที่คุณกำลังเขียนโปรแกรมแม้ว่าคุณอาจจะต้องดูแลตารางการทำแผนที่ด้วยตนเอง คุณใช้ภาษาอะไร
Thorarin

15
โปรดระวังว่าตัวอักษรบางตัวเช่นñ en.wikipedia.org/wiki/%C3%91ไม่ควรนำมาใช้ในการค้นหา Google แยกความแตกต่างระหว่าง "ano" (ทวารหนัก) และ "año" (ปี) ของสเปนได้อย่างถูกต้อง ดังนั้นหากคุณต้องการเครื่องมือค้นหาที่ดีจริง ๆ คุณไม่สามารถพึ่งพาการลบเครื่องหมายกำกับเสียงขั้นพื้นฐานได้
Eduardo

@ เอดูอาร์โด: ในบริบทที่กำหนดซึ่งอาจไม่สำคัญ ใช้ตัวอย่างที่ OP ให้เพื่อค้นหาชื่อบุคคลในบริบทหลายชาติที่คุณต้องการให้การค้นหาไม่แม่นยำเกินไป
Amir Abiri

(ส่งโดยบังเอิญก่อนหน้านี้) แม้ว่าจะมีที่ว่างสำหรับการทำแผนที่เสียงกำกับเสียงให้เทียบเท่าการออกเสียงเพื่อปรับปรุงการค้นหาแบบออกเสียง ie ñ => พรรณีจะให้ผลลัพธ์ที่ดีกว่าหากเครื่องมือค้นหาที่อยู่ภายใต้รองรับการค้นหาตามการออกเสียง (เช่น soundex)
Amir Abiri

กรณีการใช้งานที่การเปลี่ยนañoเป็น ano ฯลฯ คือการลอกตัวอักษรที่ไม่ใช่ base64 สำหรับ URLs, IDs เป็นต้น
Ondra ŽiŽka

คำตอบ:


83

ฉันเพิ่งทำสิ่งนี้ใน Java:

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;
}

สิ่งนี้จะทำตามที่คุณระบุ:

stripDiacritics("Björn")  = Bjorn

แต่มันจะล้มเหลวในตัวอย่างเช่นBiałystokเนื่องจากłอักขระไม่ได้เป็นตัวกำกับเสียง

หากคุณต้องการมีซิมแอมพลิไฟเออร์แบบสตริงแบบเต็มคุณจะต้องมีการล้างรอบที่สองสำหรับอักขระพิเศษบางตัวที่ไม่ใช่ตัวกำกับเสียง คือแผนที่นี้ฉันได้รวมอักขระพิเศษที่พบบ่อยที่สุดที่ปรากฏในชื่อลูกค้าของเรา ไม่ใช่รายการที่สมบูรณ์ แต่จะให้แนวคิดว่าจะขยายอย่างไร immutableMap เป็นเพียงคลาสง่ายๆจาก google-collection

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;
    }
}

แล้วตัวละครอย่าง╨ล่ะ?
mickthompson

พวกเขาจะผ่านไป - แม้ว่า เช่นเดียวกันตัวอักษรภาษาญี่ปุ่น ฯลฯ
Andreas Petersson

ขอบคุณ Andreas มีวิธีลบสิ่งเหล่านี้หรือไม่? อักขระเช่นらがなを覚男 (หรืออื่น ๆ ) จะรวมอยู่ในสตริงที่สร้างขึ้นและสิ่งเหล่านี้จะทำลายเอาต์พุตโดยทั่วไป ฉันกำลังพยายามใช้เอาต์พุต simplifiedString เป็นตัวสร้าง URL เหมือนที่ StackOverflow ทำกับ URL ของคำถาม
mickthompson

2
ตามที่ผมกล่าวไว้ในความคิดเห็นของคำถาม คุณไม่สามารถพึ่งพาการลบเครื่องหมายกำกับเสียงขั้นพื้นฐานได้หากคุณต้องการเครื่องมือค้นหาที่ดี
Eduardo

3
ขอบคุณ Andreas ทำงานได้อย่างมีเสน่ห์! (การทดสอบเกี่ยวกับ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

แพ็กเกจ java.text หลักได้รับการออกแบบมาเพื่อจัดการกับกรณีการใช้งานนี้ (การจับคู่สตริงโดยไม่ต้องคำนึงถึงตัวกำกับเสียงตัวพิมพ์และอื่น ๆ )

กำหนดค่าCollatorเพื่อเรียงลำดับตามPRIMARYความแตกต่างของอักขระ จากนั้นให้สร้างCollationKeyสตริงสำหรับแต่ละสตริง หากโค้ดทั้งหมดของคุณอยู่ใน Java คุณสามารถใช้ไฟล์CollationKey. หากคุณจำเป็นต้องเก็บกุญแจในฐานข้อมูลหรืออื่น ๆ ที่จัดเรียงของดัชนีคุณสามารถแปลงเป็นแถว byte

คลาสเหล่านี้ใช้ข้อมูลการพับกรณีมาตรฐาน Unicodeเพื่อพิจารณาว่าอักขระใดเทียบเท่าและสนับสนุนกลยุทธ์การสลายตัวต่างๆ

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"

โปรดทราบว่า collators นั้นขึ้นอยู่กับโลแคล เนื่องจาก "ลำดับตัวอักษร" แตกต่างกันระหว่างภาษา (และเมื่อเวลาผ่านไปเช่นเดียวกับในภาษาสเปน) Collatorระดับบรรเทาคุณจากที่มีการติดตามทั้งหมดของกฎเหล่านี้และให้พวกเขาได้ถึงวันที่


ฟังดูน่าสนใจ แต่คุณสามารถค้นหารหัสการจัดเรียงของคุณในฐานข้อมูลโดยเลือก * จากบุคคลที่ collated_name เช่น 'bjo%' ??
Andreas Petersson

ดีมากไม่รู้เกี่ยวกับเรื่องนั้น จะลองดู
Andreas Petersson

บน Android ไม่สามารถใช้ CollationKeys เป็นคำนำหน้าสำหรับการค้นหาฐานข้อมูล คีย์การจัดเรียงของสตริงaจะเปลี่ยนเป็นไบต์ 41, 1, 5, 1, 5, 0 แต่สตริงabจะเปลี่ยนเป็นไบต์ 41, 43, 1, 6, 1, 6, 0 ลำดับไบต์เหล่านี้ไม่ปรากฏตามที่เป็นอยู่ ในคำเต็ม (อาร์เรย์ไบต์สำหรับคีย์การจัดเรียงaไม่ปรากฏในอาร์เรย์ไบต์สำหรับคีย์การจัดเรียงสำหรับab)
Grzegorz Adam Hankiewicz

1
@GrzegorzAdamHankiewicz หลังจากการทดสอบบางส่วนฉันเห็นว่าสามารถเปรียบเทียบอาร์เรย์ไบต์ได้ แต่ไม่ต้องสร้างคำนำหน้าอย่างที่คุณระบุ ดังนั้นในการทำแบบสอบถามคำนำหน้าเช่นbjo%คุณจะต้องทำการสืบค้นช่วงโดยที่ collators คือ> = bjoและ < bjp(หรือสัญลักษณ์อะไรก็ตามที่อยู่ถัดไปจะอยู่ในโลแคลนั้นและไม่มีวิธีทางโปรแกรมที่จะระบุได้)
erickson

16

เป็นส่วนหนึ่งของApache Commons Langในเวอร์ชัน 3.1.

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

ผลตอบแทน An


1
สำหรับØจะให้อีกครั้งØ
Mike Argyriou

2
ขอบคุณไมค์ที่ชี้ให้เห็น วิธีนี้จัดการเฉพาะสำเนียงเท่านั้น ผลลัพธ์ของ "ńǹňñṅņṇṋṉ̈ɲƞᶇɳȵ" คือ "nnnnnnnnn ɲƞᶇɳȵ"
Kenston Choi

12

คุณสามารถใช้คลาส Normalizer ได้จากjava.text:

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

แต่ยังมีงานที่ต้องทำเนื่องจาก Java สร้างสิ่งแปลก ๆ ด้วยอักขระ Unicode ที่ไม่สามารถแปลงกลับได้ (ไม่ได้เพิกเฉยต่อพวกเขาและจะไม่เกิดข้อยกเว้น) แต่ฉันคิดว่าคุณสามารถใช้เป็นจุดเริ่มต้นได้


3
สิ่งนี้จะใช้ไม่ได้กับการกำกับเสียงที่ไม่ใช่ ascii เช่นในภาษารัสเซียพวกเขามีตัวกำกับเสียงด้วยและนอกจากนี้ยังใช้งานได้กับสตริงเอเชียทั้งหมด ไม่ได้ใช้. แทนที่จะแปลงเป็น ascii ให้ใช้ \\ p {InCombiningDiacriticalMarks} regexp ในคำตอบstackoverflow.com/questions/1453171/…
Andreas Petersson

10

มีรายงานฉบับร่างเกี่ยวกับการพับตัวอักษรบนเว็บไซต์ Unicode ซึ่งมีเนื้อหาที่เกี่ยวข้องจำนวนมาก ดูเฉพาะส่วน 4.1 "ขั้นตอนวิธีการพับ".

นี่คือการอภิปรายและการดำเนินการลบเครื่องหมายกำกับเสียงโดยใช้ Perl

คำถาม SO ที่มีอยู่เหล่านี้เกี่ยวข้อง:


5

โปรดทราบว่าเครื่องหมายเหล่านี้ไม่ใช่แค่ "เครื่องหมาย" บนอักขระ "ปกติ" บางตัวที่คุณสามารถลบออกได้โดยไม่ต้องเปลี่ยนความหมาย

ในภาษาสวีเดนåäและöเป็นอักขระชั้นหนึ่งที่แท้จริงและเหมาะสมไม่ใช่ "ตัวแปร" ของอักขระอื่น ๆ พวกเขาฟังดูแตกต่างจากอักขระอื่น ๆ ทั้งหมดเรียงลำดับต่างกันและทำให้คำเปลี่ยนความหมาย ("mätt" และ "matt" เป็นคำสองคำที่ต่างกัน)


4
แม้ว่าจะถูกต้อง แต่นี่เป็นความคิดเห็นมากกว่าคำตอบสำหรับคำถาม
Simon Forsberg

2

Unicode มีอักขระไดอะตริกเฉพาะ (ซึ่งเป็นอักขระผสม) และสามารถแปลงสตริงเพื่อให้อักขระและไดเมตริกแยกจากกัน จากนั้นคุณสามารถลบ diatricts ออกจากสตริงและคุณก็ทำเสร็จแล้ว

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการฟื้นฟูการสลายตัวและความเท่าเทียมกันให้ดูมาตรฐาน Unicode ที่หน้าแรกของ Unicode

อย่างไรก็ตามคุณจะบรรลุสิ่งนี้ได้อย่างไรนั้นขึ้นอยู่กับกรอบงาน / OS / ... ที่คุณกำลังดำเนินการอยู่ หากคุณกำลังใช้. NET คุณสามารถใช้เมธอดString.NormalizeยอมรับการแจงนับSystem.Text.NormalizationForm


2
นี่เป็นวิธีที่ฉันใช้ใน. NET แม้ว่าฉันจะต้องแมปอักขระบางตัวด้วยตนเอง พวกเขาไม่ใช่ตัวกำกับเสียง แต่เป็นดิจิกราฟ ปัญหาที่คล้ายกันแม้ว่า
Thorarin

1
แปลงเป็นรูปแบบนอร์มัลไลเซชัน "D" (เช่นถูกย่อยสลาย) และใช้อักขระพื้นฐาน
Richard

2

วิธีที่ง่ายที่สุด (สำหรับฉัน) คือการรักษาอาร์เรย์การแมปแบบกระจัดกระจายซึ่งเพียงแค่เปลี่ยนจุดรหัส Unicode ของคุณเป็นสตริงที่แสดงได้

เช่น:

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"
}
: : :

การใช้อาร์เรย์แบบกระจัดกระจายจะช่วยให้คุณแสดงการแทนที่ได้อย่างมีประสิทธิภาพแม้ว่าจะอยู่ในส่วนที่เว้นระยะห่างของตาราง Unicode ก็ตาม การแทนที่สตริงจะอนุญาตให้ลำดับตามอำเภอใจแทนที่การกำกับเสียงของคุณ (เช่นการæกลายเป็นกราฟae)

นี่เป็นคำตอบที่ไม่เชื่อเรื่องพระเจ้าดังนั้นหากคุณมีภาษาเฉพาะในใจก็จะมีวิธีที่ดีกว่านี้ (แม้ว่าพวกเขาทั้งหมดจะลงมาที่ระดับต่ำสุดก็ตาม)


การเพิ่มอักขระแปลก ๆ ที่เป็นไปได้ทั้งหมดไม่ใช่เรื่องง่าย เมื่อทำเช่นนี้เพียงไม่กี่ตัวอักษรก็เป็นทางออกที่ดี
Simon Forsberg

2

สิ่งที่ต้องพิจารณา: หากคุณพยายามหา "คำแปล" คำเดียวของแต่ละคำคุณอาจพลาดทางเลือกอื่นที่เป็นไปได้

ตัวอย่างเช่นในภาษาเยอรมันเมื่อแทนที่ "s-set" บางคนอาจใช้ "B" ในขณะที่บางคนอาจใช้ "ss" หรือแทนที่ umlauted o ด้วย "o" หรือ "oe" วิธีแก้ปัญหาที่คุณคิดขึ้นฉันคิดว่าควรรวมทั้งสองอย่าง


2

ใน Windows และ. NET ฉันเพิ่งแปลงโดยใช้การเข้ารหัสสตริง ด้วยวิธีนี้ฉันจะหลีกเลี่ยงการทำแผนที่และการเข้ารหัสด้วยตนเอง

ลองเล่นกับการเข้ารหัสสตริง


3
คุณสามารถอธิบายรายละเอียดเกี่ยวกับการเข้ารหัสสตริงได้หรือไม่? ตัวอย่างเช่นด้วยตัวอย่างรหัส
Peter Mortensen

2

ในกรณีของภาษาเยอรมันไม่ต้องการลบเครื่องหมายกำกับเสียงออกจาก Umlauts (ä, ö, ü) แต่จะถูกแทนที่ด้วยการผสมตัวอักษรสองตัว (ae, oe, ue) ตัวอย่างเช่นBjörnควรเขียนเป็น Bjoern (ไม่ใช่ Bjorn) เพื่อให้มีการออกเสียงที่ถูกต้อง

สำหรับสิ่งนั้นฉันจะค่อนข้างมีการแมปแบบฮาร์ดโค้ดซึ่งคุณสามารถกำหนดกฎการแทนที่ทีละกลุ่มสำหรับกลุ่มอักขระพิเศษแต่ละกลุ่ม


0

สำหรับการอ้างอิงในอนาคตนี่คือวิธีการขยาย C # ที่ลบสำเนียง

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");
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.