Обернуть наименования заголовков в двойные кавычки при использовании COPY TO CSV
Через Python сохраняю таблицу из Postgresql в CSV с помощью copy_expert
:
query = f'''copy ({function}) to stdout with (format csv, delimiter ',', header true, force_quote *);'''
with db_connection.cursor() as cursor, open(file_path, "w") as file:
cursor.copy_expert(query, file)
Опция FORCE_QUOTE оборачивает в двойные кавычки данные, но не имена заголовков, которые сейчас выгружаются таким образом:
ID,IPV,MAC_ADDR, ...
Мне же требуется заголовки выгружать внутри ""
:
"ID","IPV","MAC_ADDR", ...
Изначально в select оборачивание в двойные кавычки используется как способ корректно указать псевдонимы - они не идут дальше как часть имени:
select
(v.dtl_id ||'_'|| v.id)::varchar as "ID",
null:varchar as "IPV",
и т.д. ...
Попробовал экранировать двойные кавычки с помощью ещё одних:
select
(v.dtl_id ||'_'|| v.id)::varchar as """ID""",
null:varchar as """IPV""",
и т.д. ...
В том же DBeaver это работает корректно - при запросе имя колонки начинает отображаться внутри двойных кавычек. Но при выгрузке CSV вышеуказанным методом почему-то заголовки сохраняются с 3-мя парами двойных кавычек - как будто исходное экранирование пропадает и считывается как есть:
"""ID""","""IPV""","""MAC_ADDR""", ...
Есть ли иной способ экранировать двойные кавычки для псевдонимов, или иная опция для их оборачивания как FORCE_QUOTE для данных?
UPD: нашел другой способ в Postgresql указать двойные кавычки:
select
(v.dtl_id ||'_'|| v.id)::varchar as U&"\0022IDU&"\0022,
null:varchar as U&"\0022IPVU&"\0022,
и т.д. ...
Однако, он опять таки работает внутри SQL-инструментов, но при формировании CSV на выходе тройные двойные кавычки.
В качестве теста ещё проверил pandas
- там тоже заголовки оказываются по итогу в тройных двойных кавычках:
import pandas as pd
d = {'1': [1, 2, 3], '"2"': ['x', 'ooo "abc"', 'y']}
df = pd.DataFrame.from_dict(d)
df.to_csv(
'test.csv',
sep=',',
header=True,
index=False
)
На выходе CSV:
1,"""2"""
1,x
2,"ooo ""abc"""
3,y
Будто и в COPY, и в pandas механизмы экранирования кавычек и оборачивания данных в кавычки наслаиваются друг на друга, что в результате не дает просто указать заголовки только в одних двойных кавычках.
Предположение следующее:
" " " ID"""
^-- исходные двойные кавычки
^-- кавычки для экранирования исходных
^-- кавычки от оборачивания данных (почему то распространилось и на заголовок)
Ответы (1 шт):
По итогам тестов и изысканий нашелся способ не через COPY TO
, а через pandas
. По всей видимости непосредственно в COPY TO
мало опций - для того же COPY FROM
имеется больше, как FORCE_NOT_NULL и т.п.
При использовании pd.to_csv()
можно указать параметр quoting=csv.QUOTE_ALL
- он позволяет обернуть в кавычки как все данные (аналогично FORCE_QUOTE), так и заголовки (документация). В модуле csv
имеются и другие константы, благодаря которым можно настроить формирование файла гибче, чем в случае с COPY TO
+ copy_expert
.
В моём случае перегоняются большие объемы данных (до 10Гб), поэтому просто в датафрейм загрузить всю информацию из БД не выйдет - необходимо экспортировать данные из PostgreSQL в CSV по кускам (чанкам):
import os
import csv
import pandas as pd
from sqlalchemy import create_engine, Engine
def upload_to_csv(db_engine: Engine, file_name: str, query: str, chunk: int) -> None:
db_connection = db_engine.connect().execution_options(
stream_results=True,
max_row_buffer=chunk
)
logger.info(f"Загрузка CSV {file_name}")
try:
# в первый df пишем заголовки и данные
header, mode = True, 'w'
for df in pd.read_sql(query, db_connection, chunksize=chunk):
df.to_csv(file_name, mode=mode, header=header, index=False, quoting=csv.QUOTE_ALL)
# в следующие df заголовки уже пропускаем, т.к. теперь добавляем оставшиеся строки
if header:
header, mode = False, 'a'
logger.info("Загрузка успешно завершена")
except Exception as e:
logger.error(f"Ошибка загрузки CSV {file_name}: {e}")
finally:
db_connection.close()
# Требуемые аргументы для функции upload_to_csv()
db_engine = create_engine(conn_string) # conn_string - строка с параметрами подключения к БД
file_name = os.path.join('my_path', 'test.csv')
query = '''SELECT var1, var2 ... FROM schema.table WHERE ...'''
chunk = 10000
В результате получаем требуемый CSV:
"ID","IPV","MAC_ADDR", ...
"1234567890","192.168.0.1","00-00-00-00-00-00", ...
P.S.: вероятно есть какой-то вариант объединить базовый csv
и copy_expert
, чтобы без pandas
(с его нюансами по приведению типов данных), но мне собрать такую функцию не удалось. Возможный ответ с подобным решением приветствуется.