Практический гайд Время чтения: 10–12 минут Python 3.9+

CSV, JSON или Markdown: какой формат лучше для промптов

CSV, JSON или Markdown: какой формат лучше для промптов
Temporary fallback cover; replace in editorial pass.

Формат данных влияет на то, насколько легко языковой модели прочитать таблицу, понять вложенные поля и не перепутать разделители. В этой статье разберём, когда использовать CSV, JSON или Markdown, а затем соберём воспроизводимый тест и готовый Python-скрипт без внешних зависимостей.

Самое важное из мира AI — в канале MAX AgentLabОтдельные разборы, инструменты и практические схемы Читайте Agent Lab в Telegram Разборы, кейсы и новости о практических AI-агентах без лишней воды.

Короткий ответ

Для хранения исходных данных выбирайте JSON, если структура вложенная, и CSV, если данные плоские и табличные. Для объяснения данных в промпте часто удобен Markdown: таблица визуально читаема и хорошо отделяет строки и столбцы.

Однако Markdown-таблица не должна становиться главным хранилищем. Это формат представления. Надёжная схема выглядит так:

  1. хранить оригинал в CSV или JSON;
  2. преобразовывать его в Markdown только перед отправкой в промпт;
  3. экранировать вертикальные черты, обратные слэши и переносы строк;
  4. проверять количество строк, колонок и размер результата.

Что выбрать: CSV, JSON или Markdown

Формат Сильные стороны Ограничения Когда использовать
CSV Компактный, удобен для плоских таблиц, поддерживается стандартной библиотекой Python. Вложенные поля и типы данных требуют дополнительных соглашений. Экспорт из таблиц, списки записей, простые датасеты.
JSON Сохраняет вложенность, массивы, числа, логические значения и null. Может занимать больше места и быть менее удобным для беглого чтения. API-ответы, конфигурации, сложные объекты.
Markdown Хорошо виден человеку и модели, строки и колонки легко различать. Разделитель | имеет специальное значение; переносы строк ломают разметку. Финальное представление табличных данных в промпте.

Размер в байтах нельзя считать универсальным показателем качества. Он зависит от кодировки, отступов, имён полей и способа экранирования. Поэтому сравнение ниже не утверждает, что один формат всегда лучше другого: оно показывает поведение конкретного набора данных.

Почему простая замена запятых на вертикальные черты опасна

Такой код выглядит убедительно, но некорректен:

row.replace(",", " | ")

Запятая может находиться внутри значения, например в адресе или описании. Кроме того, CSV допускает поля с переносами строк внутри кавычек. В JSON проблема проявляется иначе: объект может содержать вложенный объект или массив, которые нельзя бездумно превращать в несколько колонок.

Безопасный преобразователь должен сначала разобрать формат специализированным парсером, а уже затем сериализовать каждую ячейку. Для CSV это модуль csv, для JSON — модуль json. Оба входят в стандартную библиотеку Python.

Правила безопасной Markdown-таблицы

В скрипте ниже используются четыре правила:

  1. обратный слэш экранируется первым, чтобы не возникали двойные трактовки;
  2. символ | превращается в \| и не создаёт новый столбец;
  3. перенос строки заменяется на видимую последовательность \n;
  4. вложенные JSON-значения записываются компактным JSON внутри одной ячейки.

Последнее правило сохраняет структуру вложенного поля в пределах одной ячейки. Если модели нужно активно рассуждать по вложенным полям, можно дополнительно сделать плоскую таблицу с колонками вроде user.name и user.contacts.email. Это уже другая схема представления, поэтому её следует выбирать явно.

Готовый Python-скрипт

Сохраните код в файл convert_table.py. Скрипт читает CSV или JSON, создаёт Markdown-файл и печатает размеры всех трёх представлений в UTF-8.

#!/usr/bin/env python3
import argparse
import csv
import json
from pathlib import Path
from typing import Any, Dict, Iterable, List


def escape_markdown_cell(value: str) -> str:
    """Сохраняет содержимое ячейки в одной строке Markdown-таблицы."""
    value = value.replace("\\", "\\\\")
    value = value.replace("|", "\\|")
    value = value.replace("\r\n", "\\n")
    value = value.replace("\n", "\\n")
    value = value.replace("\r", "\\n")
    return value


def json_cell(value: Any) -> str:
    """Представляет JSON-значение компактно и без потери Unicode."""
    if isinstance(value, (dict, list)):
        return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
    if value is None:
        return "null"
    if isinstance(value, bool):
        return "true" if value else "false"
    return str(value)


def read_csv(path: Path) -> List[Dict[str, str]]:
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames is None:
            raise ValueError("CSV не содержит строку заголовков.")

        rows = []
        for row_number, row in enumerate(reader, start=2):
            if None in row:
                raise ValueError(
                    f"Строка {row_number} содержит лишние значения: {row[None]!r}"
                )
            rows.append({key: value if value is not None else "" for key, value in row.items()})
        return rows


def read_json(path: Path) -> List[Dict[str, Any]]:
    with path.open("r", encoding="utf-8") as handle:
        data = json.load(handle)

    if isinstance(data, dict):
        data = [data]

    if not isinstance(data, list) or not all(isinstance(item, dict) for item in data):
        raise ValueError("JSON должен быть объектом или массивом объектов.")

    return data


def columns_for(rows: Iterable[Dict[str, Any]]) -> List[str]:
    columns = []
    seen = set()

    for row in rows:
        for key in row:
            key = str(key)
            if key not in seen:
                seen.add(key)
                columns.append(key)

    return columns


def to_markdown(rows: List[Dict[str, Any]], columns: List[str]) -> str:
    if not columns:
        raise ValueError("Не найдено ни одного поля.")

    header = "| " + " | ".join(escape_markdown_cell(c) for c in columns) + " |"
    separator = "| " + " | ".join("---" for _ in columns) + " |"
    body = []

    for row in rows:
        cells = []
        for column in columns:
            value = row.get(column, "")
            cells.append(escape_markdown_cell(json_cell(value)))
        body.append("| " + " | ".join(cells) + " |")

    return "\n".join([header, separator, *body]) + "\n"


def byte_size(path: Path) -> int:
    return len(path.read_bytes())


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Преобразование CSV или JSON в безопасную Markdown-таблицу."
    )
    parser.add_argument("input", type=Path)
    parser.add_argument("-o", "--output", type=Path, default=Path("table.md"))
    parser.add_argument(
        "--format",
        choices=("csv", "json"),
        required=True,
        help="Формат входного файла."
    )
    args = parser.parse_args()

    if args.format == "csv":
        rows = read_csv(args.input)
    else:
        rows = read_json(args.input)

    columns = columns_for(rows)
    markdown = to_markdown(rows, columns)
    args.output.write_text(markdown, encoding="utf-8", newline="\n")

    print(f"Строк данных: {len(rows)}")
    print(f"Колонок: {len(columns)}")
    print(f"Markdown: {args.output} ({byte_size(args.output)} байт UTF-8)")
    print(f"Источник: {args.input} ({byte_size(args.input)} байт UTF-8)")


if __name__ == "__main__":
    main()

Воспроизводимое сравнение размеров

Создайте тестовые файлы с одинаковыми данными. В примере есть вертикальная черта, запятая, перенос строки, массив и вложенный объект. Это пример для проверки, а не измерение внешнего набора данных.

mkdir -p prompt-format-test
cd prompt-format-test

cat > data.csv <<'EOF'
id,name,note,profile
1,"Анна, Петрова","Первая строка
Вторая строка","{""role"":""editor"",""active"":true}"
2,"Илья","Значение с | внутри","{""role"":""analyst"",""tags"":[""python"",""llm""]}"
EOF

cat > data.json <<'EOF'
[
  {
    "id": 1,
    "name": "Анна, Петрова",
    "note": "Первая строка\nВторая строка",
    "profile": {
      "role": "editor",
      "active": true
    }
  },
  {
    "id": 2,
    "name": "Илья",
    "note": "Значение с | внутри",
    "profile": {
      "role": "analyst",
      "tags": ["python", "llm"]
    }
  }
]
EOF

python3 ../convert_table.py data.csv --format csv -o from-csv.md
python3 ../convert_table.py data.json --format json -o from-json.md

wc -c data.csv data.json from-csv.md from-json.md

Команда wc -c показывает размер файлов в байтах. Результаты зависят от версии исходных файлов и не должны заранее подставляться в статью или отчёт. Для честного сравнения запускайте обе конвертации на одной машине и не меняйте входные данные между запусками.

Если нужно сравнивать именно представления, а не оформление исходников, используйте JSON без отступов и CSV с одинаковым порядком колонок. Для практического промпта важен также размер токенизированного текста, но точное число токенов зависит от конкретной модели и её токенизатора. Размер в UTF-8 — воспроизводимый приближённый показатель, а не замена токенизации.

Проверка результата

Откройте созданный файл from-csv.md и проверьте:

  • строка с Анна, Петрова осталась одной ячейкой;
  • перенос строки отображается как последовательность \n, а не ломает таблицу;
  • символ | отображается внутри ячейки;
  • вложенный объект не исчез и остался JSON-значением;
  • число строк данных совпадает с исходным файлом.

Быстрая структурная проверка для CSV:

python3 -c 'import csv; from pathlib import Path; print(sum(1 for _ in csv.DictReader(Path("data.csv").open(encoding="utf-8-sig", newline=""))))'

Быстрая проверка JSON:

python3 -c 'import json; from pathlib import Path; data=json.loads(Path("data.json").read_text(encoding="utf-8")); print(len(data) if isinstance(data, list) else 1)'

Эти команды только читают локальные файлы и печатают количество записей. Перед обработкой реальных данных убедитесь, что путь к файлу указан явно и не содержит неожиданных подстановок оболочки.

Типовые ошибки

Разбор CSV через split

line.split(",") не учитывает кавычки и переносы строк внутри полей. Используйте csv.DictReader и открывайте файл с newline="".

Незащищённый символ |

В Markdown вертикальная черта разделяет колонки. Если не заменить её на \|, число столбцов изменится.

Прямое преобразование вложенного JSON

Массивы и объекты нельзя надёжно свести к одной строке с помощью str(). Используйте json.dumps с ensure_ascii=False и компактными разделителями.

Смешивание исходника и представления

Markdown удобен для чтения, но его не следует использовать как единственную копию данных. Храните исходный CSV или JSON отдельно, чтобы можно было повторить преобразование и проверить результат.

Неверная интерпретация размера

Меньший файл не всегда означает лучший промпт. Слишком компактное представление может быть менее понятным модели или человеку. Сравнивайте размер вместе с корректностью и качеством ответа на ваших задачах.

Ограничения подхода

Скрипт рассчитан на CSV с первой строкой заголовков и JSON, который является объектом или массивом объектов. Он не угадывает разделитель CSV: для файлов с точкой с запятой, табуляцией или нестандартными кавычками потребуется явно расширить параметры csv.DictReader.

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

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

Итог

Если данные плоские и их нужно быстро передать в таблице, начните с CSV. Если важны вложенные поля и типы, храните JSON. Если задача — дать модели компактное визуально понятное представление, преобразуйте копию данных в Markdown и экранируйте содержимое ячеек.

Практическое правило простое: источник должен оставаться структурированным, а Markdown — проверяемым слоем представления. Так вы сможете повторить преобразование, сравнить размеры и обнаружить потерю данных до отправки промпта.

Дополнительные инструкции собраны в разделе гайдов, а термины — в глоссарии.