Извлечь значение ключа из свободного текста с помощью Regex

avatar
Nour
1 июля 2021 в 16:03
110
5
0

Допустим, у меня есть этот бесплатный текст

unitType:unit_failure;unitId:b7eb;unitTitle:L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A);applicable:true;comment:Decline sender:SELLER;Decline reason:VEN_ACC_SETTINGS;triggered by user:495259708;Display reason: CON_FAILURE;modified by: log_res_mon

Я хочу извлечь значения в объект с теми же именами свойств.

public class Values
{
    public string UnitType { get; set; }
    public string UnitId { get; set; }
    public string UnitTitle { get; set; }
    public string Applicable { get; set; }
    public string DeclineSender { get; set; }
    public string DeclineReason { get; set; }
    public string TriggeredByUser { get; set; }
    public string DisplayReason { get; set; }
    public string ModifiedBy { get; set; }
}

до сих пор я пробовал этот код:

    string comment = "unitType:unit_failure;unitId:b7eb;unitTitle:L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A);applicable:true;comment:Decline sender:SELLER;Decline reason:VEN_ACC_SETTINGS;triggered by user:495259708;Display reason: CON_FAILURE;modified by: log_res_mon";
    string regex = @"(.*?:\s*)(\w*);?";

    var matches = Regex.Matches(comment, regex);

Что почти сработало. но вы можете видеть, что unitTitle было вырезано после L1-.

Другое дело, comment: — это исключение, которое можно удалить либо выражением, либо я могу просто удалить его с помощью .Replace.

enter image description here

  • Как исправить выражение, чтобы оно включало полное значение unitTitle и удаляло comment: (можете сказать, проще использовать .Replace)?

  • Как лучше всего извлечь значения из matches и заполнить ими объект?

РЕДАКТИРОВАТЬ: Я пробовал это, но это действительно уродливо, есть ли лучший способ? (Не обращайте внимания на то, что я использую анонимный объект, это только для тестирования).

    var obj = new
    {
        unitType = matches.Single(x => x.Groups[1].Value == "unitType").Groups[2].Value
    };

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

Источник
Joel Coehoorn
1 июля 2021 в 16:33
1

Пожалуйста, не размещайте изображения текста.

Nour
7 июля 2021 в 17:03
0

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

Ответы (5)

avatar
Olivier Jacot-Descombes
1 июля 2021 в 17:18
0

Вы можете использовать шаблон регулярного выражения

(?<name>(\w|\s)+):\s*(?<value>[^;]*);?

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

var comment = @"unitType:unit_failure;unitId:b7eb;unitTitle:L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A);applicable:true;comment:Decline sender:SELLER;Decline reason:VEN_ACC_SETTINGS;triggered by user:495259708;Display reason: CON_FAILURE;modified by: log_res_mon";
var pattern = @"(?<name>(\w|\s)+):\s*(?<value>[^;]*);?";

// Add ";" before "Decline sender:" to place "comment:" into another match.
comment = comment.Replace("Decline sender:", ";Decline sender:");

Regex regex = new Regex(pattern);       
var matches = regex.Matches(comment);
foreach (Match m in matches) {
    string name = m.Groups["name"].Value;
    string value = m.Groups["value"].Value;

    Console.WriteLine($"{name}={value}");
}

Урожайность:

unitType=unit_failure
unitId=b7eb
unitTitle=L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A)
applicable=true
comment=
Decline sender=SELLER
Decline reason=VEN_ACC_SETTINGS
triggered by user=495259708
Display reason=CON_FAILURE
modified by=log_res_mon

Если comment: всегда пусто, то comment = comment.Replace(";comment:", ";"); удалит его. Однако если комментарий может иметь значение, я бы заменил "Decline sender:" на ";Decline sender:". Это помещает комментарий в другое совпадение.

См.: https://dotnetfiddle.net/PROf3F

Объяснение (?<name>(\w|\s)+):\s*(?<value>[^;]*);?:

  • (?<name>expression) — именованная группа.
  • (\w|\s)+ выражение именованной группы "имя". Один или несколько словесных символов или пробелов.
  • :\s* двоеточие и необязательные пробелы между именем переменной и значением.
  • [^;]* выражение именованной группы "значение". Любое количество символов, кроме ;.
  • ;? необязательная точка с запятой.

Вы можете легко поместить значения в объект с помощью

var values = new Values(); // Create object before the foreach-loop.

// In the matches loop:
switch (name) {
    "unitType":
        values.UnitType = value;
        break;
    "unitId":
        values.UnitId = value;
        break;
    ...
}

Или добавьте пары имя/значение в Dictionary<string,string> с dict[name] = value;, чтобы упростить поиск значения по имени.

Nour
9 июля 2021 в 21:04
0

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

avatar
lidqy
1 июля 2021 в 17:49
0

Мой подход также отдает предпочтение Split, ToDictionary вместо RegEx для этого варианта использования. И присвоение значений "по имени" с помощью Reflection.

var str = "unitType:unit_failure;unitId:b7eb;unitTitle:L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A);applicable:true;comment:Decline sender:SELLER;Decline reason:VEN_ACC_SETTINGS;triggered by user:495259708;Display reason: CON_FAILURE;modified by: log_res_mon";

        var nameValueMap = str.Replace("comment:", "")
                     .Split(";")
                     .Select(token => token.Split(":"))
                     .ToDictionary(arr => arr[0].Replace(" ", "").Trim(), 
                                   arr => arr[1].Trim());

        var instance = new Values();
        var type     = instance.GetType();
        foreach (var kv in nameValueMap) {
            type.InvokeMember(
                               kv.Key,
                               BindingFlags.Instance
                             | BindingFlags.SetProperty
                             | BindingFlags.IgnoreCase
                             | BindingFlags.Public,
                               null,
                               instance,
                               new object[] { kv.Value }
                              );
        }

Результат: Values in "instance":

avatar
Magnetron
1 июля 2021 в 16:52
3

Ну, если comment: всегда нужно удалять, и это всегда так, вам вообще не нужно регулярное выражение, вы можете разделить на ;, затем на ::

string input = @"unitType:unit_failure;unitId:b7eb;unitTitle:L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A);applicable:true;comment:Decline sender:SELLER;Decline reason:VEN_ACC_SETTINGS;triggered by user:495259708;Display reason: CON_FAILURE;modified by: log_res_mon";
var fields = input.Replace("comment:","")
    .Split(';')
    .Select(x => x.Split(':', 2))
    .ToDictionary(x => x[0],x => x[1]);

Values values = new Values
{
    UnitType = fields["unitType"],
    UnitId = fields["unitId"],
    UnitTitle = fields["unitTitle"],
    Applicable = fields["applicable"],
    DeclineSender = fields["Decline sender"],
    DeclineReason = fields["Decline reason"],
    TriggeredByUser = fields["triggered by user"],
    DisplayReason = fields["Display reason"],
    ModifiedBy = fields["modified by"]
};
avatar
The fourth bird
1 июля 2021 в 16:18
0

Можно также исключить соответствие ; и : для имени свойства и исключить соответствие ; в качестве значения свойства.

([^:;]+):\s*([^;]+)

Демонстрация регулярных выражений

Или, если для ключа и значения должен быть хотя бы символ слова, а не захват comment:

\bcomment:\s*[^;\w]*\w[^;]*|([^:;\w]*\w[^:;]*):\s*([^;\w]*\w[^;]*)

Шаблон соответствует:

  • \bcomment:\s*[^;\w]*\w[^;]* Соответствие нежелательному комментарию
  • | или
  • ( Захват группа 1
    • [^:;\w]* Необязательно соответствует любому символу, кроме : ; или слову char
    • \w Соответствует одному слову char
    • [^:;]* При необходимости повторите сопоставление любого символа, кроме : и ;
  • ) , соответствует как минимум слову char
  • :\s* Совпадение с : и необязательными пробелами
  • ([^;\w]*\w[^;]*) Захват группа 2 Тот же подход, что и для группы 1, только за исключением ;

Демонстрация регулярных выражений

Nour
1 июля 2021 в 16:20
0

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

The fourth bird
1 июля 2021 в 16:25
0

@Nour Шаблон не имеет именованных групп захвата, поэтому вы можете проверить значение группы 1, чтобы получить сопутствующее значение для этого свойства. Всегда ли строка находится в таком порядке и всегда ли присутствуют свойства?

Nour
1 июля 2021 в 16:36
0

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

avatar
Wiktor Stribiżew
1 июля 2021 в 16:07
0

Вы можете использовать

\b(\w+(?:\s+\w+)*):\s*(.*?)(?=\s*;\w+(?:\s+\w+)*:|$)

См. демонстрацию регулярного выражения. Подробности:

  • \b - граница слова
  • (\w+(?:\s+\w+)*) - Группа 1: один или несколько символов слова, за которыми следует ноль или более вхождений одного или нескольких пробелов и один или несколько символов слова
  • : - двоеточие
  • \s* - ноль или более пробельных символов
  • (.*?) - Группа 2:
  • (?=\s*;\w+(?:\s+\w+)*:|$) - положительный просмотр вперед, который требует, чтобы его шаблон совпадал сразу справа от текущего местоположения:
    • \s*;\w+(?:\s+\w+)*: - ноль или более пробелов, ; и затем один или несколько символов слова, за которыми следует ноль или более вхождений одного или нескольких пробелов и один или несколько символов слова
    • | - или
    • $ - конец строки.

См. демонстрацию C#:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

public class Test
{
    public class Values
    {
        public string UnitType { get; set; }
        public string UnitId { get; set; }
        public string UnitTitle { get; set; }
        public string Applicable { get; set; }
        public string DeclineSender { get; set; }
        public string DeclineReason { get; set; }
        public string TriggeredByUser { get; set; }
        public string DisplayReason { get; set; }
        public string ModifiedBy { get; set; }
        
        public override string ToString() 
        {
            return $"UnitType: {UnitType}\nUnitId: {UnitId}\nUnitTitle: {UnitTitle}\nApplicable: {Applicable}\nDeclineSender: {DeclineSender}\nDeclineReason: {DeclineReason}\nTriggeredByUser: {TriggeredByUser}\nDisplayReason: {DisplayReason}\nModifiedBy: {ModifiedBy}";
        }
        
    }
    
    public static void Main()
    {
        var pattern = @"\b(\w+(?:\s+\w+)*):\s*(.*?)(?=\s*;\w+(?:\s+\w+)*:|$)";
        var text = "unitType:unit_failure;unitId:b7eb;unitTitle:L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A);applicable:true;comment:Decline sender:SELLER;Decline reason:VEN_ACC_SETTINGS;triggered by user:495259708;Display reason: CON_FAILURE;modified by: log_res_mon";
        text = Regex.Replace(text, @"(?<![^;])comment:(?=(?:unitType|unitId|unitTitle|applicable|Decline sender|Decline reason|triggered by user|Display reason|modified by):)", string.Empty, RegexOptions.IgnoreCase);
        var vals = new List<Values>();
        var v = new Values();
        foreach (Match m in Regex.Matches(text, pattern))
        {
            if (m.Groups[1].Value == "unitType") v.UnitType=m.Groups[2].Value;
            if (m.Groups[1].Value == "unitId") v.UnitId=m.Groups[2].Value;
            if (m.Groups[1].Value == "unitTitle") v.UnitTitle=m.Groups[2].Value;
            if (m.Groups[1].Value == "applicable") v.Applicable=m.Groups[2].Value;
            if (m.Groups[1].Value == "Decline sender") v.DeclineSender=m.Groups[2].Value;
            if (m.Groups[1].Value == "Decline reason") v.DeclineReason=m.Groups[2].Value;
            if (m.Groups[1].Value == "triggered by user") v.TriggeredByUser=m.Groups[2].Value;
            if (m.Groups[1].Value == "Display reason") v.DisplayReason=m.Groups[2].Value;
            if (m.Groups[1].Value == "modified by") v.ModifiedBy=m.Groups[2].Value;
            
        }
        Console.WriteLine(v.ToString());
    }
}

Вывод:

UnitType: unit_failure
UnitId: b7eb
UnitTitle: L1-O VEN_ACC_SETTINGS>(30s)>EXCL(A)
Applicable: true
DeclineSender: SELLER
DeclineReason: VEN_ACC_SETTINGS
TriggeredByUser: 495259708
DisplayReason: CON_FAILURE
ModifiedBy: log_res_mon
Nour
1 июля 2021 в 16:13
0

Хорошо, как ты это сделал! 😁

Wiktor Stribiżew
1 июля 2021 в 16:24
0

@Nour Подождите, почему нет ; для пустого значения comment в comment:Decline sender:SELLER;? Разве ввод не должен быть таким, как здесь, regex101.com/r/XCPAhL/2?

Nour
1 июля 2021 в 16:35
1

Спасибо за ответ. Я упомянул, что «комментарий:» является исключением и должен быть исключен из текста. Не волнуйтесь, я все равно буду использовать .Replace, чтобы удалить его.

Wiktor Stribiżew
1 июля 2021 в 16:49
1

@Nour Все теперь исправлено. Вам нужно совпадение без учета регистра?