Странный escape-символ, добавленный в сообщение записи в регистраторе Python

#python #sqlite #csv #logging #escaping

Вопрос:

Я столкнулся со странной проблемой.

Следующий код

 logger.info("Setting coordinates to [lat: "   str(self.curr_coord.latitude())
                  ", lng: "   str(self.curr_coord.longitude())
                  "]")
 

Значения, возвращаемые упомянутыми выше функциями, просты double .

создает строку, которая содержит escape-символ после str(self.curr_coord.latitude()) . В моем пользовательском обработчике ведения журнала я подключаюсь к файлу базы данных SQLite3. Кроме того, у меня есть возможность выгружать записи журнала в файл CSV.

Приведенный выше код создает запись, которая экспортируется в следующую запись

 2021-10-26T14:47:39.528605 02:00,0,geointelkit,"Setting coordinates to [lat: 48.9475, lng: 8.4106]"
 

Мой CSV-файл выглядит так:

 with open(db_dump_filename, "w", newline="") as csv_dump_file:
    cols = ["Timestamp", "Level", "Source", "Message"]
    db_exporter = csv.writer(csv_dump_file)
    db_exporter.writerow(cols)
    db_exporter.writerows(entries)
 

Все остальные мои записи не имеют кавычек. Сначала я подумал, что это может быть длина сообщения и какие-то странные вещи, которые делает с ним SQLite. Однако после добавления еще более длинной строки у меня не возникло той же проблемы.

Следующее, что я сделал, — это настроил CSV-файл:

 with open(db_dump_filename, "w", newline="") as csv_dump_file:
    cols = ["Timestamp", "Level", "Source", "Message"]
    db_exporter = csv.writer(csv_dump_file,
                             quoting=csv.QUOTE_NONE,
                             delimiter=",",
                             escapechar="#")
    db_exporter.writerow(cols)
    db_exporter.writerows(entries)
 

Выходные данные измененного автора для этой записи были

 2021-10-26T14:47:39.528605 02:00,0,geointelkit,Setting coordinates to [lat: 48.9475#, lng: 8.4106]
 

Обратите # внимание на то, что будет после 48.9475 . Хотя я выполняю некоторое форматирование в своем пользовательском обработчике в отношении записей, которые он обрабатывает, оно связано только с меткой времени (первый столбец) и значением после нее (второй столбец), которое просто сопоставляет числовые значения уровня регистратора (20, 30, 40…) с теми, которые я использую в своем приложении. Сообщение записи никоим образом не затрагивается. Вот форматер, который я использую в своем классе пользовательского регистратора:

 self.formatter = logging.Formatter(fmt="%(asctime)s %(levelno)d %(name)s %(message)s",
                                   datefmt="%Y-%m-%dT%H:%M:%S")
 

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

 LogEntry = namedtuple("LogEntry", "dtime lvl src msg")

class LogHandlerSQLite(logging.Handler):
    db_init_script = """
        CREATE TABLE IF NOT EXISTS logs(
            TimeStamp   TEXT,
            Level       INT,
            Source      TEXT,
            Message     TEXT
        );
        """

    db_clear_script = """
        DROP TABLE IF EXISTS logs;
    """

    db_insert_script = """
        INSERT INTO logs(
            Timestamp,
            Level,
            Source,
            Message
        )
        VALUES (
            '%(tstamp)s',
            %(levelno)d,
            '%(name)s',
            '%(message)s'
       );
       """

    db_get_all = """
        SELECT *
        FROM logs
    """

    def __init__(self, db_path="logs.db"):
        logging.Handler.__init__(self)
        self.db = db_path
        conn = sq3.connect(self.db)
        conn.execute(LogHandlerSQLite.db_init_script)
        conn.commit()
        conn.close()

    def dump(self):
        logger = logging.getLogger("geointelkit")
        logger.info("Dumping log records to CSV file")

        conn = sq3.connect(self.db)
        cur = conn.cursor()
        cur.execute(self.db_get_all)
        entries = cur.fetchall()
        conn.close()

        db_dump_filename = self.rename_db_file(add_extension=False)   ".csv"
        with open(db_dump_filename, "w", newline="") as csv_dump_file:
            cols = ["Timestamp", "Level", "Source", "Message"]
            db_exporter = csv.writer(csv_dump_file,
                                     quoting=csv.QUOTE_NONE,
                                     delimiter=",",
                                     escapechar="#")
            db_exporter.writerow(cols)
            db_exporter.writerows(entries)

    def clear_entries(self):
        conn = sq3.connect(self.db)
        # Drop current logs table
        conn.execute(LogHandlerSQLite.db_clear_script)
        # Shrink size of DB on the filesystem as much as possible
        conn.execute("VACUUM")
        # Create an empty one
        conn.execute(LogHandlerSQLite.db_init_script)
        conn.commit()
        conn.close()

    def format_time(self, record):
        # FIXME Timestamp includes microseconds
        record.tstamp = datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).astimezone().isoformat()

    def emit(self, record):
        """
        Emits a log entry after processing it. The processing includes

         * formatting the entry's structure
         * formatting the datetime component
         * adding exception information (in case the log entry was emitted due to exception)
         * mapping of the log level (Python) to custom level that allows it to be used in the model and log console

        In addition the entry is inserted into a database, which can then be viewed by and processed with external tools

        Args:
            record: Log entry

        Returns:

        """
        # Format log entry
        self.format(record)
        self.format_time(record)
        if record.exc_info:  # for exceptions
            record.exc_text = logging._defaultFormatter.formatException(record.exc_info)
        else:
            record.exc_text = ""

        # Map Python logging module's levels to the custom ones
        if record.levelno == 10:
            record.levelno = LogEntriesModel.LoggingLevel.DEBUG # 3
        elif record.levelno == 20:
            record.levelno = LogEntriesModel.LoggingLevel.INFO  # 0
        elif record.levelno == 30:
            record.levelno = LogEntriesModel.LoggingLevel.WARN  # 1
        elif record.levelno == 40:
            record.levelno = LogEntriesModel.LoggingLevel.ERROR # 2

        # Insert the log record
        sql = LogHandlerSQLite.db_insert_script % record.__dict__
        conn = sq3.connect(self.db)
        conn.execute(sql)
        conn.commit()
        conn.close()

    def rename_db_file(self, add_extension=False):
        db_old_name = os.path.splitext(self.db)[0]
        db_new_name = db_old_name   "_" 
                        datetime.datetime.now().replace(microsecond=0).isoformat().replace(":", "-").replace(" ", "_")

        if add_extension:
            db_new_name = db_new_name   os.path.splitext(self.db)[1]

        return db_new_name

    def close(self):
        """
        Clean-up procedure called during shutdown of the logger that uses the handler
        The previously created log database is renamed by adding the current system
        time stamp. This is done to ensure that the database can be viewed later (e.g.
        bug report) and not overwritten when the logging is setup again

        Returns:

        """
        db_new_name = self.rename_db_file(add_extension=True)
        os.rename(self.db, db_new_name)
        super().close()


class SQLiteLogger(logging.Logger):
    def __init__(self, name="geointelkit", level=logging.DEBUG, db_path="logs.db"):
        logging.Logger.__init__(self, name, level)

        self.formatter = logging.Formatter(fmt="%(asctime)s %(levelno)d %(name)s %(message)s",
                                           datefmt="%Y-%m-%dT%H:%M:%S")
        self.handler = LogHandlerSQLite(db_path=db_path)
        self.handler.setFormatter(self.formatter)
        self.addHandler(self.handler)

    def clear_entries(self):
        self.handler.clear_entries()

    def dump(self):
        self.handler.dump()


def setup_logger():
    logging.setLoggerClass(SQLiteLogger)
 

И последнее, но не менее важное: изменение escapechar на пробел " " привело к появлению двойных пробелов во ВСЕХ сообщениях, даже если они представляют собой простую короткую строку. Вот несколько примеров, включая проблемную запись в журнале, приведенную выше:

 2021-10-26T15:12:09.467067 02:00,3,geointelkit,Adding  OpenStreetMaps  view
2021-10-26T15:12:09.536681 02:00,0,geointelkit,Setting  coordinates  to  [lat:  48.9475 ,  lng:  8.4106]
 

Комментарии:

1. Что именно self.curr_coord.latitude() возвращается и можно ли получить repr из этого…?

2.Чего ты ожидаешь? Это CSV-файл, и вы указываете , его в качестве разделителя и хотите, чтобы строки были без кавычек, и эти строки сами содержат запятые, поэтому их необходимо экранировать с помощью escapechar . Поэтому либо вы цитируете эти строки, "" либо используете escape-символ, либо CSV-не подходящий формат для вашего приложения.

3. @deceze Функция исходит от QGeoCoordinate и возвращает двойное значение. Я добавлю эту информацию к своему вопросу. Спасибо, что указали на это.

4. @a_guest Можете ли вы опубликовать это в качестве ответа. Я думал, что разделитель-это то, что добавляется, а не то, что автор анализирует строки, которые он получает, и извлекает из них разделитель. С вашим предложением я исправил проблему, а именно изменил разделитель, чтобы он отличался от запятой.

5. @rbaleksandar Вы можете добавить ответ самостоятельно, описав, как вы решили проблему с помощью соответствующего кода. Цель записи CSV-файла состоит в том, чтобы создать допустимый файл CSV, и для этого ему необходимо работать со строками, содержащими символ-разделитель. Способ по умолчанию-цитировать строки, что, вероятно, очень интуитивно понятно. Строки заключаются в кавычки во многих форматах, например, в JSON или в самом Python. Если вы измените разделитель, вам нужно обязательно использовать тот же разделитель для анализа файла. Обычно .csv файлы используют запятую в качестве разделителя, поскольку это то, что обещает название формата («значения, разделенные запятыми»).