Java Unicode字符串长度

57

我正在努力获取 Unicode 字符串的计数,并尝试了各种选项。看起来是一个小问题,但却陷入了困境。

在这里,我试图获取字符串 str1 的长度。我得到的答案是 6,但实际上它应该是 3。将光标移动到字符串“குமார்”上也显示它只有 3 个字符。

基本上我想测量长度并打印每个字符,如“கு”,“மா”,“ர்”。

 public class one {
    public static void main(String[] args) {
            String str1 = new String("குமார்");
            System.out.print(str1.length());
    }
}

PS:这是泰米尔语。


18
иҝҷдёӘй—®йўҳ并дёҚдјҡжңүд»»дҪ•еҪұе“ҚпјҢдҪҶжҳҜжІЎжңүеҝ…иҰҒдҪҝз”Ё new String("...")пјҢеҸӘйңҖиҰҒиҝҷж ·еҒҡпјҡString str1 = "а®•аҜҒа®®а®ҫа®°аҜҚ";гҖӮ - Jesper
5
请参见http://www.venkatarangan.com/blog/content/binary/Counting%20Letters%20in%20an%20Unicode%20String.pdf,了解有关此问题的论文。 - halex
博客非常有信息量,但是它没有给我们提供Java中将字符串分割成三个有意义的字符的选项。 - user1611248
Twitter在这里提供了非常好的计算字符的指南:https://dev.twitter.com/docs/counting-characters - benathon
关于Tamil编码的论文存档链接(@halex),Twitter开发者指南(@portforwardpodcast)以及从Twitter指南链接的Java代码示例(https://web.archive.org/web/20071031003231/http://www.unicode.org/reports/tr15/Normalizer.html)。 - Joshua Goldberg
5个回答

43

找到了解决您问题的方法。

根据这个SO答案,我编写了一个程序,使用正则表达式字符类来搜索可能具有可选修饰符的字母。它将您的字符串拆分为单个(如果必要,则组合)字符,并将它们放入列表中:

import java.util.*;
import java.lang.*;
import java.util.regex.*;

class Main
{
    public static void main (String[] args)
    {
        String s="குமார்";
        List<String> characters=new ArrayList<String>();
        Pattern pat = Pattern.compile("\\p{L}\\p{M}*");
        Matcher matcher = pat.matcher(s);
        while (matcher.find()) {
            characters.add(matcher.group());            
        }

        // Test if we have the right characters and length
        System.out.println(characters);
        System.out.println("String length: " + characters.size());

    }
}

\\p{L}表示Unicode字母,\\p{M}表示Unicode标记。

代码片段的输出结果为:

கு
மா
ர்
String length: 3

请查看 https://ideone.com/Apkapn 获取一个运行演示。


编辑

我现在使用了来自http://en.wikipedia.org/wiki/Tamil_script的表格中所有有效泰米尔字母检查了我的正则表达式。我发现当前的正则表达式无法正确捕获所有字母(Grantha复合表中最后一行的每个字母都会被拆分为两个字母),因此我将正则表达式改进为以下解决方案:

Pattern pat = Pattern.compile("\u0B95\u0BCD\u0BB7\\p{M}?|\\p{L}\\p{M}?");

使用这种模式,您应该能够将句子分割成每个有效的泰米尔文字(只要维基百科的表格是完整的)。

我用于检查的代码如下:

String s = "ஃஅஆஇஈஉஊஎஏஐஒஓஔக்ககாகிகீகுகூகெகேகைகொகோகௌங்ஙஙாஙிஙீஙுஙூஙெஙேஙைஙொஙோஙௌச்சசாசிசீசுசூசெசேசைசொசோசௌஞ்ஞஞாஞிஞீஞுஞூஞெஞேஞைஞொஞோஞௌட்டடாடிடீடுடூடெடேடைடொடோடௌண்ணணாணிணீணுணூணெணேணைணொணோணௌத்ததாதிதீதுதூதெதேதைதொதோதௌந்நநாநிநீநுநூநெநேநைநொநோநௌப்பபாபிபீபுபூபெபேபைபொபோபௌம்மமாமிமீமுமூமெமேமைமொமோமௌய்யயாயியீயுயூயெயேயையொயோயௌர்ரராரிரீருரூரெரேரைரொரோரௌல்லலாலிலீலுலூலெலேலைலொலோலௌவ்வவாவிவீவுவூவெவேவைவொவோவௌழ்ழழாழிழீழுழூழெழேழைழொழோழௌள்ளளாளிளீளுளூளெளேளைளொளோளௌற்றறாறிறீறுறூறெறேறைறொறோறௌன்னனானினீனுனூனெனேனைனொனோனௌஶ்ஶஶாஶிஶீஶுஶூஶெஶேஶைஶொஶோஶௌஜ்ஜஜாஜிஜீஜுஜூஜெஜேஜைஜொஜோஜௌஷ்ஷஷாஷிஷீஷுஷூஷெஷேஷைஷொஷோஷௌஸ்ஸஸாஸிஸீஸுஸூஸெஸேஸைஸொஸோஸௌஹ்ஹஹாஹிஹீஹுஹூஹெஹேஹைஹொஹோஹௌக்ஷ்க்ஷக்ஷாக்ஷிக்ஷீக்ஷுக்ஷூக்ஷெக்ஷேக்ஷைஷொக்ஷோஷௌ";
List<String> characters = new ArrayList<String>();
Pattern pat = Pattern.compile("\u0B95\u0BCD\u0BB7\\p{M}?|\\p{L}\\p{M}?");
Matcher matcher = pat.matcher(s);
while (matcher.find()) {
    characters.add(matcher.group());
}

System.out.println(characters);
System.out.println(characters.size() == 325);

1
是的,我不知道它是否处理了泰米尔语中可能发生的所有情况,但它绝对优雅。 - Mifeet
1
非常感谢。是的,你说得对。只有 Grantha 表中的最后一行由两个字母组成。即 3 - 4 个 Unicode 符号。你在维基百科上提到的表是正确的。这是完整的列表。 - user1611248
如果我想要包括像“_”这样的标点符号,应该使用什么正则表达式呢?例如,“குமார_கு”应该返回计数5。 - user1611248
3
请将正则表达式中加入 |\\p{P}\\p{P} 是指标点符号。请参见 https://ideone.com/NvfDDq。 - halex
可能需要更多的标点符号。例如,空格/换行符? - Joshua Goldberg

15

请查看Normalizer类。其中有解释可能导致您问题的原因。在Unicode中,您可以使用多种方式编码字符,例如Á

  U+00C1    LATIN CAPITAL LETTER A WITH ACUTE
或者
  U+0041    LATIN CAPITAL LETTER A
  U+0301    COMBINING ACUTE ACCENT

您可以尝试使用Normalizer将字符串转换为组合形式,然后迭代字符。


编辑: 根据@halex上面提出的文章,在Java中尝试如下:

    String str = new String("குமார்");

    ArrayList<String> characters = new ArrayList<String>();
    str = Normalizer.normalize(str, Form.NFC);
    StringBuilder charBuffer = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        int codePoint = str.codePointAt(i);
        int category = Character.getType(codePoint);
        if (charBuffer.length() > 0
                && category != Character.NON_SPACING_MARK
                && category != Character.COMBINING_SPACING_MARK
                && category != Character.CONTROL
                && category != Character.OTHER_SYMBOL) {
            characters.add(charBuffer.toString());
            charBuffer.delete(0, charBuffer.length());
        }
        charBuffer.appendCodePoint(codePoint);
    }
    if (charBuffer.length() > 0) {
        characters.add(charBuffer.toString());
    }
    System.out.println(characters);

我得到的结果是[கு, மா, ர்]。如果对于您的所有字符串都不起作用,请尝试在if块中调整其他Unicode字符类别。


4
尝试标准化字符串并测量长度。仍然得到6个字符的结果。如果浏览器编辑器可以通过光标导航将其识别为3个字符,我们难道没有Java中获取它的标准方法吗? - user1611248
2
在这种情况下不正确,但对于其他问题是一个好的提示。+1 - Thorsten S.
1
该文章还提到了“KSha”、“Sri”和“Ayudham”。我想这些需要作为特殊情况处理。 - Mifeet
4
当字符串中每个字母都有一个预组合字母时,规范化才是唯一的解决方案。在Unicode中,预组合字母非常罕见,几乎只存在于拉丁字母表中(并且大多数是为了与传统的非Unicode编码进行兼容)。 - Joachim Sauer
我认为字符的顺序可能存在问题。我检查了排序算法,你是正确的,规范化是多余的。 - Mifeet
显示剩余2条评论

8

这真的很丑陋...我已经调试了你的字符串并发现以下字符(以及它们的十六进制位置):

க 0x0b95
ு 0x0bc1
ம 0x0bae
ா 0x0bbe
ர 0x0bb0
் 0x0bcd

因此,泰米尔语使用类似变音符号的序列来获得所有字符,而这些字符不幸地被计算为单独的实体。

这不是UTF-8 / UTF-16的问题,尽管其他答案错误地声称是这样,而是Unicode编码泰米尔语中固有的问题。

所建议的正常化程序不起作用,似乎泰米尔语已被Unicode "专家"明确设计为使用不能规范化的组合序列。 真恼火。

我的下一个想法是不要计算字符,而是计算字形,即字符的视觉表示。

String str1 = new String(Normalizer.normalize("குமார்", Normalizer.Form.NFC ));

Font display = new Font("SansSerif",Font.PLAIN,12);
GlyphVector vec = display.createGlyphVector(new FontRenderContext(new AffineTransform(),false, false),str1);

System.out.println(vec.getNumGlyphs());
for (int i=0; i<str1.length(); i++)
        System.out.printf("%s %s %s %n",str1.charAt(i),Integer.toHexString((int) str1.charAt(i)),vec.getGlyphVisualBounds(i).getBounds2D().toString());

结果为:

க b95 [x=0.0,y=-6.0,w=7.0,h=6.0]
ு bc1 [x=8.0,y=-6.0,w=7.0,h=4.0]
ம bae [x=17.0,y=-6.0,w=6.0,h=6.0]
ா bbe [x=23.0,y=-6.0,w=5.0,h=6.0]
ர bb0 [x=30.0,y=-6.0,w=4.0,h=8.0]
் bcd [x=31.0,y=-9.0,w=1.0,h=2.0]

由于字形相交,您需要使用Java字符类型函数,就像其他解决方案一样。

解决方案:

我正在使用此链接: http://www.venkatarangan.com/blog/content/binary/Counting%20Letters%20in%20an%20Unicode%20String.pdf

public static int getTamilStringLength(String tamil) {
    int dependentCharacterLength = 0;
    for (int index = 0; index < tamil.length(); index++) {
        char code = tamil.charAt(index);
        if (code == 0xB82)
            dependentCharacterLength++;
        else if (code >= 0x0BBE && code <= 0x0BC8)
            dependentCharacterLength++;
        else if (code >= 0x0BCA && code <= 0x0BD7)
            dependentCharacterLength++;
    }
    return tamil.length() - dependentCharacterLength;
  }

您需要排除组合字符并相应地计数。


2
如上所述,您的字符串包含6个不同的码点。其中一半是字母,另一半是元音符号(组合标记)。
您可以使用ICU4J库中内置的转换来删除所有不是字母的元音符号,使用规则:
[:^Letter:] Remove 并计算结果字符串的数量。在其演示网站上尝试一下:

http://demo.icu-project.org/icu-bin/translit

我不会将结果字符串显示给最终用户,而且我也不是专家,所以规则可能需要进行微调才能适应一般情况,但这只是一个想法。

5
它包含6个字符或3个字符完全取决于你对“字符”的定义。不幸的是,这个词没有被很好地定义,并且以各种不兼容的方式使用。只有当你把“字符”理解为“码点”时,你的陈述才是正确的。 - user395760

0

这是一种新的计算Java字符串长度的方法,考虑到Unicode字符。

int unicodeLength = str.codePointCount(0, str.length);

这个由三个泰米尔字符组成的字符串包含6个代码点,如果使用codePointCount()codePoints()查看它,与str.length()得到的结果相同。但是在其他语言中可能会有所不同。(我认为这就是代码点的意图。) - Joshua Goldberg

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