| Содержание | Предисловие | Введение | Ссылки
| Глава 1 | Глава 2 | Глава 3 | Глава 4 | Глава 5 | Глава 6 | Глава 7 | Глава 8 | Глава 9 | Глава 10
| Глава 11 | Глава 12 | Глава 13 | Глава 14 | Глава 15 | Глава 16 | Глава 17 | Глава 18 | Глава 19
| Приложение А | Приложение Б | Приложение В | Приложение Г |

Глава 11

    В этой главе:


Форматы

Что такое формат

Помимо всего прочего, Perl это, как мы уже говорили, "практический язык извлечений и отчетов". Теперь самое время узнать, почему его называют языком отчетов.

В Perl существует понятие шаблона для написания отчета, который называется форматом. В формате определяется постоянная часть (заголовки столбцов, метки, неизменяемый текст и т.д.) и переменная часть (текущие данные, которые вы указываете в отчете). Структура формата близка собственно к структуре вывода и подобна форматированному выводу в Коболе или выводу при помощи клаузы print using в некоторых реализациях Бейсика.

Использование формата предполагает выполнение трех операций:

1. Определение формата.

2. Загрузка данных, подлежащих печати, в переменные части формата (поля).

3. Вызов формата.

Чаще всего первый этап выполняется один раз (в тексте программы, чтобы формат определялся во время компиляции)*, а два остальных этапа многократно.

* Форматы можно создавать и во время выполнения, пользуясь функцией eval (см. книгу Programming Perl) и man-страницу perlform(l).

Определение формата

Формат задается с помощью специальной конструкции, которая называется определением формата. Определение формата, как и подпрограмма, может стоять в любом месте программы. Выглядит оно так:

format имя_формата =

строка_полей

значение_один, значение_два, значение_три

строка_полей

значение_один, значение_два

строка_полей

значение_один, значение_два, значение_три

Первая строка содержит зарезервированное слово format, за которым следует имя формата и знак равенства (=). Имя формата выбирается из отдельного пространства имен в соответствии с теми же правилами, что и все прочие имена. Поскольку имена форматов в теле программы никогда не используются (кроме как в строковых значениях), вы спокойно можете брать имена, совпадающие с зарезервированными словами. Как вы увидите в следующем разделе, "Вызов формата", большинство имен форматов будут, вероятно, совпадать с именами дескрипторов файлов (что, конечно, делает их не идентичными зарезервированным словам, и это хорошо).

За первой строкой идет сам шаблон, который может включать несколько текстовых строк или быть "пустым", т.е. не содержать ни одной строки. Конец шаблона обозначается точкой, которая ставится в отдельной строке*. В шаблонах разные пробельные символы интерпретируются по-разному; это один из тех немногих случаев, когда вид пробельных символов (пробел, символ новой строки или знак табуляции) и их количество имеют значение для текста Perl-программы.

Определение шаблона содержит ряд строк полей. Каждая строка полей может содержать неизменяемый текст текст, который будет выведен буквально при вызове формата. Вот пример строки полей с неизменяемым текстом:

Hello, my name is Fred Flintstone.

Строки полей могут содержать поледержатели (fieldholder, или переменные полей) для изменяемого текста. Если строка содержит поледержатели, то следующая строка шаблона (которая называется строкой значений) содержит ряд скалярных значений по одному на каждый поледержатель которые будут вставляться в эти поля. Вот пример строки полей с одним поледержателем и строки значений:

Hello, my name is @ $name

* В текстовых файлах последняя строка должна заканчиваться символом новой строки.

Поледержатель здесь @ . Этот поледержатель задает текстовое поле, которое состоит из 11 символов и выравнивается по левому краю. Подробно поледержатели описаны в разделе "Еще о поледержателях".

Если в строке полей имеется несколько поледержателей, то в следующей строке должны быть указаны несколько значений. Эти значения отделяются друг от друга запятыми:

Hello, my name is @ and I'm @ years old. $name, $age

Собрав все вместе, мы создадим простой формат для вывода адреса:

format ADDRESSLABEL =

$name

$ address | @< , @< @ |

$city, $state, $zip

Обратите внимание: строки, состоящие из знаков равенства (в начале и конце формата) полей не содержат, поэтому строк значений после них нет. (Если поставить после такой строки строку значений, она будет интерпретирована как строка полей и поставленную перед ней задачу не выполнит.)

Пробельные символы в строке значений игнорируются. Некоторые предпочитают вставлять в строку значений дополнительные пробельные символы, чтобы выравнять переменные по поледержателям в предыдущей строке (например, в только что рассмотренном примере переменная $zip поставлена таким образом под третьим полем предыдущей строки), но это делается только для красоты. Perl на подобное не реагирует, и на выводимую информацию это никак не влияет.

Текст, стоящий в строке значений после первого символа новой строки, отбрасывается (за исключением особого случая с многострочными поледер-жателями, который будет рассмотрен ниже).

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

Вызов формата

Вызов формата производится с помощью функции write. Эта функция получает имя дескриптора файла и генерирует для этого дескриптора текст, используя текущий для данного дескриптора формат. По умолчанию таким форматом является формат с тем же именем, что и у дескриптора файла (так, для дескриптора stdout используется формат stdout). Скоро вы узнаете, как этот порядок можно изменить.

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

format ADDRESSLABEL =

$name

$address

$city, $state, $zip

open(ADDRESSLABEL,">labels-to-print") || die "can't create";

open (ADDRESSES,"addresses") || die "cannot open addresses";

while (<ADDRESSES>) f

chomp; # удалить символ новой строки

($name,$address,$city,$state,$zip) = split (/:/);

# загрузить глобальные переменные

write (ADDRESSLABEL); # send the output }

Здесь мы видим уже знакомое определение формата, но теперь у нас есть и выполняемый код. Сначала мы открываем дескриптор для выходного файла, который называется labels-to-print. Отметим, что имя дескриптора файла (addresslabel) совпадает с именем формата. Это важно. Затем мы открываем дескриптор для файла, содержащего список адресов. Предполагается, что этот список имеет следующий формат:

Stonehenge:4470 SW Hall Suite 107 :Beaverton:OR:97005 Fred Flintstone:3737 Hard Rock Lane:Bedrock:OZ:999bc

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

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

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

Каждое поле в формате заменяется соответствующим значением из следующей строки формата. После обработки двух приведенных в примере записей файл labels-to-print будет содержать следующее:

Stonehenge

4470 SW Hall Suite 107

Beaverton , OR 97005

Fred Flintstone 3737 Hard Rock Lane Bedrock , OZ 999bc

Еще о поледержателяж

Из примеров вы уже поняли, что поледержатель @ обозначает выровненное по левому краю поле, которое содержит пять символов, а @ выровненное по левому краю поле, содержащее одиннадцать символов. Как мы и обещали, опишем поледержатели более подробно.

Текстовые поля

Большинство поледержателей начинается со знака @. Символы, стоящие после него, обозначают тип поля, а число этих символов (включая @) соответствует ширине поля.

Если после знака @ стоят левые угловые скобки ( ), поле выровнено по левому краю, т.е. в случае, если значение окажется короче, чем отведенное для него поле, оно будет дополняться справа символами пробела. (Если значение слишком длинное, оно автоматически усекается; структура формата всегда сохраняется.)

Если после знака @ стоят правые угловые скобки ( ), поле выровнено по правому краю, т.е. в случае, если значение окажется короче, чем отведенное для него поле, оно будет заполняться пробелами слева.

Наконец, если символы, стоящие после знака @, повторяющаяся несколько раз вертикальная черта (||||), то поле центрировано. Это означает, что если значение слишком короткое, производится дополнение пробелами с обеих сторон, в результате значение будет расположено по центру поля.

Числовые поля

Следующий тип поледержателя числовое поле с фиксированной десятичной запятой, полезное для больших финансовых отчетов. Это поле также начинается со знака @; за ним следует один или более знаков # с необязательной точкой (она обозначает десятичную запятую). Опять-таки, знак @ считается одним из символов поля. Например:

format MONEY =

Assets: @#W#.* Liabilities: @#* ##.*# Net: @t ##*.*#

$assets, $liabilities, $assets-$liabilities

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

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

format MONEY =

Assets: @ < Liabilities: @ Net: @ <

&cool($assets,10), scool($liab,9), Scool($assets-$liab,10)

sub pretty (

my($n,$width) = @_;

$width - 2; # учтем отрицательные числа

$n sprintf("%.2f",$n); # sprintf описывается в одной из следующих глав if ($n < 0) (

return sprintf ("[t$width.2f]", -$n); # отрицательные числа

# заключаются в квадратные скобки ) else {

return sprintf (" %$width.2f ", $n); # положительные числа выделяются

^ пробелами } }

## body of program:

$assets = 32125.12;

$liab = 45212.15;

write (MONEY) ;

Многостроковые поля

Как уже упоминалось выше, при подстановке значения, содержащегося в строке значений. Perl обычно заканчивает обработку строки, встретив в этом значении_символ новой строки. Многостроковые поледержатели позволяют использовать значения, которые представляют собой несколько строк информации. Эти поледержатели обозначаются комбинацией @*, которая ставится в отдельной строке. Как всегда, следующая строка определяет значение, подставляемое в это поле. В данном случае это может быть выражение, которое дает в результате значение, содержащее несколько строк. Подставленное значение будет выглядеть так же, как исходный текст:

четыре строки значения становятся четырьмя строками выходной информации. Например, приведенный ниже фрагмент программы

format STDOUT = Text Before. @*

$long_string Text After.

$long_string = "Fred\nBarney\nBetty\nWilma\n";

write;

позволяет получить такие выходные данные:

Text Before.

Fred

Barney

Betty

Wilma

Text After.

Заполненные поля

Следующий вид поледержателя заполненное поле. Этот поледержатель позволяет создавать целый текстовый абзац, разбиение на строки которого выполнено по границам слов. Для этого используются несколько элементов формата, которые работают в совокупности. Но давайте рассмотрим их по отдельности.

Во-первых, заполненное поле обозначается путем замены маркера @ в текстовом поледержателе на символ А (например, /'< ). Соответствующее значение для заполненного поля (в следующей строке формата) должно быть скалярной переменной*, содержащей текст, а не выражением, которое возвращает скалярное значение. Это объясняется тем, что при заполнении этого поледержателя Perl изменяет значение переменной, а значение выражения изменить очень трудно.

Когда Perl заполняет данный поледержатель, он берет значение этой переменной и "захватывает" из него столько слов (подразумевая разумное определение термина "слово")**, сколько поместится в этом поле. Эти слова фактически "выдираются" из переменной; после заполнения поля значение переменной представляет собой то, что осталось после удаления слов. Почему делается именно так, вы скоро увидите.

* А также отдельным скалярным элементом массива или хеша, например, $а[3] или $h{"fred").

** Разделители слов задаются переменной $:.

Пока что особой разницы между заполненным полем и обычным текстовым полем не видно; мы выводим в поле ровно столько слов, сколько в нем умещается (за исключением того, что мы соблюдаем границу последнего слова, а не просто обрезаем текст по ширине поля). Удобство использования заполненного поля проявляется, когда в одном формате делается несколько ссылок на одну переменную. Взгляните на этот пример:

format PEOPLE =

Name: @< Comment: " ^ ^ $name, $comment

$comment $comment $comment

Обратите внимание: переменная $ comment появляется четыре раза. Первая строка (строка с полем имени) выводит имя и первые несколько слов значения этой переменной. В процессе вычисления этой строки $ comment изменяется так, что эти слова исчезают. Вторая строка опять ссылается на ту же переменную ($comment) и поэтому получает из нее следующие несколько слов. Это справедливо и для третьей, и для четвертой строк. По сути дела, мы создали прямоугольник, который будет максимально заполнен словами из переменной $ comment, расположенным в четырех строках.

Что будет, если полный текст занимает меньше четырех строк? Получатся одна-две пустые строки. Это, наверно, нормально, если вы печатаете этикетки и вам нужно, чтобы в каждом элементе было одинаковое количество строк. Если же вы печатаете отчет, наличие множества пустых строк приведет к неоправданному расходу бумаги.

Чтобы решить эту проблему, нужно использовать признак подавления. Строка, содержащая тильду (~), подавляется (не выводится), если при печати она окажется пустой (т.е. будет содержать только пробельные символы). Сама тильда всегда печатается как пробел и может ставиться в любом месте строки, в котором можно было бы поставить пробел. Перепишем последний пример так:

format PEOPLE = Name: @ < Comment: ^ x

$name, $comment

$comment $comment $comment

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

Что будет, если комментарий занимает больше четырех строк? Мы могли бы сделать около двадцати копий последних двух строк этого формата, надеясь, что двадцати строк хватит. Это, однако, противоречит идее о том, что Perl помогает лентяям, поэтому специально для них предусмотрено следующее: любая строка, которая содержит две рядом стоящие тильды, автоматически повторяется до тех пор, пока результат не будет полностью пустой строкой. (Эта пустая строка подавляется.) В итоге наше определение формата приобретает такой вид:

format PEOPLE = Name: @< Comment: ^ ^

$name, $comment

$comment

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

Отметим, что данный критерий прекращения повторения строки требует, чтобы в некоторый момент данная строка стала пустой. Это значит, что в этой строке не нужно размещать постоянный текст (кроме символов пробела и тильд), иначе она никогда не станет пустой.

Формат начала страницы

Многие отчеты в конечном итоге выводятся на печатающее устройство, например на принтер. Принтерная бумага обычно обрезается по размеру страницы, потому что в большинстве своем мы уже давно не пользуемся рулонами. Поэтому в тексте, поступающем на печать, должны, как правило, отмечаться границы страниц, для чего нужно вставлять пустые строки или символы перехода на новую страницу. В принципе, можно было бы взять результат работы Perl-программы и обработать его какой-либо утилитой (может быть, даже написанной на Perl), которая выполняет такую разбивку на страницы. Существует, однако, более легкий способ.

Perl позволяет определять специальный формат начала страницы, с помощью которого запускается режим постраничной обработки. Perl подсчитывает все выходные строки, генерируемые при вызове формата для конкретного дескриптора файла. Если следующая строка не умещается на оставшейся части текущей страницы, Perl выдает символ перехода на новую страницу, за которым автоматически следует вызов формата начала страницы и текст, который выводится с использованием формата для конкретного дескриптора файла. Благодаря этому текст, полученный в результате одного вызова функции write, никогда не разбивается на несколько страниц (если только он не настолько велик, что не помещается на одной странице).

Формат начала страницы определяется так же, как и всякий другой формат. Имя формата начала страницы для конкретного дескриптора файла по умолчанию состоит из имени этого дескриптора и символов _тор (обязательно прописных).

Переменная $% в Perl определяется как количество вызовов формата начала страницы для конкретного дескриптора файла, что позволяет использовать эту переменную в составе формата начала страницы для нумерации страниц. Например, добавление следующего определения формата в предыдущий фрагмент программы предотвращает разрыв адресной этикетки на границах страниц и обеспечивает указание текущего номера страницы:

format ADDRESSLABEL_TOP = My Addresses -- Page @< $%

Длина страницы по умолчанию 60 строк. Этот параметр можно изменить, присвоив значение специальной переменной, о которой вы вскоре узнаете.

Perl не замечает, если вы выполняете для этого же дескриптора файла функцию print из другого места в программе, вследствие чего число строк, которые можно разместить на текущей странице, уменьшается. Вам следует либо переписать свой код, чтобы с помощью одних и тех же форматов выводить на печать всю информацию, либо после выполнения print изменить переменную "число строк на текущей странице". Через минуту мы увидим, как можно изменить это значение.

Изменение в форматах установок по умолчанию

Мы часто говорим об использовании в тех или иных ситуациях значений "по умолчанию". В Perl имеется способ отмены этих "умолчаний" практически для всех случаев. Давайте поговорим об этом.

Изменение дескриптора файла с помощью функции select()

Когда мы говорили о функции print в главе 6, я упомянул, что print и print stdout идентичны, потому что stdout это установка по умолчанию для print. Это не совсем так. Настоящая установка по умолчанию для print (а также для write и еще нескольких операций, до которых мы скоро доберемся) это выбранный в текущий момент дескриптор файла.

Вначале выбранный в текущий момент дескриптор файла это stdout, благодаря чему упрощается отправка данных на стандартный вывод. Изменить выбранный в текущий момент дескриптор файла можно с помощью функции select. В качестве аргумента эта функция принимает отдельный дескриптор файла (или скалярную переменную, которая содержит имя дескриптора файла). Изменение выбранного в текущий момент дескриптора файла влияет на все последующие операции, которые от него зависят. Например:

print "hello world\n"; # аналогично print STDOUT "hello worldVn"

select (LOGFILE); # выбрать новый дескриптор файла

print "howdy, world\n"; # аналогично print LOGFILE "howdy, world\n"

print "more for the log\n"; # еще в LOGFILE

select (STDOOT); # вновь выбрать STDOUT

print "back to stdout\n"; # это идет на стандартный вывод

Отметим, что операция select "липкая"; после выбора нового дескриптора он остается "выбранным в текущий момент" до проведения следующей операции select.

Таким образом, более удачное определение stdout по отношению к функциям print и write будет звучать так: stdout выбранный в текущий момент дескриптор по умолчанию, или просто дескриптор по умолчанию.

В подпрограммах может возникать необходимость смены выбранного в текущий момент дескриптора файла. Представляете, какое неприятное чувство можно испытать, вызвав подпрограмму и обнаружив, что все тщательно проверенные строки текста уходили куда-то "налево", потому что подпрограмма, оказывается, изменила выбранный в текущий момент дескриптор файла и не восстановила его! Что же должна делать "хорошо воспитанная" подпрограмма? Если она "знает", что текущий дескриптор stdout, она может восстановить выбранный дескриптор с помощью кода, похожего на приведенный выше. А если программа, которая вызвала подпрограмму, уже изменила выбранный дескриптор файла что тогда?

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

$oldhandle = select LOGFILE;

print "this goes to LOGPILEW;

select ($oldhandle); # восстановить предыдущий дескриптор

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

Изменение имени формата

Имя формата по умолчанию для конкретного дескриптора файла совпадает с именем этого дескриптора. Для выбранного в текущий момент дескриптора файла этот порядок можно изменить, присвоив новое имя формата специальной переменной $~. Можно также проверить значение этой переменной и посмотреть, каков текущий формат для выбранного в текущий момент дескриптора файла.

Например, чтобы использовать формат addresslabel с дескриптором stdout, следует просто записать:

$~ = "ADDRESSLABEL";

А что, если нужно установить для дескриптора report формат summary? Для этого необходимо сделать всего лишь следующее:

$oldhanlde = select REPORT;

$~ "SUMMARY";

select ($oldhandle);

Когда в следующий раз мы напишем

write (REPORT) ;

то тем самым передадим текст на дескриптор report, но в формате summary*.

Обратите внимание на то, что вы сохранили предыдущий дескриптор в скалярной переменной, а затем восстановили его. Этот прием признак хорошего стиля программирования. В коде реальной программы мы, вероятно, решили бы предыдущий однострочный пример таким же способом, а не предполагали бы, что stdout дескриптор по умолчанию.

Изменяя текущий формат для конкретного дескриптора файла, вы можете чередовать в одном отчете много разных форматов.

Изменение имени формата начала страницы

Точно так же, как путем установки переменной $~ мы можем изменять имя формата для конкретного дескриптора файла, так путем установки переменной $ л мы можем менять формат начала страницы. Эта переменная содержит имя формата начала страницы для выбранного в текущий момент дескриптора файла и доступна для чтения и записи, т.е. вы можете проверить ее значение и узнать текущее имя формата, а также изменить его, присвоив этой переменной новое значение.

* Объектно-ориентированный модуль FileHandle, входящий в состав стандартного дистрибутива Perl, обеспечивает выполнение этой задачи более простым способом.

Изменение длины страницы

Если определен формат начала страницы, длина страницы приобретает особое значение. По умолчанию длина страницы равна 60 строкам, т.е. если результаты работы функции write не умещаются до конца 60-й строки, то перед дальнейшим выводом текста автоматически вызывается формат начала страницы.

Иногда 60 строк не то, что нужно. Этот параметр можно изменить, установив переменную $=. Данная переменная содержит текущую длину страницы для выбранного в текущий момент дескриптора файла. Опять-таки, для замены дескриптора stdout (выбранного в текущий момент дескриптора файла по умолчанию) на другой нужно использовать операцию select. Вот как следует изменить дескриптор файла logfile так, чтобы страница содержала 30 строк:

$old = select LOGFILE; # выбрать LOGFILE и сохранить старый дескриптор $= 30;

select $old;

Изменение длины страницы вступает в силу только при следующем вызове формата начала страницы. Если вы установили новую длину перед выводом текста в дескриптор файла в каком-то формате, то все будет работать как надо, потому что формат начала страницы вызывается при первом же вызове функции write.

Изменение положения на странице

Если вы выводите свой текст в дескриптор файла с помощью функции print, то значение счетчика строк будет неправильным, потому что Perl проводит подсчет строк только для функции write. Если вы хотите дать Perl знать, что выводите несколько дополнительных строк, можно настроить внутренний счетчик строк Perl, изменив значение переменной $-. Эта переменная содержит число строк, оставшихся на текущей странице для выбранного в текущий момент дескриптора файла. Каждая функция write уменьшает число оставшихся строк на число фактически выведенных строк. Когда значение этого счетчика достигает нуля, вызывается формат начала страницы и из переменной $= (задающей длину страницы) копируется значение $-.

Например, чтобы сообщить Perl, что вы послали в STDOUT дополнительную строку, нужно сделать следующее:

write; # вызвать формат STDOUT для STDOUT

print "An extra line... oops!\n"; # это идет в STDOUT $- --; # декрементировать $-, чтобы показать, что в STDOUT пошла строка не из write

write; # сработает, учтя дополнительную строку

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

Упражнения

Ответы см. в приложении А.

1. Напишите программу, которая открывает файл /etc/passwd по имени и выводит на экран имя пользователя, идентификатор (номер) пользователя и его реальное имя в форматированных столбцах. Используйте функции format И write.

2. Добавьте в предыдущую программу формат начала страницы. (Если ваш файл паролей относительно короткий, то, возможно, придется установить длину страницы где-то в 10 строк, чтобы можно было использовать несколько экземпляров начала страницы.)

3. Добавьте в начало страницы номер страницы, чтобы при их выводе указывалось page I, page 2 и т.д.




|     Назад     |     Вперед     |


| Содержание | Предисловие | Введение | Ссылки
| Глава 1 | Глава 2 | Глава 3 | Глава 4 | Глава 5 | Глава 6 | Глава 7 | Глава 8 | Глава 9 | Глава 10
| Глава 11 | Глава 12 | Глава 13 | Глава 14 | Глава 15 | Глава 16 | Глава 17 | Глава 18 | Глава 19
| Приложение А | Приложение Б | Приложение В | Приложение Г |