Поиск по блогу

четверг, 10 июня 2010 г.

Delphi: пример поиска в структуре веб-страницы значения нужного элемента

Привет! Этот пост — ответ на вопрос читателя блога. Конкретный ответ на конкретный вопрос. Именно им я хочу положить начало акции "Разберу на вашем примере". Подробнее об акции и ее условиях читайте в конце этой статьи.


Цитата из письма:

Мне нужно с сайта http://stock.rbc.ru/demo/micex.0/intraday/eod.rus.shtml скачать хоть одну ячейку например: цену открытия акций Газпрома, и сохранить это значение в какую-нибудь БД или таблицу (например в таблицу PARADOX7) или в EXCEL (лучше PARADOX7). И все, мне больше ничего не требуется, Ваша программа будет служить мне эталонным примером, а дальше я сам разберусь.
...
Я пытаюсь работать в среде С++Builder6 и пробовал использовать компонент XMLDocument, но кроме того как вытащить его на форму ничего не могу с ним сделать. Буду рад если вы напишите мне на С++Builder, но насколько я знаю, что С++Builder и DELPHI имеют множество сходств то и программе написанной на DELPHI буду благодарен...


Случай сложен тем, что, как мне кажется, если человек не программист, то ему исходный код на другом языке вряд ли поможет. А пример "сохранения в таблицу" не научит работе с базами данных. На скорую руку набросала поиск нужного элемента на странице, постаралась прокомментировать по-максимуму, так что дополнительно сказать нечего.

Итак, код для поиска элемента:

uses
...
UrlMon, MSHTML, activex;

function DownloadFile(SourceFile, DestFile: string): Boolean;
begin
try
Result := UrlDownloadToFile(nil, PChar(SourceFile), PChar(DestFile), 0, nil) = 0;
except
Result := False;
end;
end;

procedure TMainF.ButtonClick(Sender: TObject);
var
StrURL,
sFileName : string;
SLBody : TStringList;
Doc : IHTMLDocument2;
v : OleVariant;
DocAll,
DocTR,
DocTD : IHTMLElementCollection;
TRElement,
TDElement : IHtmlElement;
i : integer;
begin
// сначала загружаем страницу. это можно сделать разными способами
// я выбрала загрузку страницы в файл, так как у нас могут быть
// разные версии компонентов Indy, они могут конфликтовать

StrURL := 'http://stock.rbc.ru/demo/micex.0/intraday/eod.rus.shtml';

SLBody:=TStringList.Create;
sFileName := ExtractFilePath(Application.ExeName) + 'cache.txt';
if DownloadFile(StrURL,sFileName) then
SLBody.LoadFromFile(sFileName);
DeleteFile(sFileName);

// загружаем все в "WebBrowser". Вернее, создаем объект IHTMLDocument2
// Объясню, зачем. Дело в том, что структура конкретной
// интернет-страницы достаточно сложная. Поэтому компонент XMLDocument
// для ее анализа будет не совсем удобным. Как неудобными будут и
// регулярные выражения (ведь наверняка нужно будет работать не только
// с газпромом, а, допустим, пройтись по всей таблице)
Doc := coHTMLDocument.Create as IHTMLDocument2;
if Doc = nil then
begin
ShowMessage('Ошибка создания IHTMLDocument2');
exit;
end;
v := VarArrayCreate([0,0],VarVariant);
v[0] := SLBody.Text;
Doc.write(PSafeArray(TVarData(v).VArray));

// ищем в DOM-структуре элемент, значение которого нам нужно.
// для этого сначала анализируем страницу, чтобы определить признаки,
// которые нам помогут это сделать. Вот кусок кода:
// <TR>
// <TD><A TARGET="_top" HREF="../daily/GAZP.rus.shtml?show=all" class="n"><img src="http://pics.rbc.ru/img/up_grf.gif" width="19" height="15" border="0"> %u0413%u0430%u0437%u043F%u0440%u043E%u043C</A></TD>
// <TD class=G>158.2</TD>
// то есть надо найти элемент TR, в первом TD которого написано "Газпром",
// и взять значение из второго TD

DocAll := Doc.all;
DocTR := DocAll.Tags('TR') as IHTMLElementCollection;
for i := 0 to DocTR.length-1 do
begin
TRElement := DocTR.Item(i, 0) as IHtmlElement;
// находим все дочерние элементы (TD-элементы)
DocTD := TRElement.children as IHTMLElementCollection;

// прежде чем обращаться к первому и второму TD-элементу в коллекции,
// неплохо было бы проверить, есть ли там действительно 2 элемента (или больше)

if (DocTD.length > 1) then
begin
TDElement := DocTD.Item(0, 0) as IHtmlElement;
// если в первом столбике написано Газпром, то это то, что мы ищем
if ( AnsiCompareStr( trim(TDElement.innerText), 'Газпром') = 0 ) then
begin
ShowMessage ( (DocTD.Item(1, 0) as IHtmlElement).innerText );
exit;
end;
end;
end;

SLBody.Free;
end;


Результат нажатия на кнопку :) представлен на скрине:
Парсинг с использованием IHTMLDocument2
Вот и все. Как это будет на CBuilder-е — не знаю.
___

А сейчас об "акции", о которой я вскользь упомянула в начале поста.

Итак, если вы хотите, чтобы я разобрала работу инструмента/метода/и т.д. конкретно на вашем примере, вы можете написать мне письмо. Постараюсь один-два примера в неделю разбирать. Такие письма приходили и раньше, и в комментариях часто проскакивали вопросы, но — и вы должны меня понять, — большинство из них я пропускала мимо ушей из-за нехватки времени. "Что же изменилось? В чем подвох?" — спросите вы.

Да, подвох есть. Вернее, мои условия.

1. Ограничение под названием "не очень наглеть". :) То есть ваша задача не должна отнять у меня более, чем полтора-два часа времени. Я просто физически не смогу пачками штамповать полноценные парсеры на все запросы. Предпочтения отдаются парсингу НЕ ПОИСКОВЫХ СИСТЕМ.
Если все-таки вы заранее не оценили сложность и прислали объемную задачу, то в этом случае я, скорее всего, максимум разберу основные моменты и напишу часть кода.

2. С вас же — "ссылка навсегда" (не в noindex) на пост, который я опубликую в ответ на ваш вопрос. Да, продаюсь за ссылки. Я в очередной раз решила уделять блогу больше времени, его развитию и продвижению. Буду рада, если таким образом вы мне поможете. Не считаю, что это дорого.

Такие дела. Посмотрим, будут ли желающие :)

Чтобы участвовать в акции, достаточно написать мне письмо с вопросом (задачей), в котором указать язык написания (Delphi или PHP) и адрес блога, на котором планируете разместить ссылку.
___

Чтобы быть в курсе обновлений блога, можно подписаться на RSS.

Статьи схожей тематики:



13 комментариев:

  1. Ваш блог так хорош, что я уже поставил ссылки где можно :)

    ОтветитьУдалить
  2. Алексей, большое спасибо! :)

    ОтветитьУдалить
  3. Анонимный19 июня 2010 г., 21:18

    Спасибо большое. для меня открылась новая область в Delphi. На этих данных написал своего ICQ бота который отправляет цитаты взятые с определенного сайта.)))

    ОтветитьУдалить
  4. Masha - умница!!!
    Статья супер!
    100% то что надо...
    уже второй день ломаю голову, а вместе с ней TWebBrowse...
    Идея моей задумки... это создание
    CMS-помощника на Delphi для сайтов
    И выдернуть с хостинговой базы нужное значение, было постоянно головной болью...
    Ваша статья это лучше чем таблетка анальгина...

    ОтветитьУдалить
  5. Даже в потоке работы Indy9 работает код без запинки...

    ОтветитьУдалить
  6. Подскажите пож. ответ на такой вопрос:
    Если я прочитал текст веб.страници с помощью "WebBrowser1.OleObject.Document.Body.InnerText", возможно ли както не присваивать это значение в "MEMO", а сразу пройтись по строкам в поисках нужного значения?

    ОтветитьУдалить
  7. Анонимный, собственно, не совсем поняла, в чем вопрос и как он относится к статье. Конечно, можно не загружать ни в какое MEMO — почитайте листинг кода в статье (там нет никакого мемо) и задавайте более конкретные вопросы.

    ОтветитьУдалить
  8. К статье он относится тем что мне нужно тоже самое, но страница должна быть не скачена DownloadFile, а загружена в WebBrowser, в связи с тем что на странице мне ещё надо кликнуть (программно), а после обновления страницы адрес остаётся тот же самый и внутри под кнопкой (по которой надо кликнуть) адрес идёт не "www.сайт\страница 1", а "\страница 1".
    Пробовал переделать этот код, но видно не хватает знаний.
    А, ещё, очень не хотелось бы сохранять файл на диск, а поискать в переменной, потому и читаю с помощью "WebBrowser1.OleObject.Document.Body.InnerText" (или innerHTML).

    ОтветитьУдалить
  9. Не вижу никаких сложностей. Как-то вы неконкретно ставите вопросы.

    ОтветитьУдалить
  10. Маша,спасибо за материал очень интересно,можно дополнить этот пример и полностью спарсить таблицу в StringGrid что то у меня е выходит.

    ОтветитьУдалить
  11. alek-erokhin, мне пока некогда. Может, позже.

    ОтветитьУдалить
  12. alek-erokhin, ради примера переделал данный парсер для вытаскивания всей инфы из таблиц в один стрингрид.
    Основное отличие что это теперь процедура, и передавать ей надо параметром стринггрид в который выводить. Размер автоматом подгоняется под данные.

    procedure ParseWebPageST(Str: TStringGrid);
    var
    sFileName : string;
    Doc : IHTMLDocument2;
    v : OleVariant;
    DocAll,
    DocTR,
    DocTD : IHTMLElementCollection;
    TRElement,
    TDElement : IHtmlElement;
    i,j : integer;
    begin
    //загружаем все в "WebBrowser"
    Doc := coHTMLDocument.Create as IHTMLDocument2;
    if Doc = nil then
    begin
    ShowMessage('Ошибка создания IHTMLDocument2');
    exit;
    end;
    v := VarArrayCreate([0,0],VarVariant);
    v[0] := SLBody.Text;
    Doc.write(PSafeArray(TVarData(v).VArray));

    DocAll := Doc.all;
    DocTR := DocAll.Tags('TR') as IHTMLElementCollection;
    for i := 0 to DocTR.length-1 do
    begin
    TRElement := DocTR.Item(i, 0) as IHtmlElement;
    // находим все дочерние элементы (TD-элементы)
    DocTD := TRElement.children as IHTMLElementCollection;
    // прежде чем обращаться к первому и второму TD-элементу в коллекции,
    // неплохо было бы проверить, есть ли там действительно 2 элемента (или больше)
    if (DocTD.length > 1) then
    begin
    for j := 0 to DocTD.length - 1 do
    begin
    TDElement := DocTD.Item(j, 0) as IHtmlElement;
    if j>Str.ColCount then
    Str.ColCount:=Str.ColCount+1;
    if i>Str.RowCount then
    Str.RowCount:=Str.RowCount+1;
    Str.Cells[j,i]:=UTF8ToString((DocTD.Item(j, 0) as IHtmlElement).innerText);
    end;
    end;
    end;
    end;

    ОтветитьУдалить

Комментарии модерируются, вопросы не по теме удаляются, троллинг тоже.

К сожалению, у меня нет столько свободного времени, чтобы отвечать на все частные вопросы, так что, может, свой вопрос лучше задать на каком-нибудь форуме?

Поделиться