在Unicode中找到字形相似的字符?

18
假设我有字符 Ú、Ù、Ü。所有这些字符在视觉上都与英文字母 U 相似。
是否有一些列表或算法可以实现以下操作:
  • 给定 Ú、Ù 或 Ü,返回英文字母 U
  • 给定英文字母 U,返回所有类似 U 的字符的列表
我不确定 Unicode 字符的代码点是否在所有字体中都相同?如果是,那么可能会有一些简单和高效的方法来完成这个任务。
更新:
如果您正在使用 Ruby,则可使用 gem unicode-confusable 来帮助处理某些情况。

1
可能是将符号、重音字母转换为英文字母的问题的重复。 - Joachim Sauer
1
你看过unidecode模块吗?http://pypi.python.org/pypi/Unidecode - Thomas K
1
Unicode概念中的“混淆字符”在这里也值得一提;请参见a demofull listtechnical report - Shervin
1
@Shervin 我本来也想发表同样的评论,但意识到 confusables 几乎是完全相似的,而不是带重音符号的版本。(例如:) - karatedog
@karatedog 正确。我同意仅查看混淆可能不够,但它提供了Ù → U + ̀ 的信息。获取这样的信息更容易的方法可能是归一化 - Shervin
显示剩余2条评论
3个回答

33

非常不清楚您想要做什么。

  • 有些字符的规范分解都以相同的基础字符开头:e、é、ê、ë、ē、ĕ、ė、ę、ě、ȅ、ȇ、ȩ、ḕ、ḗ、ḙ、ḛ、ḝ、ẹ、ẻ、ẽ、ế、ề、ể、ễ、ệ、e̳、…s、ś、ŝ、ş、š、ș、ṡ、ṣ、ṥ、ṧ、ṩ、…。

  • 有些字符的兼容分解都包含特定字符:ᵉ、ₑ、ℯ、ⅇ、⒠、ⓔ、㋍、㋎、e、…s、ſ、ˢ、ẛ、₨、℁、⒮、ⓢ、㎧、㎨、㎮、㎯、㎰、㎱、㎲、㎳、㏛、ſt、st、s、…R、ᴿ、₨、ℛ、ℜ、ℝ、Ⓡ、㏚、R、…。

  • 在某些字体中会出现 长得很像的 字符,例如:ß、β 和 ϐ,或者 3、Ʒ、Ȝ、ȝ、ʒ、ӡ和 ᴣ,或者ɣ、ɤ和γ,或者 F、Ϝ和 ϝ,或者 B、Β和В,或者∅、○、0、O、০、੦、౦和૦,或者 1、l、I、Ⅰ、ᛁ、|、ǀ和∣等等。

  • 忽略大小写后相同的字符,例如 s、S和ſ,或者 ss、Ss、SS、ß和ẞ等等。

  • 所有具有相同的 数字值 的字符,例如所有这些字符都为数字 1:1¹١۱߁१১੧૧୧௧౧౹౼೧൧๑໑༡၁႑፩១៱᠑᥇᧑᧚᪁᪑᭑᮱᱁᱑₁⅟ ① ⑴ ⒈ ⓵ ❶➀➊꘡꣑꤁꧑꩑꯱ Ⅰⅰꛦ㆒㈠㊀等等。

  • 具有相同主要排序规则的字符,例如以下这些字母都与d相同: DdÐðĎďĐđ◌ͩᴰᵈᶞ◌ᷘ◌ᷙḊḋḌḍḎḏḐḑḒḓⅅⅆⅮⅾ Ⓓ ⓓ ꝹꝺDd。请注意,其中一些字符无法通过任何形式的分解访问,只能通过DUCET/UCA值进行比较;例如,常见的 ð 或新的 ꝺ 只能通过主要的UCA强度比较等同于 d;类似地,ƶ 和 z、ȼ 和 c等也是如此。

  • 在某些语境中完全相同的字符,例如 æ 和 ae, or ä 和 ae, or ä 和 aa, 或 MacKinley 和 McKinley, …。请注意,语境可能会有很大的影响,因为在某些语境中,c 和 ç 都是相同的字符,而在其他语境中不是;n 和 ñ、a 和 á 和 ã 等情况也是如此。

  • 其中一些可以处理,但有些无法处理。全部需要根据不同需求采用不同的方法。

    你真正的目标是什么?


    +1 对所有内容的肯定,但最重要的是“你的真正目标是什么?”知道这一点对于找到正确的方法是必要的! - Joachim Sauer

    14

    这种方法并不适用于所有情况,但是消除大多数重音的一种方法是将字符转换为其分解形式,然后丢弃组合重音符:

    # coding: utf8
    import unicodedata as ud
    s=u'U, Ù, Ú, Û, Ü, Ũ, Ū, Ŭ, Ů, Ű, Ų, Ư, Ǔ, Ǖ, Ǘ, Ǚ, Ǜ, Ụ, Ủ, Ứ, Ừ, Ử, Ữ, Ự'
    print ud.normalize('NFD',s).encode('ascii','ignore')
    

    输出

    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U
    

    要查找重音字符,可以使用类似以下的方法:

    import unicodedata as ud
    import string
    
    def asc(unichr):
        return ud.normalize('NFD',unichr).encode('ascii','ignore')
    
    U = u''.join(unichr(i) for i in xrange(65536))
    for c in string.letters:
        print u''.join(u for u in U if asc(u) == c)
    

    输出

    aàáâãäåāăąǎǟǡǻȁȃȧḁạảấầẩẫậắằẳẵặ
    bḃḅḇ
    cçćĉċčḉ
    dďḋḍḏḑḓ
    eèéêëēĕėęěȅȇȩḕḗḙḛḝẹẻẽếềểễệ
    fḟ
     :
    etc.
    

    1
    我真希望你的源文本中不包含æ、破折号、CJK文本、表情符号或其他数以万计与英文字母无关的Unicode字符。 - Eevee

    6

    为什么不试着用类似这样的方式来比较字形呢?

    package similarglyphcharacterdetector;
    
    import java.awt.Color;
    import java.awt.Font;
    import java.awt.Graphics2D;
    import java.awt.Rectangle;
    import java.awt.font.FontRenderContext;
    import java.awt.image.BufferedImage;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    public class SimilarGlyphCharacterDetector {
    
        static char[] TEST_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890".toCharArray();
        static BufferedImage[] SAMPLES = null;
    
        public static BufferedImage drawGlyph(Font font, String string) {
            FontRenderContext frc = ((Graphics2D) new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY).getGraphics()).getFontRenderContext();
    
            Rectangle r= font.getMaxCharBounds(frc).getBounds();
    
            BufferedImage res = new BufferedImage(r.width, r.height, BufferedImage.TYPE_BYTE_GRAY);
            Graphics2D g = (Graphics2D) res.getGraphics();
            g.setBackground(Color.WHITE);
            g.fillRect(0, 0, r.width, r.height);
            g.setPaint(Color.BLACK);
            g.setFont(font);
            g.drawString(string, 0, r.height - font.getLineMetrics(string, g.getFontRenderContext()).getDescent());
            return res;
        }
    
        private static void drawSamples(Font f) {
            SAMPLES = new BufferedImage[TEST_CHARS.length];
            for (int i = 0; i < TEST_CHARS.length; i++)
                SAMPLES[i] = drawGlyph(f, String.valueOf(TEST_CHARS[i]));
        }
    
        private static int compareImages(BufferedImage img1, BufferedImage img2) {
            if (img1.getWidth() != img2.getWidth() || img1.getHeight() != img2.getHeight())
                throw new IllegalArgumentException();
            int d = 0;
            for (int y = 0; y < img1.getHeight(); y++) {
                for (int x = 0; x < img1.getWidth(); x++) {
                    if (img1.getRGB(x, y) != img2.getRGB(x, y))
                        d++;
                }
            }
            return d;
        }
    
        private static int nearestSampleIndex(BufferedImage image, int maxDistance) {
            int best = Integer.MAX_VALUE;
            int bestIdx = -1;
            for (int i = 0; i < SAMPLES.length; i++) {
                int diff = compareImages(image, SAMPLES[i]);
                if (diff < best) {
                    best = diff;
                    bestIdx = i;
                }
            }
            if (best > maxDistance)
                return -1;
            return bestIdx;
        }
    
        public static void main(String[] args) throws Exception {
            Font f = new Font("FreeMono", Font.PLAIN, 13);
            drawSamples(f);
            HashMap<Character, StringBuilder> res = new LinkedHashMap<Character, StringBuilder>();
            for (char c : TEST_CHARS)
                res.put(c, new StringBuilder(String.valueOf(c)));
            int maxDistance = 5;
            for (int i = 0x80; i <= 0xFFFF; i++) {
                char c = (char)i;
                if (f.canDisplay(c)) {
                    int n = nearestSampleIndex(drawGlyph(f, String.valueOf(c)), maxDistance);
                    if (n != -1) {
                        char nc = TEST_CHARS[n];
                        res.get(nc).append(c);
                    }
                }
            }
            for (Map.Entry<Character, StringBuilder> entry : res.entrySet())
                if (entry.getValue().length() > 1)
                    System.out.println(entry.getValue());
        }
    }
    

    输出:

    AÀÁÂÃÄÅĀĂĄǍǞȀȦΆΑΛАѦӒẠẢἈἉᾸᾹᾺᾼ₳Å
    BƁƂΒБВЬḂḄḆ
    CĆĈĊČƇΓЄГСὉℂⅭ
    ...
    

    好吃的黑客技巧。我怀疑它是否会在大多数字体中正确地区分Il1,或者0oO是不同的,同时仍然说所有那些像B一样的东西都是相同的。 - OrangeDog
    嘿,我怎么会跌入代码高尔夫中? - Charles Wood

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接