C# Разделить строку со смешанным языком на разные языковые блоки
Я пытаюсь решить проблему, где у меня есть строка со смешанным языком в качестве ввода.
Например, "Hyundai Motor Company 현대자동차 现代 Некоторые другие английские слова"
И я хочу разбить строку на разные языковые фрагменты.
Например, ["Hyundai Motor Company", "현대자동차", "现代", "Некоторые другие английские слова"]
ИЛИ (знаки пробела и пунктуации и порядок не имеют значения)
["Hyundai Motor Company", "현대자동차", "现代", "Some other English words"]
Есть ли простой способ решить эту проблему? Или любой пакет сборки /nuget, который я могу использовать?
Спасибо
Редактировать: я понял, что мои "языковые куски" неоднозначны. То, что я хочу от "кусков языка", это наборы языковых символов.
Например, "Hyundai Motor Company" в английском наборе символов, "현대 자동차" в корейском наборе, "现代" в китайском наборе, "Некоторые другие английские слова" в английском наборе.
Дополнения, чтобы уточнить требование моей проблемы:
1: у ввода могут быть пробелы или любые другие знаки препинания, но я всегда могу использовать регулярные выражения, чтобы их игнорировать.
2: я буду предварительно обрабатывать ввод, чтобы игнорировать диакритические знаки. Таким образом, "å" становится "а" в моем входе. Таким образом, все английские символы станут английскими.
Что я действительно хочу, так это найти способ разбить входные данные на разные наборы языковых символов, игнорируя пробелы и знаки препинания.
Например, из "HyundaiMotorCompany 现代 자동차 现代 Some other English words"
В ["Hyundai Motor Company", "현대자동차", "现代", "Some other English words"]
3 ответа
Языковые блоки могут быть определены с использованием блоков UNICODE. Текущий список блоков UNICODE доступен по адресу ftp://www.unicode.org/Public/UNIDATA/Blocks.txt. Вот выдержка из списка:
0000..007F; Базовая латынь 0080..00FF; Приложение Latin-1 0100..017F; Латинская Расширенная-A 0180..024F; Латинская Расширенная-B 0250..02AF; IPA Расширения 02B0..02FF; Буквы модификатора интервалов 0300..036F; Объединение диакритических знаков 0370..03FF; Греческий и коптский 0400..04FF; кириллица 0500..052F; Дополнение кириллицы
Идея состоит в том, чтобы классифицировать символы, используя блок UNICODE. Последовательные символы, принадлежащие одному и тому же блоку UNICODE, определяют фрагмент языка.
Первая проблема с этим определением состоит в том, что то, что вы можете рассматривать как один скрипт (или язык), охватывает несколько блоков, таких как кириллица и дополнение кириллицы. Для этого вы можете объединить блоки, содержащие одно и то же имя, чтобы все латинские блоки были объединены в один латинский скрипт и т. Д.
Однако это создает несколько новых проблем:
- Следует ли объединить блоки греческого и коптского, коптского и греческого приложений в единый сценарий, или вы должны попытаться провести различие между греческим и коптским шрифтом?
- Вам, вероятно, следует объединить все блоки CJK. Однако, поскольку эти блоки содержат символы как китайского, так и японского и кандзи (корейского), вы не сможете различить эти сценарии, когда используются символы CJK.
Предполагая, что у вас есть план использования блоков UNICODE для классификации символов в сценарии, вам необходимо решить, как обрабатывать интервалы и знаки пунктуации. Символ пробела и несколько знаков препинания относятся к основному латинскому блоку. Однако другие блоки также могут содержать не буквенные символы.
Стратегия для решения этой проблемы состоит в том, чтобы "игнорировать" блок UNICODE из не буквенных символов, но включать их в куски. В вашем примере у вас есть два нелатинских блока, которые не содержат пробела или пунктуации, но многие скрипты будут использовать пробел так, как он используется в латинском скрипте, например, кириллица. Несмотря на то, что пробел классифицирован как латиница, вы все равно хотите, чтобы последовательность слов в кириллице, разделенных пробелами, рассматривалась как один кусок с использованием кириллического алфавита вместо кириллического слова, за которым следовал латинский пробел, а затем другое кириллическое слово и т. Д.
Наконец, вам нужно решить, как обрабатывать числа. Вы можете рассматривать их как пробел и пунктуацию или классифицировать их как блок, к которому они принадлежат, например, латинские цифры - латинские, а цифры деванагари - деванагари и т. Д.
Вот некоторый код, объединяющий все это. Сначала класс для представления скрипта (на основе блоков UNICODE, таких как "Greek and Coptic": 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
на ваш текст вернется что-то вроде этого:
Текст | скрипт ----------+---------------- Hyundai | латынь [пробел] | [пустой строки] Мотор | латынь [пробел] | [пустой строки] Компания | латынь [пробел] | [пустой строки] 자동차 자동차 | Слоги хангыль [пробел] | [пустой строки] 现代 | Дальневосточные...
Следующим шагом является объединение последовательных слов, принадлежащих одному и тому же сценарию, игнорируя пробел и пунктуацию:
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));
Это приведет к следующим кускам:
- Hyundai Motor Company
- 현대 자동차
- 现代
- Некоторые другие английские слова
Все куски кроме последнего имеют завершающий пробел.
Это проблема идентификации языка. Вам нужно использовать соответствующую библиотеку для этого. Есть пакет C#, который поддерживает 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,현대자동차,现代