#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
файлы используют запятую в качестве разделителя, поскольку это то, что обещает название формата («значения, разделенные запятыми»).