В прошлой статье про выдачу Яндекса я привел лишь один вариант и один пример парсинга выдачи с целью определения позиции сайта в результатах поиска Яндекс. На самом деле нет ничего сверхъестественного в написании подобных компонентов под свои нужды.
А если учесть то обстоятельство, что мы не используем для парсинга всякого рода ухищрения наподобие прокси, потоков и т.д., а лишь делаем небольшую паузу между запросами документов, то задача становится и вовсе простой. Всё, что по сути от нас требуется — определить шаблон страницы с выдачей, написать регулярку и чуть-чуть исправить первоначальный алгоритм парсинга, чем мы сегодня и займемся.
В качестве своих «подопытных» я выбрал следующие поисковые системы:
- Яндекс
- Bing
- Rambler
- Mail.ru
- Yahoo
- Апорт
В принципе, для наглядного примера этих ПС будет вполне предостаточно. Если возникнет желание дополнить список, то думаю, после прочтения этой статьи, проблем у Вас не возникнет.
Первое с чего я обычно начинаю подобного рода работы — забиваю в поисковую форму запрос и пробую найти шаблон URL для перехода по страницам поисковой выдачи. При этом также следует учесть одно очень важное обстоятельство — чем больше ссылок на одной странице вернет нам поисковая система, тем меньше нам потребуется обращаться к ней, а следовательно и сам парсинг пройдет быстрее. Для этого желательно пользоваться расширенным поиском, которые имеет практически любая поисковая система. Итак, приступим. Про Яндекс уже говорили, поэтому сегодня первым на очереди стоит Google. Сразу же заходим на страницу расширенного поиска и забиваем любой запрос, например «Delphi в Internet»
Количество результатов выбираем по максимумы — 100. Жмем «Поиск в Google» и попадаем на страницу с результатами. Получаем URL следующего вида:
http://www.google.ru/search?as_q=Delphi+%D0%B2+Internet&hl=ru&newwindow=1&num=100&btnG=Поиск+в+Google&as_epq=&as_oq=&as_eq=&lr=&cr=&as_ft=i&as_filetype=&as_qdr=all&as_occt=any&as_dt=i&as_sitesearch=&as_rights=&safe=images
Длинно, непонятно и вообще не эстетично. Пробуем упростить URL без потери первоначального содержания страницы. У меня получился вот такой:
http://www.google.ru/search?as_q=Delphi+в+Internet&num=100
Но это только пол дела — первая страница, а надо определить общий для всех страниц поиска шаблон. Не долго думая, переходим на вторую страницу и опять производим те же манипуляции с URL. В результате Вы должны получить, что-то наподобие:
http://www.google.ru/search?hl=ru&num=100&q=Delphi+в+Internet&start=100
Что нам это дало? А то, что мы теперь можем легко сопоставить два URL и вывести общий шаблон для этих и последующих страниц выдачи. Проводим последнюю проверку:
Если в URL второй страницы поиска поставить в параметр start вместо 100 ноль, то мы должны попасть на первую страницу поиска. Если это произойдет — шаблон найден. Проверяем:
http://www.google.ru/search?hl=ru&num=100&q=Delphi+в+Internet&start=0
И попадаем аккурат на первую страничку с теме же результатами выдачи Google. Шаблон найден и выглядит довольно простенько:
http://www.google.ru/search?hl=ru&num=100&q=[ЗАПРОС]&start=[СТРАНИЦА*100]
Двигаемся дальше — составляем регулярное выражение для парсинга. Здесь, признаться я несколько потерялся. Дело в том, что Google может выдавать для одного домена несколько результатов в которых один — основной, второй — дополнительный и оба видны на странице выдачи. Выглядит это примерно так:
Общее число URL сайтов на странице, включая дополнительные результаты, как и было задано 100. И как тут определить позицию в выдаче: с дополнительными результатами или без? Так как я далеко не web-мастер и не SEO-шник, то решил немного подстраховаться — написал сразу два регулярных выражения:
Для всей выдачи регулярка получилась такая:
*.?
Если же требуется отсеять повторяющиеся домена, то можно написать регулярное выражение так:
-
*.?
Думаю, что смысл поиска шаблона страницы Вам понятен. Регулярные выражения тоже из себя ничего сверхъестественного не представляют, просто в отдельную группу выносится адрес сайта и все. Поэтому, чтобы не расписывать ещё пять раз этот же алгоритм — просто приведу список шаблонов страниц и регулярных выражений для каждого поисковика из списка и приступим уже, как говориться, кодить.
Bing
Шаблон страницы поиска:
http://www.bing.com/search?q=[ЗАПРОС]&filt=all&first=[КОЛИЧЕСТВО_URL_НА_СТРАНИЦЕ+1]&FORM=PERE1
К сожалению не нашел в этом поисковике где регулируется количество выдаваемых запросов на страницу.
Регулярное выражение:
Rambler
Шаблон страницы поиска:
http://nova.rambler.ru/srch?query=[ЗАПРОС]&pagelen=50&page=[СТРАНИЦА]
Выдается 50 результатов на страницу. В отличие от Яндекс и Google у Рамблера нумерация страниц идет не с нуля (как полагается у программистов :)), а с единицы и это обстоятельство надо будет учесть при построении алгоритма.
Регулярное выражение:
- class=»title n_title_.*?href=(.*?)»>
Mail.ru
Шаблон страницы результатов:
http://go.mail.ru/search?q=[ЗАПРОС]&num=40&sf=[НОМЕР_СТРАНИЦЫ*40]
Нумерация страницы начинается с нуля.
Yahoo
Шаблон страницы результатов:
http://ru.search.yahoo.com/search?n=100&ei=UTF-8va=[ЗАПРОС]&xargs=0&pstart=1&b=[НОМЕР_СТРАНИЦЫ*100+1]
Регулярное выражение
<a class="yschttl spt" href="http:.*?http.*?//(.*?)"
Апорт
Шаблон страницы результатов:
http://sm.aport.ru/scripts/template.dll?r=[ЗАПРОС]&p=[НОМЕР_СТРАНИЦЫ]
Нумерация, как и полагается у порядочных программистов, начинается с нуля.
Регулярное выражение:
.*n.*s.*?
Признаться, исходник страницы результатов Апорта просто пестрит непонятными переводами на новую строку, кучей пробелов и прочими вещами, ухудшающими самочувствие при составлении регулярного выражения. Постарался сделать его как можно точнее — вроде бы по десятку различных запросов ошибок не было.
Теперь, имея в руках все необходимые данные для парсинга, можно приступать к программированию на Delphi.
Я решил, что лучше всего не создавать 7 отдельных компонентов для каждого поисковика, а просто доработать немного уже готовый компонент для парсинга выдачи Яндекс. И так как основные сведения о нем уже достаточно подробно описаны, то здесь я расскажу только то, что касается непосредственной его доработки. Итак, во-первых, теперь каждый поисковик может быть задан в свойствах компонента. Для того, чтобы облегчить немного процесс настройки компонента, каждый поисковик в компоненте описан константой следующего вида (для Google):
GoogleStr: array [1..2] of string = ('http://www.google.com/search?q=%s&&num=100&&hl=ru&&start=%d', '
‘)
Чтобы выбрать необходимую для парсинга выдачи поисковую систему, в компоненте определено свойство типа:
type TIncFormula = (tiYandex, tiGoogle, tiBing, tiYahoo, tiMail, tiRambler, tiAport);
которая определяет:
1. Шаблон страницы с выдачей и регулярное выражение
2. Номер в URL, определяющий номер страницы с выдачей в поисковой системе.
Номер в URL, определяющий номер страницы, определяется следующим образом:
function TYaParser.NextInc(var CurrPage: integer): integer; begin case FIncFormula of tiYandex,tiAport: result:=CurrPage; tiRambler: result:=CurrPage+1; tiGoogle: result:=CurrPage*100; tiBing: result:=CurrPage*10+1; tiYahoo: result:=CurrPage*100+1; tiMail: result:=CurrPage*40; end; end;
где CurrPage — переменная в цикле, определяющее номер сачиваемой страницы поисковика. Чтобы стало предельно ясно как это работает, приведу ещё раз функцию для парсинга выдачи и определения положения сайта, но уже с внесенными изменениями:
function TYaParser.FindPos(Key: string): integer; // поиск позиции домена var Page, i, LinkNum: integer; TextList: TStringList; isFind: boolean; RE: IRegExp2; mc: MatchCollection; mm: Match; sm: SubMatches; FindedLink: string; begin Page := 0;//принимаем, что первая скачиваемая страница имеет порядковый номер 0 LinkNum := 0; while (isFind = false) and (LinkNum < FCountPos) do begin if FActive then begin {скачиваем страницу, сразу определяя все необходимые данные для шаблона} if DownloadPage(StringBuilder(FNextPage, Key, NextInc(Page)), TextList) then begin TextList.Text := Utf8ToAnsi(TextList.Text); //перекодируем в ANSI // начинаем парсить страницу try try RE := coRegExp.Create as IRegExp2; RE.Pattern := FRegular; RE.Global := true; RE.IgnoreCase := true; RE.Multiline := true; mc := RE.Execute(TextList.Text) as MatchCollection; for i := 0 to mc.Count - 1 do // проходим по порядку все совпадения begin mm := mc[i] as Match; sm := mm.SubMatches as SubMatches; FindedLink := sm.Item[0];// проверка, является ли ссылка искомой (входит ли она подстрокой в найденную) inc(LinkNum); if Pos(FDomain, FindedLink) > 0 then begin isFind := true; Result := LinkNum; break; end; end; except if Assigned(FOnError) then OnError('Ошибка парсинга полученных данных'); end; if mc.Count = 0 then LinkNum := FCountPos; // если совпадения не были найдены finally RE := nil; FreeAndNil(TextList); end; inc(Page); Sleep(FTimeShift); // ждем заданное время перед повторным скачиванием end else begin Result := -1; // если была ошибка при скачивании страницы, то результат не достигнут Exit; end; end end; end;
При этом функция StringBuilder производит простую замену ключевых подстрок %s и %d на поисковый запрос и номер соответственно:
function TYaParser.StringBuilder(const Template, Key: string; Page: integer) : string; function StrReplace(const Str, Str1, Str2: string): string; var P, L: integer; begin Result := Str; L := length(Str1); repeat P := Pos(Str1, Result); // ищем подстроку if P > 0 then begin Delete(Result, P, L); // удаляем ее Insert(Str2, Result, P); // вставляем новую end; until P = 0; end; begin Result := StrReplace(Template, '%d', IntToStr(Page)); Result := StrReplace(Result, '%s', Key); end;
Вот так, не прибегая к коренной переделке всего алгоритма парсинга выдачи Яндекс мы смогли разработать компонент для парсинга сразу семи поисковых систем. Кстати сказать, Вы и сейчас можете использовать компонент парсинга Яндекс (старый вариант) для парсинга, например Апорта — просто задайте в компоненте вручную шаблон и регулярное выражение. В шаблоне обязательно должня быть подстроки %s и %d.
Ну, а пока Вы тренируетесь в работе с поисковыми системами я всё-таки попробую сделать компонент, который будет парсить всё, что потребуется в потоках. Тем более, что Серёга уже как можно более подробно постарался донести до нас информацию по разработке многопоточных приложений.
Для Google, Yahoo, Bing уже не актуально.
У них есть очень удобные Search API. Выдавать могут как XML, так и JSON.
Ключевая фраза для поиска: «{Google|Yahoo|Bing} Search API»
Ну, что тут сказать. К сожалению посты в блоге имеют свой срок давности. Когда писал этот пост, то, по крайней мере Bing только начинал работатать и про API речи там не шло :)
А у меня странная ситуация получается с Гуглом. с Картинками.
При первом запросе он все отлично ищет и выдает. Потом в программе меняется запрос для поиска, поиск проводится еще раз (разумеется, с обнулением всех доступных параметров и с динамическим созданием и уничтожением idhttp), при этом между поисками есть задержка по времени — и гугл выдает только did not match any documents. Если запустить программу заново, то все найдется (в первый раз). Непонятно…need help!
Сорри, не прикрепился( [code] procedure google; var s: TStringstream; e,ssize,slink: string; jtmp,z:Byte; idhttp12:TIdHTTP; begin s:=TStringstream.Create(''); e:=''; goj:=0; idhttp12:=tidhttp.Create(nil); for jtmp:=1 to 21 do begin go[jtmp].url:=''; go[jtmp].w:=0; go[jtmp].h:=0; end; slink:='http://www.google.com/images?um=1&hl=en&newwindow=1&tbs=isch%3A1&sa=1&q='+sxnamepl+'+'+sxalbumpl+'&aq=f&aqi=&aql=&oq=&gs_rfai='; repeat try begin IdHTTP12.Disconnect; IdHTTP12.AllowCookies:=false; idHTTP12.Response.KeepAlive:=true; IdHTTP12.request.useragent:=useragents[Random(8)+1];IdHTTP12.Disconnect; IdHTTP12.Get(slink,s); end; finally idhttp12.Free; end; e:=s.DataString; if e'' then Break; until False; repeat if Pos('["/imgres?imgurl\x3d',e)=0 then begin break; end; Delete(e,1,Pos('["/imgres?imgurl\x3d',e)+length('["/imgres?imgurl\x3d')-1); inc(goj); go[goj].url:=Copy(e,1,Pos('\',e)-1); for jtmp:=1 to 18 do begin Delete(e,1,pos('"',e)); end; ssize:=Copy(e,1,Pos('×',e)-2); z:=0; while z0 then begin delete(ssize,pos(' ',ssize),1); dec(z); end; if pos(#$A,ssize)>0 then begin delete(ssize,pos(#$A,ssize),1); dec(z); end; if pos(#$D,ssize)>0 then begin delete(ssize,pos(#$D,ssize),1); dec(z); end; end; go[goj].w:=StrToInt(ssize); ssize:=Copy(e,Pos('×',e)+8, pos('-',e)-pos('×',e)-8); z:=0; while z0 then begin delete(ssize,pos('… Подробнее »
Вообще я с Indy не дружу :) но что-то я немного не понял вот этот кусок кода:
repeat
try
begin
idhttp12.Disconnect;
idhttp12.AllowCookies := false;
idhttp12.Response.KeepAlive := true;
idhttp12.request.useragent := useragents[Random(8) + 1];
idhttp12.Disconnect;
idhttp12.Get(slink, s);
end;
finally
idhttp12.Free;
end;
e := s.DataString;
if e '' then
Break;
until false;
А точнее, почему дважды происходит Disconnect, когда по сути ещё ничего не взяли с сервера. И как компонент остается в рабочем состоянии после первого прохода в цикле? Он ведь убивается в конце, а в начале цикла не создается. Или я не прав?
с дисконнектами это нормально — их рекомендуют вообще ставить едва ли не перед каждой работой с idhttp, иначе вероятно образование ошибок, я с этим сталкивалась ранее
А по поводу idHTTP12.Free? Он ведь в одном цикле убивается, но не создается по новой.
это поправила, спасибо
оно только в исключительных случаях срабатывало бы, поэтому основные сложности сохраняются.
смену юзерагента он не любит почему-то.))
все заработало
спасибо за помощь)
да, собственно, я и помочть-то не упел :) Так что — не за что.
[…] и в-третьих, в одном из постов рассматривался вопрос парсинга выдачи поисковиков. Там я никак не затрагивал вопрос «Что делать если […]