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



我正在尝试解决一个问题,即我有一个混合语言作为输入的字符串。

例如"现代汽车公司현대자동차 现代其他一些英语单词">

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

例如 ["现代汽车公司"、"현대자동차"、"现代"、"其他一些英语单词"]

OR (空格/标点符号和顺序无关紧要)

["现代汽车公司", "현대자동차", "现代", "其他英语单词"]

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

谢谢

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

例如,"现代汽车公司"是英文字符集,"현대자동차"是韩文集,"现代"是中文集,"一些其他英语单词"是英文集。

澄清我的问题要求的补充是:

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

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

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

例如,来自"HyundaiMotorCompany현대자동차现代其他英语单词">

至 ["现代汽车公司", ">

현대자동차", "现代", "其他英语单词"]

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

0000..007F;基本拉丁语 0080..00FF;拉丁语-1 补充 0100..017F;拉丁语扩展-A 0180..024F;拉丁语扩展-B 0250..02AF;国际音标扩展 02B0..02FF;间距修饰符字母 0300..036F;组合变音符号 0370..03FF;希腊语和科普特语 0400..04FF;西里尔 0500..052F;西里尔文补充

这个想法是使用 UNICODE 块对字符进行分类。属于同一 UNICODE 块的连续字符定义语言

这个定义的第一个问题是,你可能认为单一的文字(或语言)跨越了几个块,如西里尔文西里尔文补充。要解决此问题,您可以合并包含相同名称的块,以便将所有拉丁块合并为单个拉丁脚本等。

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

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

假设您有一个如何使用 UNICODE 块将字符分类到脚本中的计划,那么您必须决定如何处理间距和标点符号。空格字符和几种标点符号形式属于基本拉丁语块。但是,其他块也可能包含非字母字符。

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

最后,您需要决定如何处理数字。您可以将它们视为空格和标点符号,或将它们归类为它们所属的块,例如拉丁数字是拉丁数字,而梵文数字是文等。

这是将所有这些放在一起的一些代码。首先是一个表示脚本的类(基于 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;
}

接下来是用于下载和分析 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 块。它将返回由属于同一脚本(元组的第二个元素)的连续字符组成的单词。scripts变量是上面定义的Scripts类的实例。

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将返回如下内容:

文本 |脚本 ----------+---------------- 现代 |拉丁语 [空间] |[空字符串] 电机 |拉丁语 [空间] |[空字符串] 公司简介 |拉丁语 [空间] |[空字符串] 현대자동차 |韩文音节 [空间] |[空字符串] 现代      |中日韩 ...

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

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

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

把所有的东西放在一起:

var chunks = JoinWords(SplitIntoWords(text));

这将导致以下块:

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

除最后一个块外,所有块都有一个尾随空格。

这是一个语言识别问题。为此,您需要使用适当的库。有一个C#包,它支持在维基百科和Twitter上训练的78种语言。但总的来说,Python更适合解决这类问题。对于Python,我可以推荐这个包。

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

据我从您的问题中了解到,您想区分英语和非英语(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,현대자동차,现代

最新更新