Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 20 из 25 ~18 мин +20 XP

Импорт: таблица в структуры Go

Excelize импорт решает задачу, обратную экспорту: на вход приходит чужой xlsx — прайс поставщика, ведомость, выгрузка из чужой системы, — а на выходе нужны структуры Go с числами, датами и понятными ошибками данных. Парсинг excel таблицы отличается от чтения своего файла тем, что вы не контролируете источник: колонки переставляют, числа пишут с пробелами, строки оставляют недозаполненными. В уроке разберём маппинг колонок по заголовкам, парсинг через strconv и time, накопление ошибок с номерами строк и честные ловушки: хвостовые пустые ячейки, форматированные значения и формулы без кэша.

Содержание урока · 9 разделов

Чтение своего файла — это проверка того, что вы записали: слаги колонок известны, типы гарантированы, границы посчитаны. Чтение чужого файла устроено иначе: лист — это договор, который составил не вы, и каждый импорт начинается с разведки. Первый шаг разведки — шапка: заголовки колонок скажут, что в файле лежит, а их порядок ничего не гарантирует. Отсюда главный принцип урока: маппим колонки по заголовкам, а не по индексам, потому что индексы живут ровно до первого файла, где поставщик вставил колонку посередине. Второй принцип: всё, что вынимается из ячеек, — строки, и приведение к числам и датам — ваша работа с обработкой ошибок. Третий принцип: битая строка — это не повод ронять импорт, а запись в журнал с номером строки, который пользователь сопоставит со своим файлом. Эти три принципа мы и разберём по очереди, с кодом и таблицей типовых поломок.

Excelize импорт: маппинг по заголовкам#

Схема маппинга одна и та же в любом импорте: прочитать первую строку, построить карту заголовок — индекс колонки, дальше разбирать строки по именам колонок. Заголовки перед построением карты чистят TrimSpace: лишний пробел в конце названия колонки — классическая причина, почему колонка «не находится». Для доступа к ячейке пишется маленький хелпер: он принимает строку, имя колонки и возвращает значение или пустоту, если колонки нет либо она пуста. Хелпер заодно решает вторую проблему — недозаполненные строки: срез из GetRows может быть короче, чем колонок в шапке, и прямое обращение по индексу паникует. Структуры-приёмники описываются под ваш домен: артикул строкой, количество целым, цена дробным, дата поставки временем. Так excelize импорт превращается из угадывания индексов в чтение договора: колонка ищется по имени, а сдвиги в файле перестают быть катастрофой.

Маппинг прайса в структуры

GO
rows, err := f.GetRows("Прайс")
if err != nil {
    panic(err)
}
if len(rows) == 0 {
    panic(errors.New("пустой лист"))
}

header := map[string]int{}
for i, name := range rows[0] {
    header[strings.TrimSpace(name)] = i
}

type Item struct {
    SKU   string
    Name  string
    Price float64
    Qty   int
}

pick := func(row []string, name string) string {
    idx, ok := header[name]
    if !ok || idx >= len(row) {
        return ""
    }
    return strings.TrimSpace(row[idx])
}

items := make([]Item, 0, len(rows)-1)
for _, row := range rows[1:] {
    price, _ := strconv.ParseFloat(
        strings.ReplaceAll(pick(row, "Цена"), ",", "."), 64)

    items = append(items, Item{
        SKU:   pick(row, "Артикул"),
        Name:  pick(row, "Наименование"),
        Price: price,
    })
}

Карта заголовков строится один раз по первой строке, хелпер pick достаёт значения по именам колонок и не паникует на недозаполненных строках. Цена приходит форматированной, поэтому запятая заменяется на точку до ParseFloat — о разбросе форматов дальше и отдельно.

Числа и даты: strconv и time#

Ячейка с числом в чужом файле — это отформатированная строка: с разделителями групп «1 234», с запятой вместо точки, с валютой на конце. strconv.ParseFloat такое не съест — сначала значения приводят к машинному виду: пробелы и неразрывные пробелы убираются, запятая меняется на точку, валютные символы срезаются. Опция RawCellValue при чтении отдаёт сырые значения без наложения формата и часто экономит половину чистки. Даты разбираются time.Parse с лэйаутом конкретной колонки — «02.01.2006» для российских прайсов, и это отдельная колонка, потому что даты в разных колонках одного файла встречаются в разных форматах. Булевы значения приходят «TRUE», «1», «да» — и сводятся к bool вашими правилами. Ошибки парсинга — это данные о данных: строка, где количество читается как «10 шт», не парсится в число, но сообщить об этом стоит человеку, а не исключению.

Парсинг с накоплением ошибок

GO
type RowError struct {
    Row int
    Msg string
}

var rowErrs []RowError

for i, row := range rows[1:] {
    excelRow := i + 2

    qty, err := strconv.Atoi(pick(row, "Количество"))
    if err != nil {
        rowErrs = append(rowErrs, RowError{
            Row: excelRow, Msg: "количество: " + pick(row, "Количество"),
        })
        continue
    }

    date, err := time.Parse("02.01.2006", pick(row, "Дата поставки"))
    if err != nil {
        rowErrs = append(rowErrs, RowError{
            Row: excelRow, Msg: "дата: " + pick(row, "Дата поставки"),
        })
        continue
    }

    items = append(items, Item{Qty: qty, Date: date})
}

fmt.Printf("импортировано %d, ошибок %d
", len(items), len(rowErrs))

Номер строки для журнала считается как индекс плюс два: первая строка данных — вторая в Excel. Битые значения пишутся в срез ошибок с текстом ячейки, строка пропускается, и импорт доходит до конца файла — отчёт об ошибках пользователь получает одним списком.

Валидация: копите ошибки, не роняйте импорт#

Импорт, который умирает на первой битой строке, бесполезен в бою: поставщик исправит одну колонку, присланный файл сломается на другой, и цикл переписки затянется на неделю. Рабочая модель — накопление: каждая строка проходит разбор и валидацию, ошибки собираются в срез с номером строки и текстом, а импорт доходит до конца файла. Номер строки обязателен: пользователь ищет проблему в своём Excel, и «строка 214» для него значима, а «индекс 212» — нет; помните про сдвиг, потому что шапка занимает первую строку. Ошибки стоит разделять на фатальные и предупреждения: строка без артикула — фатально, она не попадёт в базу; округлённая цена — предупреждение, строка импортируется с пометкой. Журнал ошибок отдаётся тем же слоем, что и результат: срезом в ответе API, файлом-отчётом или списком в интерфейсе. Пустые строки в конце листа — не ошибки: GetRows хвостовые пустые не отдаёт, а полностью пустые строки в середине встречайте и пропускайте явно.

Типовые поломки импорта#

СимптомПричинаЧто делать
Паника на строкеlen(row) меньше индексапроверять длину или хелпер-возвращатель пустоты
Число читается в нольформатированное значение с пробеламиTrimSpace, убрать пробелы, RawCellValue
Дата не разбираетсялэйаут другой колонкиtime.Parse с известным форматом
Колонки съехалипоставщик вставил колонкумаппинг по заголовкам, не по индексам
В ячейке =SUM вместо числасвежий файл без кэша формулGetCellFormula или пересохранённый файл
Импорт умер на серединепадение на первой ошибкекопить ошибки с номерами строк

Большие файлы: итератор вместо GetRows#

Схема маппинга не зависит от способа чтения, но способ зависит от размера. GetRows собирает весь лист в память, и на прайсе в миллион строк процесс упрётся в лимит окружения задолго до конца разбора. Для больших файлов тот же код перестраивается на итератор f.Rows из урока про потоковое чтение: цикл Next и Columns держит в памяти только текущую строку, Close ставится через defer, а ошибки проверяются после цикла. Вторая сторона масштаба — запись результата: обработанные строки не копятся в огромном срезе структур, а уходят в базу пакетами или в файл по ходу разбора. Проверки на маленьком файле и на большом — это разные тесты: первый ловит логику маппинга, второй ловит память, и в уроке про тесты генерации мы вернёмся к обоим. Схема же остаётся той же: разведка шапки, чтение по именам колонок, парсинг с журналом ошибок.

Чек-лист импорта#

  • Маппинг колонок по заголовкам: карта имя — индекс строится по первой строке.
  • Заголовки и значения чистятся TrimSpace: пробелы ломают и поиск колонок, и парсинг.
  • len(row) проверяется перед индексами: хвостовые пустые в срез не попадают.
  • Числа — strconv после чистки формата, даты — time.Parse с лэйаутом колонки.
  • Ошибки копятся с номерами строк Excel: индекс плюс два при шапке в первой строке.
  • Большие файлы читаются итератором Rows, а результат пишется по ходу разбора.

Кейс про прайс, который сломался посреди квартала. Импортёр читал прайс поставщика по индексам: колонка B — артикул, D — цена, E — количество. Полгода всё работало, потом поставщик добавил колонку со скидкой между ценой и количеством, и ночной импорт начал складывать скидки в количество: числа сходились, а отчёты по остаткам разъехались на миллионы. Восстановление заняло вечер, перенос на маппинг по заголовкам — ещё два, вместе с журналом ошибок и проверками длин срезов. Второй сюрприз ждал в ценах: часть прайсов приходила из новой системы с формулами и без кэша, и вместо цены импортёр читал строку с равенством — договорились требовать пересохранённый файл, а итоги считать в Go по диапазону данных. Спорная сторона осталась в строгости: одна часть команды выступает за жёсткий импорт, где файл с ошибками не принимается целиком, вторая — за толерантный, где строки с ошибками пропускаются, а журнал уходит поставщику. Победил второй режим по умолчанию и первый — по кнопке: обе ветки выросли из одного и того же парсера.

Проверка: почему по заголовкам

Почему колонки чужого файла маппятся по заголовкам, а не по индексам?

Проверка: цена со форматом

В колонке цены значение «1 234,50». Что сделать перед strconv.ParseFloat?

Что вернёт Atoi?

Прочитайте код и выберите, что он выведет:

n, err := strconv.Atoi("1 200")
fmt.Println(n, err)

Соберите ядро импорта

Допишите чтение листа, парсинг числа, даты и чистку значения.

rows, err := f.("Прайс") qty, err := .Atoi(pick(row, "Количество")) date, err := .Parse("02.01.2006", pick(row, "Дата")) name := strings.(pick(row, "Наименование"))

Соберите порядок импорта

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: импорт

Термин

Маппинг по заголовкам

нажмите, чтобы перевернуть
Определение

Карта имя колонки — индекс строится по первой строке. Индексы чужого файла ненадёжны: колонку вставят или переставят.

1 / 6

Импорт закрыт: маппинг по заголовкам, парсинг с чисткой формата, журнал ошибок с номерами строк и итератор для больших файлов. Круг замкнулся — вы умеете возить данные в обе стороны: экспорт из базы собирал файл по слоям, этот урок возвращает его в структуры, а потоковое чтение обслуживает оба маршрута на больших объёмах. Основы чтения ячеек и приведение типов лежат в уроке про чтение ячеек, кэш формул — в уроке про формулы, а коллекция граблей чужих файлов пополняет урок про типичные ошибки. Проверить свой импорт тестами и golden-файлами можно по уроку про тесты генерации. Домашнее задание — взять настоящий чужой файл и написать для него импорт с журналом ошибок.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Колонки чужого файла маппятся по заголовкам: карта имя — индекс строится по первой строке.
  • len(row) короче ширины листа: перед индексом проверяют длину, за границей хелпер возвращает пустоту.
  • Числа парсятся strconv после чистки формата, даты — time.Parse с лэйаутом колонки.
  • Ошибки копятся с номерами строк Excel: индекс данных плюс два при шапке в первой строке.
  • В свежем файле на месте итога — текст формулы: кэш значений появляется только после Excel.
  • Большие файлы читаются итератором Rows, а результат пишется в базу по ходу разбора.

Домашнее задание

Возьмите настоящий чужой файл — прайс, ведомость или выгрузку: постройте импорт по схеме урока с картой заголовков, хелпером доступа к ячейкам, парсингом strconv и time и журналом ошибок с номерами строк. Проверьте его на трёх вариантах файла: нормальном, со вставленной колонкой и с битыми значениями — и убедитесь, что паники нет, ошибки собрались списком, а данные дошли до структур. Для большого файла перепишите чтение на итератор Rows и сравните память.

Личные заметкиПусто
Хранятся только в вашем браузере (Excelize · Импорт: таблица в структуры Go)

Вопросы и ответы по уроку

Импорт падает с паникой на некоторых строках — где искать?

Почти наверняка обращение по индексу за пределами среза: строка короче шапки, потому что пустой хвост ячеек в срез не попадает. Проверяйте len(row) перед индексом или читайте значения через хелпер, который возвращает пустоту за границами среза.

Почему вместо цены импорт читает строку с равенством?

В этой ячейке формула, а файл собран без Excel и кэша значений не имеет. Проверьте GetCellFormula, посчитайте итог в Go по диапазону данных или договоритесь о пересохранённых файлах, где Excel записал вычисленные значения.

Как быть с файлами на сотни тысяч строк?

Читайте итератором f.Rows из урока про потоковое чтение: цикл Next и Columns держит в памяти только текущую строку, Close ставится через defer. Обработанные строки пишите в базу пакетами по ходу разбора, не копя срез на весь файл.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Cron25 уроков~9 чпрактика25 уроков уже доступны

Фоновые задачи в Go: планировщик на robfig/cron

Курс по cron в Go: robfig/cron v3, синтаксис выражений, пояса, wrappers, конкурентность и graceful shutdown. 25 уроков с примерами и проектом.

  • Собирать планировщик на robfig/cron v3 и запускать задачи по расписанию
  • Читать и писать cron-выражения: пять полей, спецсимволы, @daily
  • Управлять жизненным циклом: Start, Stop, Entry, Remove
  • Держать часовые пояса и секундные расписания под контролем