Вы находитесь на странице: 1из 6

программирование

Как программы на Си взаимодействуют


с сервером БД PostgreSQL

Владимир Мешков
PostgreSQL является эффективным средством для хранения и обработки информации.
Разработчики этой СУБД предоставили интерфейсы для многих языков программирования.
Поддержка таких языков, как Perl, PHP, Python, обеспечивает широкое применение PostgreSQL
в области веб-программирования. Язык системного программирования Cи позволит использовать
эту СУБД, когда необходимо добиться от приложения максимального быстродействия.

Обзор библиотеки libpq


С
егодня мы рассмотрим пример взаимодействия
программы на языке Си и сервера баз данных Библиотека libpq является программным интерфейсом,
PostgreSQL c использованием библиотеки libpq. обеспечивающим взаимодействие программы, состав-
В случае отсутствия опыта работы с СУБД PostgreSQL ре- ленной на языке Си, с сервером баз данных PostgreSQL.
комендую начать изучение этой темы со статьи Сергея Эта библиотека содержит набор функций, позволяющих
Супрунова [1]. клиентской программе обмениваться информацией с ба-

60
программирование
зой данных. Библиотека входит в состав дистрибутива for(;;) {
newsock = accept(sock, NULL, NULL);
СУБД PostgreSQL. if(fork() == 0) {
Для выполнения информационного обмена клиентская while(recv(newsock, &c, 1, 0) > 0) {
send(newsock, &i, 1, 0);
программа вначале должна подключиться к базе данных. i++;
Для связи с сервером баз данных используется механизм }
close(newsock);
сокетов, при этом если клиент и сервер расположены на од- exit(0);
ной локальной машине, используется сокет домена AF_ }
close(newsock);
UNIX, в случае расположения на удаленных машинах – со- }
кет домена AF_INET. Тип домена указывается в парамет- return 0;
}
рах системного вызова socket.
Для хранения адресной информации сокет домена AF_ Листинг 2. Клиентский процесс
UNIX использует структурный тип sockaddr: int main()
{
struct sockaddr { int sock;
sa _ family _ t sa _ family; struct sockaddr saddr;
char sa _ data[14]; char c, rc;
}
sock = socket(AF _ UNIX, SOCK _ STREAM, 0);

memset((void *)&saddr, 0, sizeof(saddr));


Поле sa_family определяет тип домена, к которому при- saddr.sa _ family = AF _ UNIX;
надлежит сокет (AF_UNIX в нашем случае), массив sa_data memcpy(saddr.sa _ data, "/tmp/.sock.new", 14);
содержит путь к файлу, который описывает сокет. connect(sock, (struct sockaddr *)&saddr, ↵
Таким образом, сокет домена AF_UNIX представляет sizeof(struct sockaddr));
собой специальный файл. Сервер PostgreSQL после за- for(;;) {
пуска по умолчанию создает в каталоге /tmp сокет домена c = getchar();
send(sock, &c, 1, 0);
AF_UNIX в виде файла .s.PGSQL.5432, посмотреть на кото- if(recv(sock, &rc, 1, 0) > 0) ↵
рый можно при помощи команды ls -la. Среди прочих фай- printf("From server: %d\n", rc);
else {
лов будет запись следующего вида: close(sock);
exit(0);
srwxrwxrwt 1 pgsql users 0 Okt 4 10:37 .s.PGSQL.5432 }
}
Литера «s» перед правами доступа означает, что дан- return 0;
}
ный файл является сокетом.
Команда netstat -a позволяет нам убедиться, что файл Запустим процессы в разных терминалах. Сервер после
/tmp/.s.PGSQL.5432 входит в список активных сокетов до- запуска создаст в каталоге /tmp файл .sock.new. Через этот
мена AF_UNIX. Введем эту команду и увидим запись при- файл будет осуществляться взаимодействие между клиен-
мерно такого вида: том и сервером: клиент будет отправлять серверу символы,
вводимые пользователем, а сервер будет возвращать чис-
unix 2 [ ACC ] STREAM LISTENING 20636071 /tmp/.s.PGSQL.5432
ловые значения, каждый раз увеличивая их на 1. При этом
Для лучшего понимания рассмотрим тестовый пример на каждый введенный символ сервер отвечает двумя.
взаимодействия процессов через сокет домена AF_UNIX. Тут все правильно, т.к. серверу передается еще и символ
Ниже представлены два листинга – серверного и клиент- перевода строки «\n», вот он на него и реагирует.
ского процесса. В целях экономии места обработка оши- После остановки сервера сигналом SIGINT (комбина-
бок пропущена. ция клавиш <Ctrl+C>) файл .sock.new останется в катало-
ге /tmp. Его необходимо удалить вручную, или переопреде-
Листинг 1. Серверный процесс лить обработчик сигнала SIGINT для закрытия сокета и уда-
#include <sys/types.h> ления файла .sock.new, иначе при повторном запуске сер-
#include <sys/socket.h> вера системный вызов bind не сможет привязать адресную
int main() структуру к сокету, сообщая нам, что «Address already in use
{ (адрес уже используется)».
int sock, newsock;
struct sockaddr saddr; Вернемся к рассмотрению темы статьи. Итак, для под-
char c; ключения к серверу баз данных библиотека предоставляет
static char rc = 1;
несколько функций, но мы рассмотрим одну – PQsetdbLogin.
/* Создаем сокет домена AF _ UNIX */ Прототип этой функции имеет следующий вид:
sock = socket(AF _ UNIX, SOCK _ STREAM, 0);

/* Заполняем адресную структуру saddr */ PGconn *PQsetdbLogin(const char *pghost,


memset((void *)&saddr, 0, sizeof(saddr)); const char *pgport, const char *pgoptions,
saddr.sa _ family = AF _ UNIX; /* тип домена */ const char *pgtty, const char *dbName,
/* путь к файлу */ const char *login, const char *pwd);
memcpy(saddr.sa _ data, "/tmp/.sock.new", 14);

bind(sock, (struct sockaddr *)&saddr, ↵ Эта функция устанавливает новое соединение с ба-
sizeof(struct sockaddr));
listen(sock, 1); зой данных, которое описывается при помощи объекта ти-
па PGconn. Параметрами функции являются:

№10, октябрь 2005 61


программирование
! pghost – если сервер и клиент расположены на локаль- Данные представляют собой последовательность (кортеж)
ном хосте, этот параметр принимает значение NULL, строк таблицы, и каждая строка состоит из нескольких яче-
и взаимодействие с сервером осуществляется через ек. Выполнить выборку содержимого определенной ячей-
сокет домена AF_UNIX, по умолчанию расположенный ки можно при помощи функции PQgetvalue:
в каталоге /tmp. При работе через сеть это поле содер-
жит имя или IP-адрес хоста, на котором находится сер- char* PQgetvalue(const PGresult *res, int tup _ num, ↵
int Þeld _ num);
вер баз данных;
! pgport – номер порта (NULL для локального хоста); Здесь tup_num – это номер строки таблицы, а field_num –
! pgoptions – дополнительные опции, посылаемые серве- номер ячейки в строке, из которой считываются данные.
ру для трассировки/отладки соединения; Для определения числа строк, считанных из таблицы, ис-
! pgtty – терминал или файл для вывода отладочной ин- пользуется функция PQntuples (tuple в переводе с англий-
формации; ского означает кортеж, последовательность):
! dbName – имя базы данных;
! login, pwd – имя пользователя и пароль доступа к базе int PQntuples(const PGresult *res);
данных.
Функция PQnfields вернет число ячеек в одной стро-
Функция PQsetdbLogin всегда возвращает указатель ке таблицы:
на объект типа PGconn, независимо от того, успешно бы-
ло установлено соединение или нет. Проверку состоя- int PQnÞelds(const PGresult *res);
ния соединения выполняет функция PQstatus. Объект ти-
па PGconn передается этой функции в качестве парамет- По окончании информационного обмена с базой дан-
ра, возвращаемое функцией значение характеризует со- ных клиентская программа должна при помощи функции
стояние соединения: PQclear освободить структуру PGresult, содержащую ре-
! CONNECTION_BAD – не удалось установить соедине- зультаты запроса, и отключиться от базы, вызвав функ-
ние с базой данных; цию PQfinish:
! CONNECTION_OK – соединение с базой данных успеш-
но установлено. void PQclear(PQresult *res);
void PQÞnish(PGconn *conn)

Эти значения определены в заголовочном файле libpq-fe.h.


После установления соединения клиентская програм- Пример использования библиотеки libpq
ма может приступить к обмену информацией с базой дан- Рассмотрим простой пример использования библиотеки.
ных. Для этой цели библиотека libpq предоставляет функ- Предположим, что у нас имеется каталог, содержащий фай-
цию PQexec, прототип которой имеет следующий вид: лы различных типов (в том числе и специальные). Мы со-
ставим две программы на языке Си: первая программа бу-
PGresult *PQexec(PGconn *conn, const char *query); дет выполнять обход указанного ей каталога, считывать
и заносить в базу данных имена и размеры всех регуляр-
Параметрами функции PQexec являются указатель ных файлов из этого каталога и всех вложенных каталогов.
на объект типа PGconn (результат работы функции PQset- Вторая программа будет считывать информацию об этих
dbLogin) и строка, содержащая запрос к базе данных. От- файлах из базы данных и выводить ее на экран.
правив запрос, функция ожидает ответ от базы и сохраня- Для выполнения этой задачи устанавливаем на локаль-
ет в структуре типа Pgresult статус запроса и данные, по- ную машину СУБД PostgreSQL (см. [1]). После инициали-
лученные от базы. Для обработки статуса запроса к базе зации базы данных создаем нового пользователя my_user
данных используется функция PGresultStatus. и новую базу my_database:

ExecStatusType PQresultStatus(const PGresult *res); createuser -a -d my _ user -E -P


createdb -O my _ user my _ database

Функция PQresultStatus может возвращать следующие Для доступа к базе данных пользователь my_user дол-
значения, определенные в файле libpq-fe.h: жен указать пароль. Сам пароль будет храниться в зашиф-
! PGRES_EMPTY_QUERY – серверу отправлена пустая рованном виде, в конфигурационном файле pg_hba.conf ме-
строка запроса; няем значение поля METHOD c trust на md5.
! PGRES_COMMAND_OK – запрос, не требующий воз- Далее, подключаемся к базе данных my_database и со-
врата данных из базы, выполнен успешно; здаем в ней таблицу, состоящую из двух полей: поля fname
! PGRES_TUPLES_OK – успешное чтение данных из ба- типа char(100) для хранения имен файлов и поля fsize типа
зы; int для хранения размеров файлов.
! PGRES_FATAL_ERROR – при обращении к базе данных
произошла критическая ошибка. Заполнение базы данных информацией
Первый этап разработки – программа для заполнения базы
Если статус запроса равен PGRES_TUPLES_OK, струк- данных информацией. Назовем ее insert_data. Входные па-
тура PGresult будет содержать данные, полученные от базы. раметры – имя базы данных, имя таблицы в базе и имя ка-

62
программирование
талога, из которого будут считываться данные о файлах – #include <termios.h>
int tcgetattr(int ttyfd, struct termios *told);
передаются в параметрах командной строки: int tcsetattr(int ttyfd, int actions, const struct ↵
termios *tnew);
# ./insert _ data -d [имя базы данных] -t [имя таблицы] ↵
-p [имя каталога]
Функция tcgetattr сохраняет текущее состояние тер-
Определим переменные для хранения имен базы дан- минала в структуре told типа termios. Параметр ttyfd дол-
ных, таблицы и каталога для чтения: жен быть дескриптором файла, описывающего терминал.
Для получения доступа к своему управляющему термина-
unsigned char *dbname = NULL; /* имя базы данных */ лу процесс может использовать имя файла /dev/tty, кото-
unsigned char *table = NULL; /* имя таблицы */
/* каталог, из которого считываются данные */ рое всегда интерпретируется как текущий управляющий
unsigned char *pathname = NULL; терминал или стандартный вывод с дескриптором 0. Вы-
зов функции tcsetattr установит новое состояние термина-
Проверяем число переданных аргументов. Их долж- ла, заданное структурой tnew, а параметр actions опреде-
но быть 7: ляет, когда и как будут установлены новые атрибуты тер-
минала:
if(argc != 7) usage(); ! TCSNOW – немедленное выполнение изменений;
! TCSADRAIN – перед установкой новых параметров ожи-
Если количество переданных аргументов не соответс- дается опустошение очереди вывода;
твует указанному значению, при помощи функции usage() ! TCSAFLUSH – ожидается опустошение очереди выво-
отобразим формат вызова нашей программы: да, затем также очищается очередь ввода.

void usage() Для доступа к управляющему терминалу открываем со-


{
fprintf(stderr, "Usage: insert _ data ↵ ответствующий файл устройства:
-d [имя базы данных] -t [имя таблицы] ↵
-p [исходный каталог]\n"); int ttyfd = open("/dev/tty", O _ RDWR);
exit(0);
}
Далее считываем текущее состояние терминала в струк-
Считываем параметры командной строки. Разбор ко- туру struct termios t, снимаем флаг отображения символов
мандной строки выполним при помощи функции getopt: ECHO в поле c_lflag и устанавливаем новое состояние тер-
минала:
while((int c = getopt(argc, argv, "d:t:p:")) != EOF) {
switch(c) { tcgetattr(ttyfd, &t); /* сохраняем настройки терминала */
case 'd': t.c _ lßag &= ~ECHO; /* сбрасываем флаг ECHO */
/* имя базы данных */ /* устанавливаем новое состояние терминала */
dbname = (unsigned char *)optarg; tcsetattr(ttyfd, TCSANOW, &t);
break;
case 't':
/* имя таблицы */
table = (unsigned char *)optarg; Наличие флага TCSANOW требует немедленного вы-
break; полнения изменений. Подробности управления термина-
case 'p':
/* имя каталога */ лом смотрите в man termios.
pathname = (unsigned char *)optarg; После этих действий вводим пароль для доступа к ба-
break;
/* ошибка в параметрах */ зе данных:
case '?':
default: scanf("%s", pwd);
usage();
}
}
Вернем настройки терминала в исходное состояние –
Считываем имя пользователя и пароль для доступа включим отображение вводимых символов на экране:
к базе данных:
t.c _ lßag |= ECHO; /* устанавливаем флаг ECHO */
tcsetattr(ttyfd, TCSANOW, &t);
unsigned char user[80]; /* имя пользователя */ close(ttyfd);
unsigned char pwd[80]; /* пароль доступа к базе данных */

memset(user, 0, sizeof(user)); Подключаемся к базе данных, вызвав функцию PQ-


printf("Login: ");
scanf("%s", user); setdbLogin. Эта функция вернет указатель на объект типа
PGconn, независимо от того, успешно было установлено
memset(pwd, 0, 80);
printf("Password: "); соединение или нет:

Перед тем как ввести пароль, из соображений безопас- PGconn *conn = PQsetdbLogin(NULL, NULL, NULL, NULL, ↵
dbname, user, pwd);
ности отключим отображение вводимых символов на экра-
не, изменив настройки управляющего терминала. Для уп- Первые четыре параметра функции PQsetdbLogin ус-
равления свойствами терминала используются функции тановлены в NULL, так как сервер баз данных находится
tcgetattr и tcsetattr: на локальной машине, и дополнительных опций мы ему

№10, октябрь 2005 63


программирование
не передаем. Если сервер расположен на удаленной ма- /* PGRES _ COMMAND _ OK, т.к. данных от базы мы не */
/* получаем */
шине, то вызов функции PQsetdbLogin примет следую- if(PQresultStatus(res) != ↵
щий вид: PGRES _ COMMAND _ OK) {
fprintf(stderr, "INSERT ↵
query failed.\n");
PQsetdbLogin("192.168.1.1", "5432", NULL, NULL, dbname, ↵ break;
user, pwd), }
}
где 192.168.1.1 – IP адрес хоста, на котором установлен сер- closedir(dp);
PQclear(res);
вер баз данных, 5432 – порт, который слушает база. return 0;
Анализируем состояние соединения и в случае ошибки }
завершаем выполнение программы:
После записи информации в базу данных отключаем-
if(PQstatus(conn) == CONNECTION _ BAD) { ся от нее:
fprintf(stderr, "Connection to database failed.\n");
fprintf(stderr, "%s", PQerrorMessage(conn));
exit(1); PQÞnish(conn);
}

При успешном установлении соединения с базой дан- Если функцию PQfinish не вызвать, то в данном случае
ных считываем необходимую нам информацию из указан- ничего страшного не произойдет, потому что процесс завер-
ного каталога. Считывание выполняет рекурсивная фун- шает выполнение. Ядро удаляет процесс из общего списка,
кция list_dir(), в параметрах которой мы передаем указа- уничтожая все служебные структуры, описывающие фай-
тель на объект типа PGconn, имя таблицы в базе данных лы и сокеты, с которыми процесс работал, а значение де-
и имя каталога: скриптора сокета (так же как и файла) имеет смысл толь-
ко в контексте процесса, так как по сути это индекс в мас-
int list _ dir(PGconn *conn, unsigned char *table, ↵ сиве структур.
unsigned char *pathname)
{ Если вместо функции отключения от базы перед выхо-
struct dirent *d; дом из программы организовать бесконечный цикл и ввес-
struct stat s;
DIR *dp; ти в соседнем терминале команду netstat, то можно увидеть,
/* результат обращения к базе данных */ что процесс установил соединение с базой данных через
PGresult *res;
/* абсолютное путевое имя файла */ сокет домена AF_UNIX. При остановке процесса сигналом
unsigned char full _ path[256]; SIGINT (комбинация клавиш Ctrl-C) это соединение исче-
/* строка запроса к базе данных */
unsigned char query[QUERY _ LEN]; зает, даже если мы не вызываем функцию PQfinish. Дру-
/* данные, передаваемые базе */ гое дело, если процесс не закрыл соединение и продолжает
unsigned char escape _ string[80];
функционировать (например, если это фоновый процесс).
/* Открываем каталог */ Тогда возможна ситуация несанкционированного исполь-
if((dp = opendir(pathname)) == NULL) {
perror("opendir"); зования уже установленного соединения (сокет не закрыт)
return -1; для доступа к базе данных, и при этом необязательно знать
}
пароль. Поэтому закрывать соединение надо явно.
/* Пропускаем родительский и текущий каталоги */ Вместо рассмотренной рекурсивной функции list_dir
d = readdir(dp); // "."
d = readdir(dp); // ".." в нашем примере удобнее использовать функцию ftw, ко-
торая выполняет обход дерева каталогов, начиная с задан-
/* Цикл чтения записей каталог */
while(d = readdir(dp)) { ного, и вызывающая процедуру, определенную пользова-
телем для каждой встретившейся записи каталога. Функ-
/* Формируем абсолютное путевое имя файла */
/* и получаем информацию о нем */ ция ftw имеет следующий вид:
memset(full _ path, 0, 256);
sprintf(full _ path, "%s/%s", pathname, ↵
d->d _ name); #include <ftw.h>
stat(full _ path, &s); int ftw(const char *path, int(* func)(), int depth);

/* Если это каталог – выполняем рекурсивный */ Первый параметр path определяет имя каталога, с ко-
/* вызов функции */ торого должен начаться рекурсивный обход дерева. Пара-
if(S _ ISDIR(s.st _ mode)) list _ dir(conn, ↵
table, full _ path); метр depth управляет числом используемых функцией ftw
различных дескрипторов файлов. Чем больше значение
/* Добавляем в базу информацию о файле, при этом */
/* преобразуем путевое имя файла при помощи */ depth, тем меньше будет случаев повторного открытия ка-
/* функции PQescapeString */ талогов, что сократит общее время обработки вызова. Вто-
memset(escape _ string, 0, 80);
PQescapeString(escape _ string, ↵ рой параметр func – это определенная пользователем функ-
full _ path, 80); ция, вызываемая для каждого файла или каталога, найден-
/* Формируем запрос и отправляем его базе данных */ ного в поддереве каталога path. При каждом вызове фун-
memset(query, 0, QUERY _ LEN); кции func будут передаваться три аргумента: заканчиваю-
sprintf(query, "INSERT INTO %s ↵
values('%s','%u')", table, ↵ щаяся нулевым символом строка с именем объекта, указа-
full _ path, s.st _ size); тель на структуру stat с данными об объекте и целочислен-
res = PQexec(conn, query);
ный код. Функция func, следовательно, должна быть пост-
/* Проверяем статус запроса. Он должен быть равен */ роена следующим образом:

64
программирование
int func(const char *name, const struct stat *sptr, ↵ }
int type) return 0;
{ }
/* Тело функции */
Для получения исполняемого модуля введем команду:
}
# gcc -o insert _ data insert _ data.c -lpq
Целочисленный аргумент type может принимать одно
из нескольких возможных значений, определенных в за-
головочном файле и описывающих тип встретившегося Чтение информации из базы данных
объекта: Второй этап разработки – программа для чтения информа-
! FTW_F – объект является файлом; ции из базы данных. Строится она по такому же принципу,
! FTW_D – объект является каталогом; как и предыдущая: в параметрах командной строки переда-
! FTW_DNR – объект является каталогом, который нельзя ются имя базы данных и таблицы, выполняется ввод имени
прочесть; и пароля, при этом отображение вводимых символов отклю-
! FTW_SL – объект является символьной ссылкой; чается. После этого подключаемся к базе данных:
! FTW_NS – объект не является символьной ссылкой,
для него нельзя успешно выполнить вызов stat. conn = PQsetdbLogin(NULL, NULL, NULL, NULL, dbname, ↵
user, pwd);

Работа вызова будет продолжаться до тех пор, пока if(PQstatus(conn) == CONNECTION _ BAD) {
fprintf(stderr, "Connection to database failed.\n");
не будет завершен обход дерева или не возникнет ошиб- fprintf(stderr, "%s", PQerrorMessage(conn));
ка внутри функции ftw. Обход также закончится, если оп- exit(1);
}
ределенная пользователем функция возвратит ненулевое
значение. Тогда функция ftw прекратит работу и вернет Формируем и отправляем запрос к базе для выборки
значение, возвращенное функцией пользователя. Ошиб- всех полей из таблицы:
ки внутри функции ftw приведут к возврату значения -1,
тогда в переменной errno будет выставлен соответствую- memset(query, 0, QUERY _ LEN);
sprintf(query, "SELECT * FROM %s", table);
щий код ошибки. res = PQexec(conn, query);
Вызовем в нашей программе вместо рекурсивной фун-
кции list_dir функцию ftw: В случае успешного чтения данных из базы статус за-
проса должен быть равен PGRES_TUPLES_OK. Проверя-
ftw(pathname, list _ dir1, 1); ем это:

Функция list_dir1 передает базе данных информацию if(PQresultStatus(res) != PGRES _ TUPLES _ OK) {
fprintf(stderr, "SELECT query failed.\n");
о каждом регулярном файле: goto out;
}
int list _ dir1(const char *name, const struct stat *s, ↵
int type)
{ Отображаем результаты чтения:
PGresult *res;

/* строка запроса к базе данных */ for(i = 0; i < PQntuples(res); i++) {


unsigned char query[QUERY _ LEN]; for(n = 0; n < PQnÞelds(res); n++) printf("%-20s", ↵
unsigned char escape _ string[80]; PQgetvalue(res, i, n));
printf("\n");
/* Возвращаемся, если вызов stat завершился неудачно */ }
if(type == FTW _ NS) return 0;
Функция PQntuples вернет число прочитанных из таб-
/* Если объект является регулярным файлом, */ лицы строк, а функция PQnfields – число ячеек в одной
/* добавляем информацию о нем в базу */
if((type == FTW _ F) && S _ ISREG(s->st _ mode)) { строке.
Работоспособность программ была проверена для ОС
memset(escape _ string, 0, ↵
sizeof(escape _ string)); Linux Slackware 10.2 и FreeBSD 5.2, использовался сервер
PQescapeString(escape _ string, ↵ баз данных PostgreSQL 8.0.3.
name, sizeof(escape _ string));

memset(query, 0, QUERY _ LEN); Литература:


sprintf(query, "INSERT INTO ↵
%s values('%s','%u')", table, ↵ 1. Супрунов С. PostgreSQL: первые шаги. – Журнал «Системный
name, s->st _ size); администратор», №7, 2004 г. – 26-33 с.
res = PQexec(conn, query); 2. PostgreSQL 7.3.2 Programmer’s Guide by The PostgreSQL Global
Development Group.
/* Проверяем статус запроса */
if(PQresultStatus(res) != ↵ 3. Кейт Хэвиленд, Дайна Грей, Бен Салама. Системное про-
PGRES _ COMMAND _ OK) { граммирование в UNIX. Руководство программиста по раз-
fprintf(stderr, "INSERT ↵
query failed.\n"); работке ПО = Unix System Programming. A programmer’s guide
return -1; to software development: Пер. с англ. – М., ДМК Пресс, 2000 г. –
}
PQclear(res); 368 с., ил.

№10, октябрь 2005 65

Оценить