Генератор advanced_regex
Используйте его, когда нужно всё, что умеет regex, но сама форма
строки несёт статистическое распределение вариантов — например, ровно 70% кодов
начинаются с RU, 20% — с US, 10% — с DE.
Обычный regex выбирает альтернативу (RU|US|DE) случайно, поэтому
разбивка сходится только в среднем. advanced_regex добавляет взвешенный выбор
(weighted choice), который раскладывает варианты на точные количества.
<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{6}"/>
advanced_regex — надмножество regex: обычный генератор
type="regex" остаётся стабильным и TDC-нейтральным, а
advanced_regex добавляет TDC-специфичную мощь поверх того же конечного,
переносимого движка. Сегодня эта дополнительная мощь — ровно одна конструкция,
взвешенный выбор, а всё остальное на этой странице унаследовано без изменений из
Regex.
Зачем это нужно
Без взвешенного выбора задачу «70% RU, 20% US, 10% DE, у каждого после префикса
ещё шесть случайных цифр» пришлось бы описывать несколькими последовательностями или
через <mix>. С advanced_regex это
сворачивается в один генератор. Он естественно подходит для:
- кодов стран, филиалов, регионов или типов клиентов;
- тестовых идентификаторов с разной структурой, но фиксированными пропорциями;
- номеров документов, где одна часть строки должна повторять другую;
- синтетических данных, где доли важны не меньше самих значений;
- AI-генерации
.tdc, когда агенту проще выдать один компактный паттерн, чем дерево последовательностей.
Примеры вывода ниже иллюстративны — конкретные строки зависят от сида и могут слегка меняться между версиями ядра, — но количества, которые обещает распределение, точны.
Weighted-choice
Конструкция такая:
(?%{PERCENT:BRANCH;PERCENT:BRANCH;...})
Разбор по частям:
(?%{ 70:RU ; 20:US ; 10:DE })
│ │ │ │ │ │ │ │
│ │ ветка │ ветка │ ветка │
│ процент процент процент │
└── начало weighted-choice ┘
Правила:
- проценты должны быть числами и неотрицательными;
- сумма процентов должна быть равна 100;
- ветка может быть пустой;
- ветка сама является полноценным выражением
advanced_regex(значит, они вкладываются); ;,}и:— управляющие символы; экранируйте их (см. раздел «Экранирование внутри weighted-choice»), если они нужны в ветке буквально.
Точные проценты по умолчанию
Это главное: взвешенный выбор обещает точные проценты (ровно 70 из 100, а не «примерно 70»), и TDC выдаёт их из коробки — специально ничего включать не нужно.
Чтобы отмерить разбивку точно, движок строит весь столбец разом и раздаёт ветки по
алгоритму Гамильтона (наибольшего остатка). Он делает это автоматически, как только в
паттерне встречается (?%{…}) — даже в режиме по умолчанию disk. От вас нужен только
паттерн, доли получатся ровными.
<env count="100" seed="countries">
<sequence name="CountryCode">
<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{2}"/>
</sequence>
</env>
Считаем префиксы на всех 100 строках — ровно 70/20/10:
RU 70 US 20 DE 10
Чтобы отмерить разбивку точно, столбец с взвешенным выбором строится целиком в
оперативной памяти. Для небольших и средних наборов это норма. Если нужны точные доли
при потоковой генерации (память O(1), выгрузка любого размера) — их так же дают
<mix percent> и
<gen type="text" percent="…">: они отдают
точные проценты потоком, не держа весь столбец в памяти. А если вы вручную форсируете
чисто потоковый движок (mode="stream" на <env> — легаси-алиас, который существует
только как атрибут, — или --engine 2 в командной строке; --mode stream не бывает),
TDC не станет молча портить
проценты — посчитать их построчно нельзя, поэтому он честно откажется с понятной
ошибкой. Уберите форс — и всё снова точно.
Веса действительно сдвигают распределение
Проценты — не украшение, а реальный состав столбца. Возьмём один паттерн
(?%{…})-[0-9]{2} при count="1000", поменяем только веса и посчитаем префиксы:
<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{2}"/> <!-- вариант 1 -->
<gen type="advanced_regex" value="(?%{34:RU;33:US;33:DE})-[0-9]{2}"/> <!-- вариант 2 -->
<gen type="advanced_regex" value="(?%{10:RU;10:US;80:DE})-[0-9]{2}"/> <!-- вариант 3 -->
веса RU US DE 70 / 20 / 10 700 200 100 34 / 33 / 33 340 330 330 10 / 10 / 80 100 100 800
На 1000 строк количества повторяют веса один в один. Поменяли веса — поменялся состав столбца.
regex против advanced_regex
| Возможность | regex | advanced_regex |
|---|---|---|
| Сгенерировать строку по паттерну | да | да |
| Character classes | да | да |
| Именованные Unicode-алфавиты | да | да |
| Groups и backreferences | да | да |
Ограничение длины через regex_max_length | да | да |
| Точные проценты внутри паттерна | нет | да |
| Вкладывать процентные варианты | нет | да |
| weighted-choice в блоке вывода | нет | нет |
Разницу проще всего увидеть рядом. Обычный regex выбирает каждый символ
свободно — форма фиксирована, доли нет:
<gen type="regex" value="[A-Z]{2}[0-9]{6}"/>
FZ399441 YH481897 LR586083 YA900972 WT831899
advanced_regex сохраняет ту же природу кода, но закрепляет доли префиксов — 70% RU,
20% US, 10% DE на всём прогоне:
<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{6}"/>
RU-441627 RU-476822 RU-948319 US-450875 RU-398584 RU-131212 RU-418648 RU-830959
Коротко: используйте regex для строки заданной формы; используйте advanced_regex,
когда сама форма несёт статистическое распределение.
Наследует весь regex-язык
Каждая конечная конструкция из regex работает и здесь — литералы,
экранирование, character classes, BMP-диапазоны, именованные алфавиты \a{…},
\d/\w/\s и их инверсии, ., alternation, groups, backreferences, ограниченные
квантификаторы и ограничение regex_max_length. Простая
взвешенная разбивка над обычными латинскими кодами:
<gen type="advanced_regex" value="(?%{70:[A-Z]{2};30:[A-Z]{3}})-[0-9]{4}"/>
QY-3500 ZT-3381 GSK-1914 VO-5921 DW-7570 SO-1660 MSE-2247
Unicode-демо. Поскольку ветки принимают именованные алфавиты, можно держать точные доли между письменностями. Здесь 7 из 10 кодов получают кириллический префикс, а 3 — латинский; это намеренный пример по Unicode/локализации, показывающий, что механика точных процентов не зависит от письменности:
<gen type="advanced_regex" value="(?%{70:\a{cyrillic.ru.upper}{2};30:\a{latin.upper}{2}})-[0-9]{4}"/>
ЭЗ-2477 WJ-0170 ЧП-8026 СЦ-1020 ЫЦ-2747 FJ-7879 РЛ-6827 ЩЕ-4485 ПВ-0297 UD-1550
Вложенные weighted-choice
Поскольку ветки — это полноценные выражения, взвешенные выборы вкладываются, и внутренняя разбивка считается внутри подмножества, которое попало во внешнюю ветку:
<gen type="advanced_regex" value="(?%{50:A(?%{80:X;20:Y});50:B})"/>
При count="100":
AX 40 AY 10 B 50
80% от 50 строк A — это 40 строк AX; 20% — это 10 строк AY. Это поведение
«проценты внутри подмножества» в точности совпадает с философией
иерархии последовательностей в TDC.
Несколько weighted-choice в одном паттерне
<gen type="advanced_regex" value="(?%{60:M;40:F})-(?%{25:00;75:99})"/>
Каждое распределение отмеряется точно на текущих строках. При count="100":
M-99 44 F-99 31 M-00 16 F-00 9
Сложите по частям — и оба точны: M = 44 + 16 = 60 и F = 31 + 9 = 40 (60/40); 99 =
44 + 31 = 75 и 00 = 16 + 9 = 25 (75/25). Два независимых выбора, каждый разложен тем
же методом точных процентов.
С parent-фильтром
advanced_regex живёт внутри обычной модели зависимостей последовательностей. Если
последовательность отфильтрована через
parent, проценты
считаются только внутри отфильтрованного подмножества:
<sequence name="Gender">
<gen type="text" value="M,F" percent="50,50"/>
</sequence>
<sequence name="MaleCode" parent="Gender.M">
<gen type="advanced_regex" value="M-(?%{40:A;60:B})-[0-9]{2}"/>
</sequence>
При count="100" (50 мужских строк):
F 50 (MaleCode пустой) M-A 20 M-B 30
Разбивка 40/60 отмерена от 50 отфильтрованных строк, а не от всех 100.
Захваты и backreferences
Backreference повторяет уже сгенерированную группу. Это работает точно так же, как в
обычном regex — первые три цифры повторяются в конце:
<gen type="advanced_regex" value="([0-9]{3})-[A-Z]{2}-\1"/>
299-YZ-299 929-UE-929 462-VR-462 905-BC-905 876-JF-876
Взвешенную ветку можно захватить и повторить через \1 — захваченная часть
воспроизводится дословно, и проценты сохраняются. При count="40":
<gen type="advanced_regex" value="((?%{25:AB;75:CD}))-\1"/>
AB-AB 10 CD-CD 30
Захваченная пара повторяется буквально, а разбивка 25/75 сохраняется.
Захват, сделанный до взвешенного выбора, можно использовать внутри ветки. Здесь
половина строк повторяет захваченные две буквы, половина печатает фиксированное XX
(count="8"):
<gen type="advanced_regex" value="([A-W]{2})-(?%{50:\1;50:XX})"/>
TV-XX GR-GR RN-XX OU-OU WM-WM SS-XX CL-XX QG-QG
Backreference может жить внутри ветки. Где взята ветка (A[0-9]), \1 повторяет её
захват; где взята ветка B, захвата нет, поэтому \1 пустой (count="20"):
<gen type="advanced_regex" value="(?%{40:(A[0-9]);60:B})-\1"/>
A2-A2 B- B- A1-A1 B- A8-A8 B-
Это ещё не полноценный if, но уже полезная логическая связь: одна часть строки может
зависеть от уже сгенерированной группы.
Экранирование внутри weighted-choice
;, } и : — управляющие символы взвешенного выбора. Чтобы использовать их как
буквальный текст в ветке, экранируйте их (count="6"):
<gen type="advanced_regex" value="(?%{50:A\;\}\:;50:B})"/>
A;}: A;}: B B A;}: B
Ветка A\;\}\: печатает буквальные A;}:, а ветка B печатает просто B.
Где работает взвешенный выбор
Взвешенный выбор задаёт точную процентную разбивку, поэтому среда выполнения должна знать,
сколько строк придётся на каждую ветку. Это известно везде, где вообще может стоять
<gen>:
- внутри
<sequence>— изcountили подмножестваparent; - внутри
<case>у<mix>— из размера этого кейса.
Оба варианта валидны:
<sequence name="CountryCode">
<gen type="advanced_regex" value="(?%{70:RU;20:US;10:DE})-[0-9]{2}"/>
</sequence>
<mix name="Country" percent="50,50">
<case><gen type="advanced_regex" value="(?%{70:RU;30:US})"/></case>
<case><data>-</data></case>
</mix>
В блоке вывода генераторов нет вовсе —
<line> только форматирует, — поэтому
взвешенный выбор туда не попадает. Чтобы вывести его в строке, объявите его как
последовательность и вставьте через интерполяцию ${{Name}}:
<tdc>
<env count="100" seed="demo" inject="${{%}}">
<sequence name="Code">
<gen type="advanced_regex" value="(?%{70:A;30:B})-[0-9]{4}"/>
</sequence>
</env>
<block>
<line><data>code=${{Code}}</data></line>
</block>
</tdc>
Первые строки, где прогон делится ровно 70 A / 30 B:
code=A-8870 code=B-2495 code=B-1961 code=A-8865 code=A-9221 code=A-3234
Порядок строк детерминированно перемешан по seed; итоги остаются точными.
Практические примеры
Код клиента по сегменту — count="1000", считаем префиксы:
<gen type="advanced_regex" value="(?%{80:REG;15:VIP;5:TEST})-[A-Z]{2}[0-9]{4}"/>
REG 800 VIP 150 TEST 50
Документ с повторяющимся блоком — первые три цифры и последние три цифры всегда
совпадают, а середина делится 60% A / 40% B (count="100"):
<gen type="advanced_regex" value="([0-9]{3})-(?%{60:A;40:B})-\1"/>
924-B-924 419-B-419 788-A-788 692-B-692
Короткие и длинные технические коды — 85% коротких, 15% длинных (count="100"):
<gen type="advanced_regex" value="(?%{85:[A-Z]{2}[0-9]{2};15:[A-Z]{4}[0-9]{8}})"/>
длина 4 85 (AB42) длина 12 15 (ABCD12345678)
<mix> или advanced_regex?
Оба умеют точные проценты, но для разных задач.
Используйте <mix>, когда у веток
разная структура — свой набор генераторов и литерального текста в каждой (а ещё
<mix percent> даёт точные доли потоком, не держа столбец в памяти):
<mix name="Kind" percent="70,30">
<case><data>{"type":"regular"}</data></case>
<case><data>{"type":"vip","bonus":true}</data></case>
</mix>
Используйте advanced_regex, когда всё укладывается в один паттерн (точно по умолчанию;
столбец строится в памяти — для очень больших выгрузок берите <mix percent>):
<gen type="advanced_regex" value="(?%{70:REG;30:VIP})-[0-9]{6}"/>
Невалидные паттерны
<gen type="advanced_regex" value="(?%{70:A;20:B})"/> <!-- сумма 90, нужна 100 -->
<gen type="advanced_regex" value="[a-z]+"/> <!-- бесконечная длина, как и в обычном regex -->
Обе отклоняются ещё до генерации — например, вторая:
error: invalid advanced_regex generator pattern: unbounded "+"
quantifier is not allowed; use "{1,n}"Имя для группы — (?<name>…)
У группы кроме номера может быть имя:
(?<sex>(?%{50:male;50:female}))
Это обычная захватывающая группа — \1 по-прежнему её читает — только с ярлыком.
Ярлык нужен, чтобы что-то ДАЛЬШЕ по шаблону могло спросить, что эта группа выдала;
об этом следующий раздел.
\k<name> повторяет группу по этому имени — так же, как в regex, — и
это та же самая группа, которую повторяет \1:
<gen type="advanced_regex" value="(?<c>(?%{50:AB;50:CD}))-\k<c>"/>
Имя должно принадлежать группе, которая уже закрылась: это то же правило, которому
следуют \1 и (?if{…}) — группа дальше по шаблону ещё ничего не выдала, чтобы это
повторять.
Имя начинается с буквы или _ и состоит из букв, цифр и _. Двум группам одно имя
нельзя: тогда (?if{sex=…}) стало бы броском монетки между ними. Это верно и когда
одна из двух групп находится внутри другой, а не рядом с ней.
Чтобы одна часть следовала за другой — (?if{…})
Когда нужно: две части одного значения обязаны согласовываться. Всё остальное в
этом генераторе решает значение из одной только случайности — поэтому шаблон мог
описать индекс или идентификатор, но не обращение, которое совпадает с полом,
выбранным двумя символами раньше. Раньше это означало бросить advanced_regex и
пересобирать колонку через <switch>.
<gen type="advanced_regex" value="(?<sex>(?%{50:male;50:female}))/(?if{sex=male:Mr;sex=female:Ms})"/>
female/Ms male/Mr male/Mr female/Ms male/Mr female/Ms female/Ms male/Mr
Каждая строка согласована сама с собой. За male никогда не идёт Ms.
Как это читается
(?if{имя=значение:ветка;имя=значение:ветка}). Ветки разделяются ;, каждая — это
что проверяем, потом :, потом что выдаём. Они пробуются в написанном
порядке, побеждает первая совпавшая.
Ветка — полноценное выражение advanced_regex, поэтому внутрь вкладываются и
взвешенные выборы, и другие условия:
<gen type="advanced_regex" value="(?<country>(?%{60:RU;30:US;10:DE}))-(?if{country=RU:[0-9]{3};country=US:[A-Z]{3};*:[A-Z]{2}[0-9]})"/>
RU-683 US-NGS DE-ZQ5 US-VNS RU-867 RU-722 RU-372 RU-890 US-SEA RU-589
* — ветка «во всех остальных случаях», здесь это строки DE. Без неё строка, не
подошедшая ни под одну ветку, выдаёт на этом месте вообще ничего:
(?<c>(?%{50:a;50:b}))-(?if{c=zzz:NEVER}) → a- b- a- b- …
Так задумано. Шаблон ничего не сказал про то, что должна выдать строка a, и тихий
откат к первой ветке склеил бы неправильные пары в файле, который в остальном
выглядит нормально. Напишите ветку *, чтобы сказать, что вы имели в виду.
Чего оно не делает
- Не читает группу, объявленную позже. Шаблон собирается слева направо, поэтому группа дальше по строке ещё ничего не выдала, и ветку нельзя было бы взять никогда. Это отклоняется, а не оставляется выдавать пустые половинки.
- Не читает ничего, кроме именованной группы — ни другую колонку, ни
${{…}}. Для логики между колонками есть<switch>и фильтрparent; это же — про части ОДНОГО значения. - Не сравнивает ничего, кроме текста. Проверка — это
имя=значение, точное совпадение с тем, что выдала группа. Значение не может содержать:— там начинается ветка.
Квоты остаются точными
Условие читает взвешенный выбор, но не трогает его. На 200 строках
(?<c>(?%{70:RU;20:US;10:DE})) по-прежнему даёт ровно 140, 40 и 20 — и вторая
половина каждой строки следует из её собственной первой.
См. также
- Regex — конечные конструкции, которые наследует эта страница.
- Symbol — именованные алфавиты
(
\a{name}). regex_max_lengthв справочнике атрибутов.<mix>— точные проценты для структурно разных веток.