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, определяют фрагмент языка.

Первая проблема с этим определением состоит в том, что то, что вы можете рассматривать как один скрипт (или язык), охватывает несколько блоков, таких как кириллица и дополнение кириллицы. Для этого вы можете объединить блоки, содержащие одно и то же имя, чтобы все латинские блоки были объединены в один латинский скрипт и т. Д.

Однако это создает несколько новых проблем:

  1. Следует ли объединить блоки греческого и коптского, коптского и греческого приложений в единый сценарий, или вы должны попытаться провести различие между греческим и коптским шрифтом?
  2. Вам, вероятно, следует объединить все блоки 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,현대자동차,现代
Другие вопросы по тегам