Спецификация языка текстового формата
Язык текстового формата protobuf определяет синтаксис для представления данных protobuf в текстовой форме, что часто полезно для конфигураций или тестов.
Этот формат отличается от формата текста
внутри схемы .proto, например. Этот документ содержит справочную
документацию с использованием синтаксиса, указанного в
ISO/IEC 14977 EBNF.
{{% alert title="Примечание" color="note" %}} Это черновой спецификация, созданный путем обратного проектирования из реализации текстового формата C++ implementation и может измениться на основе дальнейших обсуждений и проверки. Несмотря на усилия по сохранению согласованности текстовых форматов в поддерживаемых языках, вероятно существование несовместимостей. {{% /alert %}}
Пример
convolution_benchmark {
label: "NHWC_128x20x20x56x160"
input {
dimension: [128, 56, 20, 20]
data_type: DATA_HALF
format: TENSOR_NHWC
}
}
Обзор парсинга
Элементы языка в этой спецификации разделены на лексические и синтаксические
категории. Лексические элементы должны точно соответствовать входному тексту, как описано, но
синтаксические элементы могут разделяться необязательными токенами WHITESPACE и COMMENT.
Например, знаковое число с плавающей точкой состоит из двух синтаксических элементов: знака (-)
и литерала FLOAT. Необязательные пробелы и комментарии могут существовать между знаком и числом, но не внутри числа. Пример:
value: -2.0 # Допустимо: нет дополнительных пробелов.
value: - 2.0 # Допустимо: пробел между '-' и '2.0'.
value: -
# комментарий
2.0 # Допустимо: пробелы и комментарии между '-' и '2.0'.
value: 2 . 0 # Неверно: точка в числе с плавающей точкой является частью лексического
# элемента, поэтому дополнительные пробелы не допускаются.
Существует один крайний случай, требующий особого внимания: токен числа (FLOAT,
DEC_INT, OCT_INT или HEX_INT) не может немедленно следовать за токеном
IDENT. Пример:
foo: 10 bar: 20 # Допустимо: пробел разделяет '10' и 'bar'
foo: 10,bar: 20 # Допустимо: ',' разделяет '10' и 'bar'
foo: 10[com.foo.ext]: 20 # Допустимо: за '10' сразу следует '[', который
# не является идентификатором.
foo: 10bar: 20 # Неверно: нет пробела между '10' и идентификатором 'bar'.
Лексические элементы
Лексические элементы, описанные ниже, делятся на две категории: основные элементы в верхнем регистре и фрагменты в нижнем регистре. Только основные элементы включаются в выходной поток токенов, используемый во время синтаксического анализа; фрагменты существуют только для упрощения построения основных элементов.
При разборе входного текста побеждает самый длинный совпадающий основной элемент. Пример:
value: 10 # '10' разбирается как токен DEC_INT.
value: 10f # '10f' разбирается как токен FLOAT, несмотря на содержание '10', который
# также соответствовал бы DEC_INT. В этом случае FLOAT соответствует более длинной
# подпоследовательности входных данных.
Символы
char = ? Любой не-NUL символ Unicode ? ;
newline = ? ASCII #10 (перевод строки) ? ;
letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M"
| "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
| "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m"
| "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
| "_" ;
oct = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" ;
dec = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
| "A" | "B" | "C" | "D" | "E" | "F"
| "a" | "b" | "c" | "d" | "e" | "f" ;
Пробелы и комментарии
COMMENT = "#", { char - newline }, [ newline ] ;
WHITESPACE = " "
| newline
| ? ASCII #9 (горизонтальная табуляция) ?
| ? ASCII #11 (вертикальная табуляция) ?
| ? ASCII #12 (прогон страницы) ?
| ? ASCII #13 (возврат каретки) ? ;
Идентификаторы
IDENT = letter, { letter | dec } ;
Числовые литералы
dec_lit = "0"
| ( dec - "0" ), { dec } ;
float_lit = ".", dec, { dec }, [ exp ]
| dec_lit, ".", { dec }, [ exp ]
| dec_lit, exp ;
exp = ( "E" | "e" ), [ "+" | "-" ], dec, { dec } ;
DEC_INT = dec_lit
OCT_INT = "0", oct, { oct } ;
HEX_INT = "0", ( "X" | "x" ), hex, { hex } ;
FLOAT = float_lit, [ "F" | "f" ]
| dec_lit, ( "F" | "f" ) ;
Десятичные целые числа могут быть приведены к значениям с плавающей точкой с использованием суффиксов F и f.
Пример:
foo: 10 # Это целочисленное значение.
foo: 10f # Это значение с плавающей точкой.
foo: 1.0f # Также необязательно для литералов с плавающей точкой.
Строковые литералы
STRING = single_string | double_string ;
single_string = "'", { escape | char - "'" - newline - "\" }, "'" ;
double_string = '"', { escape | char - '"' - newline - "\" }, '"' ;
escape = "\a" (* ASCII #7 (звонок) *)
| "\b" (* ASCII #8 (забой) *)
| "\f" (* ASCII #12 (прогон страницы) *)
| "\n" (* ASCII #10 (перевод строки) *)
| "\r" (* ASCII #13 (возврат каретки) *)
| "\t" (* ASCII #9 (горизонтальная табуляция) *)
| "\v" (* ASCII #11 (вертикальная табуляция) *)
| "\?" (* ASCII #63 (вопросительный знак) *)
| "\\" (* ASCII #92 (обратная косая черта) *)
| "\'" (* ASCII #39 (апостроф) *)
| '\"' (* ASCII #34 (кавычка) *)
| "\", oct, [ oct, [ oct ] ] (* восьмеричное экранированное значение байта *)
| "\x", hex, [ hex ] (* шестнадцатеричное экранированное значение байта *)
| "\u", hex, hex, hex, hex (* Кодовая точка Unicode до 0xffff *)
| "\U000",
hex, hex, hex, hex, hex (* Кодовая точка Unicode до 0xfffff *)
| "\U0010",
hex, hex, hex, hex ; (* Кодовая точка Unicode между 0x100000 и 0x10ffff *)
Восьмеричные escape-последовательности потребляют до трех восьмеричных цифр. Дополнительные цифры
передаются без экранирования. Например, при разэкранировании ввода \1234,
парсер потребляет три восьмеричные цифры (123) для разэкранирования значения байта 0x53
(ASCII 'S', 83 в десятичной системе), и последующая '4' передается как значение байта 0x34 (ASCII '4'). Чтобы обеспечить правильный разбор, выражайте восьмеричные escape-
последовательности с 3 восьмеричными цифрами, используя ведущие нули по мере необходимости, например: \000,
\001, \063, \377. Менее трех цифр потребляется, когда за числовыми
символами следует нечисловой символ, например \5Hello.
Шестнадцатеричные escape-последовательности потребляют до двух шестнадцатеричных цифр. Например,
при разэкранировании \x213 парсер потребляет только первые две цифры (21) для
разэкранирования значения байта 0x21 (ASCII '!'). Чтобы обеспечить правильный разбор, выражайте
шестнадцатеричные escape-последовательности с 2 шестнадцатеричными цифрами, используя ведущие нули по мере
необходимости, например: \x00, \x01, \xFF. Менее двух цифр потребляется, когда
за числовым символом следует нешестнадцатеричный символ, например \xFHello или
\x3world.
Используйте побайтовое экранирование только для полей с типом bytes. Хотя это возможно
использовать побайтовое экранирование в полях с типом string, эти escape-последовательности
должны формировать действительные последовательности UTF-8. Использование побайтового экранирования для выражения
последовательностей UTF-8 чревато ошибками. Предпочитайте escape-последовательности Unicode для непечатаемых
символов и символов разрыва строки в литералах для полей типа string.
Более длинные строки могут быть разбиты на несколько строк в кавычках на последовательных строках. Например:
quote:
"Когда мы пришли к власти, больше всего меня удивило то, "
"что дела были такими же плохими, как мы и говорили.\n\n"
" -- Джон Ф. Кеннеди"
Кодовые точки Unicode интерпретируются в соответствии с Unicode 13 Table A-1 Extended BNF и кодируются как UTF-8.
{{% alert title="Предупреждение" color="warning" %}}
Реализация на C++ в настоящее время интерпретирует экранированные кодовые точки высоких суррогатов как
кодовые единицы UTF-16 и ожидает, что за ними немедленно последует кодовая точка низкого суррогата \uHHHH
без какого-либо разделения по отдельным строкам в кавычках.
Кроме того, неспаренные суррогаты будут отображаться непосредственно в также недопустимый UTF-8.
Оба эти поведения являются несоответствующими1 и на них не следует полагаться.
{{% /alert %}}
Синтаксические элементы
Сообщение
Сообщение — это набор полей. Файл текстового формата представляет собой одно Сообщение.
Message = { Field } ;
Литералы
Литеральные значения полей могут быть числами, строками или идентификаторами, такими как true или
значения перечислений.
String = STRING, { STRING } ;
Float = [ "-" ], FLOAT ;
Identifier = IDENT ;
SignedIdentifier = "-", IDENT ; (* Например, "-inf" *)
DecSignedInteger = "-", DEC_INT ;
OctSignedInteger = "-", OCT_INT ;
HexSignedInteger = "-", HEX_INT ;
DecUnsignedInteger = DEC_INT ;
OctUnsignedInteger = OCT_INT ;
HexUnsignedInteger = HEX_INT ;
Одно строковое значение может состоять из нескольких частей в кавычках, разделенных необязательными пробелами. Пример:
a_string: "первая часть" 'вторая часть'
"третья часть"
без_пробелов: "первая""вторая"'третья''четвертая'
Имена полей
Поля, которые являются частью содержащего сообщения, используют простые Identifier в качестве
имен.
Extension и
Any имена полей
заключены в квадратные скобки и полностью квалифицированы. Имена полей Any имеют префикс
с квалифицирующим доменным именем, таким как type.googleapis.com/.
FieldName = ExtensionName | AnyName | IDENT ;
ExtensionName = "[", TypeName, "]" ;
AnyName = "[", Domain, "/", TypeName, "]" ;
TypeName = IDENT, { ".", IDENT } ;
Domain = IDENT, { ".", IDENT } ;
Обычные поля и поля расширений могут иметь скалярные значения или значения сообщений. Поля Any
всегда являются сообщениями. Пример:
reg_scalar: 10
reg_message { foo: "bar" }
[com.foo.ext.scalar]: 10
[com.foo.ext.message] { foo: "bar" }
any_value {
[type.googleapis.com/com.foo.any] { foo: "bar" }
}
Неизвестные поля
Парсеры текстового формата не могут поддерживать неизвестные поля, представленные как необработанные номера полей
вместо имен полей, потому что три из шести типов проводов
представлены одинаково в textformat. Некоторые реализации сериализаторов text-format
кодируют неизвестные поля в формате, который использует номер поля и
числовое представление значения, но это по своей природе ущербно, потому что
информация о типе провода игнорируется. Для сравнения, wire-format не является ущербным,
потому что он включает тип провода в каждый тег поля как (field_number << 3) | wire_type. Для получения дополнительной информации о кодировании см. тему
Кодирование.
Без информации о типе поля из схемы сообщения значение не может быть правильно закодировано в сообщение proto в wire-формате.
Поля
Значения полей могут быть литералами (строками, числами или идентификаторами) или вложенными сообщениями.
Field = ScalarField | MessageField ;
MessageField = FieldName, [ ":" ], ( MessageValue | MessageList ) [ ";" | "," ];
ScalarField = FieldName, ":", ( ScalarValue | ScalarList ) [ ";" | "," ];
MessageList = "[", [ MessageValue, { ",", MessageValue } ], "]" ;
ScalarList = "[", [ ScalarValue, { ",", ScalarValue } ], "]" ;
MessageValue = "{", Message, "}" | "<", Message, ">" ;
ScalarValue = String
| Float
| Identifier
| SignedIdentifier
| DecSignedInteger
| OctSignedInteger
| HexSignedInteger
| DecUnsignedInteger
| OctUnsignedInteger
| HexUnsignedInteger ;
Разделитель : между именем поля и значением обязателен для скалярных полей,
но необязателен для полей сообщений (включая списки). Пример:
scalar: 10 # Допустимо
scalar 10 # Неверно
scalars: [1, 2, 3] # Допустимо
scalars [1, 2, 3] # Неверно
message: {} # Допустимо
message {} # Допустимо
messages: [{}, {}] # Допустимо
messages [{}, {}] # Допустимо
Значения полей сообщений могут быть окружены фигурными скобками или угловыми скобками:
message: { foo: "bar" }
message: < foo: "bar" >
Поля, помеченные repeated, могут иметь несколько значений, указанных путем повторения поля,
используя специальный синтаксис списка [] или комбинацию обоих способов.
Порядок значений сохраняется. Пример:
repeated_field: 1
repeated_field: 2
repeated_field: [3, 4, 5]
repeated_field: 6
repeated_field: [7, 8, 9]
эквивалентно:
repeated_field: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Не-repeated поля не могут использовать синтаксис списка. Например, [0] не
допустимо для optional или required полей. Поля, помеченные optional, могут быть
опущены или указаны один раз. Поля, помеченные required, должны быть указаны ровно один раз.
required — это устаревшая функция proto2 и недоступна в proto3.
Обратная совместимость доступна для сообщений в Редакциях с использованием
features.field_presence = LEGACY_REQUIRED.
Поля, не указанные в связанном сообщении .proto, не допускаются, если только
имя поля не присутствует в списке reserved полей сообщения. reserved
поля, если они присутствуют в любой форме (скалярные, списки, сообщения), просто игнорируются
текстовым форматом.
Типы значений
Когда известен связанный .proto тип значения поля, применяются следующие описания значений и ограничения. Для целей этого раздела мы объявляем следующие элементы-контейнеры:
signedInteger = DecSignedInteger | OctSignedInteger | HexSignedInteger ;
unsignedInteger = DecUnsignedInteger | OctUnsignedInteger | HexUnsignedInteger ;
integer = signedInteger | unsignedInteger ;
| .proto Тип | Значения |
|---|---|
float, double
|
Элемент Float, DecSignedInteger или
DecUnsignedInteger, или элемент Identifier
или SignedIdentifier, чья часть IDENT
равна "inf", "infinity" или
"nan" (без учета регистра). Переполнения обрабатываются как infinity или
-infinity. Восьмеричные и шестнадцатеричные значения недопустимы.
Примечание: "nan" следует интерпретировать как Тихий NaN |
int32, sint32, sfixed32
|
Любой из элементов integer в диапазоне
-0x80000000 до 0x7FFFFFFF.
|
int64, sint64, sfixed64
|
Любой из элементов integer в диапазоне
-0x8000000000000000 до 0x7FFFFFFFFFFFFFFF.
|
uint32, fixed32
|
Любой из элементов unsignedInteger в диапазоне
0 до 0xFFFFFFFF. Обратите внимание, что знаковые значения
(-0) недопустимы.
|
uint64, fixed64
|
Любой из элементов unsignedInteger в диапазоне
0 до 0xFFFFFFFFFFFFFFFF. Обратите внимание, что знаковые
значения (-0) недопустимы.
|
string
|
Элемент String, содержащий действительные данные UTF-8. Любые escape-
последовательности должны формировать действительные последовательности байтов UTF-8 при разэкранировании.
|
bytes
|
Элемент String, возможно, включая недопустимые UTF-8 escape-
последовательности.
|
bool
|
Элемент Identifier или любой из
элементов unsignedInteger, соответствующих одному из следующих
значений.Истинные значения: "True", "true", "t", 1 Ложные значения: "False", "false", "f", 0 Любое представление беззнакового целого числа 0 или 1 разрешено: 00, 0x0, 01, 0x1 и т.д. |
| значения перечислений |
Элемент Identifier, содержащий имя значения перечисления, или любой
из элементов integer в диапазоне
-0x80000000 до 0x7FFFFFFF, содержащий номер
значения перечисления. Недопустимо указывать имя, которое не является
членом определения enum типа поля. В зависимости от
конкретной реализации среды выполнения protobuf может быть допустимо или недопустимо
указывать номер, который не является членом определения enum
типа поля. Процессоры текстового формата, не привязанные к конкретной
среде выполнения (например, поддержка IDE), могут выдавать
предупреждение, когда предоставленное числовое значение не является допустимым членом. Обратите внимание,
что определенные имена, которые являются допустимыми ключевыми словами в других контекстах, такие как
"true" или "infinity", также являются допустимыми именами значений перечисления.
|
| значения сообщений |
Элемент MessageValue.
|
Поля расширений
Поля расширений указываются с использованием их квалифицированных имен. Пример:
local_field: 10
[com.example.ext_field]: 20
Поля расширений обычно определяются в других .proto файлах. Язык текстового формата не предоставляет механизма для указания местоположения файлов, которые определяют поля расширений; вместо этого парсер должен иметь предварительные знания об их местоположениях.
Поля Any
Текстовый формат поддерживает расширенную форму
google.protobuf.Any
известного типа с использованием специального синтаксиса, напоминающего поля расширений. Пример:
local_field: 10
# Значение Any с использованием обычных полей.
any_value {
type_url: "type.googleapis.com/com.example.SomeType"
value: "\x0a\x05hello" # сериализованные байты com.example.SomeType
}
# То же значение с использованием расширения Any
any_value {
[type.googleapis.com/com.example.SomeType] {
field1: "hello"
}
}
В этом примере any_value — это поле типа google.protobuf.Any, и оно
хранит сериализованное сообщение com.example.SomeType, содержащее field1: hello.
Поля group
В текстовом формате поле group использует нормальный элемент MessageValue в качестве своего
значения, но указывается с использованием имени группы с заглавной буквы, а не
неявного имени поля в нижнем регистре. Пример:
// proto2
message MessageWithGroup {
optional group MyGroup = 1 {
optional int32 my_value = 1;
}
}
При приведенном выше определении .proto следующий текстовый формат является допустимым
MessageWithGroup:
MyGroup {
my_value: 1
}
Аналогично полям сообщений, разделитель : между именем группы и значением является
необязательным.
Эта функциональность включена в Редакции для обратной совместимости. Обычно
поля DELIMITED сериализуются как обычные сообщения. Ниже показано
поведение с Редакциями:
edition = "2024";
message Parent {
message GroupLike {
int32 foo = 1;
}
GroupLike grouplike = 1 [features.message_encoding = DELIMITED];
}
Содержимое этого .proto файла будет сериализовано как группа proto2:
GroupLike {
foo: 2;
}
Поля map
Текстовый формат не предоставляет пользовательский синтаксис для указания записей полей карты.
Когда поле map определено в .proto файле, неявно определяется сообщение Entry
содержащее поля key и value. Поля карты всегда повторяются,
принимая несколько записей ключ/значение. Пример:
// Редакции
edition = "2024";
message MessageWithMap {
map<string, int32> my_map = 1;
}
При приведенном выше определении .proto следующий текстовый формат является допустимым
MessageWithMap:
my_map { key: "entry1" value: 1 }
my_map { key: "entry2" value: 2 }
# Вы также можете использовать синтаксис списка
my_map: [
{ key: "entry3" value: 3 },
{ key: "entry4" value: 4 }
]
Оба поля key и value являются необязательными и по умолчанию имеют нулевое значение
соответствующих типов, если не указано иное. Если ключ дублируется, только
последнее указанное значение будет сохранено в разобранной карте.
Порядок карт не сохраняется в textprotos.
Поля oneof
Хотя в текстовом формате нет специального синтаксиса, связанного с полями oneof, только
один член oneof может быть указан за раз. Указание нескольких членов
одновременно недопустимо. Пример:
// Редакции
edition = "2024";
message OneofExample {
message MessageWithOneof {
string not_part_of_oneof = 1;
oneof Example {
string first_oneof_field = 2;
string second_oneof_field = 3;
}
}
repeated MessageWithOneof message = 1;
}
Приведенное выше определение .proto приводит к следующему поведению текстового формата:
# Допустимо: установлено только одно поле из oneof Example.
message {
not_part_of_oneof: "всегда допустимо"
first_oneof_field: "допустимо само по себе"
}
# Допустимо: установлено другое поле oneof.
message {
not_part_of_oneof: "всегда допустимо"
second_oneof_field: "допустимо само по себе"
}
# Неверно: установлено несколько полей из oneof Example.
message {
not_part_of_oneof: "всегда допустимо"
first_oneof_field: "недопустимо"
second_oneof_field: "недопустимо"
}
Файлы текстового формата
Файл текстового формата использует суффикс имени файла .txtpb и содержит одно
Message. Файлы текстового формата кодируются в UTF-8. Пример файла textproto приведен ниже.
{{% alert title="Важно" color="warning" %}}
.txtpb является каноническим расширением файла текстового формата и ему следует отдавать предпочтение перед
альтернативами. Этот суффикс предпочтителен из-за своей краткости и согласованности с
официальным расширением файла wire-формата .binpb. Устаревшее каноническое расширение
.textproto все еще широко используется и имеет поддержку инструментов.
Некоторые инструменты также
поддерживают устаревшие расширения .textpb и .pbtxt. Все другие расширения,
кроме перечисленных выше, категорически не рекомендуются; в частности, расширения, такие как
.protoascii, ошибочно подразумевают, что текстовый формат является только ascii, а другие, такие как
.pb.txt, не распознаются распространенными инструментами.
{{% /alert %}}
# Это пример текстового формата Protocol Buffer.
# В отличие от файлов .proto, поддерживаются только строчные комментарии в стиле shell.
name: "Иван Иванов"
pet {
kind: DOG
name: "Пушистик"
tail_wagginess: 0.65f
}
pet <
kind: LIZARD
name: "Ящерица"
legs: 4
>
string_value_with_escape: "допустимый \n escape"
repeated_values: [ "один", "два", "три" ]
Заголовок
Комментарии заголовка proto-file и proto-message информируют инструменты разработки о
схеме, чтобы они могли предоставлять различные функции.
# proto-file: some/proto/my_file.proto
# proto-message: MyMessage
Работа с форматом программно
Из-за того, что отдельные реализации Protocol Buffer выдают ни последовательный, ни каноничный текстовый формат, инструменты или библиотеки, которые изменяют файлы TextProto или выводят вывод TextProto, должны явно использовать https://github.com/protocolbuffers/txtpbfmt для форматирования своего вывода.
-
См. Unicode 13 §3.8 Surrogates, §3.2 Conformance Requirements, C1 и §3.9 Unicode Encoding Forms, D92. ↩