CGI
01.01.2007
Аббревиатура CGI означает Common Gateway Interface, и является связевым протоколом между формой в Web браузере (клиент) и приложением запущенным на Web сервере (сервер). Приложение обычно называется CGI скрипт, но мы можем использовать Дельфи для написания CGI приложений без скриптов.
Имеется два типа CGI: стандартное или консольное CGI приложение и позже появилась версия для Windows называемая WinCGI. 1.3.1. Консольное CGI приложение Стандартное или консольное CGI приложение взаимодействует с формой на клиенте с помощью переменных среды (управляющая информация), стандартным входом (данные формы) и стандартным выводом (возвращаемая динамическая HTML страница). 1.3.2. WinCGI Приложение WinCGI взаимодействует с формой на клиенте с помощью Windows .INI файла вместо переменных среды. Windows .INI файл содержит управляющую информацию, иногда данные формы и имена входного, данных и выходного файлов. 1.3.3. Delphi и CGI В данной главе я расскажу, как написать простое Дельфи CGI приложение, без использования Web Modules или других Client/Server модулей. Во первых аббревиатура CGI означает Common Gateway Interface, и это только имя для передачи информации от клиента серверу. На клиентской стороне это реализуется с помощью формы, содержащей только теги. На серверной стороне На сервере запускается CGI приложение, которое иногда называется CGI скрипт (для примера на Unix машинах, где Perl используется для исполнения CGI скриптов). В данной главе я сконцентрирую внимание на написание CGI приложения для Windows NT web сервера, и использовании 32-битной Дельфи (например Delphi 2.x или 3.x) для данной задачи, данный код может быть также без проблем откомпилирован в C++Builder. Стандартное CGI приложение получает данные через стандартный вход и должно выдать ответ через стандартный вывод. (например сгенерированную HTML страницу). Это означает необходимость написания консольного приложения. Если даже нет входных данных мы все равно можем использовать CGI приложение для генерации динамических HTML страниц (например для выдачи данных их таблицы). 1.3.4. Динамический вывод Для начала посмотрим на стандартное "hello world" CGI приложение. Единственное, что оно должно сделать, это вернуть HTML страницу со строкой "hello, world". Перед тем как мы начнем делать это - обратим внимание на следующее: CGI приложение должно сообщить миру какой (MIME) формат оно выдает. В нашем случае это "text/html", которое мы должны указать как: content-type: text/html, и затем одну пустую строку. Вот код нашего первого "Hello, world!" CGI приложения:program CGI1;Если вы откомпилируете данную программу в Дельфи 2 или 3 и затем запустите ее из web браузера подключенного к web серверу, где оно записано в исполнимом виде в исполняемом каталоге таком как cgi-bin, то вы увидите текст "Hello, world!" на странице. 1.3.5. CGI ввод Теперь, мы знаем как создавать CGI приложение, которое может генерировать динамическую HTML страницу (или в действительности почти статическую). Но как насчет ввода? Здесь более чем одно действие: мы должны проверять переменную DOS 'CONTENT LENGTH' что бы знать как много символов мы можем прочитать со стандартного ввода (если мы попытаемся читать больше чем есть, то мы повиснем навсегда). Конечно, это широко известный факт. Я написал компонент TBDosEnvironment чтобы вы могли иметь доступ до переменных среды DOS:
{$APPTYPE CONSOLE}
begin
writeln('content-type: text/html');
writeln;
writeln('<HTML');
writeln('<BODY');
writeln('Hello, world!');
writeln('</BODY');
writeln('</HTML')
end.
unit DrBobDOS;Здесь список переменных среды (предоставленный Deepak Shenoy), которые доступны для CGI программ. Даже ISAPI программы могут использовать эти переменные:
interface
uses
SysUtils, WinTypes, WinProcs, Classes;
type
TBDosEnvironment = class(TComponent)
public
{ Public class declarations (override) }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
private
{ Private field declarations }
FDosEnvList: TStringList;
procedure DoNothing(const Value: TStringList);
protected
{ Protected method declarations }
Dummy: Word;
function GetDosEnvCount: Word;
public
{ Public interface declarations }
function GetDosEnvStr(const Name: String): String;
{ This function is a modified version of the GetEnvVar function that appears in the WinDos unit that comes with Delphi. This function's interface uses Pascal strings instead of null-terminated strings.
}
published
{ Published design declarations }
property DosEnvCount: Word read GetDosEnvCount write Dummy;
property DosEnvList: TStringList read FDosEnvList write DoNothing;
end;
implementation
constructor TBDosEnvironment.Create(AOwner: TComponent);
var
P: PChar;
begin
inherited Create(AOwner);
FDosEnvList := TStringList.Create;
{$IFDEF WIN32}
P := GetEnvironmentStrings;
{$ELSE}
P := GetDosEnvironment;
{$ENDIF}
while P^ <> #0 do
begin
FDosEnvList.Add(StrPas(P));
Inc(P, StrLen(P)+1) { Fast Jump to Next Var }
end;
end {Create};
destructor TBDosEnvironment.Destroy;
begin
FDosEnvList.Free;
FDosEnvList := nil;
inherited Destroy
end {Destroy};
procedure TBDosEnvironment.DoNothing(const Value: StringList);
begin
end {DoNothing};
function TBDosEnvironment.GetDosEnvCount: Word;
begin
if Assigned(FDosEnvList) then
Result := FDosEnvList.Count
else
Result := 0;
end {GetDosEnvCount};
function TBDosEnvironment.GetDosEnvStr(const Name: String): String;
var
i: Integer;
Tmp: String;
begin
i := 0;
Result := '';
if Assigned(FDosEnvList) then while i < FDosEnvList.Count do
begin
Tmp := FDosEnvList[i];
Inc(i);
if Pos(Name,Tmp) = 1 then
begin
Delete(Tmp,1,Length(Name));
if Tmp[1] = '=' then
begin
Delete(Tmp,1,1);
Result := Tmp;
i := FDosEnvList.Count { end while-loop }
end
end
end
end {GetDosEnvStr};
end.
Environment Variable |
Purpose/Meaning/Value |
GATEWAY_INTERFACE | Версия CGI для которой скомпилирован web сервер |
SERVER_NAME | IP адрес сервера или имя. |
SERVER_PORT | Порт на сервер, которые принимает HTTP запросы. |
SERVER_PROTOCOL | Имя и версия протокола, используемая для обработки запросов. |
SERVER_SOFTWARE | Имя (и обычно версия) программного обеспечения сервера. |
AUTH_TYPE | Схема проверки прав используемая сервером (NULL , BASIC) |
CONTENT_FILE | Файл используемый для передачи данных CGI программе (только Windows HTTPd/WinCGI). |
CONTENT_LENGTH | Количество байтов переданное на стандартный вход (STDIN) как содержимое POST запроса. |
CONTENT_TYPE | Тип данных переданных на сервер. |
OUTPUT_FILE | Имя файла для результата (только Windows HTTPd/WinCGI). |
PATH_INFO | Дополнительный, относительный путь переданный на сервер после имени скрипта, но до данных запроса. |
PATH_TRANSLATED | Та же самая информация, но преобразованная из относительного пути в абсолютный. |
QUERY_STRING | Данные переданные как часть URL, все после символа ? в URL. |
REMOTE_ADDR | Адрес IP или имя сервера конечного пользователя. |
REMOTE_USER | Имя пользователя, если используется схема проверки прав. |
REQUEST_LINE | Полный HTTP запрос представляемый сервером (зависит от сервера). |
REQUEST_METHOD | Указывает метод передачи данных, как часть URL (GET) или через стандартный ввод STDIN (POST). |
SCRIPT_NAME | Имя запущенного скрипта. |
unit DrBobCGI;Я написал кучу CGI приложений за последний год и все они используют модуль DrBobCGIю Теперь реальное пример: стандартное CGI приложение - гостевая книга (guestbook), в которой запрашивается ваше имя и небольшой комментарий, написанное с помощью всего нескольких строк на Дельфи. Вначале CGI форма: <HTML> <BODY> <H2>Dr.Bob's Guestbook</H2> <FORM ACTION=http://www.drbob42.com/cgi-bin/guest.exe METHOD=POST> Name: <INPUT TYPE=text NAME=name><BR> Comments: <TEXTAREA COLS=42 LINES=4 NAME=comments> <P> <INPUT TYPE=SUBMIT VALUE="Send Comments to Dr.Bob"> </FORM> </BODY> </HTML> Теперь консольное приложение:
{$I-}
interface
var
ContentLength: Integer = 0;
function Value(const Field: ShortString): ShortString;
{ use this function to get the CGI inputquery values }
implementation
uses
SysUtils, Windows;
var
Data: String = '';
function Value(const Field: ShortString): ShortString;
var
i: Integer;
begin
Result := '';
i := Pos(Field+'=',Data);
if i > 0 then
begin
Inc(i,Length(Field)+1);
while Data[i] <> '&' do
begin
Result := Result + Data[i];
Inc(i)
end
end
end {Value};
var
P: PChar;
i: Integer;
Str: ShortString;
type
TRequestMethod = (Unknown,Get,Post);
var
RequestMethod: TRequestMethod = Unknown;
initialization
P := GetEnvironmentStrings;
while P^ <> #0 do
begin
Str := StrPas(P);
if Pos('REQUEST_METHOD=',Str) > 0 then
begin
Delete(Str,1,Pos('=',Str));
if Str = 'POST' then RequestMethod := Post
else
if Str = 'GET' then RequestMethod := Get
end;
if Pos('CONTENT_LENGTH=',Str) = 1 then
begin
Delete(Str,1,Pos('=',Str));
ContentLength := StrToInt(Str)
end;
if Pos('QUERY_STRING=',Str) > 0 then
begin
Delete(Str,1,Pos('=',Str));
SetLength(Data,Length(Str)+1);
Data := Str
end;
Inc(P, StrLen(P)+1)
end;
if RequestMethod = Post then
begin
SetLength(Data,ContentLength+1);
for i:=1 to ContentLength do read(Data[i]);
Data[ContentLength+1] := '&';
{ if IOResult <> 0 then { skip }
end;
i := 0;
while i < Length(Data) do
begin
Inc(i);
if Data[i] = '+' then Data[i] := ' ';
if (Data[i] = '%') then { special code }
begin
Str := '$00';
Str[2] := Data[i+1];
Str[3] := Data[i+2];
Delete(Data,i+1,2);
Data[i] := Chr(StrToInt(Str))
end
end;
if i > 0 then Data[i+1] := '&'
else Data := '&'
finalization
Data := ''
end.
program CGI;Вопрос: У меня на форме две "submit" клавиши, одна на переход на предыдущую страницу, другая переход на следующую страницу. Как определить какая из них была нажата, чтобы я мог выполнить соответствующее действие. Доктор Боб отвечает: Вы должны назначить уникальное значение для каждой кнопки "type=submit", ниже приведен соответствующий код: <HTML> <BODY> Edit the information and press the SAVE button<BR> To Delete information, press the DELETE button<BR> <P> <FORM METHOD=POST ACTION=http://www.drbob42.com/cgi-bin/debug.exe> <HR> <input type=text name=name> <P> <input type=reset value="RESET"> <input type=submit name=action value="SAVE"> <input type=submit name=action value="DELETE"> </FORM> </BODY> </HTML> Вы должны получить "Action=SAVE" или "Action=DELETE" после нажатия одной из этих кнопок. 2. HTML и CGI/WinCGI "трудный путь" В данной главе показывается, как опубликовать вашу базу данных в Интернете путем (1) генерации статических страниц из таблиц базы данных, (2) написания CGI/WinCGI приложений для выполнения запросов к базе данных без использования Delphi Web Modules. 2.1. HTML-страницы Допустим, вы имеете базу данных с продуктами. Бумажная реклама очень накладна. Но реклама в web это что-то новое и за приемлемую цену. Хорошо было бы иметь вашу базу опубликованной в Интернете, не так ли? Но организация своего собственного сайта на NT Web Server, работающего с инструментом типа IntraBuilder или WebHub стоит больших денег, включая время ни эксперименты и настройку. В данной главе мы покажем быстрый путь и простой путь публикации вашей базы данных на web: просто генерируя статические HTML страницы, базируясь на записях в таблице. Накладно? Нет. Сложно? Я так не думаю. Позвольте указать простой путь на небольшой базе данных. 2.1.1. Delphi и HTML Мой главный инструмент разработчики это Дельфи, и мы напишем Delphi Database HTML Expert в данной главе. Дельфи позволяет подсоединяться практически к базе данных любого формата. С помощью BDE к Парадоксу и dBASE, с помощью ODBC например к Access, и с помощью SQL Links к большим DBMS типа InterBase, Oracle, Sybase и Informix. Также, вы можете купить дополнительные продукты типа Apollo для связи с таблицами Clipper и FoxPro. В этой главе мы будем использовать базы формата Парадокс. Парадокс имеет достаточно развитый формат, что решает многие проблемы при преобразовании полей, типов и значения из базы в HTML. 2.1.2. Basic HTML Ввод будет преобразовываться в формат базы данных, а вывод в формат HTML-страниц. 2.1.3. Преобразование полей HTML страница может содержать только простой ASCII текст. Конечно, здесь могут быть и другие вещи встроенный в текст, обычно картинки в .GIF или .JPEG формат. Таблица базы данных содержит поля, у которых есть значения, которые можно преобразовать в строки символов. Дельфи даже имеет встроенное свойство "AsString" для всех основных классов наследованных от TField. Свойство AsString в действительно преобразующие свойство. Для TStringField, AsString может использоваться для чтения значения из поля как строка. Для TBCDField, TCurrencyField, TDateField, TDateTimeField, TFloatField, TIntegerField, TSmallintField, TTimeField, и TWordField, свойство AsString преобразует тип в строку при чтении из поля. Для TBooleanField, свойство AsString возвращает 'T' или 'F'. Для TMemoField, TGraphicField, TBlobField, TBytesField или TVarBytesField, свойство AsString должно использоваться только для чтения из поля. Это возвращает строковое выражение '(Memo)', '(Graphic)', '(Blob)', '(Bytes)' или '(Var Bytes)' соответственно. Так как мемо поля могут содержать важную текстовую информацию, я решил игнорировать все кроме TMemoField, и при работе с TMemoField мы можем использовать метод SaveToStream для чтения данных из поля, как мы увидим это позже. Так что мы можем разделить их на две группы: те у которых мы можем использовать свойство AsString, и те у которых нет. Мы можем определить третий тип (неизвестный - unknown), и использовать следующие определения лоя таблиц не более 255 полей: const MaxField = 255; sf_UnKnown = 0; sf_String = 1; sf_Memo = 2; var FieldTypes: Array[0..Pred(MaxField)] of Byte; { default unknowns } Мы должны просмотреть структуру таблицы для получения информации об типах полей:
{$I-}
{$APPTYPE CONSOLE}
uses
DrBobCGI;
var
guest: Text;
Str: String;
begin
Assign(guest,'book.htm'); // assuming that's the guestbook
Append(guest);
if IOResult <> 0 then // open new guestbook
begin
Rewrite(guest);
writeln(guest,'<HTML>');
writeln(guest,'<BODY>')
end;
writeln(guest,'Date: ',DateTimeToStr(Now),'<BR>');
writeln(guest,'Name: ',Value('name'),'<BR>');
writeln(guest,'Comments: ',Value('comments'),'<HR>');
reset(guest);
while not eof(guest) do // now output guestbook itself
begin
readln(guest,Str);
writeln(Str)
end;
close(guest);
writeln('</BODY>');
writeln('</HTML>')
end.
with TTable.Create(nil) do2.1.4. Записи После анализа полей таблицы, мы можем пройтись по всей таблице и получить значения полей. Для каждой записи в таблице мы сгенерируем HTML-страницу. Мы можем использовать имена полей как заголовки, используя тег <H2> для ключевых полей и тег <H3> для не ключевых полей. Код просматривает всю таблицу т преобразовывает поля в текст и выводит их в HTML-файл:
try
DatabaseName := ADatabase;
TableName := ATable;
Active := True;
keys := -1; { no key in table }
for i:=0 to Pred(FieldDefs.Count) do
begin
if Fields[i].IsIndexField then keys := i;
FieldTypes[i] := sf_String; { default }
if (FieldDefs[i].FieldClass = TMemoField) then
FieldTypes[i] := sf_Memo
else
if (FieldDefs[i].FieldClass = TGraphicField) or
(FieldDefs[i].FieldClass = TBlobField) or
(FieldDefs[i].FieldClass = TBytesField) or
(FieldDefs[i].FieldClass = TVarBytesField) then
FieldTypes[i] := sf_UnKnown { ignore }
end
finally
Free
end;
while not Eof doЗаметим, что я использую здесь одно недокументированное свойство HTML: для окончания заголовка вы можете написать </B>, но вы должны использовать <BR> для разрыва строки. Таким образом, вы можете иметь заголовки, и текст, начинающийся правее и ниже заголовка. Пожалуйста, учтите, что это недокументированное свойство и вы должны заменить его раз комментировав </H2> и </H3> если вы не желаете жить на угле <юмор>. Следующий листинг показывает как получить информацию из мемо поля базы данных и поместить его в текстовый файл. И наконец после этого мы отформатируем немного, помня что HTML игнорирует множественные переводы строки и пробелы.
begin
Inc(RecNr);
System.Assign(f,FileName+'/'+PageNr(RecNr));
System.Rewrite(f);
writeln(f,'<HTML>');
writeln(f,'<HEADER>');
writeln(f,'<TITLE>');
writeln(f,Format('%s %d/%d',[ATable,RecNr,RecordCount]));
writeln(f,'</TITLE>');
writeln(f,'</HEADER>');
writeln(f,'<BODY>');
{ print fields }
for i:=0 to Pred(FieldCount) do
if FieldTypes[i] > sf_UnKnown then
begin
if (keys >= i) then writeln(f,'<H2>')
else writeln(f,'<H3>');
writeln(f,FieldDefs[i].Name,':');
if (keys >= i) then writeln(f,'</B><BR>') { </H2> }
else writeln(f,'</B><BR>'); { </H3> }
if FieldTypes[i] = sf_Memo then
writeMemo(f,Fields[i])
else writeln(f,Fields[i].AsString);
if (keys = i) then writeln(f,'<HR>');
end;
writeln(f,'</BODY>');
writeln(f,'</HTML>');
System.Close(f);
Next
end;
procedure WriteStream(var f: Text; var Stream: TMemoryStream);2.1.5. Страницы Теперь у нас есть метод преобразования записей в HTML страницы, нам также нужен путь уникальной идентификации каждой записи. Допустим, что база данных не не содержит более 100,000 записей (Если таблица содержит свыше 100,000 записей, то конвертирование их в HTML страницы наверно не очень хорошая идея), Я думаю что подойдет схема где каждая запись помещается в файл с именем "pag#####.htm", где ##### номер записи в базе данных. Для уменьшения конфликта имен, каждая таблица должна размещаться в своем собственном каталоге (например, BIOLIFE.HTM каталог для BIOLIFE.DB таблиц, так что мы будем иметь BIOLIFE.HTM/PAG00001.HTM для первой записи из BIOLIFE.DB таблицы).
const
LF = #10;
BufSize = 8192; { bigger memos are chopped off!! }
var
Buffer: Array[0..Pred(BufSize)] of Char;
i: Integer;
begin
Stream.Seek(0,0);
if Stream.Size > 0 then
begin
Stream.Read(Buffer,Stream.Size);
for i:=0 to Pred(Pred(Stream.Size)) do
begin
{ empty line converted to <P> break }
if (Buffer[i] = LF) and (Buffer[i+1] = LF) then writeln(f,'<P>');
{ strip multiple spaces (are ignored anyway) }
if not ((Buffer[i] = ' ') and (Buffer[i+1] = ' ')) then write(f,Buffer[i]);
{ start new sentence on a new line (but only in HTML doc itself }
if (Buffer[i] = '.') and (Buffer[i+1] = ' ') then writeln(f)
end;
writeln(f,Buffer[Pred(Stream.Size)])
end
else writeln(f,' ') { empty memo }
end {WriteStream};
procedure WriteMemo(var f: Text; Field: TField);
var Stream: TMemoryStream;
begin
Stream := TMemoryStream.Create;
(Field AS TMemoField).SaveToStream(Stream);
WriteStream(f,Stream);
Stream.Free
end {WriteMemo};
constКроме первой страницы PAG00001.HTM, нам также необходимо знать имя последней страницы, и функцию, которая нам даст номер текущей страницы для номера записи. 2.1.6. HTML "Живые" клавиши Неплохо также иметь путь для навигации по записям таблицы, для этого я использую IMAGE MAP, встроенный в HTML-страницу и работающий даже если браузер загружает локальный файл. HTML-синтаксис для отображения картинки следующий: <IMG SRC="image.gif"> где image.gif это файл типа .GIF или .JPEG. Мы можем вставить опцию USEMAP в тег, для указания имени карты образа, например: <IMG SRC="image.gif" USEMAP="#map"> Внутри страницы мы можем ссылаться на "#map", а в действительности на картинку. Image map ничего более чем список координат и ссылок. Переход на ссылку произойдет, мы щелкнем мышкой в указанных координатах. HTML-синтаксис карты образа, the image map выглядит как навигационная панель размером 25x125 пикселей: <MAP NAME="map"> <AREA SHAPE="rect" COORDS="51,0,75,25" HREF="next"> <AREA SHAPE="rect" COORDS="76,0,100,25" HREF="last"> <AREA SHAPE="rect" COORDS="101,0,125,25"HREF="this"> </MAP> Таким образом, мы можем свой навигатор по базе данных. Для этого нам необходимо иметь три картинки: одна для первой записи (клавиши первая и предыдущая запрещены), одна для последней записи (клавиши следующая и последняя запись запрещены) и одна для записей в середине таблицы (все клавиши разрешены). В каждой ситуации я назначаю ссылку для одной из клавиш на другую страницу. Это обеспечивает сильную обратную связь между нажатиями на клавиши. Конечно, клавиши не могут быть надавлены, но мы имеем очень быстрый ответ по сравнению с Java или CGI-приложениями (все что происходит это только прыжок на другую страницу). Вот код на Дельфи, который генерирует корректный образ и карту для каждой записи:
FirstPage = 'pag00001.htm';
LastPage: TPageName = 'pag%.5d.htm'; { format }
function PageNr(Nr: Word): TPageName;
begin
Result := Format('pag%.5d.htm',[Nr])
end {PageNr};
if (RecNr = 1) then { first record }Все три образа панели навигации хранятся в общем каталоге "../images" и дают мне шанс конвертировать множество таблиц в одно и тоже время для всех точек только с помощью этих трех образов. В действительности, в нашей локальной интрасети мы имеем порядка 23 таблиц преобразованных в 200 HTML страниц, и все они используют эти самые три образа. 2.1.7. Первый результат После конвертирования базы BIOLIFE.DB, которая содержит много текстовых данных в мемо поле и одно поле, которое мы игнорируем (image field), мы получили следующий результат (обратите внимание на заголовок, который показывает запись 1 из 28):
begin
writeln(f,'<IMG SRC="../images/navigatl.gif" '+
'ALIGN=RIGHT USEMAP="#map" BORDER="0">');
writeln(f,'<MAP NAME="map">');
writeln(f,'<AREA SHAPE="rect" COORDS="51,0,75,25" HREF="'+
PageNr(2)+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="76,0,100,25" HREF="'+
LastPage+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="101,0,125,25"HREF="'+
PageNr(RecNr)+'">');
end
else
if (RecNr = RecordCount) then { last record }
begin
writeln(f,'<IMG SRC="../images/navigatr.gif" '+
'ALIGN=RIGHT USEMAP="#map" BORDER="0">');
writeln(f,'<MAP NAME="map">');
writeln(f,'<AREA SHAPE="rect" COORDS="0,0,25,25" HREF="'+
FirstPage+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="26,0,50,25" HREF="'+
PageNr(RecNr-1)+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="101,0,125,25"HREF="'+
PageNr(RecNr)+'">');
end
else { middle record }
begin
writeln(f,'<IMG SRC="../images/navigat.gif" '+
'ALIGN=RIGHT USEMAP="#map" BORDER="0">');
writeln(f,'<MAP NAME="map">');
writeln(f,'<AREA SHAPE="rect" COORDS="0,0,25,25" HREF="'+
FirstPage+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="26,0,50,25" HREF="'+
PageNr(RecNr-1)+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="51,0,75,25" HREF="'+
PageNr(RecNr+1)+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="76,0,100,25" HREF="'+
LastPage+'">');
writeln(f,'<AREA SHAPE="rect" COORDS="101,0,125,25"HREF="'+
PageNr(RecNr)+'">');
end;
writeln(f,'</MAP>');
2.1.8. Расширенный HTML Конечно, не всегда таблица содержит только текстовые поля. Иногда данные из таблице удобнее представлять в виде таблицы (grid или таблице подобной структуре). Для этого я должен ввести вас в расширенные HTML свойства: фреймы и таблицы. 2.1.8.1. Фреймы Фреймы это в действительности расширение HTML+, которое не поддерживается некоторыми web браузерами. Фреймы это свойство разделения вашей web страницы на две или более страниц. Основное свойство фреймом то, что каждый фрейм может иметь свое собственное имя и может переходить в другое местонахождение. Так, вы можете иметь индекс или таблицу оглавления с левой стороны, и например действительное содержимое с правой стороны. Для таблицы со многими записями вы можете иметь список ключей слева (главный индекс) и одну индивидуальную запись справа. Ключевое значение слева конечно ссылка на актуальную страницу с данными в правом фрейме. Как только мы щелкнем по ссылке в главном индексе (левый фрейм) в правом фрейме появятся данные относящиеся к этому ключу. Дополнительно к двум фреймам мы должны иметь главную специальную страницу, в которой определяем количество и относительные позиции (и размер) этих фреймов. Я использую для левого фрейма имя "Menu" и размер 32% от текущей ширины экрана, для правого фрейма имя "Main" и остаток ширины экрана. В HTML коде это выглядит следующим образом: <HTML> <FRAMESET COLS="32%,*"> <FRAME SRC="pag00000.htm" NAME="Menu"> <FRAME SRC="pag00001.htm" NAME="Main"> </FRAMESET> </HTML> Конечно, вы можете иметь более значимые имена для фреймов (например имена таблиц), но Я оставлю это на совесть читателя. 2.1.8.2. Таблицы Использование фреймов для показа содержимого индекса и одной записи это одна из возможностей. Но имеется возможность отображать это и как таблицу. HTML 3.0 поддерживает ТАБЛИЦЫ, которое является одним из наиболее используемых свойств наших дней. Таблицы с рамками и без могут использоваться для всего, что вы не можете сделать нормальным путем (например, нет метода иметь множественные колонки в HTML странице, без использования таблиц). В нашем случае это может быть двух колоночная таблица с рамкой. В левой колонке мы просто отображаем название каждого поля, а правой колонке - значение этого поля. Подобно предыдущему текстовому решению, единственная вещь которую нужно изменить это коды заголовков в коды таблицы. <TR> начинает новую строку таблицы, заканчивая ее тегом </TR>. Тег <TD> открывает новое поле , закачивающее тегом </TD>. Для окончательно преобразования, мы должны написать специальную индексную HTML страницу как файл (файл g в нашем случае). Преобразованный листинг выглядит следующим образом:
if (keys >= 0) then2.1.9. Последний вариант конвертора Имея объединенные фреймы и таблицы ы нашем конверторе, мы можем переходить от простой BIOLIFE.DB таблицы к более реалистичной таблицы продуктов, например PARTS.DB. Данная таблица имеет больше цифровых и меньше "memo" (или тестовых) данных, и поэтому выглядит лучше когда данные отображаются в табличном виде с простыми заголовками.
begin
writeln(g,'<TR>');
write(g,'<TD><A HREF="../',FileName,'/',PageNr(RecNr), '"TARGET="Main">');
writeln(g,RecNr:3,'</A></TD>')
end;
{ print fields }
writeln(f,'<TABLE BORDER>');
for i:=0 to Pred(FieldCount) do if FieldTypes[i] > sf_UnKnown then
begin
writeln(f,'<TR>');
write(f,'<TD><B>',FieldDefs[i].Name,'</B></TD><TD>');
if FieldTypes[i] = sf_Memo then
writeMemo(f,Fields[i])
else writeln(f,Fields[i].AsString);
writeln(f,'</TD></TR>');
if (keys >= i) then
writeln(g,'<TD>',Fields[i].AsString,'</TD>')
end;
if (keys >= 0) then writeln(g,'</TR>');
writeln(f,'</TABLE>');
"Живые" HTML кнопки работают также как и ранее, и мы можем выбирать любую запись из фрейма с индексом. Заметим, что содержимое правого фрейма также содержит текущую позицию (и общее количество записей) в таблице, так как это тоже генерируется на лету. В данный момент мы уже имеем два пути для преобразования таблицы в HTML страницу, или с помощью простого текстового конвертора или с помощью более сложного конвертора фрейм /таблица, Я написал маленькую программу, которая использует оба метода. Это простое консольное приложение, которое нуждается только в имени таблицы как аргумент командной строки (таблица должна находиться в текущем каталоге). По умолчанию используется нормальный метод преобразования, тем не менее, если ввести более одного параметра, то будет использоваться метод преобразования во фреймы с таблицами (сам дополнительный параметр игнорируется).
program BDE2HTML;2.1.10. Линейка прогресса Конвертирование маленьких таблиц в небольшое количество страниц не занимает много времени, не более нескольких секунд. Но конвертирование больших таблиц в сотни или тысячи страниц может занять несколько минут. По этой причине я сделал небольшой прогресс индикатор к конвертору. Простая форма с компонентом TGauge. Мы устанавливаем MinValue и Value в 0, а MaxValue в количество записей в таблице, и после генерации страницы мы увеличиваем значение Value на единицу. Небольшие часики в левом верхнем углу показываю количество пройденного времени:
{$IFDEF WIN32}
{$APPTYPE CONSOLE}
uses
{$ELSE}
uses WinCrt,
{$ENDIF}
Convert, HTables;
begin
case ParamCount of
0: writeln('Usage: BDE2HTML tablename');
1: Convert.DatabaseHTML('',ParamStr(1));
else HTables.DatabaseHTML('',ParamStr(1))
end
end.
2.1.11. Производительность Единственная разница между реальным приложением обработки баз данных (например с использованием BDE) и браузером базы данных это производительность. Наше "приложение" не нуждается ни в каких других приложениях, кроме стандартного браузера. Посылка данных по сети и взаимодействие эмулируется с помощью щелчков по картинке-навигатору и перехода по гипер-ссылке. Ни BDE или ISAPI/NSAPI программы не могут выполнять подобную архитектуру. Конечно, мы имеем только статические страницы, поэтому здесь нет возможности делать динамические запросы или преобразование базы данных. Поэтому нам нет нужды разрабатывать другие вещи, как CGI скрипты. Но наши сгенерированные страницы могут "имитировать" Парадокс базу, даже не Unix Web сервере! И особенно для баз, в которых изменения очень редки, например раз в неделю, это превосходная схема быстрой и простой организации web сайта. 2.1.12. Заключение В данной главе мы увидели быстрый и простой путь преобразование Дельфи таблиц в платформа независимые HTML страницы; или текстовые с заголовками или в виде фрейм/таблица. Мы изучили, как использовать HTML технологию, включая карты-картинки, для имитации "живых" клавиш и выполнения действия. Данная технология может быть применима, как в Интернет, так и в Интранет приложениях (как минимум для меня). И в результате хорошая производительность по сравнению с другими решениями (ограничением может быть нехватка места на сервере, если вы имеете действительно большое количество HTML страниц). Что еще осталось (как задание для читателя) это поддержка показа картинок из базы данных и запросы (например, для генерации master-detail HTML страниц). 2.2. CGI/WinCGI приложения Допустим, вы имеете базу данных с продуктами. Бумажная реклама очень накладна. Но реклама в web это что-то новое и за приемлемую цену. Хорошо бы было иметь вашу базу опубликованной в Интернете, не так ли? Но организация своего собственного сайта на NT Web Server, работающего с инструментом типа IntraBuilder или WebHub стоит больших денег, включая время ни эксперименты и настройку. В данной главе мы покажем быстрый путь и простой путь публикации вашей базы данных на web: просто генерируя статические HTML страницы, базируясь на записях в таблице. Накладно? Нет. Сложно? Я так не думаю. Позвольте указать простой путь на небольшой базе данных. 2.2.1.Дельфи и CGI В то время как HTML это стандарт для гипертекстовых документов, CGI означает Common Gateway Interface, и реализует связевой интерфейс между клиентом (Web браузер) и сервером (Web сервером). Имеется, по крайней мере, две различных формы CGI, стандартный CGI и высокоуровневый, называемый WinCGI (Windows (NT) CGI). Первый использует переменные среды и стандартные потоки ввода/вывода, второй использует файл формата Windows INI (в которых указываются имена входного и выходного файлов) для связи между клиентом и сервером, на котором запущено CGI приложение. Дельфи 2 CGI приложения являются не визуальными приложениями, то есть консольными приложениями, где на входе информация (запрос) от клиента, а на выходе динамический HTML документ, сгенерированный на лету и отправляемый обратно на клиенту в Web браузер. Информация, введенная на клиенте посылается серверу и используется для генерации HTML страницы, может быть послана двумя путями: или с помощью переменных среды (стандартный CGI) или с помощью Windows INI файлов (WinCGI). В данной главе мы сфокусируем свое внимание только на стандартных CGI приложениях! 2.2.2. CGI Формы Для начала мы должны определить, что хочет клиент, как выглядит клиентская сторона. Как можно послать серверу информацию для выполнения? Для этого мы должны использовать специальное расширение HTML, называемое FORMS. Подобно Дельфи формам, форма это место на котором располагаются органы управления, такие как edit box, listbox, combobox, button или multi-line text field. В отличии от Дельфи мы имеем не визуальную среду разработки формы с использованием HTML кодов. Для примера приведу часть файла DELBOOKS.HTM. Полный файл можно взять на http://members.aol.com/drbobnl/delbooks.htm. <FORM ACTION="http://www.drbob42.com/cgi-bin/delbooks.exe" METHOD="POST"> <UL> <INPUT TYPE="radio" NAME="DELPHI" VALUE="0" CHECKED>Delphi 1.0x or 2.0x<BR> <INPUT TYPE="radio" NAME="DELPHI" VALUE="1">Delphi 1.0x only<BR> <INPUT TYPE="radio" NAME="DELPHI" VALUE="2">Delphi 2.0x only <P> <LI>Level: <BR><SELECT NAME="Level"> <OPTION VALUE=""> don't care <OPTION VALUE="1"> Beginning <OPTION VALUE="2"> Intermediate <OPTION VALUE="3"> Advanced </SELECT> <P> </UL> <HR> <P> <INPUT TYPE="RESET" VALUE="Reset Query"> <INPUT TYPE="SUBMIT" VALUE="Get Results"> </FORM> Данный код показывает на форме два типа органов управления: три радио кнопки (выбор между "Delphi 1.0x or 2.0x", "Delphi 1.0x only" и "Delphi 2.0x only"), и combobox с четырьмя значениями ("don't care", "Beginning", "Intermediate" и "Advanced"). Так же имеется две обычные кнопки, одна типа "RESET", для сброса введенной информации и одна типа "SUBMIT", для отправки введенной информации. Для выполнения запроса из Web браузера на Web сервер необходимо нажать кнопку типа SUBMIT (в нашем случае кнопку с текстом "Get Results"). Но как сервер узнает, какое CGI приложение запускать для обработки запроса? Для этого мы должны обратить внимание на параметр ACTION в теге FORM (первая строка кода). Параметр ACTION указывает точное местонахождение CGI приложения, в нашем случае это http://www.drbob42.com/cgi-bin/delbooks.exe (но ребята не пытайтесь запускать это у себя дома, так как это ссылка внутри моей Интрасети, а не Интернета). В действительности "официальная" DELBOOKS.HTM содержит гораздо больше органов управления. Она также доступна на http://members.aol.com/drbobnl/delbooks.htm. Нажатие на клавишу "Get Result" посылает информацию на Web сервер, котрый запускает delbooks.exe приложение с информацией введенной на форме. В нашем случае это может быть DELPHI="2", LEVEL="3", TITLE="", AUTHOR="Bob_Swart", PUBLISHER="" и ISBN="" (символ подчеркивания здесь означает пробел). Delphi 2 CGI приложение delbooks.exe обрабатывает полученную информацию, выполняет запрос и генерирует динамическую HTML страницу, которую отправляет на стандартный вывод. Web затем отправляет ее клиенту в его Webбраузеру который отображает ее на экране. 2.2.3. Переменные среды Стандартное CGI приложение должно анализировать переменные среды для определения метода передачи и размера посылаемой информации через стандартный ввод. Для получения списка переменных среды я всегда использую простой компонент, который я написал очень давно и компилирую его с помощью директив условной компиляции, как в Дельфи 1, так и в Дельфи 2.
unit TBDosEnv;Данный компонент получает список переменных среды во время своего создания. Свойство DosEnvCount и DosEnvList является свойством только для чтения и поэтому лучше его создавать его в на ходу, а не бросать на форму, так как берется только 'свежий' список переменных среды, а не загружается из .DFM файла). 2.2.4. Анализ Среди переменных среды есть переменная с именем REQUEST_METHOD. Она должна иметь значение POST для нашего примера (Я не люблю другие методы). Затем мы должны найти размер информации, которая передана нам. Для этого мы должны получить переменную CONTENT_LENGTH. Сама информация поступает к нам через стандартный ввод (без маркера конца файла, поэтому наша задача не пытаться читать больше, чем нам передано). Данные поступающие через стандартный ввод имеют следующую форму FIELD=VALUE и разделяется с помощью символа '&'. Например: AUTHOR="Bob_Swart"&. Поскольку мы имеем весь входной поток, как одну длинную строку, то мы можем быстро найти параметр AUTHOR с помощью следующей функции:
interface
uses
SysUtils, WinTypes, WinProcs, Classes;
type
TBDosEnvironment = class(TComponent)
public
{ Public class declarations (override) }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
private
{ Private field declarations }
FDosEnvList: TStringList;
protected
{ Protected method declarations }
function GetDosEnvCount: Word;
public
{ Public interface declarations }
function GetDosEnvStr(Const Name: String): String;
{ This function is a modified version of the GetEnvVar function that
appears in the WinDos unit that comes with Delphi. This function's
interface uses Pascal strings instead of null-terminated strings.
}
property DosEnvCount: Word read GetDosEnvCount;
property DosEnvList: TStringList read FDosEnvList;
end;
implementation
constructor TBDosEnvironment.Create(AOwner: TComponent);
var P: PChar;
i: Integer;
begin
inherited Create(AOwner);
FDosEnvList := TStringList.Create;
{$IFDEF WIN32}
P := GetEnvironmentStrings;
{$ELSE}
P := GetDosEnvironment; { Win API }
{$ENDIF}
i := 0;
while P^ <> #0 do
begin
Inc(i);
FDosEnvList.Add(StrPas(P));
Inc(P, StrLen(P)+1) { Fast Jump to Next Var }
end;
end {Create};
destructor TBDosEnvironment.Destroy;
begin
FDosEnvList.Free;
FDosEnvList := nil;
inherited Destroy
end {Destroy};
function TBDosEnvironment.GetDosEnvCount: Word;
begin
Result := 0;
if Assigned(FDosEnvList) then Result := FDosEnvList.Count
end {GetDosEnvCount};
function TBDosEnvironment.GetDosEnvStr(Const Name: String): String;
var i: Integer;
Tmp: String;
begin
i := 0;
Result := '';
if Assigned(FDosEnvList) then while i <FDosEnvList.Count >do
begin
Tmp := FDosEnvList[i];
Inc(i);
if Pos(Name,Tmp) = 1 then
begin
Delete(Tmp,1,Length(Name));
if Tmp[1] = '=' then
begin
Delete(Tmp,1,1);
Result := Tmp;
i := FDosEnvList.Count { end while-loop }
end
end
end
end {GetDosEnvStr};
end.
varСледующий шаблон кода показывает как динамически создать переменную TBDosEnvironment, прочитать информацию со стандартного ввода и получить строку готовую для анализа переменных формы.
Data: String;
function Value(Const Field: ShortString): ShortString;
var i: Integer;
begin
Result := '';
i := Pos(Field+'=',Data);
if i = 0 then
begin
Inc(i,Length(Field)+1);
while Data[i] <> '&' do
begin
Result := Result + Data[i];
Inc(i)
end
end
end {Value};
{$APPTYPE CONSOLE}Заметим, что первые три "writeln" строки, посылаемые на стандартный вывод, необходимы для браузера, что бы сообщить ему, что содержимое страницы имеет тип TEXT/HTML. 2.2.5. Базы данных При написании CGI приложений, вам необходим, какой то путь для доступа к данным базы. Одним из простых решений будет использование BDE и помещение ваших данных в таблицы Парадокса или dBASE. Если по какой либо причине BDE не инсталлировано на вашем NT Web сервере (может быть ваш дружественный Internet Provider не предоставляет вам BDE), вы можете использовать технику старых дней, используйте вместо базы данных файл записей.. Все что вам нужно, определить тип TRecord и написать программу, которая конвертирует вашу базу данных в file of TRecord. 2.2.6. Преобразование Если вы посмотрите на список полей Парадокса, то вам не составит труда понять, что не все поля можно просто конвертировать в текстовый формат, например типа Memo обычно не помещаются в короткие строки (Short String). А как начет Blob? Для данного типа полей я составил небольшую таблицу конвертирования.
var
Data: String;
ContentLength,i,j: Integer;
begin
writeln('HTTP/1.0 200 OK');
writeln('SERVER: Dr.Bob''s Intranet WebServer 1.0');
writeln('CONTENT-TYPE: TEXT/HTML');
writeln;
writeln('<HTML>');
writeln('<BODY>');
writeln('<I>Generated by Dr.Bobs CGI-Expert on </I>',DateTimeToStr(Now));
with TBDosEnvironment.Create(nil) do
begin
for i := 0 to Pred(DosEnvCount) do
begin
if Pos('REQUEST_METHOD',DosEnvList[i]) 0 then
begin
Data := DosEnvList[i];
Delete(Data,1,Pos('=',Data))
end
end;
if Data = 'POST' then
begin
ContentLength := StrToInt(GetDosEnvStr('CONTENT_LENGTH'));
SetLength(Data,ContentLength+1);
j := 0;
for i:=1 to ContentLength do
begin
Inc(j);
read(Data[j]);
end;
Data[j+1] := '&';
{ now call Value or ValueAsInteger to obtain individual values }
end;
Paradox field type | ObjectPascal conversion type |
TStringField (size) | String[length] |
TIntegerField, TWordField, TSmallIntField | Integer |
Currency | Double |
Memo, Blob | n/a (ignored) |
{$APPTYPE CONSOLE}Конечно, таблица трансляции и программа определения записи должны быть расширены, что включить и другие типы полей Парадокса, но для примера и этого достаточно. 2.2.7. Записи После осознания, что мы можем писать на Delphi 2 CGI приложения без использования BDE, мы решили сгенерировать тип записи для нашей таблицы delbooks.db и конвертировать ее записи в файл записей. Использую программ RECORD.EXE из предыдущей главы мы получили следующее определение записи.
uses DB, DBTables;
var i: Integer;
begin
if ParamCount = 1 then with TTable.Create(nil) do
try
TableName := ParamStr(1);
Active := True;
writeln('Type');
writeln(' TRecord = record');
for i:=0 to Pred(FieldDefs.Count) do
begin
if (FieldDefs[i].FieldClass = TStringField) then
writeln(' ':4,FieldDefs[i].Name,': String[',FieldDefs[i].Size,'];')
else
begin
if (FieldDefs[i].FieldClass = TIntegerField) or
(FieldDefs[i].FieldClass = TWordField) or
(FieldDefs[i].FieldClass = TSmallintField) then
writeln(' ':4,FieldDefs[i].Name,': Integer;')
else
if (FieldDefs[i].FieldClass = TCurrencyField) then
writeln(' ':4,FieldDefs[i].Name,': Double;')
else
writeln('{ ':6,FieldDefs[i].Name,' }')
end
end
finally
writeln(' end;');
Free
end
else
writeln('Usage: record tablename')
end.
TypeТеперь нам осталось написать сам конвертор, который в цикле просматривает записи таблицы, помещает их в запись и записывает в файл.
TRecord = record
ISBN: String[16];
Title: String[64];
Author: String[64];
Publisher: String[32];
Price: Double;
Code: String[7];
{ Comments }
Level: Integer;
TechnicalContentsQuality: Integer;
QualityOfWriting: Integer;
ValueForMoney: Integer;
OverallAssessment: Integer;
{ Cover }
end;
{$APPTYPE CONSOLE}Данная программа может использоваться для полного преобразования таблицы delbooks.db в файл delbooks.rec с типом записи TRecord. Delphi 2 CGI приложение может просто открыть этот файл и читать любую запись без использования BDE. Конечно, преобразование записей не просто сделать, но для этого мы имеем всегда оригинальную базу и можем запускать периодически программу преобразования. Так как я добавляю всего несколько записей примерно раз в два месяца, то меня это не очень волнует. 2.2.8. Производительность Единственное различие между обычным CGI приложением, которое использует BDE для получения данных и нашим приложением без использования BDE это производительность. Кроме того, наше CGI всего лишь 70 KB, оно не нуждается в загрузке BDE, так что время загрузки еще меньше (в результате еще более высокая производительность). В действительности реальные CGI приложения, использующие BDE, часто используют ISAPI (Information Server API) или NSAPI (Netscape Server API) расширения для сохранения CGI приложения "все-время-в-полете (in the air)". Еще больше можно повысить производительность, если вместо файла записей использовать массив записей с предварительно инициализированными значениями! Вместо создания файла с записями, Я генерирую Паскаль код для этой цели. Таким образом, я могу генерировать исходный Паскаль код сразу с нужной информацией. Не нужды в файле записей. И сразу после компиляции я имею одиночное приложение на Дельфи 2, размером всего 77824 байта, которое содержит информацию об 44 книгах внутри самого себя. Книги внутри, разбор переменных среды, чтение стандартного ввода, генерация HTML страницы и отправка ее на стандартный вывод с динамическим формированием содержимого в зависимости от запроса на форме. Уверен, что единственный способ получить еще более быстрое приложение, это вернуться обратно к статическим страницам без запросов. 2.2.9. Подсчет обращений Код для подсчета обращений весьма прост. Для поля на форме, которое было выбрано, мы проходим через все записи и добавляем единичку в соответствующую запись при совпадении информации.
uses DB, DBTables, SysUtils;
var i: Integer;
Rec: TRecord;
F: File of TRecord;
begin
if ParamCount = 1 then with TTable.Create(nil) do
try
System.Assign(f,ChangeFileExt(ParamStr(1),'.REC'));
Rewrite(f);
TableName := ParamStr(1);
Active := True;
First;
while not Eof do with Rec do
begin
ISBN := FieldByName('ISBN').AsString;
Title := FieldByName('Title').AsString;
Author := FieldByName('Author').AsString;
Publisher := FieldByName('Publisher').AsString;
Price := FieldByName('Price').AsFloat;
Code := FieldByName('Code').AsString;
Level := FieldByName('Level').AsInteger;
TechnicalContentsQuality :=
FieldByName('TechnicalContentsQuality').AsInteger;
QualityOfWriting := FieldByName('QualityOfWriting').AsInteger;
ValueForMoney := FieldByName('ValueForMoney').AsInteger;
OverallAssessment := FieldByName('OverallAssessment').AsInteger;
write(f,Rec);
Next
end
finally
System.Close(f);
Free
end
else
writeln('Usage: convert tablename')
end.
if DataRec.Author <> '' thenЗаметим, что конструкция {$IFDEF DEBUG} может быть использована для вывода значения входного поля в стандартный вывод, так что мы можем использовать наше CGI приложение для отладки формы. Отладка вашего CGI приложения может оказать трудной задачей, поскольку вам нужен Web сервер и браузер для этого... 2.2.10. Результаты запроса Теперь посмотрим на последнюю часть CGI приложения: часть, в которой генерируется HTML код. Здесь я использую другое свойство расширенного HTML, именованные таблицы, что бы вывод выглядел красивее. Для каждой записи, у которой счетчик более единицы, я выводу счетчик, название, автора, издательство, ISBN, уровень, техническое содержание, качество книги, стоимость и общее значение. Я также включаю ссылку из названия на другое место, где находится более подробное описание. С помощью этого великолепного свойства динамических HTML страниц: вы даже можете включать ссылки на статические страницы, так как результат запроса, часто стартовая точка для прыжка в другое место!
begin
{$IFDEF DEBUG}
writeln('Author: ',DataRec.Author,'<BR>');
{$ENDIF}
for i:=1 to Books16 do
if Pos(DataRec.Author,Book16[i].Author) <> 0 then
Inc(Result16[i]);
for i:=1 to Books32 do
if Pos(DataRec.Author,Book32[i].Author) <> 0 then
Inc(Result32[i])
end;
writeln('<HR>');После того как заголовок написан, самое время выводить сами записи. Я не хочу сортировать их по рейтингу от 5 до 1, так что я просто иду по списку книг и печатаю каждую со своим рейтингом. Этот путь, потому что я знаю, что книги уже отсортированы по рейтингу в основной базе delbooks.db (которая отсортирована по уровню и рейтингу). Обычно книги в верху списка уже лучший ответ на заданный вопрос.
writeln('<P>');
writeln('<H3>The following books have been found for you:</H3>');
writeln('<TABLE BORDER>');
writeln('<TR>');
writeln('<TH><B>Hits</B></TH>');
writeln('<TH><B>Title</B></TH>');
writeln('<TH><B>Author</B></TH>');
writeln('<TH><B>Publisher</B></TH>');
writeln('<TH><B>ISBN</B></TH>');
writeln('<TH><B>Level</B></TH>');
writeln('<TH>Con</TH>');
writeln('<TH>Wri</TH>');
writeln('<TH>Val</TH>');
writeln('<TH><B>Tot</B></TH>');
writeln('</TR>');
if DataRec.Delphi2 then2.2.11. Отладка CGI Страницу HTML с результатом, сгенерированную по запросу мы модем увидеть выполнив CGI приложение. Для этого требуется (персональный) Web сервер. По этому я написал небольшую программу отладки, используя Delphi 2.01 и NetManage HTML control:
begin
for Hits := 5 downto 1 do
begin
for i:=1 to Books32 do if Result32[i] = Hits then
begin
writeln('<TR>');
writeln('<TD>',Roman[Hits],'</TD>');
writeln('<TD><A HREF="',root32,Book32[i].HREF,'">',Book32[i].Title,'</A></TD>');
writeln('<TD>',Book32[i].Author,'</TD>');
writeln('<TD>',Book32[i].Publisher,'</TD>');
writeln('<TD>',Book32[i].ISBN,'</TD>');
writeln('<TD>',Level[Book32[i].Level],'</TD>');
writeln('<TD>',Book32[i].TechnicalContentsQuality,'</TD>');
writeln('<TD>',Book32[i].QualityOfWriting,'</TD>');
writeln('<TD>',Book32[i].ValueForMoney,'</TD>');
writeln('<TD><B>',Book32[i].OverallAssessment,'</B></TD>');
writeln('</TR>')
end
end;
if DataRec.Delphi1 then writeln('<TR></TR>')
end;
if DataRec.Delphi1 then
begin
for Hits := 5 downto 1 do
begin
for i:=1 to Books16 do if Result16[i] = Hits then
begin
writeln('<TR>');
writeln('<TD>',Roman[Hits],'</TD>');
writeln('<TD><A HREF="',root16,Book16[i].HREF,'">',Book16[i].Title,'</A></TD>');
writeln('<TD>',Book16[i].Author,'</TD>');
writeln('<TD>',Book16[i].Publisher,'</TD>');
writeln('<TD>',Book16[i].ISBN,'</TD>');
writeln('<TD>',Level[Book16[i].Level],'</TD>');
writeln('<TD>',Book16[i].TechnicalContentsQuality,'</TD>');
writeln('<TD>',Book16[i].QualityOfWriting,'</TD>');
writeln('<TD>',Book16[i].ValueForMoney,'</TD>');
writeln('<TD><B>',Book16[i].OverallAssessment,'</B></TD>');
writeln('</TR>')
end
end
end;
writeln('</TABLE>');
writeln('<HR>');
writeln('<A HREF="http://www.drbob42.com">Dr.Bobs Delphi Clinic</A>');
writeln('</BODY>');
writeln('</HTML>');
writeln;
Free
end
2.2.12. Заключение Я надеюсь, что я показал, как мы можем писать интерактивные Интернет (Интранет) CGI приложения с помощью Delphi 2 используя CGI, WinCGI и Delphi 3 ISAPI/NSAPI Web Modules. Лично я планирую делать многое с помощью Дельфи для Интернет и Интранет.
Интернет решения от доктора Боба (http://www.drbob42.com)
(c) 2000, Анатолий Подгорецкий, перевод на русский язык (https://nps.vnet.ee/ftp)