Кто-нибудь знает более быстрый метод, чтобы сделать String.Split ()? - PullRequest
18 голосов
/ 20 февраля 2009

Я читаю каждую строку файла CSV и мне нужно получить отдельные значения в каждом столбце. Так что сейчас я просто использую:

values = line.Split(delimiter);

где line - строка, содержащая значения, разделенные разделителем.

Измеряя производительность моего ReadNextRow метода, я заметил, что он расходует 66% на String.Split, поэтому мне было интересно, знает ли кто-нибудь более быстрый метод для этого.

Спасибо!

Ответы [ 13 ]

21 голосов
/ 20 февраля 2009

BCL-реализация string.Split на самом деле довольно быстрая, я провел здесь некоторое тестирование, пытаясь ее предварительно отформатировать, и это не легко.

Но есть одна вещь, которую вы можете сделать, и реализовать ее как генератор:

public static IEnumerable<string> GetSplit( this string s, char c )
{
    int l = s.Length;
    int i = 0, j = s.IndexOf( c, 0, l );
    if ( j == -1 ) // No such substring
    {
        yield return s; // Return original and break
        yield break;
    }

    while ( j != -1 )
    {
        if ( j - i > 0 ) // Non empty? 
        {
            yield return s.Substring( i, j - i ); // Return non-empty match
        }
        i = j + 1;
        j = s.IndexOf( c, i, l - i );
    }

    if ( i < l ) // Has remainder?
    {
        yield return s.Substring( i, l - i ); // Return remaining trail
    }
}

Вышеуказанный метод не обязательно быстрее, чем string.Split для маленьких строк, но он возвращает результаты, когда находит их, это сила ленивых вычислений. Если у вас длинные очереди или вам нужно сэкономить память, это путь.

Вышеуказанный метод ограничен производительностью IndexOf и Substring, которые слишком много делают для проверки индекса вне диапазона, и чтобы быть быстрее, вам нужно оптимизировать их и реализовать свои собственные вспомогательные методы. Вы можете победить струну. Сплит-производительность, но для этого понадобится хитрый взлом. Вы можете прочитать мой пост об этом здесь .

18 голосов
/ 20 февраля 2009

Следует отметить, что split() является сомнительным подходом для анализа файлов CSV в случае, если вы встретите запятые в файле, например:

1,"Something, with a comma",2,3

Еще одна вещь, на которую я укажу, не зная, как вы профилировали, - будьте осторожны с профилированием такого рода низкоуровневых деталей. Детализация таймера Windows / PC может вступить в игру, и у вас могут возникнуть значительные издержки при простом цикле, поэтому используйте какое-то контрольное значение.

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

Так что, если вы хотите ускорить его (и мне трудно поверить, что производительность этой части действительно является проблемой), то вы хотите сделать это вручную, и вы хотите повторно использовать объекты буфера, чтобы не создавать постоянно объекты и дать сборщику мусора работу по их очистке.

Алгоритм для этого относительно прост:

  • Стоп на каждой запятой;
  • Когда вы нажимаете на кавычки, продолжайте, пока не нажмете следующий набор кавычек;
  • Обрабатывать экранированные кавычки (то есть \ ") и, возможно, экранированные запятые (\,).

Да, и чтобы дать вам некоторое представление о стоимости регулярных выражений, возник вопрос (Java не C #, но принцип был тем же), когда кто-то хотел заменить каждый n-й символ строкой. Я предложил использовать replaceAll() для String. Джон Скит вручную закодировал петлю. Из любопытства я сравнил эти две версии, и он оказался на порядок лучше.

Так что, если вы действительно хотите производительность, самое время разобрать.

Или, еще лучше, используйте чье-то оптимизированное решение, подобное этому быстрое CSV-ридер .

Кстати, в то время как это относится к Java, это касается производительности регулярных выражений в целом (что является универсальным) и replaceAll() против цикла с ручным кодированием: Помещение char в строку Java для каждого N символов .

4 голосов
/ 04 августа 2009

В зависимости от использования вы можете ускорить это, используя Pattern.split вместо String.split. Если у вас есть этот код в цикле (что я предполагаю, что вы, вероятно, делаете, так как это звучит так, как будто вы анализируете строки из файла), String.split (String Regex) будет вызывать Pattern.compile для вашей строки регулярного выражения каждый раз, когда этот оператор цикла выполняет. Чтобы оптимизировать это, Pattern.com скопируйте шаблон один раз за пределы цикла, а затем используйте Pattern.split, пропустив линию, которую вы хотите разделить, внутри цикла.

Надеюсь, это поможет

2 голосов
/ 16 марта 2016

Я обнаружил, что эта реализация на 30% быстрее, чем Блог Деяна Пельзеля . Я оттуда:

Решение

Имея это в виду, я решил создать разделитель строк, который будет использовать внутренний буфер аналогично StringBuilder. Он использует очень простую логику прохождения строки и сохранения частей значения в буфере по ходу процесса.

public int Split(string value, char separator)
{
    int resultIndex = 0;
    int startIndex = 0;

    // Find the mid-parts
    for (int i = 0; i < value.Length; i++)
    {
        if (value[i] == separator)
        {
            this.buffer[resultIndex] = value.Substring(startIndex, i - startIndex);
            resultIndex++;
            startIndex = i + 1;
        }
    }

    // Find the last part
    this.buffer[resultIndex] = value.Substring(startIndex, value.Length - startIndex);
    resultIndex++;

    return resultIndex;

Как использовать

Класс StringSplitter невероятно прост в использовании, как вы можете видеть в примере ниже. Просто будьте осторожны, чтобы повторно использовать объект StringSplitter и не создавать его новый экземпляр в циклах или для однократного использования. В этом случае было бы лучше использовать встроенный String.Split.

var splitter = new StringSplitter(2);
splitter.Split("Hello World", ' ');
if (splitter.Results[0] == "Hello" && splitter.Results[1] == "World")
{
    Console.WriteLine("It works!");
}

Методы Split возвращают количество найденных элементов, поэтому вы можете легко перебирать результаты, как показано ниже:

var splitter = new StringSplitter(2);
var len = splitter.Split("Hello World", ' ');
for (int i = 0; i < len; i++)
{
    Console.WriteLine(splitter.Results[i]);
}

Этот подход имеет свои преимущества и недостатки.

1 голос
/ 20 февраля 2009

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

Вы могли бы, например, сделать разделение «сами» и пройтись по всем символам и обработать каждый столбец, как только встретите его, но вы все равно скопируете все части строки в долгосрочной перспективе.

Одна из оптимизаций, которую мы могли бы сделать, например, в C или C ++, это заменить все разделители символами '\ 0' и сохранить указатели на начало столбца. Тогда нам не нужно было бы копировать все строковые данные только для того, чтобы получить их часть. Но этого вы не можете делать в C # и не хотели бы.

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

Мне сказали, что 90% процессорного времени тратится на 10% кода. Есть вариации этой «правды». На мой взгляд, тратить 66% вашего времени в Сплите не так уж и плохо, если обработка вашего CSV - это то, что нужно вашему приложению.

Dave

0 голосов
/ 11 мая 2018
    public static unsafe List<string> SplitString(char separator, string input)
    {
        List<string> result = new List<string>();
        int i = 0;
        fixed(char* buffer = input)
        {
            for (int j = 0; j < input.Length; j++)
            {
                if (buffer[j] == separator)
                {
                    buffer[i] = (char)0;
                    result.Add(new String(buffer));
                    i = 0;
                }
                else
                {
                    buffer[i] = buffer[j];
                    i++;
                }
            }
            buffer[i] = (char)0;
            result.Add(new String(buffer));
        }
        return result;
    }
0 голосов
/ 18 июля 2016

Это мое решение:

Public Shared Function FastSplit(inputString As String, separator As String) As String()
        Dim kwds(1) As String
        Dim k = 0
        Dim tmp As String = ""

        For l = 1 To inputString.Length - 1
            tmp = Mid(inputString, l, 1)
            If tmp = separator Then k += 1 : tmp = "" : ReDim Preserve kwds(k + 1)
            kwds(k) &= tmp
        Next

        Return kwds
End Function

Вот версия с бенчмаркингом:

Public Shared Function FastSplit(inputString As String, separator As String) As String()
        Dim sw As New Stopwatch
        sw.Start()
        Dim kwds(1) As String
        Dim k = 0
        Dim tmp As String = ""

        For l = 1 To inputString.Length - 1
            tmp = Mid(inputString, l, 1)
            If tmp = separator Then k += 1 : tmp = "" : ReDim Preserve kwds(k + 1)
            kwds(k) &= tmp
        Next
        sw.Stop()
        Dim fsTime As Long = sw.ElapsedTicks

        sw.Start()
        Dim strings() As String = inputString.Split(separator)
        sw.Stop()

        Debug.Print("FastSplit took " + fsTime.ToString + " whereas split took " + sw.ElapsedTicks.ToString)

        Return kwds
End Function

Вот некоторые результаты для сравнительно небольших строк, но с различными размерами, до 8 КБ блоков. (времена в тиках)

FastSplit занял 8, тогда как сплит занял 10

FastSplit занял 214, тогда как сплит занял 216

FastSplit занял 10, тогда как сплит занял 12

FastSplit занял 8, тогда как сплит взял 9

FastSplit занял 8, тогда как сплит занял 10

FastSplit занял 10, тогда как сплит занял 12

FastSplit занял 7, тогда как сплит взял 9

FastSplit занял 6, тогда как сплит занял 8

FastSplit занял 5, тогда как сплит занял 7

FastSplit занял 10, тогда как сплит занял 13

FastSplit занял 9, тогда как сплит занял 232

FastSplit занял 7, тогда как сплит занял 8

FastSplit занял 8, тогда как сплит взял 9

FastSplit занял 8, тогда как сплит занял 10

FastSplit занял 215, тогда как сплит занял 217

FastSplit занял 10, тогда как сплит занял 231

FastSplit занял 8, тогда как сплит занял 10

FastSplit занял 8, тогда как сплит занял 10

FastSplit занял 7, тогда как сплит взял 9

FastSplit занял 8, тогда как сплит занял 10

FastSplit занял 10, тогда как сплит занял 1405

FastSplit занял 9, тогда как сплит занял 11

FastSplit занял 8, тогда как сплит занял 10

Кроме того, я знаю, что кто-то будет препятствовать моему использованию ReDim Preserve вместо использования списка ... Причина в том, что в списке действительно не было никакой разницы в скорости в моих тестах, поэтому я вернулся к «простому» .

0 голосов
/ 04 марта 2011

Как уже говорили другие, String.Split() не всегда будет хорошо работать с файлами CSV. Рассмотрим файл, который выглядит следующим образом:

"First Name","Last Name","Address","Town","Postcode"
David,O'Leary,"12 Acacia Avenue",London,NW5 3DF
June,Robinson,"14, Abbey Court","Putney",SW6 4FG
Greg,Hampton,"",,
Stephen,James,"""Dunroamin"" 45 Bridge Street",Bristol,BS2 6TG

(например, некорректное использование речевых меток, строк, включая запятые и речевые метки и т. Д.)

Эта среда чтения CSV справится со всем этим, а также очень эффективна:

LumenWorks.Framework.IO.Csv от Себастьяна Лориена

0 голосов
/ 09 декабря 2010

String.split довольно медленно, если вам нужны более быстрые методы, вот и все. :)

Однако CSV намного лучше анализируется синтаксическим анализатором на основе правил.

Этот парень создал токенизатор на основе правил для Java. (к сожалению, требуется копия и вставка)

http://www.csdgn.org/code/rule-tokenizer

private static final String[] fSplit(String src, char delim) {
    ArrayList<String> output = new ArrayList<String>();
    int index = 0;
    int lindex = 0;
    while((index = src.indexOf(delim,lindex)) != -1) {
        output.add(src.substring(lindex,index));
        lindex = index+1;
    }
    output.add(src.substring(lindex));
    return output.toArray(new String[output.size()]);
}

private static final String[] fSplit(String src, String delim) {
    ArrayList<String> output = new ArrayList<String>();
    int index = 0;
    int lindex = 0;
    while((index = src.indexOf(delim,lindex)) != -1) {
        output.add(src.substring(lindex,index));
        lindex = index+delim.length();
    }
    output.add(src.substring(lindex));
    return output.toArray(new String[output.size()]);
}
0 голосов
/ 20 февраля 2009

Синтаксический анализ CSV на самом деле очень сложен для понимания, я использовал классы, основанные на переносе драйвера ODBC Text, один раз, когда мне пришлось это сделать.

Решение ODBC, рекомендованное выше, на первый взгляд выглядит в основном таким же.

Я настоятельно рекомендую вам провести некоторое исследование синтаксического анализа CSV, прежде чем вы зайдете слишком далеко по пути, который почти, но не совсем работает (слишком часто). В Excel только двойные кавычки, которые нужны , это один из самых хитрых в моем опыте.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...