Шаблон регулярного выражения и/или NSRegularExpression слишком медленный поиск по очень большому файлу, можно ли его оптимизировать?

В среде iOS я ищу произношение в этом файле размером 3,2 МБ: https://cmusphinx.svn.sourceforge.net/svnroot/cmusphinx/trunk/pocketsphinx/model/lm/en_US/cmu07a.dic

Я использую NSRegularExpression для поиска произвольного набора слов, которые заданы как NSArray. Поиск осуществляется по содержимому большого файла в виде NSString. Мне нужно сопоставить любое слово, которое появляется в квадратных скобках с помощью новой строки и символа табуляции, а затем взять всю строку, например, если у меня есть слово «понедельник» в моем NSArray, я хочу сопоставить эту строку в файле словаря:

monday  M AH N D IY

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

monday(2)   M AH N D EY

Альтернативное произношение всегда начинается с (2) и может достигать (5). Поэтому я также ищу повторения слова, за которым следуют круглые скобки, содержащие одно число, заключенное в скобки с новой строкой и символом табуляции.

У меня есть 100% рабочий метод NSRegularExpression следующим образом:

NSArray *array = [NSArray arrayWithObjects:@"friday",@"monday",@"saturday",@"sunday", @"thursday",@"tuesday",@"wednesday",nil]; // This array could contain any arbitrary words but they will always be in alphabetical order by the time they get here.

// Use this string to build up the pattern.
NSMutableString *mutablePatternString = [[NSMutableString alloc]initWithString:@"^("]; 

int firstRound = 0;
for(NSString *word in array) {
    if(firstRound == 0) { // this is the first round

        firstRound++;
    } else { // After the first iteration we need an OR operator first.
        [mutablePatternString appendString:[NSString stringWithFormat:@"|"]];
     }
    [mutablePatternString appendString:[NSString stringWithFormat:@"(%@(\\(.\\)|))",word]];
}

[mutablePatternString appendString:@")\\t.*$"];

// This results in this regex pattern:

// ^((change(\(.\)|))|(friday(\(.\)|))|(monday(\(.\)|))|(saturday(\(.\)|))|(sunday(\(.\)|))|(thursday(\(.\)|))|(tuesday(\(.\)|))|(wednesday(\(.\)|)))\t.*$

NSRegularExpression * regularExpression = [NSRegularExpression regularExpressionWithPattern:mutablePatternString
                                                                                     options:NSRegularExpressionAnchorsMatchLines
                                                                                       error:nil];
int rangeLocation = 0;
int rangeLength = [string length];
NSMutableArray * matches = [NSMutableArray array];
[regularExpression enumerateMatchesInString:string
                                     options:0
                                       range:NSMakeRange(rangeLocation, rangeLength)
                                  usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop){
                                      [matches addObject:[string substringWithRange:result.range]];
                                  }];

[mutablePatternString release];

// matches array is returned to the caller.

Моя проблема в том, что, учитывая большой текстовый файл, он недостаточно быстр на iPhone. 8 слов занимают 1,3 секунды на iPhone 4, что слишком долго для приложения. Учитывая следующие известные факторы:

• В текстовом файле размером 3,2 МБ слова для соответствия перечислены в алфавитном порядке.

• Массив произвольных слов для поиска всегда находится в алфавитном порядке, когда они попадают в этот метод.

• Альтернативное произношение начинается с (2) в скобках после слова, а не (1)

• Если нет (2), не будет (3), (4) или более

• Наличие одного альтернативного произношения встречается редко, в среднем 1 раз из 8. Дальнейшее альтернативное произношение встречается еще реже.

Можно ли оптимизировать этот метод, улучшив регулярное выражение или какой-либо аспект Objective-C? Я предполагаю, что NSRegularExpression уже достаточно оптимизирован, поэтому не стоит пытаться сделать это с другой библиотекой Objective-C или в C, но если я ошибаюсь, дайте мне знать. В противном случае, очень благодарен за любые предложения по улучшению производительности. Я надеюсь сделать это обобщенным для любого файла произношения, поэтому я стараюсь держаться подальше от таких решений, как расчет алфавитных диапазонов заранее, чтобы выполнять более ограниченный поиск.

****РЕДАКТИРОВАТЬ****

Вот время на iPhone 4 для всех связанных с поиском ответов, данных до 16 августа 2012 года:

подход dasblinkenlight к созданию NSDictionary https://stackoverflow.com/a/11958852/119717: 5,259676 секунд

Самое быстрое регулярное выражение Ωmega на https://stackoverflow.com/a/11957535/119717: 0,609593 секунды

подход dasblinkenlight с несколькими NSRegularExpression в https://stackoverflow.com/a/11969602/119717: 1,255130 секунд

мой первый гибридный подход на https://stackoverflow.com/a/11970549/119717: 0,372215 секунд

мой второй гибридный подход на https://stackoverflow.com/a/11970549/119717: 0,337549 секунд

Лучшее время пока - вторая версия моего ответа. Я не могу отметить ни один из ответов как лучший, поскольку все ответы, связанные с поиском, сообщают о подходе, который я использовал в своей версии, поэтому все они очень полезны, а мой просто основан на других. Я многому научился, и мой метод закончился в четверть первоначального времени, так что это было чрезвычайно полезно, спасибо dasblinkenlight и Ωmega за обсуждение этого со мной.


person Halle    schedule 14.08.2012    source источник
comment
Если ваш файл находится под вашим контролем и в нем нет мусора, вы можете немного оптимизировать свое регулярное выражение: ^(sunday|monday|tuesday|...)(\t|\\().*$: вы знаете, что все, что входит в круглые скобки, представляет собой одиночный символ, за которым следует закрывающая круглая скобка, поэтому вы можете пропустить эту часть соответствовать. Объединение всех ваших строк в один блок OR также может помочь, но я не уверен, что это сильно поможет.   -  person Sergey Kalinichenko    schedule 14.08.2012
comment
Я не знаю, являетесь ли вы тем, кто предоставляет файл произношения, но если да, вы можете хранить его по-другому. Если бы у вас были все произношения, сгруппированные с их словом в одном объекте, вы могли бы искать гораздо быстрее по имени (я полагаю, что поиск по алфавитному массиву может быть выполнен в журнале (n)).   -  person Dustin    schedule 14.08.2012
comment
^(воскресенье|понедельник|вторник|...)(\t|\().*$ удалось сократить время до 0,90 секунды, что очень помогло, спасибо. Как работает одиночный блок ИЛИ?   -  person Halle    schedule 14.08.2012
comment
@Halle Он работает так же, как тот, который вы предоставили, но, поскольку отдельные компоненты проще, механизм регулярных выражений может генерировать более быстрый конечный автомат для выполнения сопоставления.   -  person Sergey Kalinichenko    schedule 14.08.2012
comment
Правильно, извините, я неправильно понял, что вы рекомендуете дальнейшую оптимизацию, которой не было в вашем примере.   -  person Halle    schedule 14.08.2012
comment
@Halle Какая кодировка вашего файла? Как вы сейчас это читаете?   -  person Sergey Kalinichenko    schedule 14.08.2012
comment
Это UTF-8, и я прочитал его с [[NSString alloc] initWithContentsOfFile:pathToFileAsString encoding:NSUTF8StringEncoding error:&error];   -  person Halle    schedule 14.08.2012


Ответы (4)


Поскольку вы все равно помещаете весь файл в память, вы можете также представить его в виде структуры, которую легко найти:

  • Создайте изменяемый NSDictionary words с NSString ключами и NSMutableArray значениями
  • Прочитать файл в память
  • Пройдите строку, представляющую файл, построчно
  • Для каждого line отделите часть слова, выполнив поиск символа '(' или '\t'.
  • Получить подстроку для слова (от нуля до индекса '(' или '\t' минус единица); это твой key.
  • Проверьте, содержит ли words ваш key; если нет, добавьте новый NSMutableArray
  • Добавьте line к NSMutableArray, которые вы нашли/создали в конкретном key
  • Как только вы закончите, выбросьте исходную строку, представляющую файл.

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

** РЕДАКТИРОВАТЬ: ** Я проверил относительную скорость этого решения по сравнению с регулярным выражением, на симуляторе оно примерно в 60 раз быстрее. В этом нет ничего удивительного, потому что шансы решения на основе регулярных выражений очень высоки.

Чтение файла:

NSBundle *bdl = [NSBundle bundleWithIdentifier:@"com.poof-poof.TestAnim"];
NSString *path = [NSString stringWithFormat:@"%@/words_pron.dic", [bdl bundlePath]];
data = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
NSMutableDictionary *tmp = [NSMutableDictionary dictionary];
NSUInteger pos = 0;
NSMutableCharacterSet *terminator = [NSMutableCharacterSet characterSetWithCharactersInString:@"\t("];
while (pos != data.length) {
    NSRange remaining = NSMakeRange(pos, data.length-pos);
    NSRange next = [data
        rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet]
        options:NSLiteralSearch
        range:remaining
    ];
    if (next.location != NSNotFound) {
        next.length = next.location - pos;
        next.location = pos;
    } else {
        next = remaining;
    }
    pos += (next.length+1);
    NSString *line = [data substringWithRange:next];
    NSRange keyRange = [line rangeOfCharacterFromSet:terminator];
    keyRange.length = keyRange.location;
    keyRange.location = 0;
    NSString *key = [line substringWithRange:keyRange];
    NSMutableArray *array = [tmp objectForKey:key];
    if (!array) {
        array = [NSMutableArray array];
        [tmp setObject:array forKey:key];
    }
    [array addObject:line];
}
dict = tmp; // dict is your NSMutableDictionary ivar

Поиск:

NSArray *keys = [NSArray arrayWithObjects:@"sunday", @"monday", @"tuesday", @"wednesday", @"thursday", @"friday", @"saturday", nil];
NSMutableArray *all = [NSMutableArray array];
NSLog(@"Starting...");
for (NSString *key in keys) {
    for (NSString *s in [dict objectForKey:key]) {
        [all addObject:s];
    }
}
NSLog(@"Done! %u", all.count);
person Sergey Kalinichenko    schedule 14.08.2012
comment
Мне очень нравится эта идея, но для меня, использующего файл cmu07a.dic, это занимает около 0,4 секунды на симуляторе, а регулярное выражение из Ωmega занимает 0,07 секунды. Не уверен, почему разница в наших результатах. Я также попробовал вариант того же подхода, преобразовав строку в массив в начале, используя компонентыSeparatedBy:@\n, а затем быстро перечислив их со вторым раундом componentSeparatedBy:@\t, и время было примерно таким же (хотя память следа нет). - person Halle; 15.08.2012
comment
@Halle Это очень удивительно - мое время на симуляторе составляет около 0,002 для NSMutableDictionary и 0,12 для регулярного выражения. Однако я не включаю построение NSMutableDictionary в свое время: я строю dict один раз в методе viewDidLoad и больше никогда к нему не прикасаюсь. Это только часть Searching: приведенного выше кода, которая синхронизируется. - person Sergey Kalinichenko; 15.08.2012
comment
Понятно, неудивительно, если вы не включаете создание словаря, но регулярные выражения рассчитаны по времени от чтения в словаре в виде строки до завершения поиска, поскольку время обработки файла учитывается для общего вопроса о задержке приложения. . Эта операция, скорее всего, будет выполняться только один раз в сеансе приложения и сразу после инициализации класса, поэтому нет большой возможности для предварительного кэширования файла. - person Halle; 15.08.2012
comment
@Halle Тогда нет необходимости создавать словарь. Простого просмотра файла один раз будет достаточно. Я попробую и посмотрю, что я получу. - person Sergey Kalinichenko; 15.08.2012
comment
Хорошая точка зрения! Со своей стороны, мне сейчас интересно, эффективно ли обрабатывать строку в XML (учитывая, что ее форматирование стандартизировано) и сериализовать ее непосредственно в NSDictionary без промежуточных шагов. Однако обработка текста, вероятно, слишком медленная. Я мог бы попробовать регулярное выражение;). - person Halle; 15.08.2012
comment
Хорошо, я проверил, взяв окончательный вывод NSDictionary и записав его в виде двоичного plist, который я мог бы распространять вместо текстового файла .dic (не идеально, но хорошо в качестве временной меры - мне нужно было бы предоставить методы для преобразования другого произношения). словари в один и тот же формат, но это не запрещено), но даже время для извлечения двоичных данных plist непосредственно в NSDictionary было в два раза больше, чем время регулярного выражения. - person Halle; 15.08.2012
comment
@Halle Я бы не пошел по этому пути, потому что количество объектов, которые вам нужно создать при чтении словаря из файла, значительно выше. Создание объектов здесь явно доминирует в этом процессе, поэтому я постараюсь избегать его, насколько это возможно. - person Sergey Kalinichenko; 15.08.2012
comment
Да, это ничего не решает, но вводит много новых побочных проблем. - person Halle; 15.08.2012
comment
Я могу сократить это время до половины, если создам NSSet в начале из слов для соответствия следующим образом: NSSet *comparisonSet = [NSSet setWithArray:arrayOfWordsToMatch]; а затем я удаляю все после NSString *key = [line substringWithRange:keyRange]; и вместо этого проверяйте ключ по списку слов следующим образом: if([comparisonSet containsObject:key]) и затем просто сохраняйте совпадения. К сожалению, это все еще почти в 3 раза больше, чем регулярное выражение. - person Halle; 15.08.2012
comment
@Halle Я не думаю, что вы можете победить регулярное выражение без предварительной обработки. Даже очень быстрый поиск, который проходит по словам одно за другим, немного медленнее, чем регулярное выражение. Вы можете попробовать обыграть одно регулярное выражение несколькими (посмотрю, смогу ли я придумать альтернативный ответ). - person Sergey Kalinichenko; 15.08.2012

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

^(?:change|monday|tuesday|wednesday|thursday|friday|saturday|sunday)(?:\([2-5]\))?\t.*$

а также этот (используя положительный просмотр со списком возможных первых букв):

^(?=[cmtwfs])(?:change|monday|tuesday|wednesday|thursday|friday|saturday|sunday)(?:\([2-5]\))?\t.*$

и в конце версия с некоторой оптимизацией:

^(?=[cmtwfs])(?:change|monday|t(?:uesday|hursday)|wednesday|friday|s(?:aturday|unday))(?:\([2-5]\))?\t.*$
person Ωmega    schedule 14.08.2012
comment
Хорошо, это сократило время до 0,76 секунды. - person Halle; 14.08.2012
comment
Хорошо, это я должен попробовать завтра, так как это потребует рефакторинга ввода слова, чтобы сгруппировать слова по первому символу, поскольку слова не известны до времени выполнения. - person Halle; 14.08.2012
comment
@Halle - последний может подходить или не подходить лучше всего, в зависимости от размера ввода и количества совпадений. Удачи! - person Ωmega; 14.08.2012

Вот мой гибридный подход к ответам dasblinkenlight и Ωmega, который я подумал, что должен добавить в качестве ответа и на этом этапе. Он использует метод dasblinkenlight для прямого поиска по строке, а затем выполняет полное регулярное выражение в небольшом диапазоне в случае совпадения, поэтому он использует тот факт, что словарь и слова для поиска находятся в алфавитном порядке и извлекают выгоду из оптимизированное регулярное выражение. Хотел бы я раздать два чека за лучший ответ! Это дает правильные результаты и занимает примерно половину времени чистого регулярного выражения в симуляторе (я должен протестировать устройство позже, чтобы увидеть, какое сравнение времени происходит на iPhone 4, который является эталонным устройством):

NSMutableArray *mutableArrayOfWordsToMatch = [[NSMutableArray alloc] initWithArray:array];
NSMutableArray *mutableArrayOfUnfoundWords = [[NSMutableArray alloc] init]; // I also need to know the unfound words.

NSUInteger pos = 0;

NSMutableString *mutablePatternString = [[NSMutableString alloc]initWithString:@"^(?:"];
int firstRound = 0;
for(NSString *word in array) {
    if(firstRound == 0) { // this is the first round

        firstRound++;
    } else { // this is all later rounds
        [mutablePatternString appendString:[NSString stringWithFormat:@"|"]];
    }
    [mutablePatternString appendString:[NSString stringWithFormat:@"%@",word]];
}

[mutablePatternString appendString:@")(?:\\([2-5]\\))?\t.*$"];

// This creates a string that reads "^(?:change|friday|model|monday|quidnunc|saturday|sunday|thursday|tuesday|wednesday)(?:\([2-5]\))?\t.*$"

// We don't want to instantiate the NSRegularExpression in the loop so let's use a pattern that matches everything we're interested in.

NSRegularExpression * regularExpression = [NSRegularExpression regularExpressionWithPattern:mutablePatternString
                                                                                    options:NSRegularExpressionAnchorsMatchLines
                                                                                      error:nil];
NSMutableArray * matches = [NSMutableArray array];

while (pos != data.length) {

    if([mutableArrayOfWordsToMatch count] <= 0) { // If we're at the top of the loop without any more words, stop.
        break;
    }  

    NSRange remaining = NSMakeRange(pos, data.length-pos);
    NSRange next = [data
                    rangeOfString:[NSString stringWithFormat:@"\n%@\t",[mutableArrayOfWordsToMatch objectAtIndex:0]]
                    options:NSLiteralSearch
                    range:remaining
                    ]; // Just search for the first pronunciation.
    if (next.location != NSNotFound) {

        // If we find the first pronunciation, run the whole regex on a range of {position, 500} only.

        int rangeLocation = next.location;
        int searchPadding = 500;
        int rangeLength = searchPadding;

        if(data.length - next.location < searchPadding) { // Only use 500 if there is 500 more length in the data.
            rangeLength = data.length - next.location;
        } 

        [regularExpression enumerateMatchesInString:data 
                                            options:0
                                              range:NSMakeRange(rangeLocation, rangeLength)
                                         usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop){
                                             [matches addObject:[data substringWithRange:result.range]];
                                         }]; // Grab all the hits at once.

        next.length = next.location - pos;
        next.location = pos;
        [mutableArrayOfWordsToMatch removeObjectAtIndex:0]; // Remove the word.
        pos += (next.length+1);
    } else { // No hits.
        [mutableArrayOfUnfoundWords addObject:[mutableArrayOfWordsToMatch objectAtIndex:0]]; // Add to unfound words.
        [mutableArrayOfWordsToMatch removeObjectAtIndex:0]; // Remove from the word list.
    }
}    

[mutablePatternString release];
[mutableArrayOfUnfoundWords release];
[mutableArrayOfWordsToMatch release];

// return matches to caller

РЕДАКТИРОВАТЬ: вот еще одна версия, которая не использует регулярное выражение и сокращает время метода:

NSMutableArray *mutableArrayOfWordsToMatch = [[NSMutableArray alloc] initWithArray:array];
NSMutableArray *mutableArrayOfUnfoundWords = [[NSMutableArray alloc] init]; // I also need to know the unfound words.

NSUInteger pos = 0;

NSMutableArray * matches = [NSMutableArray array];

while (pos != data.length) {

    if([mutableArrayOfWordsToMatch count] <= 0) { // If we're at the top of the loop without any more words, stop.
        break;
    }  

    NSRange remaining = NSMakeRange(pos, data.length-pos);
    NSRange next = [data
                    rangeOfString:[NSString stringWithFormat:@"\n%@\t",[mutableArrayOfWordsToMatch objectAtIndex:0]]
                    options:NSLiteralSearch
                    range:remaining
                    ]; // Just search for the first pronunciation.
    if (next.location != NSNotFound) {
        NSRange lineRange = [data lineRangeForRange:NSMakeRange(next.location+1, next.length)];
        [matches addObject:[data substringWithRange:NSMakeRange(lineRange.location, lineRange.length-1)]]; // Grab the whole line of the hit.
        int rangeLocation = next.location;
        int rangeLength = 750;

        if(data.length - next.location < rangeLength) { // Only use the searchPadding if there is that much room left in the string.
            rangeLength = data.length - next.location;
        } 
        rangeLength = rangeLength/5;
        int newlocation = rangeLocation;

        for(int i = 2;i < 6; i++) { // We really only need to do this from 2-5.
            NSRange morematches = [data
                            rangeOfString:[NSString stringWithFormat:@"\n%@(%d",[mutableArrayOfWordsToMatch objectAtIndex:0],i]
                            options:NSLiteralSearch
                            range:NSMakeRange(newlocation, rangeLength)
                            ];
            if(morematches.location != NSNotFound) {
                NSRange moreMatchesLineRange = [data lineRangeForRange:NSMakeRange(morematches.location+1, morematches.length)]; // Plus one because I don't actually want the line break at the beginning.
                 [matches addObject:[data substringWithRange:NSMakeRange(moreMatchesLineRange.location, moreMatchesLineRange.length-1)]]; // Minus one because I don't actually want the line break at the end.
                newlocation = morematches.location;

            } else {
                break;   
            }
        }

        next.length = next.location - pos;
        next.location = pos;
        [mutableArrayOfWordsToMatch removeObjectAtIndex:0]; // Remove the word.
        pos += (next.length+1);
    } else { // No hits.
        [mutableArrayOfUnfoundWords addObject:[mutableArrayOfWordsToMatch objectAtIndex:0]]; // Add to unfound words.
        [mutableArrayOfWordsToMatch removeObjectAtIndex:0]; // Remove from the word list.
    }
}    

[mutableArrayOfUnfoundWords release];
[mutableArrayOfWordsToMatch release];
person Halle    schedule 15.08.2012
comment
Если вы пойдете по этому пути, вы можете заменить код, выполняющий поиск с помощью регулярного выражения, циклом, который ищет @"\n%@(", потому что вы знаете, что других совпадений с @"\n%@\t" не будет. - person Sergey Kalinichenko; 15.08.2012
comment
Это помогло, время симулятора сократилось с 0,04 (и изменение) до 0,035. Я отредактирую его ниже. - person Halle; 15.08.2012

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

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

Затем вы можете выполнить бинарный поиск по данным - даже при ОГРОМНОМ количестве объектов бинарный поиск очень быстро найдет то, что вы ищете (при условии, что в алфавитном порядке).

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

person Dustin    schedule 14.08.2012
comment
Я думаю, что это не было бы отличным решением для этого конкретного случая, так как это была бы новая зависимость для фреймворка. Тем не менее, спасибо за предложение, я буду иметь его в виду. - person Halle; 14.08.2012
comment
Это всего лишь предложение на случай, если окажется, что с помощью регулярных выражений невозможно получить необходимую производительность. Я могу понять нежелание менять код, который уже работает довольно хорошо. - person Dustin; 14.08.2012