C# 将混合语言的字符串拆分为不同的语言块

C# Split a string with mixed language into different language chunks

我正在尝试解决输入为混合语言的字符串的问题。

E.g. "Hyundai Motor Company 현대자동차 现代 Some other English words"

我想将字符串拆分成不同的语言块

E.g. ["Hyundai Motor Company", "현대자동차", "现代", "Some other English words"]

OR(Space/Punctuation 标记和顺序无关紧要)

["HyundaiMotorCompany", "현대자동차", "现代", "SomeotherEnglishwords"]

有没有简单的方法可以解决这个问题? 或者我可以使用的任何 assembly/nuget 包?

谢谢

编辑: 我认为我的“语言块”是模棱两可的。 我想要的“语言块”是语言字符集。

For example "Hyundai Motor Company" is in English character set, "현대자동차" in Korean set, "现代" in Chinese set, "Some other English words" in English set.

补充说明我的问题的要求是:

1:输入可以有空格或任何其他标点符号,但我总是可以使用正则表达式来忽略它们。

2:我将预处理输入以忽略变音符号。所以“å”在我的输入中变成了“a”。所以所有的英文字符都会变成英文字符。

我真正想要的是找到一种方法将输入解析为不同的语言字符集,忽略空格和标点符号。

E.g. From "HyundaiMotorCompany현대자동차现代SomeotherEnglishwords"

To ["HyundaiMotorCompany", "현대자동차", "现代", "SomeotherEnglishwords"]

这是一个language identification problem. You need to use the proper library for this. There's C# package which supports 78 languages trained on Wikipedia and Twitter. But in general Python is better for solving such kind of problems. For Python I can recommend this package

因此,您需要将文本拆分为句子或单词,并应用文本检测算法来识别语言。接下来,您可以按语言对结果进行分组。

据我从你的问题中了解到,你想区分英语和非英语 (Unicode) 字符。我们可以在这里使用 [\x00-\x7F]+ 正则表达式。请注意,^ 用于非英语字符。

string input = "Hyundai Motor Company 현대자동차 现代 Some other English words";

string englishCharsPattern = "[\x00-\x7F]+";
var englishParts = Regex.Matches(input, englishCharsPattern)
                        .OfType<Match>()
                        .Where(m => !string.IsNullOrWhiteSpace(m.Groups[0].Value))
                        .Select(m => m.Groups[0].Value.Trim())
                        .ToList();

string nonEnglishCharsPattern = "[^\x00-\x7F]+";
var nonEnglishParts = Regex.Matches(input, nonEnglishCharsPattern)
                            .OfType<Match>()
                            .Select(m => m.Groups[0].Value)
                            .ToList();

var finalParts = englishParts;
finalParts.AddRange(nonEnglishParts);

Console.WriteLine(string.Join(",", finalParts.ToArray()));  

这给了我们:

Hyundai Motor Company,Some other English words,현대자동차,现代

语言块 可以使用 UNICODE 块定义。当前的 UNICODE 块列表可在 ftp://www.unicode.org/Public/UNIDATA/Blocks.txt 获得。以下是列表的摘录:

0000..007F; Basic Latin
0080..00FF; Latin-1 Supplement
0100..017F; Latin Extended-A
0180..024F; Latin Extended-B
0250..02AF; IPA Extensions
02B0..02FF; Spacing Modifier Letters
0300..036F; Combining Diacritical Marks
0370..03FF; Greek and Coptic
0400..04FF; Cyrillic
0500..052F; Cyrillic Supplement

想法是class使用 UNICODE 块来确定字符。属于同一 UNICODE 块的连续字符定义一个 语言块 .

这个定义的第一个问题是,您可能认为单个脚本(或 语言)跨越多个块,例如 西里尔文西里尔字母增补。要处理此问题,您可以合并包含相同名称的块,以便所有 Latin 块合并到单个 Latin 脚本等

但是,这会产生几个新问题:

  1. Greek and CopticCopticGreek Supplement 模块是否应该合并成一个单一文字还是您应该尝试区分希腊文字和科普特文字?
  2. 您可能应该合并所有 CJK 块。但是,由于这些块包含中文以及汉字(日语)和汉字(韩语)字符,因此在使用 CJK 字符时您将无法区分这些脚本。

假设您已计划如何使用 UNICODE 块 class将字符转换为脚本,那么您必须决定如何处理空格和标点符号。 space 字符和几种标点符号形式属于 基本拉丁语 块。但是,其他块也可能包含非字母字符。

处理这个问题的策略是 "ignore" 非字母字符的 UNICODE 块,但将它们包含在块中。在您的示例中,您有两个非拉丁语块恰好不包含 space 或标点符号,但许多脚本将使用 space ,因为它在拉丁语脚本中使用,例如西里尔。即使 space 被 classifed 为 拉丁文 ,您仍然希望将由 space 分隔的西里尔文单词序列视为单个块使用西里尔文字而不是西里尔文字后跟拉丁文 space 然后是另一个西里尔文字等

最后,您需要决定如何处理数字。您可以将它们视为 space 和标点符号或 class 将它们视为它们所属的块,例如拉丁文数字是 Latin 而 Devanagari 数字是 Devanagari

这里有一些代码将所有这些放在一起。首先是一个 class 来表示一个脚本(基于像 "Greek and Coptic" 这样的 UNICODE 块:0x0370 - 0x03FF):

public class Script
{
    public Script(int from, int to, string name)
    {
        From = from;
        To = to;
        Name = name;
    }

    public int From { get; }
    public int To { get; }
    public string Name { get; }

    public bool Contains(char c) => From <= (int) c && (int) c <= To;
}

下一步class 用于下载和解析 UNICODE 块文件。此代码在可能不理想的构造函数中下载文本。相反,您可以使用文件的本地副本或类似的东西。

public class Scripts
{
    readonly List<Script> scripts;

    public Scripts()
    {
        using (var webClient = new WebClient())
        {
            const string url = "ftp://www.unicode.org/Public/UNIDATA/Blocks.txt";
            var blocks = webClient.DownloadString(url);
            var regex = new Regex(@"^(?<from>[0-9A-F]{4})\.\.(?<to>[0-9A-F]{4}); (?<name>.+)$");
            scripts = blocks
                .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(line => regex.Match(line))
                .Where(match => match.Success)
                .Select(match => new Script(
                    Convert.ToInt32(match.Groups["from"].Value, 16),
                    Convert.ToInt32(match.Groups["to"].Value, 16),
                    NormalizeName(match.Groups["name"].Value)))
                .ToList();
        }
    }

    public string GetScript(char c)
    {
        if (!char.IsLetterOrDigit(c))
            // Use the empty string to signal space and punctuation.
            return string.Empty;
        // Linear search - can be improved by using binary search.
        foreach (var script in scripts)
            if (script.Contains(c))
                return script.Name;
        return string.Empty;
    }

    // Add more special names if required.
    readonly string[] specialNames = new[] { "Latin", "Cyrillic", "Arabic", "CJK" };

    string NormalizeName(string name) => specialNames.FirstOrDefault(sn => name.Contains(sn)) ?? name;
}

请注意,UNICODE 代码点 0xFFFF 以上的块将被忽略。如果您必须使用这些字符,则必须对我提供的代码进行大量扩展,这些代码假定 UNICODE 字符由 16 位值表示。

下一个任务是将字符串拆分为 UNICODE 块。它将 return 个由属于同一脚本的连续字符组成的字符串组成的单词(元组的第二个元素)。 scripts 变量是上面定义的 Scripts class 的实例。

public IEnumerable<(string text, string script)> SplitIntoWords(string text)
{
    if (text.Length == 0)
        yield break;
    var script = scripts.GetScript(text[0]);
    var start = 0;
    for (var i = 1; i < text.Length - 1; i += 1)
    {
        var nextScript = scripts.GetScript(text[i]);
        if (nextScript != script)
        {
            yield return (text.Substring(start, i - start), script);
            start = i;
            script = nextScript;
        }
    }
    yield return (text.Substring(start, text.Length - start), script);
}

对您的文本执行 SplitIntoWords 将 return 像这样:

Text      | Script
----------+----------------
Hyundai   | Latin
[space]   | [empty string]
Motor     | Latin
[space]   | [empty string]
Company   | Latin
[space]   | [empty string]
현대자동차 | Hangul Syllables
[space]   | [empty string]
现代      | CJK
...

下一步是连接属于同一脚本的连续单词,忽略 space 和标点符号:

public IEnumerable<string> JoinWords(IEnumerable<(string text, string script)> words)
{
    using (var enumerator = words.GetEnumerator())
    {
        if (!enumerator.MoveNext())
            yield break;
        var (text, script) = enumerator.Current;
        var stringBuilder = new StringBuilder(text);
        while (enumerator.MoveNext())
        {
            var (nextText, nextScript) = enumerator.Current;
            if (script == string.Empty)
            {
                stringBuilder.Append(nextText);
                script = nextScript;
            }
            else if (nextScript != string.Empty && nextScript != script)
            {
                yield return stringBuilder.ToString();
                stringBuilder = new StringBuilder(nextText);
                script = nextScript;
            }
            else
                stringBuilder.Append(nextText);
        }
        yield return stringBuilder.ToString();
    }
}

此代码将使用相同的脚本包括任何 space 和标点符号以及前面的单词。

综合起来:

var chunks = JoinWords(SplitIntoWords(text));

这将导致这些块:

  • 现代汽车公司
  • 현대자동차
  • 现代
  • 其他一些英文单词

除最后一个之外的所有块都有尾随 space。