.NET RegEx - первые N символов первых M строк

Я хочу 4 общих выражения RegEx для следующих 4 основных случаев:

  1. До символов A, начинающихся после символов B от начала строки до строк C, начинающихся после строк D от начала файла
  2. До символов A, начинающихся после символов B от начала строки до строк C, встречающихся до строк D от конца файла
  3. До символов A, начинающихся до символов B от конца строки до строк C, начинающихся после строк D от начала файла
  4. До символов A, начинающихся до символов B от конца строки до строк C, начинающихся до строк D от конца файла

Это позволит выбрать произвольные текстовые блоки в любом месте файла.

До сих пор мне удавалось придумать случаи, которые работают только для строк и символов отдельно:

  • (?<=(?m:^[^\r]{N}))[^\r]{1,M} = До M символов каждой строки, после N символов
  • [^\r]{1,M}(?=(?m:.{N}\r$))= До M символов каждой строки, до последних N символов

Вышеприведенные 2 выражения предназначены для символов, и они возвращают МНОГИЕ совпадения (по одному для каждой строки).

  • (?<=(\A([^\r]*\r\n){N}))(?m:\n*[^\r]*\r$){1,M} = До M строк ПОСЛЕ ПЕРВЫХ N строк
  • (((?=\r?)\n[^\r]*\r)|((?=\r?)\n[^\r]+\r?)){1,M}(?=((\n[^\r]*\r)|(\n[^\r]+\r?)){N}\Z) = ДО М строк, ДО ПОСЛЕДНИХ N строк от конца

Эти 2 выражения являются эквивалентами для строк, но они всегда возвращают только ОДНО совпадение.

Задача состоит в том, чтобы объединить эти выражения, чтобы учесть сценарии 1-4. Кто-нибудь может помочь?

Обратите внимание, что случай в заголовке вопроса, это просто подкласс сценария #1, где оба B = 0 и D = 0.

ПРИМЕР 1: Символы 3-6 строк 3-5. Всего 3 матча.

ИСТОЧНИК:

line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6

РЕЗУЛЬТАТ:

<match>ne3 </match>
<match>ne4 </match>
<match>ne5 </match>

ПРИМЕР 2: последние 4 символа из 2 строк перед 1 последней строкой. Всего 2 матча.

ИСТОЧНИК:

line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6

РЕЗУЛЬТАТ:

<match>ah 4</match>
<match>ah 5</match>

7 ответов

Решение

Для начала вот ответ для "Базового случая 1":

Regex regexObj = new Regex(
    @"(?<=            # Assert that the following can be matched before the current position
     \A               # Start of string
     (?:.*\r\n){2,4}  # 2 to 4 entire lines (D = 2, C = 4+1-2)
     .{2}             # 2 characters (B = 2)
    )                 # End of lookbehind assertion
    .{1,3}            # Match 1-3 characters (A = 3)", 
    RegexOptions.IgnorePatternWhitespace);

Теперь вы можете перебирать совпадения, используя

Match matchResults = regexObj.Match(subjectString);
while (matchResults.Success) {
    // matched text: matchResults.Value
    // match start: matchResults.Index
    // match length: matchResults.Length
    matchResults = matchResults.NextMatch();
}

Итак, в тексте

line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6

это будет соответствовать

ne3
ne4
ne5

Вот одно регулярное выражение для основного случая 2:

Regex regexObj = new Regex(
    @"(?<=              # Assert that the following can be matched before the current position
     ^                # Start of line
     .{2}             # 2 characters (B = 2)
    )                 # End of lookbehind assertion
    .{1,3}            # Match 1-3 characters (A = 3)
    (?=               # Assert that the following can be matched after the current position
     .*$              # rest of the current line
     (?:\r\n.*){2,4}  # 2 to 4 entire lines (D = 2, C = 4+1-2)
     \z               # end of the string
    )", 
    RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace);

В тексте

line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6

это будет соответствовать

ne2
ne3
ne4

(ne2 начинается с третьего символа (B=2) в строке с пятой по последнюю (C+D = 5) и т. д.)

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

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

Я полагал, что у вас, вероятно, были реальные причины требовать решения Regex; но так как эти причины не были полностью объяснены, я чувствовал, что есть шанс, что ты просто упрям;)


Вы говорите, что это должно быть сделано в Regex, но я не уверен!

Прежде всего я ограничен.NET 2.0 [ .,, ]

Нет проблем. Кто сказал, что для такой проблемы нужен LINQ? LINQ просто делает вещи проще; это не делает невозможное возможным.

Вот один способ, которым вы могли бы реализовать, например, первый случай из вашего вопроса (и было бы довольно просто преобразовать это в нечто более гибкое, позволяя вам также охватить случаи 2–3):

public IEnumerable<string> ScanText(TextReader reader,
                                    int start,
                                    int count,
                                    int lineStart,
                                    int lineCount)
{
    int i = 0;
    while (i < lineStart && reader.Peek() != -1)
    {
        reader.ReadLine();
        ++i;
    }

    i = 0;
    while (i < lineCount && reader.Peek() != -1)
    {
        string line = reader.ReadLine();

        if (line.Length < start)
        {
            yield return ""; // or null? or continue?
        }
        else
        {
            int length = Math.Min(count, line.Length - start);
            yield return line.Substring(start, length);
        }

        ++i;
    }
}

Таким образом, есть решение для общей проблемы, совместимое с.NET 2.0, без использования регулярных выражений (или LINQ).

Во-вторых, мне нужна гибкость RegEx, чтобы учесть более сложные выражения, которые будут основываться на этих [.,, ]

Может быть, я просто плотный; Что мешает вам начать с чего-то не-Regex, а затем использовать Regex для более "изощренного" поведения? Если вам нужно выполнить дополнительную обработку строк, возвращаемых ScanText Например, выше, вы можете сделать это с помощью Regex. Но настаивать на использовании Regex с самого начала кажется... Я не знаю, просто ненужно.

К сожалению, из-за характера проекта это должно быть сделано в RegEx [ .,, ]

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

Если, с другой стороны, вы вынуждены использовать Regex по какой-то произвольной причине - скажем, кто-то решил написать в каком-то требовании / спецификации, возможно, не слишком задумываясь, что для этой задачи будут использоваться регулярные выражения - ну, я бы лично посоветовал бороться с этим. Объясните всем, кто в состоянии изменить это требование, что Regex не является необходимым и что проблема может быть легко решена без использования Regex... или с использованием комбинации "нормального" кода и Regex.

Единственная другая возможность, о которой я могу подумать (хотя это может быть результатом моей нехватки воображения), которая объясняет, что вам нужно использовать Regex для решения проблемы, которую вы описали в своем вопросе, заключается в том, что вы ограничены использованием определенного инструмент, который исключительно принимает регулярные выражения в качестве пользовательского ввода. Но ваш вопрос помечен .net и поэтому я должен предположить, что есть некоторая степень, в которой вы можете написать свой собственный код, который будет использоваться для решения этой проблемы. И если это так, то я скажу это снова: я не думаю, что вам нужно Regex;)

Вот один для основного случая 3:

Regex regexObj = new Regex(
    @"(?<=            # Assert that the following can be matched before the current position
     \A               # Start of string
     (?:.*\r\n){2,4}  # 2 to 4 entire lines (D = 2, C = 4+1-2)
     .*               # any number of characters
    )                 # End of lookbehind assertion
    (?=               # Assert that the following can be matched after the current position
     .{8}             # 8 characters (B = 8)
     $                # end of line
    )                 # End of lookahead assertion
    .{1,3}            # Match 1-3 characters (A = 3)", 
    RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace);

Так в тексте

line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6

это будет соответствовать

3 b
4 b
5 b

(3 b потому что это 3 символа (A = 3), начиная с 8-го до последнего символа (B = 8), начиная с третьей строки (D = 2) и т. д.)

И, наконец, одно решение для основного случая 4:

Regex regexObj = new Regex(
    @"(?=             # Assert that the following can be matched after the current position
     .{8}             # 8 characters (B = 8)
     (?:\r\n.*){2,4}  # 2 to 4 entire lines (D = 2, C = 4+1-2)
     \z               # end of the string
    )                 # End of lookahead assertion
    .{1,3}            # Match three characters (A = 3)", 
    RegexOptions.IgnorePatternWhitespace);

В тексте

line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6

это будет соответствовать

2 b
3 b
4 b

(2 b потому что это три символа (A = 3), начиная с 8-го до последнего символа (B = 8) в пятой-последней строке (C+D = 5) и т. д.)

Извините за два момента:

  • Я предлагаю решения, которые не полностью основаны на Regex. Я знаю, я читал, что вам нужны чистые решения Regex. Но я вошел в интересную проблему и быстро пришел к выводу, что использование регулярных выражений для этой проблемы усложняет ее. Я не смог ответить чистыми решениями Regex. Я нашел следующие, и я показываю их; Может быть, они могли бы дать вам идеи.

  • Я не знаю C# или.NET, только Python. Поскольку регулярные выражения почти одинаковы во всех языках, я подумал, что собираюсь ответить только регулярными выражениями, поэтому я начал искать проблему. Теперь я все равно показываю свои решения на Python, потому что думаю, что в любом случае это легко понять.

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

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

Для поиска букв в строке, регулярное выражение показалось мне в порядке в первую очередь. ТАК решение с функцией selectRE().

После войны я понял, что выделение букв в строке - это то же самое, что разрезание строки в удобных индексах, и то же самое, что и разрезание списка. Отсюда и функция select().

Я даю два решения вместе, поэтому можно проверить равенство двух результатов двух функций.

import re

def selectRE(a,which_chars,b,x,which_lines,y,ch):
    ch = ch[:-1] if ch[1]=='\n' else ch # to obtain an exact number of lines
    NL = ch.count('\n') +1 # number of lines

    def pat(a,which_chars,b):
        if which_chars=='to':
            print repr(('.{'+str(a-1)+'}' if a else '') + '(.{'+str(b-a+1)+'}).*(?:\n|$)')
            return re.compile(('.{'+str(a-1)+'}' if a else '') + '(.{'+str(b-a+1)+'}).*(?:\n|$)')
        elif which_chars=='before':
            print repr('.*(.{'+str(a)+'})'+('.{'+str(b)+'}' if b else '')+'(?:\n|$)')
            return re.compile('.*(.{'+str(a)+'})'+('.{'+str(b)+'}' if b else '')+'(?:\n|$)')
        elif which_chars=='after':
            print repr(('.{'+str(b)+'}' if b else '')+'(.{'+str(a)+'}).*(?:\n|$)')
            return re.compile(('.{'+str(b)+'}' if b else '')+'(.{'+str(a)+'}).*(?:\n|$)')

    if   which_lines=='to'    :  x   = x-1
    elif which_lines=='before':  x,y = NL-x-y,NL-y
    elif which_lines=='after' :  x,y = y,y+x

    return pat(a,which_chars,b).findall(ch)[x:y]


def select(a,which_chars,b,x,which_lines,y,ch):
    ch = ch[:-1] if ch[1]=='\n' else ch # to obtain an exact number of lines
    NL = ch.count('\n') +1 # number of lines

    if   which_chars=='to'    :  a   = a-1
    elif which_chars=='after' :  a,b = b,a+b

    if   which_lines=='to'    :  x   = x-1
    elif which_lines=='before':  x,y = NL-x-y,NL-y
    elif which_lines=='after' :  x,y = y,y+x

    return [ line[len(line)-a-b:len(line)-b] if which_chars=='before' else line[a:b]
             for i,line in enumerate(ch.splitlines()) if x<=i<y ]


ch = '''line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6
'''
print ch,'\n'

print 'Characters 3-6 of lines 3-5. A total of 3 matches.'
print selectRE(3,'to',6,3,'to',5,ch)
print   select(3,'to',6,3,'to',5,ch)
print
print 'Characters 1-5 of lines 4-5. A total of 2 matches.'
print selectRE(1,'to',5,4,'to',5,ch)
print   select(1,'to',5,4,'to',5,ch)
print
print '7 characters before the last 3 chars of lines 2-6. A total of 5 matches.'
print selectRE(7,'before',3,2,'to',6,ch)
print   select(7,'before',3,2,'to',6,ch)
print
print '6 characters before the 2 last characters of 3 lines before the 3 last lines.'
print selectRE(6,'before',2,3,'before',3,ch)
print   select(6,'before',2,3,'before',3,ch)
print 
print '4 last characters of 2 lines before 1 last line. A total of 2 matches.'
print selectRE(4,'before',0,2,'before',1,ch)
print   select(4,'before',0,2,'before',1,ch)
print
print 'last 1 character of 4 last lines. A total of 2 matches.'
print selectRE(1,'before',0,4,'before',0,ch)
print   select(1,'before',0,4,'before',0,ch)
print
print '7 characters before the last 3 chars of 3 lines after the 2 first lines. A total of 5 matches.'
print selectRE(7,'before',3,3,'after',2,ch)
print   select(7,'before',3,3,'after',2,ch)
print
print '5 characters before the 3 last chars of the 5 first lines'
print selectRE(5,'before',3,5,'after',0,ch)
print   select(5,'before',3,5,'after',0,ch)
print
print 'Characters 3-6 of the 4 first lines'
print selectRE(3,'to',6,4,'after',0,ch)
print   select(3,'to',6,4,'after',0,ch)
print
print '9 characters after the 2 first chars of the 3 lines after the 1 first line'
print selectRE(9,'after',2,3,'after',1,ch)
print   select(9,'after',2,3,'after',1,ch)

результат

line1 blah 1
line2 blah 2
line3 blah 3
line4 blah 4
line5 blah 5
line6 blah 6


Characters 3-6 of lines 3-5. A total of 3 matches.
'.{2}(.{4}).*(?:\n|$)'
['ne3 ', 'ne4 ', 'ne5 ']
['ne3 ', 'ne4 ', 'ne5 ']

Characters 1-5 of lines 4-5. A total of 2 matches.
'.{0}(.{5}).*(?:\n|$)'
['line4', 'line5']
['line4', 'line5']

7 characters before the last 3 chars of lines 2-6. A total of 5 matches.
'.*(.{7}).{3}(?:\n|$)'
['ne2 bla', 'ne3 bla', 'ne4 bla', 'ne5 bla', 'ne6 bla']
['ne2 bla', 'ne3 bla', 'ne4 bla', 'ne5 bla', 'ne6 bla']

6 characters before the 2 last characters of 3 lines before the 3 last lines.
'.*(.{6}).{2}(?:\n|$)'
['2 blah', '3 blah', '4 blah']
['2 blah', '3 blah', '4 blah']

4 last characters of 2 lines before 1 last line. A total of 2 matches.
'.*(.{4})(?:\n|$)'
['ah 5', 'ah 6']
['ah 5', 'ah 6']

last 1 character of 4 last lines. A total of 2 matches.
'.*(.{1})(?:\n|$)'
['4', '5', '6']
['4', '5', '6']

7 characters before the last 3 chars of 3 lines after the 2 first lines. A total of 5 matches.
'.*(.{7}).{3}(?:\n|$)'
['ne3 bla', 'ne4 bla', 'ne5 bla']
['ne3 bla', 'ne4 bla', 'ne5 bla']

5 characters before the 3 last chars of the 5 first lines
'.*(.{5}).{3}(?:\n|$)'
['1 bla', '2 bla', '3 bla', '4 bla', '5 bla']
['1 bla', '2 bla', '3 bla', '4 bla', '5 bla']

Characters 3-6 of the 4 first lines
'.{2}(.{4}).*(?:\n|$)'
['ne1 ', 'ne2 ', 'ne3 ', 'ne4 ']
['ne1 ', 'ne2 ', 'ne3 ', 'ne4 ']

9 characters after the 2 first chars of the 3 lines after the 1 first line
'.{2}(.{9}).*(?:\n|$)'
['ne2 blah ', 'ne3 blah ', 'ne4 blah ']
['ne2 blah ', 'ne3 blah ', 'ne4 blah ']

А теперь я буду изучать хитрые решения Тима Пицкера

Почему бы вам просто не сделать что-то вроде этого:

//Assuming you have it read into a string name sourceString
String[] SplitString = sourceString.Split(Environment.Newline); //You will probably need to account for any line delimeter
String[M] NewStrings;
for(i=0;i<M;i++) {
    NewStrings[i] = SplitString[i].SubString(0,N) //Or (N, SplitString[i].Length -1) depending on what you need
}

Вам не нужен RegEx, вам не нужен LINQ.

Ну, я перечитал начало вашего вопроса, и вы могли бы просто параметризовать начало и конец цикла for и Split, чтобы получить именно то, что вам нужно.

Другие вопросы по тегам