Как обнаружить и отклонить подстроки json внутри более длинного текста, отличного от json?

У меня есть существующее приложение Python, которое регистрируется следующим образом:

import logging
import json
logger = logging.getLogger()

some_var = 'abc'
data = {
   1: 2,
   'blah': {
      ['hello']
   }
}

logger.info(f"The value of some_var is {some_var} and data is {json.dumps(data)}")

Итак, функция logger.info задана:

The value of some_var is abc and data is {1: 2,"blah": {["hello"]}}

В настоящее время мои журналы отправляются в AWS CloudWatch, который творит чудеса и отображает это с отступом, например:

The value of some_var is abc and data is {
   1: 2,
   "blah": {
      ["hello"]
   }
}

Это делает журналы очень четкими для чтения.

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

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

So I want a function which takes in a string, and detects which subset(s) of that string are json, then inserts \n and to pretty-print that json.

example input:

Hello, {"a": {"b": "c"}} is some json data, but also {"c": [1,2,3]} is too

example output

Hello, 
{
  "a": {
    "b": "c"
  }
} 
is some json data, but also 
{
  "c": [
    1,
    2,
    3
  ]
}
is too

I have considered splitting up each entry into everything before and after the first {. Leave the left half as is, and pass the right half to json.dumps(json.loads(x), indent=4).

Но что, если после объекта json в файле журнала есть что-то еще? Хорошо, мы можем просто выбрать все после первого { и до последнего }. Затем передайте средний бит в библиотеку JSON.

Но что, если в этой записи журнала есть два объекта JSON? (Как в приведенном выше примере.) Нам придется использовать стек, чтобы выяснить, появляется ли какое-либо { после того, как все предыдущие { были закрыты соответствующим }.

Но что, если есть что-то вроде {"a": "\}"}. Хм, хорошо, нам нужно справиться с побегом. Теперь мне приходится писать целый синтаксический анализатор json с нуля.

Есть ли простой способ сделать это?

Я полагаю, что мог бы использовать регулярное выражение для замены каждого экземпляра json.dumps(x) во всем моем репо на json.dumps(x, indent=4). Но json.dumps иногда используется вне операторов ведения журнала, и это просто делает все мои строки ведения журнала немного длиннее. Есть ли изящное элегантное решение?

(Бонусные баллы, если он может анализировать и делать отступы в json-подобном выводе, который str(x) создает в python. Это в основном json с одинарными кавычками вместо двойных.)


person falsePockets    schedule 23.04.2020    source источник


Ответы (1)


Чтобы извлечь объекты JSON из строки, см. этот ответ. Функция extract_json_objects() из этого ответа будет обрабатывать объекты JSON и вложенные объекты JSON, но больше ничего. Если в вашем журнале есть список за пределами объекта JSON, он не будет выбран.

В вашем случае измените функцию, чтобы также возвращала строки/текст вокруг всех объектов JSON, чтобы вы могли поместить их все вместе в журнал (или заменить строку журнала):

from json import JSONDecoder

def extract_json_objects(text, decoder=JSONDecoder()):
    pos = 0
    while True:
        match = text.find('{', pos)
        if match == -1:
            yield text[pos:]  # return the remaining text
            break
        yield text[pos:match]  # modification for the non-JSON parts
        try:
            result, index = decoder.raw_decode(text[match:])
            yield result
            pos = match + index
        except ValueError:
            pos = match + 1

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

def jsonify_logline(line):
    line_parts = []
    for result in extract_json_objects(line):
        if isinstance(result, dict):  # got a JSON obj
            line_parts.append(json.dumps(result, indent=4))
        else:                         # got text/non-JSON-obj
            line_parts.append(result)
    # (don't make that a list comprehension, quite un-readable)

    return ''.join(line_parts)

Пример:

>>> demo_text = """Hello, {"a": {"b": "c"}} is some json data, but also {"c": [1,2,3]} is too"""
>>> print(jsonify_logline(demo_text))
Hello, {
    "a": {
        "b": "c"
    }
} is some json data, but also {
    "c": [
        1,
        2,
        3
    ]
} is too
>>>

Другие вещи, не связанные напрямую, которые помогли бы:

  • Instead of using json.dumps(x) for all your log lines, following the DRY principle and create a function like logdump(x) which does whatever you'd want to do, like json.dumps(x), or json.dumps(x, indent=4), or jsonify_logline(x). That way, if you needed to change the JSON format for all your logs, you just change that one function; no need for mass "search & replace", which comes with its own issues and edge-cases.
    • You can even add an optional parameter to it pretty=True to decide if you want it indented or not.
  • Вы можете выполнить массовый поиск и замену всех существующих логлайнов, чтобы выполнить logger.blah(jsonify_logline(<previous log f-string or text>))
  • If you are JSON-dumping custom objects/class instances, then use their __str__ method to always output pretty-printed JSON. And the __repr__ to be non-pretty/compact.
    • Then you wouldn't need to modify the logline at all. Doing logger.info(f'here is my object {x}') would directly invoke obj.__str__.
person aneroid    schedule 23.04.2020
comment
Большой! Такая функция logdump была бы хороша. В частности, у меня есть функция, которую я использую иногда, когда хочу сбросить в json словарь, содержащий дату и время (что приводит к сбою json.dumps) - person falsePockets; 24.04.2020
comment
jsonify_logline — ваш новый logdump. Это не должно давать ошибок для даты и времени, поскольку это не объект dict/json. Но это также может быть заключено в блок try-else. Обновление: исправлена ​​ошибка, из-за которой завершающий текст после последнего объекта JSON не возвращался. - person aneroid; 24.04.2020