Запрос Linq для создания словаря из reg-файла

Я создаю простой словарь из reg-файла (экспорт из Windows Regedit). Файл .reg содержит ключ в квадратных скобках, за которым следует ноль или более строк текста, за которыми следует пустая строка. Этот код создаст словарь, который мне нужен:

var a = File.ReadLines("test.reg");

var dict = new Dictionary<String, List<String>>();
foreach (var key in a) {
    if (key.StartsWith("[HKEY")) {
        var iter = a.GetEnumerator();
        var value = new List<String>();
        do {
            iter.MoveNext();
            value.Add(iter.Current);
            } while (String.IsNullOrWhiteSpace(iter.Current) == false);
        dict.Add(key, value);
        }
    }

Я чувствую, что есть более чистый (красивый?) способ сделать это в одном операторе Linq (используя group by), но мне неясно, как реализовать итерацию элементов значений в список. Я подозреваю, что мог бы сделать то же самое GetEnumerator в операторе let, но похоже, что должен быть способ реализовать это, не прибегая к явному итератору.

Образец данных:

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.msu]
@="Microsoft.System.Update.1"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.MTS]
@="WMP11.AssocFile.M2TS"
"Content Type"="video/vnd.dlna.mpeg-tts"
"PerceivedType"="video"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.MTS\OpenWithProgIds]
"WMP11.AssocFile.M2TS"=hex(0):

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.MTS\ShellEx]

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.MTS\ShellEx\{BB2E617C-0920-11D1-9A0B-00C04FC2D6C1}]
@="{9DBD2C50-62AD-11D0-B806-00C04FD706EC}"

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


person Dweeberly    schedule 11.10.2017    source источник
comment
Не могли бы вы добавить небольшой пример того, как выглядит файл?   -  person JuanR    schedule 11.10.2017
comment
Вы можете использовать Aggregate, но это не сильно отличается   -  person Gilad Green    schedule 11.10.2017
comment
Итак, если я правильно понимаю, вы хотите сохранить [..] в качестве ключа, а все остальное до разделительного пробела в качестве значения, верно?   -  person JuanR    schedule 11.10.2017


Ответы (3)


Вы всегда можете использовать Regex:

var dict = new Dictionary<String, List<String>>();
var a = File.ReadAllText(@"test.reg");
var results = Regex.Matches(a, "(\\[[^\\]]+\\])([^\\[]+)\r\n\r\n", RegexOptions.Singleline);           
foreach (Match item in results)
{
    dict.Add(
        item.Groups[1].Value, 
        item.Groups[2].Value.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList()
    );
}

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

person JuanR    schedule 11.10.2017

Вместо использования GetEnumerator вы можете воспользоваться методами TakeWhile и Split, чтобы разбить список на меньший список (каждый подсписок представляет один ключ и его значения)

var registryLines = File.ReadLines("test.reg");

Dictionary<string, List<string>> resultKeys = new Dictionary<string, List<string>>();

while (registryLines.Count() > 0)
{
    // Take the key and values into a single list
    var keyValues = registryLines.TakeWhile(x => !String.IsNullOrWhiteSpace(x)).ToList();

    // Adds a new entry to the dictionary using the first value as key and the rest of the list as value
    if (keyValues != null && keyValues.Count > 0)
        resultKeys.Add(keyValues[0], keyValues.Skip(1).ToList());

    // Jumps to the next registry (+1 to skip the blank line)
    registryLines = registryLines.Skip(keyValues.Count + 1);
}

РЕДАКТИРОВАТЬ на основе вашего обновления

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

Что ж, если вы не можете прочитать весь файл в память, мне не имеет смысла просить решение LINQ. Вот пример того, как вы можете сделать это, читая строку за строкой (все еще нет необходимости в GetEnumerator)

Dictionary<string, List<string>> resultKeys = new Dictionary<string, List<string>>();

using (StreamReader reader = File.OpenText("test.reg"))
{
    List<string> keyAndValues = new List<string>();
    while (!reader.EndOfStream)
    {
        string line = reader.ReadLine();

        // Adds key and values to a list until it finds a blank line
        if (!string.IsNullOrWhiteSpace(line))
            keyAndValues.Add(line);
        else
        {
            // Adds a new entry to the dictionary using the first value as key and the rest of the list as value
            if (keyAndValues != null && keyAndValues.Count > 0)
                resultKeys.Add(keyAndValues[0], keyAndValues.Skip(1).ToList());

            // Starts a new Key collection
            keyAndValues = new List<string>();
        }
    }
}
person romerotg    schedule 11.10.2017
comment
Перечисленный linq File.ReadLines считывает только строку за раз. Я не понимаю, почему linq не имеет смысла для такого решения. Мой исходный код эквивалентен вашему расширенному коду. - person Dweeberly; 14.10.2017

Я думаю, вы можете использовать такой код - если вы можете использовать память -:

var lines = File.ReadAllText(fileName);
var result =
    Regex.Matches(lines, @"\[(?<key>HKEY[^]]+)\]\s+(?<value>[^[]+)")
        .OfType<Match>()
        .ToDictionary(k => k.Groups["key"], v => v.Groups["value"].ToString().Trim('\n', '\r', ' '));

C# Demo
Это займет 24,173 секунды для файла с более чем 4 миллионов строк - Размер: ~ 550 МБ - с использованием памяти 1,2 ГБ.


Редактировать:
Лучше всего использовать File.ReadAllLines, так как это лениво:

var lines = File.ReadAllLines(fileName);
var keyRegex = new Regex(@"\[(?<key>HKEY[^]]+)\]");

var currentKey = string.Empty;
var currentValue = string.Empty;
var result = new Dictionary<string, string>();
foreach (var line in lines)
{
    var match = keyRegex.Match(line);
    if (match.Length > 0)
    {
        if (!string.IsNullOrEmpty(currentKey))
        {
            result.Add(currentKey, currentValue);
            currentValue = string.Empty;
        }

        currentKey = match.Groups["key"].ToString();
    }
    else
    {
        currentValue += line;
    }
}

Это займет 17093 миллисекунды для файла с 795180 строками.

person shA.t    schedule 11.10.2017
comment
Близко, но я думаю, вам нужно k => k.Groups["key"].Value, v => v.Groups["value"].Captures.Cast<Capture>().Select(gc => gc.ToString().Trim('\n', '\r', ' ')).Where(c => !String.IsNullOrEmpty(c)).ToList(). - person NetMage; 11.10.2017