Артикул материала в БАЗИС-Мебельщике через Bazis Script API: 3 формата, ловушки и готовая функция парсинга
TL;DR. Артикул материала в Bazis Script API живёт не в p.Material.Article (он обычно null), а внутри p.MaterialName — в одном из трёх форматов: (Артикул NNN), через CR-separator \r, или просто в хвосте имени. Артикулы всегда с ведущими нулями (00039428, не 39428), Bazis работает в cp1251. Готовая функция парсинга — в конце статьи.
Зачем артикул вообще нужен
Если вы пишете скрипт импорта из БАЗИС-Мебельщика — в свою ERP, в 1С, в SaaS-учётку, в самописный сервис — то рано или поздно вы упрётесь в одну и ту же проблему: как сматчить материал из проекта Bazis с материалом в своём справочнике. Имя для этого не годится. «ЛДСП Эггер 16мм H1334 ST9», «ЛДСП H1334 ST9 16», «Эггер H1334 ST9 16мм матовый» — это всё один и тот же материал, технолог завёл его в проект так, как у него легло на язык. Дальше будут уменьшительные, опечатки, варианты раскладки и порядок слов. По имени матчить — обречь себя на ручную сверку каждой партии.
Артикул — это стандартный идентификатор каталога производителя (Egger, Kronospan, Cleaf и других). Он стабилен, его невозможно перепутать, его можно класть в индекс и матчить точным сравнением строк. Поэтому в нормальной интеграции с БАЗИСом любой материал тянется в учётную систему именно по артикулу, а имя — это «человеческое» поле для интерфейса. Проблема в том, что Bazis Script API не отдаёт артикул отдельным полем — точнее, отдаёт, но почти всегда пустым. Где же он на самом деле и как его оттуда вытащить — об этом весь текст.
Где Bazis Script API хранит артикул
Когда вы открываете API панели и смотрите на свойства материала детали, видите примерно следующее (псевдокод; в реальном Bazis Script API объект называется по-другому, но смысловая модель такая):
// Свойство детали p
p.MaterialName // строка, имя материала «как видно в дереве проекта»
p.Material // ссылка на объект материала из каталога
p.Material.Name // имя из каталога
p.Material.Article // артикул из каталога — но почти всегда null
Логично было бы ожидать, что p.Material.Article и есть тот самый артикул. На практике это поле заполнено только в одном случае — если в БАЗИС-Смете у пользователя загружен .fdb-каталог с этим материалом, и материал в проекте привязан именно к каталожной записи. У большинства реальных пользователей .fdb-каталога нет. Технолог создаёт материалы «на лету» — копированием из соседнего проекта, ручным вводом, импортом из чужого .b3d. В этих случаях p.Material.Article === null, а артикул, если он есть, лежит внутри p.MaterialName.
Конкретный пример из реального импорта — материал с именем:
МДФ ПВХ 16мм Адилет SEA-13 Альвиано матовый (Подольск)(Артикул 00039428)
p.Material.Article здесь null. А 00039428 — это и есть артикул, который надо вытащить. Дальше — как именно его оттуда достать.
Формат 1: артикул в круглых скобках (Артикул NNN)
Самый распространённый формат — артикул в конце имени, в круглых скобках, с русским словом-префиксом «Артикул»:
ЛДСП 16 Эггер H1334 ST9 (Артикул 00039428)
МДФ ПВХ 16мм Адилет SEA-13 Альвиано матовый (Подольск)(Артикул 00039428)
HPL Arpa 0612 LU Bianco (Артикул 00012051)
Регулярное выражение для парсинга:
const ARTICLE_PAREN_RE = /\(Артикул\s+(\d{4,10})\)/i;
const m = materialName.match(ARTICLE_PAREN_RE);
if (m) {
const article = m[1]; // строка, ведущие нули сохранены
const name = materialName.replace(ARTICLE_PAREN_RE, '').trim();
return { name, article };
}
Ловушка номер один — другие скобки в имени. В нашем примере есть (Подольск) — это указание производителя, не артикул. Если вы напишете «вырежи всё, что в скобках», вы потеряете осмысленную часть имени материала. Поэтому регэксп должен быть привязан именно к слову Артикул внутри скобок, а не к любому содержимому скобок.
Ловушка номер два — разный регистр и пробелы. Технолог может ввести «Артикул», «артикул», «АРТИКУЛ», между словом и цифрами может быть один пробел, два или вообще NBSP. Флаг i в регэкспе обязателен, \s+ (не пробел ' ') — тоже обязателен. NBSP ( ) попадает в \s в современных движках, но если вы парсите в каком-нибудь старом окружении — лучше нормализовать строку через materialName.replace(/ /g, ' ') до парсинга.
Ловушка номер три — латинские/кириллические артикулы. В большинстве случаев артикул — чисто цифровой (00039428), но изредка встречается буквенный, например H1334. Если вы работаете только с цифровыми каталогами Egger/Kronospan/Cleaf — паттерн (\d{4,10}) подходит. Если у вас в работе есть смешанные каталоги — расширяйте до ([A-Za-z0-9]{4,10}), но тогда придётся отдельно разбираться с риском поймать кусок маркировки декора (H1334, ST9).
Формат 2: артикул через CR-separator \r
Второй формат — когда Bazis вшивает артикул в имя через символ возврата каретки \r (0x0D в cp1251):
ЛДСП Эггер 16 мм H1334 ST9\r00039428
МДФ ПВХ 16мм Адилет SEA-13 Альвиано матовый (Подольск)\r00039428
В дереве проекта БАЗИС-Мебельщика пользователь этого \r не видит — интерфейс рендерит его как перенос строки и показывает артикул второй строкой под именем. А Bazis Script API возвращает поле как одну строку с реальным управляющим символом 0x0D внутри.
Парсинг:
const ARTICLE_CR_RE = /\r(\d{4,10})\s*$/;
const m = materialName.match(ARTICLE_CR_RE);
if (m) {
const article = m[1];
const name = materialName.split('\r')[0].trim();
return { name, article };
}
Ловушка — \r\n вместо чистого \r. Если строка прошла через какую-нибудь нормализацию переводов строк (например, вы её писали в файл и читали обратно), \r мог стать \r\n или вообще одним \n. Поэтому надёжнее парсить через /[\r\n]+(\d{4,10})\s*$/. У нас в продакшене именно такой паттерн, потому что между Bazis Script API и JS-скриптом импорта данные проходят через файл-обмен, и переводы строк нормализуются на лету.
Почему именно \r, а не \n. Это исторический формат БАЗИСа — он родом из Windows-приложений 2000-х, где \r был стандартным символом «мягкого переноса» в однострочных текстовых полях. Если в вашей выгрузке вы видите \n вместо \r — почти наверняка это значит, что между API и вашим скриптом стоит что-то, что нормализует переводы строк. Это не сломает парсинг, если регэксп учитывает оба символа.
Формат 3: артикул в хвосте имени (6-8 цифр)
Самый редкий, но встречающийся формат — артикул просто приписан к имени без разделителя:
ЛДСП Эггер H1334 ST9 1600x2070x1600039428
МДФ ПВХ 16мм Адилет SEA-1300039428
Получается такая «склейка» обычно одним из двух способов: либо технолог копирует артикул из накладной и забывает поставить пробел, либо при импорте из CSV/Excel колонка имени и колонка артикула слились в одну.
Парсинг:
const ARTICLE_TAIL_RE = /(\d{6,8})\s*$/;
const m = materialName.match(ARTICLE_TAIL_RE);
if (m) {
const article = m[1];
const name = materialName.replace(ARTICLE_TAIL_RE, '').trim();
return { name, article };
}
Главная ловушка — размеры детали в имени материала. Многие технологи кладут размер прямо в имя: «ЛДСП Эггер H1334 16x2070x2800». Жадный паттерн /\d+$/ зацепит 2800. И это будет ошибочно интерпретировано как артикул. Поэтому в третьем формате жёстко фиксируем длину — артикулы Bazis всегда 6-8 цифр, чаще всего ровно 8. Если в строке заканчивается 4-значное число (вроде размера 2800) — мы его не трогаем.
Второй момент — порядок проверки. Формат 3 должен идти ПОСЛЕДНИМ в цепочке. Если в имени есть и (Артикул 00039428), и какое-то 8-значное число где-то в середине — приоритет должен быть у явного паттерна. Поэтому в готовой функции порядок именно такой: скобки → CR → хвост.
Кодировка cp1251 — почему критично
Bazis Script API — это COM/ActiveX поверх десктопного БАЗИС-Мебельщика, работающего на Windows. Все строки внутри Bazis — в кодировке Windows-1251 (cp1251). Когда вы получаете строку из API в свой скрипт, она может прийти уже в UTF-16 (если вы работаете через JScript внутри Bazis Script) или в cp1251 как байтовый поток (если вы выгружаете в файл и читаете снаружи).
Типовая ситуация: скрипт внутри Bazis записывает результат в .json или .csv, и записывает его в cp1251, потому что это дефолт для WScript.OpenAsTextStream под Windows. Внешний скрипт (Node.js, Python, Go) читает этот файл как UTF-8 — и получает на месте всех русских букв U+FFFD (символ replacement). Парсер материалов после этого не понимает ничего: вместо «ЛДСП Эггер» он видит «��??� ��?��».
Правило простое и без исключений: всегда явно конвертируйте кодировку при чтении выгрузки из Bazis. На Node.js:
const iconv = require('iconv-lite');
const raw = fs.readFileSync('bazis-export.json'); // Buffer, не строка
const text = iconv.decode(raw, 'windows-1251');
const data = JSON.parse(text);
В шелле:
iconv -f WINDOWS-1251 -t UTF-8 bazis-export.json > bazis-export.utf8.json
В Python:
with open('bazis-export.json', encoding='cp1251') as f:
data = json.load(f)
Если вы работаете напрямую внутри Bazis Script (а это JScript) — кодировка не проблема, строки уже в UTF-16 в памяти. Проблема начинается на стыке: Bazis Script → файл → внешний скрипт. Этот стык надо явно перекодировать. В одном из реальных импортов мы потеряли два дня на «почему артикулы парсятся, а имена ломаются» — оказалось, регэкспы работали на байтах, а имена надо было сначала декодировать.
Ведущие нули — почему parseInt убивает артикул
Артикулы Bazis (и каталогов Egger/Kronospan/Cleaf, на которые они ссылаются) — всегда с ведущими нулями. Канонический вид:
00039428
00012051
00000423
Это не визуальное оформление, это часть идентификатора. В каталоге .fdb они хранятся именно так, в накладных производителя — тоже. Если вы где-то по пути преобразуете строку в число — например, через parseInt(article) или JSON-десериализатор, который автоматически приведёт "00039428" к 39428 — артикул сломается.
Конкретно:
parseInt('00039428'); // 39428 — ВСЁ, ведущие нули потеряны
Number('00039428'); // 39428 — тоже потеряны
JSON.parse('00039428'); // 39428 (если поле без кавычек в JSON)
При обратном матчинге 39428 против 00039428 в каталоге — точное совпадение строк не сработает, и вам придётся либо паддить нули обратно (а вы не знаете, сколько их было), либо сравнивать как числа везде (а каталоги хранят их как строки, и SQL WHERE article = 39428 против '00039428' не сматчит).
Правило: артикул — всегда строка. Никогда не приводите его к числу. В TypeScript типизируйте как string. В SQL — VARCHAR/TEXT, не INTEGER. В JSON — всегда в кавычках. Регэксп возвращает результат как строку по умолчанию (m[1] — это string), поэтому если вы аккуратно дёргаете m[1] без parseInt, всё хорошо.
Готовая функция парсинга на JavaScript
Собираем три формата в одну функцию с правильным порядком проверки:
/**
* Извлекает артикул материала из имени, полученного через Bazis Script API.
*
* Проверяет 3 формата по убыванию надёжности:
* 1. "Имя (Артикул NNN)" — явная скобка с префиксом
* 2. "Имя\rNNN" — артикул через CR-separator
* 3. "ИмяNNNNNNNN" — артикул в хвосте (6-8 цифр)
*
* @param {string} materialName — строка из p.MaterialName
* @returns {{name: string, article: string|null}}
*/
function extractArticleFromName(materialName) {
if (!materialName || typeof materialName !== 'string') {
return { name: '', article: null };
}
// Нормализуем NBSP в обычные пробелы — иначе \s+ может промахнуться
const src = materialName.replace(/ /g, ' ');
// Формат 1: (Артикул NNN)
const PAREN_RE = /\s*\(Артикул\s+(\d{4,10})\)\s*/i;
let m = src.match(PAREN_RE);
if (m) {
return {
name: src.replace(PAREN_RE, '').trim(),
article: m[1],
};
}
// Формат 2: \r или \n + NNN в конце
const CR_RE = /[\r\n]+(\d{4,10})\s*$/;
m = src.match(CR_RE);
if (m) {
return {
name: src.split(/[\r\n]+/)[0].trim(),
article: m[1],
};
}
// Формат 3: NNNNNNNN в самом хвосте (6-8 цифр, без разделителя)
const TAIL_RE = /(\d{6,8})\s*$/;
m = src.match(TAIL_RE);
if (m) {
return {
name: src.replace(TAIL_RE, '').trim(),
article: m[1],
};
}
return { name: src.trim(), article: null };
}
Тесты, чтобы убедиться, что функция работает на ваших данных:
extractArticleFromName('ЛДСП 16 Эггер H1334 ST9 (Артикул 00039428)');
// { name: 'ЛДСП 16 Эггер H1334 ST9', article: '00039428' }
extractArticleFromName('МДФ ПВХ 16мм Адилет SEA-13 (Подольск)(Артикул 00039428)');
// { name: 'МДФ ПВХ 16мм Адилет SEA-13 (Подольск)', article: '00039428' }
extractArticleFromName('ЛДСП Эггер 16 мм H1334 ST9\r00039428');
// { name: 'ЛДСП Эггер 16 мм H1334 ST9', article: '00039428' }
extractArticleFromName('МДФ ПВХ 16мм Адилет SEA-1300039428');
// { name: 'МДФ ПВХ 16мм Адилет SEA-13', article: '00039428' }
extractArticleFromName('ЛДСП Эггер H1334 ST9');
// { name: 'ЛДСП Эггер H1334 ST9', article: null }
Функция возвращает article: null если ни один из форматов не сработал — это нормально, технолог мог просто не записать артикул, или материал собственного производства фабрики, или образец без каталога. В этом случае матчить придётся по имени, через fuzzy-match — но это уже другая история.
Приоритет: имя > каталог
В нашей текущей продакшен-логике приоритет источников артикула такой:
const article =
extractArticleFromName(p.MaterialName).article // 1. из имени
|| (p.Material && p.Material.Article) // 2. из каталога .fdb
|| null;
Имя проверяется первым, каталог — резервный источник. Это контр-интуитивно: казалось бы, каталожная запись надёжнее, чем разбор строки. Но на практике мы нашли проблему: в одной фабрике технолог использовал старую версию .fdb-каталога, где артикул 00039428 был привязан к декору H1334, а в проекте уже использовался обновлённый материал с артикулом 00041155. БАЗИС-Мебельщик при обновлении проекта оставил ссылку на старую запись каталога (через GUID), но в p.MaterialName подтянулся актуальный артикул 00041155. Если бы мы брали p.Material.Article первым — мы импортировали бы устаревший артикул, и материал не сматчился бы в учётке.
С тех пор правило простое: актуальные данные — в имени. Каталог .fdb — это снимок на момент его загрузки, и в реальных проектах он часто устаревший. То, что технолог видит в дереве проекта (а это именно p.MaterialName) — то и есть «истина». Каталог используем только как фолбэк, если в имени артикула нет.
Если не хотите писать парсер сами
Если вы пишете свой импорт из БАЗИСа — берите функцию выше, проверяйте её на своих данных, не забывайте про cp1251 и ведущие нули. Если не хотите писать сами и поддерживать парсер каждый раз, когда у вас появится материал с новым форматом артикула — посмотрите, как это работает в АвтоМебельПро. У нас вся эта логика уже работает в продакшене на реальных фабриках, артикулы матчатся автоматически, и .fdb-каталог не нужен. Бесплатный пробный период — без оплаты, без обязательств, можно прогнать на своём реальном проекте и посмотреть.
Попробуйте АвтоМебельПро на своей фабрике
Пробный период бесплатно. Импорт деталировки из БАЗИС-Мебельщика, спецификация, QR-учёт операций — за день.
Попробовать бесплатно