Эффективный способ извлечь только один элемент из строки с разделителями

ПРИМЕЧАНИЕ: я задал тот же вопрос вопрос здесь, но, поскольку некоторые люди пометили его как дубликат, хотя у него были хитрые, изящные решения, мне пришлось создать этот дополнительный (дублированный) вопрос, чтобы упростить его. для тех, кто сталкивается с подобными сомнениями. Добавлен вопрос, основанный на предложении других участников переполнения стека.

Каков эффективный способ анализа большой строки с разделителями, чтобы я мог получить доступ только к одному элементу из набора с разделителями без необходимости сохранять другие задействованные подстроки?

Меня особенно не интересует сохранение остальных значений элементов, как это делается при использовании метода Split(), поскольку вся эта информация не имеет отношения к рассматриваемой проблеме. Кроме того, я хочу сэкономить память, делая то же самое.

Постановка задачи.
Учитывая точную позицию с разделителями, мне нужно извлечь элемент, содержащийся в этой позиции, наиболее эффективным способом с точки зрения потребляемой памяти и времени.

Простой пример строки: "1,2,3,4,....,21,22,23,24"
Разделитель: ,
Позиция с разделителем: 22
Ответ ожидается: 23

Другой пример строки: "61d2e3f6-bcb7-4cd1-a81e-4f8f497f0da2;0;192.100.0.102:4362;2014-02-14;283;0;354;23;0;;;"" 0x8D15A2913C934DE"";Четверг, 19 июня 2014 г., 22:58:10 GMT;"
Разделитель: ;
Позиция с разделителями: 7
Ожидаемый ответ: 23


person re3el    schedule 18.01.2019    source источник
comment
Считаете ли вы, что использование split() отнимает много времени/памяти? если это так, вам следует отредактировать свой вопрос и добавить сравнения времени/памяти, используя разные способы, которые вы пробовали. например, если вы использовали регулярное выражение, добавьте выражение регулярного выражения и используемое время/память, добавьте split(), которое вы использовали, а также используемое время/память. Эта задача очень требовательна в вашем приложении?   -  person Mauricio Arias Olave    schedule 19.01.2019
comment
Прежде чем тратить много времени на оптимизацию, убедитесь, что решаете проблему, которая на самом деле является проблемой. У вас есть как минимум два миллиарда байтов доступного адресного пространства; какую часть этого вы тратите впустую со своим сплитом? Если ответ 0,00001%, то вы можете спросить себя, стоит ли тратить хотя бы одну минуту на решение этой задачи. Нагрузка на сбор данных, скорее всего, будет реальной проблемой, чем использование виртуальной памяти.   -  person Eric Lippert    schedule 19.01.2019


Ответы (5)


В документации для String.Split, хотя я написал следующее, прежде чем обнаружил это.

Один из способов сделать это — найти разделитель с String.IndexOf — вы можете указать индекс, с которого следует начать поиск, чтобы можно было пропускать элементы, не проверяя каждый символ. (Изучение каждого персонажа происходит за кулисами, но это немного быстрее, чем делать это самому.)

Я создал метод расширения, добавив в решение новый класс с именем «ExtensionMethods.cs» с таким содержимым:

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        /// <summary>
        /// Get the nth item from a delimited string.
        /// </summary>
        /// <param name="s">The string to retrieve a delimited item from.</param>
        /// <param name="delimiter">The character used as the item delimiter.</param>
        /// <param name="n">Zero-based index of item to return.</param>
        /// <returns>The nth item or an empty string.</returns>
        public static string Split(this string s, char delimiter, int n)
        {

            int pos = pos = s.IndexOf(delimiter);

            if (n == 0 || pos < 0)
            { return (pos >= 0) ? s.Substring(0, pos) : s; }

            int nDelims = 1;

            while (nDelims < n && pos >= 0)
            {
                pos = s.IndexOf(delimiter, pos + 1);
                nDelims++;
            }

            string result = "";

            if (pos >= 0)
            {
                int nextDelim = s.IndexOf(delimiter, pos + 1);
                result = (nextDelim < 0) ? s.Substring(pos + 1) : s.Substring(pos + 1, nextDelim - pos - 1);
            }

            return result;
        }

    }
}

И небольшая программа для проверки:

using System;
using System.Diagnostics;
using System.Linq;
using ExtensionMethods;

namespace ConsoleApp1
{

    class Program
    {

        static void Main(string[] args)
        {
            // test data...
            string s = string.Join(";", Enumerable.Range(65, 26).Select(c => (char)c));
            s = s.Insert(3, ";;;");

            string o = "";

            Stopwatch sw = new Stopwatch();

            sw.Start();
            for (int i = 1; i <= 1000000; i++) {
                o = s.Split(';', 21);
            }
            sw.Stop();
            Console.WriteLine("Item directly selected: " + sw.ElapsedMilliseconds);

            sw.Restart();
            for (int i = 1; i <= 1000000; i++) {
                o = s.Split(';')[21];
            }
            sw.Stop();
            Console.WriteLine("Item from split array:  " + sw.ElapsedMilliseconds + "\r\n");


            Console.WriteLine(s);
            Console.WriteLine(o);

            Console.ReadLine();

        }
    }
}

Пример вывода:

Непосредственно выбранный элемент: 1016
Элемент из разделенного массива: 1345

A;B;;;;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;Y;Z
S


Ссылка: Как реализовать и вызвать пользовательский метод расширения (Руководство по программированию на C#)

person Andrew Morton    schedule 18.01.2019
comment
Это какая-то удивительная разница, которую я вижу здесь :) Спасибо за это, не могли бы вы также добавить Regex в этот список? Я предполагаю, что это было бы достаточно убедительным из имеющихся вариантов. Я пробовал с Regex regex = new Regex(@"^(\w+,){21}(\w+)", RegexOptions.Singleline), но это не сработало, как ожидалось. Не могли бы вы попробовать отсюда, если вы не думали об этом? Спасибо еще раз! - person re3el; 19.01.2019
comment
@ re3el Первоначально я начал с решения для регулярных выражений, но сегодня мое регулярное выражение слабое. Возможно, вы хотите направить комментарий пользователю lagripe в исходной версии этого вопроса и предложить создать ответ здесь. . - person Andrew Morton; 19.01.2019
comment
Ваше решение работает быстрее, чем все другие решения, опубликованные на этой странице. Не могли бы вы проверить, видите ли вы то же самое? Не совсем уверен, как это может быть специфично для машины, но я просто не могу получить результаты тестов, опубликованные @Simonare из его ответа. - person re3el; 19.01.2019

попробуй это:

public static string MyExtension(this string s, char delimiter, int n)
{
    var begin = n== 0 ? 0 : Westwind.Utilities.StringUtils.IndexOfNth(s, delimiter, n);
    if (begin == -1)
        return null;
    var end = s.IndexOf(delimiter, begin +  (n==0?0:1));
    if (end == -1 ) end = s.Length;
    //var end = Westwind.Utilities.StringUtils.IndexOfNth(s, delimiter, n + 1);
    var result = s.Substring(begin +1, end - begin -1 );

    return result;
}

PS: используется библиотека Westwind.Utilities


Эталонный код:

void Main()
{

     string s = string.Join(";", Enumerable.Range(65, 26).Select(c => (char)c));
            s = s.Insert(3, ";;;");

            string o = "";

            Stopwatch sw = new Stopwatch();

            sw.Start();
            for (int i = 1; i <= 1000000; i++) {
                o = s.Split(';', 21);
            }
            sw.Stop();
            Console.WriteLine("Item directly selected: " + sw.ElapsedMilliseconds);


            sw.Restart();
            for (int i = 1; i <= 1000000; i++) {
                o = s.MyExtension(';', 21);
            }
            sw.Stop();
            Console.WriteLine("Item directly selected by MyExtension: " + sw.ElapsedMilliseconds);

            sw.Restart();
            for (int i = 1; i <= 1000000; i++) {
                o = s.Split(';')[21];
            }
            sw.Stop();
            Console.WriteLine("Item from split array:  " + sw.ElapsedMilliseconds + "\r\n");


            Console.WriteLine(s);
            Console.WriteLine(o);

}

public static class MyExtensions
{
    /// <summary>
    /// Get the nth item from a delimited string.
    /// </summary>
    /// <param name="s">The string to retrieve a delimited item from.</param>
    /// <param name="delimiter">The character used as the item delimiter.</param>
    /// <param name="n">Zero-based index of item to return.</param>
    /// <returns>The nth item or an empty string.</returns>
    public static string Split(this string s, char delimiter, int n)
    {

        int pos = pos = s.IndexOf(delimiter);

        if (n == 0 || pos < 0)
        { return (pos >= 0) ? s.Substring(0, pos) : s; }

        int nDelims = 1;

        while (nDelims < n && pos >= 0)
        {
            pos = s.IndexOf(delimiter, pos + 1);
            nDelims++;
        }

        string result = "";

        if (pos >= 0)
        {
            int nextDelim = s.IndexOf(delimiter, pos + 1);
            result = (nextDelim < 0) ? s.Substring(pos + 1) : s.Substring(pos + 1, nextDelim - pos - 1);
        }

        return result;
    }

    public static string MyExtension(this string s, char delimiter, int n)
    {
        var begin = n== 0 ? 0 : Westwind.Utilities.StringUtils.IndexOfNth(s, delimiter, n);
        if (begin == -1)
            return null;
        var end = s.IndexOf(delimiter, begin +  (n==0?0:1));
        if (end == -1 ) end = s.Length;
        //var end = Westwind.Utilities.StringUtils.IndexOfNth(s, delimiter, n + 1);
        var result = s.Substring(begin +1, end - begin -1 );

        return result;
    }

}

Полученные результаты:

Item directly selected: 277
Item directly selected by MyExtension: 114
Item from split array:  1297

A;B;;;;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;Y;Z
S

Редактировать: благодаря @Kalten я еще больше улучшил решение. В результатах тестов была замечена значительная разница.

person Derviş Kayımbaşıoğlu    schedule 18.01.2019
comment
Я думаю, что можно дать ссылку на https://github.com/rickstrahl/westwind.utilities — если вы каким-то образом связаны с ним (например, участник), просто скажите об этом. - person Andrew Morton; 19.01.2019
comment
Есть ли способ не перезапускать с начала строки для конечного индекса? - person Kalten; 19.01.2019
comment
Я нет, но я также собираюсь поделиться результатами тестов. - person Derviş Kayımbaşıoğlu; 19.01.2019
comment
@Kalten Спасибо, я еще больше улучшил свое решение. прошедшее время еще больше упало (было замечено значительное падение) - person Derviş Kayımbaşıoğlu; 19.01.2019
comment
Вам нужно проверить, не находитесь ли вы в конце строки (последний элемент может не иметь правильного разделителя). А также вы не можете выбрать первый столбец: p - person Kalten; 19.01.2019
comment
мотивация приносит успех :) пожалуйста, проверьте мой ответ - person Derviş Kayımbaşıoğlu; 19.01.2019
comment
Да, последний столбец теперь выглядит нормально. Но не первый. Вы должны искать begin сначала и после: if n == 0 && begin == -1, затем возвращать всю строку, if n == 0 && begin >= 0 затем возвращать подстроку от 0 до начала -1. Что-то в этом роде. - person Kalten; 19.01.2019
comment
Теперь они оба работают. Но я злюсь на себя за то, что не приложил к этому достаточно усилий в начале. Я, вероятно, улучшу его больше - person Derviş Kayımbaşıoğlu; 19.01.2019
comment
@Simonare Эта утилита кажется полезным открытием. Я думаю, что любые улучшения (например, разделение строки вместо символа) могут выйти за рамки того, о чем задавался вопрос. Конечно, если кому-то нужна позолоченная версия... :) - person Andrew Morton; 19.01.2019
comment
\o/ Для информации: исходный код IndexOfNth: здесь. Не более чем цикл for - person Kalten; 19.01.2019
comment
@Kalten, да, я уже копался, но я не хотел его удалять. Репозиторий выглядит полезным, и я считаю, что стоит поддержать энтузиастов-разработчиков :) - person Derviş Kayımbaşıoğlu; 19.01.2019
comment
@Simonare Теперь это заставляет меня задаться вопросом, что я сделал неэффективно в своей попытке, поскольку я ожидал, что использование IndexOf для пропуска частей будет быстрее, чем просто просмотр каждого символа в пользовательском коде. - person Andrew Morton; 19.01.2019
comment
Я тоже ищу, если что-то найдёшь, дай знать :) - person Derviş Kayımbaşıoğlu; 19.01.2019
comment
@AndrewMorton Взгляните на предварительную выборку кэша. Это часто дает лучший результат с последовательным циклом массива. С прогнозированием ветвлений вам всегда нужно запускать тест - person Kalten; 19.01.2019

Используя следующее регулярное выражение: ^([^;]*;){21}(.*?); , вам не нужно создавать список разделенных отверстий для поиска нужной позиции, и как только вы ее достигнете, это будет вопрос о том, существует она или нет.

Пояснение :

^ --> start of a line.

([^;]*;){Position - 1} --> notice that the symbol ; here is the delimiter, the expression will loop Pos - 1 times

(.*?) --> Non-Greedy .*

ДЕМО

Дополнительные сведения о регулярных выражениях в C#: документация

В приведенном ниже примере я реализовал два примера, чтобы показать вам, как это работает.

Метод сопоставления: documentation (в основном он ищет только первое вхождение шаблона) RegexOptions.Singleline : обрабатывает ввод как символьную строку.

Код C#

Console.WriteLine("First Delimiter : ");
        int Position = 22;
        char delimiter = ',';
        string pattern = @"^([^" + delimiter + "]*" + delimiter + "){" + (Position - 1) + @"}(.*?)" + delimiter;
        Regex regex = new Regex(pattern, RegexOptions.Singleline);
        // First Example
        string Data = @"AAV,zzz,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22ABC,23,24,24";
        Match Re = regex.Match(Data);
        if (Re.Groups.Count > 0)
            Console.WriteLine("\tMatch found : " + Re.Groups[2]);


        // Second Example
        Console.WriteLine("Second Delimiter : ");
        Position = 8;
        delimiter = ';';
        pattern = @"^([^" + delimiter + "]*" + delimiter + "){" + (Position - 1) + @"}(.*?)" + delimiter;
        Data = @"61d2e3f6-bcb7-4cd1-a81e-4f8f497f0da2;0;192.100.0.102:4362;2014-02-14;283;0;354;23;0;;;""0x8D15A2913C934DE"";Thursday, 19-Jun-14 22:58:10 GMT;";
        regex = new Regex(pattern, RegexOptions.Singleline);
        Re = regex.Match(Data);
        if (Re.Groups.Count > 0)
            Console.WriteLine("\tMatch found : " + Re.Groups[2]);

Выход :

Первый разделитель:

    Match found : 22ABC

Второй разделитель:

    Match found : 23
person lagripe    schedule 18.01.2019
comment
Большое спасибо за ваше время и четкое объяснение :) - person re3el; 19.01.2019

Если вы хотите быть уверены, что код анализирует строку только за один проход и анализирует только то, что необходимо, вы можете написать процедуру, которая перебирает строку самостоятельно.

Поскольку все строки C# реализуют IEnumerable<char>, довольно просто разработать метод, который не требует выделения строк:

static public IEnumerable<char> GetDelimitedField(this IEnumerable<char> source, char delimiter, int index)
{
    foreach (var c in source)
    {
        if (c == delimiter) 
        {
            if (--index < 0) yield break;
        }
        else
        {
            if (index == 0) yield return c;
        }
    }
}

Это возвращает результат как IEnumerable<char>, но его дешево преобразовать в строку. В любом случае, на данный момент это будет гораздо более короткая строка.

static public string GetDelimitedString(this string source, char delimiter, int index)
{
    var result = source.GetDelimitedField(delimiter, index);
    return new string(result.ToArray());
}

И вы можете назвать это так:

var input ="Zero,One,Two,Three,Four,Five,Six";
var output = input.GetDelimitedString(',',5);
Console.WriteLine(output);

Выход:

Five

Пример на DotNetFiddle

person John Wu    schedule 19.01.2019

Слишком поздно для «ответа», но этот код дает мне время выполнения около 0,75 секунды, когда обе строки обрабатываются 1 000 000 раз. Разница на этот раз в том, что теперь я не маршалирую объект, а использую указатели.

И на этот раз я возвращаю одну новую строку (String.Substring).

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

class Program
{
    static void Main(string[] args)
    {
        string testString1 = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24";
        string testString2 = "61d2e3f6-bcb7-4cd1-a81e-4f8f497f0da2;0;192.100.0.102:4362;2014-02-14;283;0;354;23;0;;;\"0x8D15A2913C934DE\";Thursday, 19-Jun-14 22:58:10 GMT;";

        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 1; i < 1000000; i++)
        {
            Delimit(testString1, ',', 22);
            Delimit(testString2, ';', 6);
        }
        sw.Stop();
        Console.WriteLine($"==>{sw.ElapsedMilliseconds}");
        Console.ReadLine();
    }

    static string Delimit(string stringUnderTest, char delimiter, int skipCount)
    {
        const int SIZE_OF_UNICHAR = 2;

        int i = 0;
        int index = 0;
        char c = Char.MinValue;

        GCHandle handle = GCHandle.Alloc(stringUnderTest, GCHandleType.Pinned);
        try
        {
            IntPtr ptr = handle.AddrOfPinnedObject();
            for (i = 0; i < skipCount; i++)
                while ((char)Marshal.ReadByte(ptr, index += SIZE_OF_UNICHAR) != delimiter) ;
            i = index;
            while ((c = (char)Marshal.ReadByte(ptr, i += SIZE_OF_UNICHAR)) != delimiter) ;
        }
        finally
        {
            if (handle.IsAllocated)
                handle.Free();
        }

        return stringUnderTest.Substring((index + SIZE_OF_UNICHAR) >> 1, (i - index - SIZE_OF_UNICHAR) >> 1);
    }
}
person Clay Ver Valen    schedule 18.01.2019
comment
так в чем выгода? выполнение занимает вечность (около 18-25 секунд) - person Derviş Kayımbaşıoğlu; 19.01.2019
comment
Console.WriteLine работает очень медленно. При зацикливании оба вызова Delimit в цикле for 1 000 000 раз дают мне время выполнения ~ 35 секунд, если я не вызываю Console.Write или Console.WriteLine. Если я вызываю только Console.Write, а не Console.WriteLine, время выполнения увеличивается примерно в 100 раз по сравнению с предыдущим временем выполнения. Что касается того, чтобы не создавать новые строки, создание смещения и подсчета отслеживания функции, а затем возврат новой строки через testUnderTest.Substring приводит к тому, что память процесса составляет ~ 12 МБ, а во время работы и достигает максимума ~ 9 МБ, когда новая строка не создается. Не уверен, как вы получаете время работы 25 секунд. - person Clay Ver Valen; 19.01.2019
comment
@Simonare - я думаю, вы обнаружите, что это новое решение работает достаточно быстро. - person Clay Ver Valen; 19.01.2019