Не удается разобрать JSON? Или это текст JavaScript (EDIT: как разобрать пользовательский файл конфигурации)

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

После некоторых попыток мне удалось перемещаться и сворачивать разделы в Notepad++, когда я выбрал средство форматирования как JavaScript.

Однако я застрял в том, как я могу конвертировать/анализировать эти данные в JSON/другой формат, никакие онлайн-инструменты не смогли помочь с этим.

Как я могу разобрать этот текст? В идеале я пытался использовать PowerShell, но Python также был бы вариантом, если бы я мог понять, как я могу даже начать преобразование.

Благодарю вас!

Например, я пытаюсь разобрать каждый из серверов, т.е. test1, test2, test3 и получить данные, перечисленные в каждом блоке.

Вот пример формата файла конфигурации:

servername {
  store {
    servers {
      * {
        value<>
        port<>
        folder<C:\windows>
        monitor<yes>
        args<-T -H>
        xrg<store>
        wysargs<-t -g -b>
        accept_any<yes>
        pdu_length<23622>
      }
      test1 {
        name<test1>
        port<123>
        root<c:\test>
        monitor<yes>
      }
      test2 {
        name<test2>
        port<124>
        root<c:\test>
        monitor<yes>
      }
      test3 {
        name<test3>
        port<125>
        root<c:\test>
        monitor<yes>
      }
    }
    senders
    timeout<30>
  }
}

Обновлено: Добавление вознаграждения постфактум для @zett42 за его потрясающую работу и усилия по созданию решения моей проблемы!

🤔 А знаете ли вы, что...
JavaScript поддерживает работу с графикой и аудио, что позволяет создавать мультимедийные веб-приложения.


1
115
3

Ответы:

Вот что-то, что преобразует вышеуказанный файл конфигурации в dict/json в python. Я просто делаю регулярное выражение, как предложил @zett42.

import re
import json

lines = open('configfile', 'r').read()

# Quotations around the keys (next 3 lines)
lines2 = re.sub(r'([a-zA-Z\d_*]+)\s?{', r'"\1": {', lines)
# Process k<v> as Key, Value pairs 
lines3 = re.sub(r'([a-zA-Z\d_*]+)\s?<([^<]*)>', r'"\1": "\2"', lines2) 
# Process single key word on the line as Key, value pair with empty value
lines4 = re.sub(r'^\s*([a-zA-Z\d_*]+)\s*$', r'"\1": ""', lines3, flags=re.MULTILINE)

# Insert replace \n with commas in lines ending with "
lines5 = re.sub(r'"\n', '",', lines4)

# Remove the comma before the closing bracket
lines6 = re.sub(r',\s*}', '}', lines5)

# Remove quotes from numerical values
lines7 = re.sub(r'"(\d+)"', r'\1', lines6)

# Add commas after closing brackets when needed
lines8 = re.sub(r'[ \t\r\f]+(?!-)', '', lines7)
lines9 = re.sub(r'(?<=})\n(? = ")', r",\n", lines8)

# Enclose in brackets and escape backslash for json parsing
lines10 = '{' + lines9.replace('\\', '\\\\') + '}'

j = json.JSONDecoder().decode(lines10)

Редактировать: Вот альтернатива, которая может быть немного чище

# Replace line with just key with key<>
lines2 = re.sub(r'^([^{<>}]+)$', r'\1<>', lines, flags=re.MULTILINE)
# Remove spaces not within <>
lines3 = re.sub(r'\s(?!.*?>)|\s(?![^<]+>)', '', lines2, flags=re.MULTILINE)
# Quotations
lines4 = re.sub(r'([^{<>}]+)(? = {)', r'"\1":', lines3)
lines5 = re.sub(r'([^:{<>}]+)<([^{<>}]*)>', r'"\1":"\2"', lines4)
# Add commas
lines6 = re.sub(r'(?< = ")"(?!")', ',"', lines5)
lines7 = re.sub(r'}(?!}|$)', '},', lines6)
# Remove quotes from numbers
lines8 = re.sub(r'"(\d+)"', r'\1', lines7)
# Escape \
lines9 = '{' + re.sub(r'\\', r'\\\\', lines8) + '}'

Редактировать: С тех пор я придумал гораздо более простое решение только для PowerShell, который рекомендую использовать.

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


МЮсефи уже опубликовал полезный ответ с реализацией Python.

Для PowerShell я придумал решение, которое работает без шага преобразования в JSON. Вместо этого я принял и обобщил Код токенизатора на основе RegEx Джека Ванлайтли (см. также связанный пост в блоге). токенизатор (он же лексер) разбивает и классифицирует элементы входного текста и выводит плоский поток жетоны (категорий) и связанных данных. парсер может использовать их в качестве входных данных для создания структурированного представления входного текста.

Токенизатор написан на универсальном языке C# и может использоваться для любых входных данных, которые можно разделить с помощью RegEx. Код C# включается в PowerShell с помощью команды Add-Type, поэтому компилятор C# не требуется.

Функция парсера ConvertFrom-ServerData написана на PowerShell для простоты. Вы используете только синтаксический анализатор напрямую, поэтому вам не нужно ничего знать о C#-коде токенизатора. Если вы хотите адаптировать код к другому вводу, вам нужно только изменить код синтаксического анализатора PowerShell.

Сохраните следующий файл в том же каталоге, что и скрипт PowerShell:

"RegExTokenizer.cs":

// Generic, precedence-based RegEx tokenizer.
// This code is based on https://github.com/Vanlightly/DslParser 
// from Jack Vanlightly (https://jack-vanlightly.com).
// Modifications:
// - Interface improved for ease-of-use from PowerShell.
// - Return all groups from the RegEx match instead of just the value. This simplifies parsing of key/value pairs by requiring only a single token definition.
// - Some code simplifications, e. g. replacing "for" loops by "foreach".

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.RegularExpressions;

namespace DslTokenizer {
    public class DslToken<TokenType> {
        public TokenType Token { get; set; }
        public GroupCollection Groups { get; set; }
    }

    public class TokenMatch<TokenType> {
        public TokenType Token { get; set; }
        public GroupCollection Groups { get; set; }
        public int StartIndex { get; set; }
        public int EndIndex { get; set; }
        public int Precedence { get; set; }
    }

    public class TokenDefinition<TokenType> {
        private Regex _regex;
        private readonly TokenType _returnsToken;
        private readonly int _precedence;

        public TokenDefinition( TokenType returnsToken, string regexPattern, int precedence ) {
            _regex = new Regex( regexPattern, RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled );
            _returnsToken = returnsToken;
            _precedence = precedence;
        }

        public IEnumerable<TokenMatch<TokenType>> FindMatches( string inputString ) {

            foreach( Match match in _regex.Matches( inputString ) ) {
                yield return new TokenMatch<TokenType>() {
                    StartIndex = match.Index,
                    EndIndex   = match.Index + match.Length,
                    Token      = _returnsToken,
                    Groups     = match.Groups,
                    Precedence = _precedence
                };
            }
        }
    }

    public class PrecedenceBasedRegexTokenizer<TokenType> {

        private List<TokenDefinition<TokenType>> _tokenDefinitions = new List<TokenDefinition<TokenType>>();

        public PrecedenceBasedRegexTokenizer() {}

        public PrecedenceBasedRegexTokenizer( IEnumerable<TokenDefinition<TokenType>> tokenDefinitions ) {
            _tokenDefinitions = tokenDefinitions.ToList();
        }

        // Easy-to-use interface as alternative to constructor that takes an IEnumerable.
        public void AddTokenDef( TokenType returnsToken, string regexPattern, int precedence = 0 ) {
            _tokenDefinitions.Add( new TokenDefinition<TokenType>( returnsToken, regexPattern, precedence ) );
        }

        public IEnumerable<DslToken<TokenType>> Tokenize( string lqlText ) {

            var tokenMatches = FindTokenMatches( lqlText );

            var groupedByIndex = tokenMatches.GroupBy( x => x.StartIndex )
                .OrderBy( x => x.Key )
                .ToList();

            TokenMatch<TokenType> lastMatch = null;

            foreach( var match in groupedByIndex ) {

                var bestMatch = match.OrderBy( x => x.Precedence ).First();
                if ( lastMatch != null && bestMatch.StartIndex < lastMatch.EndIndex ) {
                    continue;
                }

                yield return new DslToken<TokenType>(){ Token = bestMatch.Token, Groups = bestMatch.Groups };

                lastMatch = bestMatch;
            }
        }

        private List<TokenMatch<TokenType>> FindTokenMatches( string lqlText ) {

            var tokenMatches = new List<TokenMatch<TokenType>>();

            foreach( var tokenDefinition in _tokenDefinitions ) {
                tokenMatches.AddRange( tokenDefinition.FindMatches( lqlText ).ToList() );
            }
            return tokenMatches;
        }
    }        
}

Функция парсера, написанная в PowerShell:

$ErrorActionPreference = 'Stop'

Add-Type -TypeDefinition (Get-Content $PSScriptRoot\RegExTokenizer.cs -Raw)

Function ConvertFrom-ServerData {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [string] $InputObject
    )

    begin {
        # Define the kind of possible tokens.
        enum ServerDataTokens {
            ObjectBegin
            ObjectEnd
            ValueInt
            ValueBool
            ValueString
            KeyOnly
        }
        
        # Create an instance of the tokenizer from "RegExTokenizer.cs".
        $tokenizer = [DslTokenizer.PrecedenceBasedRegexTokenizer[ServerDataTokens]]::new()

        # Define a RegEx for each token where 1st group matches key and 2nd matches value (if any).
        # To resolve ambiguities, most specific RegEx must come first 
        # (e. g. ValueInt line must come before ValueString line).
        # Alternatively pass a 3rd integer parameter that defines the precedence.        
        $tokenizer.AddTokenDef( [ServerDataTokens]::ObjectBegin, '^\s*([\w*]+)\s*{' )
        $tokenizer.AddTokenDef( [ServerDataTokens]::ObjectEnd,   '^\s*}\s*$' )
        $tokenizer.AddTokenDef( [ServerDataTokens]::ValueInt,    '^\s*(\w+)\s*<([+-]?\d+)>\s*$' )
        $tokenizer.AddTokenDef( [ServerDataTokens]::ValueBool,   '^\s*(\w+)\s*<(yes|no)>\s*$' )
        $tokenizer.AddTokenDef( [ServerDataTokens]::ValueString, '^\s*(\w+)\s*<(.*)>\s*$' )
        $tokenizer.AddTokenDef( [ServerDataTokens]::KeyOnly,     '^\s*(\w+)\s*$' )
    }

    process {
        # Output is an ordered hashtable
        $outputObject = [ordered] @{}

        $curObject = $outputObject

        # A stack is used to keep track of nested objects.
        $stack = [Collections.Stack]::new()
        
        # For each token produced by the tokenizer
        $tokenizer.Tokenize( $InputObject ).ForEach{
        
            # $_.Groups[0] is the full match, which we discard by assigning to $null 
            $null, $key, $value = $_.Groups.Value
            
            switch( $_.Token ) {
                ([ServerDataTokens]::ObjectBegin) {  
                    $child = [ordered] @{} 
                    $curObject[ $key ] = $child
                    $stack.Push( $curObject )
                    $curObject = $child
                    break
                }
                ([ServerDataTokens]::ObjectEnd) {
                    $curObject = $stack.Pop()
                    break
                }
                ([ServerDataTokens]::ValueInt) {
                    $intValue = 0
                    $curObject[ $key ] = if ( [int]::TryParse( $value, [ref] $intValue ) ) { $intValue } else { $value }
                    break
                }
                ([ServerDataTokens]::ValueBool) {
                    $curObject[ $key ] = $value -eq 'yes'
                    break
                }
                ([ServerDataTokens]::ValueString) {
                    $curObject[ $key ] = $value
                    break
                }
                ([ServerDataTokens]::KeyOnly) {
                    $curObject[ $key ] = $null
                    break
                }
            }
        }

        $outputObject  # Implicit output
    }
}

Пример использования:

$sampleData = @'
servername {
    store {
      servers {
        * {
          value<>
          port<>
          folder<C:\windows>
          monitor<yes>
          args<-T -H>
          xrg<store>
          wysargs<-t -g -b>
          accept_any<yes>
          pdu_length<23622>
        }
        test1 {
          name<test1>
          port<123>
          root<c:\test>
          monitor<yes>
        }
        test2 {
          name<test2>
          port<124>
          root<c:\test>
          monitor<yes>
        }
        test3 {
          name<test3>
          port<125>
          root<c:\test>
          monitor<yes>
        }
      }
      senders
      timeout<30>
    }
  }
'@

# Call the parser
$objects = $sampleData | ConvertFrom-ServerData

# The parser outputs nested hashtables, so we have to use GetEnumerator() to
# iterate over the key/value pairs.

$objects.servername.store.servers.GetEnumerator().ForEach{
    "[ SERVER: $($_.Key) ]"
    # Convert server values hashtable to PSCustomObject for better output formatting
    [PSCustomObject] $_.Value | Format-List
}

Выход:

[ SERVER: * ]

value      : 
port       : 
folder     : C:\windows
monitor    : True      
args       : -T -H     
xrg        : store     
wysargs    : -t -g -b  
accept_any : True      
pdu_length : 23622     


[ SERVER: test1 ]      

name    : test1        
port    : 123
root    : c:\test      
monitor : True


[ SERVER: test2 ]      

name    : test2        
port    : 124
root    : c:\test      
monitor : True


[ SERVER: test3 ]

name    : test3
port    : 125
root    : c:\test
monitor : True

Примечания:

  • Если вы передаете ввод из Get-Content парсеру, обязательно используйте параметр -Raw, например. грамм. $objects = Get-Content input.cfg -Raw | ConvertFrom-ServerData. В противном случае синтаксический анализатор будет пытаться анализировать каждую входную строку самостоятельно.
  • Я решил преобразовать значения «да»/«нет» в bool, поэтому они выводятся как «Истина»/«Ложь». Удалите строку $tokenizer.AddTokenDef( 'ValueBool', ..., чтобы проанализировать их как string и вывести как есть.
  • Ключи без значений <> («отправители» в примере) хранятся как ключи со значением $null.
  • RegEx обеспечивает, чтобы значения могли быть только однострочными (как следует из примера данных). Это позволяет нам встраивать символы > без необходимости экранировать их.

Решено

Я придумал еще более простое решение, чем мой Предыдущая, в котором используется только код PowerShell.

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

Остальной код структурно похож на версию C#/PS.

using namespace System.Text.RegularExpressions

$ErrorActionPreference = 'Stop'

Function ConvertFrom-ServerData {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [string] $InputObject
    )

    begin {
        # Key can consist of anything except whitespace and < > { }
        $keyPattern = '[^\s<>{}]+'

        # Order of the patterns is important 
        $pattern = (
            "(?<IntKey>$keyPattern)\s*<(?<IntValue>\d+)>",
            "(?<TrueKey>$keyPattern)\s*<yes>",
            "(?<FalseKey>$keyPattern)\s*<no>",
            "(?<StrKey>$keyPattern)\s*<(?<StrValue>.*?)>",
            "(?<ObjectBegin>$keyPattern)\s*{",
            "(?<ObjectEnd>})",
            "(?<KeyOnly>$keyPattern)",
            "(?<Invalid>\S+)"  # any non-whitespace sequence that didn't match the valid patterns
        ) -join '|'
    }

    process {
        # Output is an ordered hashtable
        $curObject = $outputObject = [ordered] @{}

        # A stack is used to keep track of nested objects.
        $stack = [Collections.Stack]::new()
        
        # For each pattern match
        foreach( $match in [RegEx]::Matches( $InputObject, $pattern, [RegexOptions]::Multiline ) ) {

            # Get the RegEx groups that have actually matched.
            $matchGroups = $match.Groups.Where{ $_.Success -and $_.Name.Length -gt 1 }

            $key = $matchGroups[ 0 ].Value

            switch( $matchGroups[ 0 ].Name ) {
                'ObjectBegin' {
                    $child = [ordered] @{} 
                    $curObject[ $key ] = $child
                    $stack.Push( $curObject )
                    $curObject = $child
                    break                    
                }
                'ObjectEnd' {
                    $curObject = $stack.Pop()
                    break
                }
                'IntKey' {
                    $value = $matchGroups[ 1 ].Value 
                    $intValue = 0
                    $curObject[ $key ] = if ( [int]::TryParse( $value, [ref] $intValue ) ) { $intValue } else { $value }
                    break
                }
                'TrueKey' {
                    $curObject[ $key ] = $true
                    break
                }
                'FalseKey' {
                    $curObject[ $key ] = $false
                    break
                }
                'StrKey' {
                    $value = $matchGroups[ 1 ].Value
                    $curObject[ $key ] = $value
                    break
                }
                'KeyOnly' {
                    $curObject[ $key ] = $null
                    break
                }
                'Invalid' {
                    Write-Warning "Invalid token at index $($match.Index): $key"
                    break
                }
            }
        }

        $outputObject  # Implicit output
    }
}

Пример использования:


$sampleData = @'
test-server {
    store {
      servers {
        * {
          value<>
          port<>
          folder<C:\windows> monitor<yes>
          args<-T -H>
          xrg<store>
          wysargs<-t -g -b>
          accept_any<yes>
          pdu_length<23622>
        }
        test1 {
          name<test1>
          port<123>
          root<c:\test>
          monitor<yes>
        }
        test2 {
          name<test2>
          port<124>
          root<c:\test>
          monitor<yes>
        }
        test3 {
          name<test3>
          port<125>
          root<c:\test>
          monitor<yes>
        }
      }
      senders
      timeout<30>
    }
  }
'@

# Call the parser
$objects = $sampleData | ConvertFrom-ServerData

# Uncomment to verify the whole result
#$objects | ConvertTo-Json -Depth 10

# The parser outputs nested hashtables, so we have to use GetEnumerator() to
# iterate over the key/value pairs.
$objects.'test-server'.store.servers.GetEnumerator().ForEach{
    "[ SERVER: $($_.Key) ]"
    # Convert server values hashtable to PSCustomObject for better output formatting
    [PSCustomObject] $_.Value | Format-List
}

Выход:

[ SERVER: * ]

value      : 
port       : 
folder     : C:\windows
monitor    : True
args       : -T -H
xrg        : store
wysargs    : -t -g -b
accept_any : True
pdu_length : 23622


[ SERVER: test1 ]

name    : test1
port    : 123
root    : c:\test
monitor : True


[ SERVER: test2 ]

name    : test2
port    : 124
root    : c:\test
monitor : True


[ SERVER: test3 ]

name    : test3
port    : 125
root    : c:\test
monitor : True

Примечания:

  • Я еще больше ослабил регулярные выражения. Ключи теперь могут состоять из любых символов, кроме пробелов, <, >, { и }.
  • Разрывы строк больше не требуются. Это более гибко, но у вас не может быть строк со встроенными символами >. Дайте мне знать, если это проблема.
  • Я добавил обнаружение недопустимых токенов, которые выводятся в виде предупреждений. Удалите строку "(?<Invalid>\S+)", если вы хотите вместо этого игнорировать недействительные токены.