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

пятница, 31 июля 2009 г.

Особенности составления регулярных выражений для парсинга страниц некоторых сайтов

Привет всем!

Во-первых, поздравляю всех сисадминов с профессиональным праздником! ;)

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

На что еще следует обратить внимание при парсинге web-страниц. Регулярное выражение, составленное для содержимого страницы, полученного одним способом (например, через DownloadFile), может не подойти для поиска в содержимом страницы, полученном другим способом (например, через idHTTP.Get). Так что при составлении регулярного выражения используйте в качестве основы html-код, полученный тем же способом, каким вы планируете получать его в своем приложении. Или составляйте универсальное регулярное выражение. Это касается в большей степени разработки каких-нибудь многофункциональных универсальных приложений, в которых с ссылками приходится работать по несколько раз, хранить их в базе и т.д..

Приведу в качестве примера свое приложение для парсинга и автоматизации постинга сообщений на форумы. При скачивании страницы через idHTTP.Get в ссылках будет дописываться сессия (соответственно, с учетом этого все ссылки приходилось обрабатыввать, чтобы в базу сохранять "чистые" ссылки, удаляя "s=..."). Если бы мне потом взбрело в голову пытаться достать какие-нибудь данные из кода, скачанного с помощью DownloadFile, то ни в одной ссылке сессии не было бы и составленная для первого случая регулярка не подошла бы. А вот такая подошла бы в обоих случаях:

<a href="(showthread.php\?(?:|s=.*?&amp;)t=[0-9] )" id="thread_title_[0-9] ".*?>(.*?)</a>


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

/---

Чтобы научиться писать программы на персональном компьютере, нужно сначала этот самый персональный компьютер купить.

---/

среда, 29 июля 2009 г.

Пост для урегулирования потока писем

Здравствуйте, читатели блога! Извиняюсь, конечно, но таки отважилась это сказать вслух :) В последнее время мне приходит все больше и больше писем с просьбой о помощи в тех или иных вопросах программирования. По возможности я даже стараюсь отвечать. Но их становится МНОГО, реально много. И если я буду и дальше продолжать на все отвечать, то все мое свободное время будет уходить на разгребание почты. Так что прошу: по возможности, поищите сначала решение хотя бы в интернете, ведь там так много уже всего написано и о многом рассказано!

P.S. И уж если не нашли, написали письмо, и я вам ответила, и этот ответ вам помог — сделайте в свою очередь приятно и мне ;)))) Например, поставьте + на фрилансе, если у вас есть там аккаунт... %D

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

суббота, 25 июля 2009 г.

Парсинг для начинающих. Практическая работа. Часть II

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

Классы для работы я описала еще в первой части. Небольшие пояснения относительно функций для класса списка компаний (TCompanies).

function IsNewCompany (sLink : string) : boolean;

- проверяет, есть ли уже в списке такая фирма. В качестве параметра — ссылка на информацию о фирме. Листинг:
function TCompanies.IsNewCompany(sLink: string): boolean;
var
i : integer;
tempC : TCompany;
begin
Result := true;
for i := 0 to Count-1 do
begin
tempC := TCompany(Items[i]);
if (tempC.CInfoURL = sLink) then
begin
Result := false;
break;
end;
end;
end;


procedure LoadFromQuery ( Query : TpFIBDataSet );

Листинг:
procedure TCompanies.LoadFromQuery(Query: TpFIBDataSet);
var
tempC : TCompany;
v : Variant;
begin
Query.First;
while (not Query.Eof) do
begin
tempC := TCompany.Create;
v := Query.FieldValues['N'];
tempC.N := Integer(v);
tempC.CName := Query.FieldValues['COMP_NAME'];
tempC.CInfoURL := Query.FieldValues['INFO_URL'];
Add(tempC);
Query.Next;
end;
end;

- для удобства, загрузка из выборки базы. Допустим, парсим всю информацию не за раз, а за несколько (объемы большие, как никак). После первого запуска напарсенные компании складываем в базу. Через день запускаем парсер снова. И, чтобы не парсить информацию о некоторых предприятиях повторно (выдача в список может меняться, простого указания страницы, с которой начинать, - недостаточно), мы загружаем в список уже присутствующие в базе компании. А потом после парсинга перед добавлением проверяем, есть ли компания в списке по InfoURL (с помощью IsNewCompany). После проведения парсинга записываем в базу только новые компании (их можно выделить по отсутствию у них внутреннего идентификатора). Я все записывала в фаербед, но вы с таким же успехом можете все писать в MySQL.

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

В программе будет две основных процедуры. Первая — обход списка предприятий по страницам и сбор названий предприятий и ссылок на более подробную информацию о них, вторая — сбор информации о каждом предприятии.

Принтскрин примерной формы, которая уменя получилась, я приводила в предыдущей части инструкции. Поясню только, что на форме у меня:

cxSpinEdit1 — с какой страницы начинать (нумерация с нуля)
cxSpinEdit3 — по какую страницу
cxSpinEdit2 — таймаут между запросами, в секундах
mLog - TMemo просто для отладки

procedure TMainF.StartParsingHH;
var
i, j, k : integer;
S,
ReqStr,
StrLink : string;
NeedNextPage : boolean;
v : OleVariant;
Doc : IHTMLDocument2;
DocAll,
DocA : IHTMLElementCollection;
DocElement : IHtmlElement;
idHttp1 : TidHttp;
tmpComp : TCompany;
begin
StatusBar.Panels[0].Text := 'Идет парсинг';
for i := 0 to 10 do
begin
Application.ProcessMessages;
Sleep(10);
end;

// цикл по страницам
i := cxSpinEdit1.Value;
NewVacCnt := 0;
NeedNextPage := true;
vStop := false;
while (NeedNextPage) and (not vStop) do
begin
// загрузка страницы
ReqStr := StringReplace(HHCompanyPageURL,'[PageNum]',IntToStr(i),[rfReplaceAll,rfIgnoreCase]);
sLog('log.txt','[HH] ReqStr = '+ReqStr);
NeedNextPage := true;
try
idHttp1 := TidHttp.Create(nil);
idHttp1.Request.UserAgent := 'Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.3) Gecko/2008092417 AdCentriaIM/1.7 Firefox/3.0.3';
idHttp1.Request.AcceptLanguage := 'ru';
idHttp1.Request.Referer := 'http://hh.ru';
idHttp1.ConnectTimeout := 5000;
idHttp1.ReadTimeout := 5000;
idHttp1.Response.KeepAlive := true;
idHttp1.HandleRedirects := true;
try
// опущу проверку EIDHttpProtocolException, хотя по правилам надо сделать
S := idHttp1.Get(ReqStr);
except on e : Exception do
begin
sLog('log.txt','[HH] Не удалось скачать страницу '+ReqStr);
sLog('log.txt','Error: '+e.Message);
vStop := true;
end;
end;
S := UTF8ToAnsi(S); // опытным путем мы заметили, что надо поменять кодировку
finally
idHttp1.Free;
end;
// будем работать с DOM
Doc := coHTMLDocument.Create as IHTMLDocument2;
if Doc = nil then
begin
ShowMessage('Ошибка создания IHTMLDocument2');
exit;
end;
v := VarArrayCreate([0,0],VarVariant);
v[0] := S;
Doc.write(PSafeArray(TVarData(v).VArray));

DocAll := Doc.all;
DocA := DocAll.Tags('A') as IHTMLElementCollection;
for k := 0 to DocA.length-1 do
begin
DocElement := DocA.Item(k, 0) as IHtmlElement;
if Pos('/employer/',LowerCase(DocElement.getAttribute('href',0))) > 0 then
begin
// обрабатываем каждую ссылку, убирая about:blank (я уже писала о такой особенности ссылок в IHTMLDocument2, созданном на основе html-кода)
StrLink := 'http://hh.ru'+StringReplace(LowerCase(DocElement.getAttribute('href',0)),'about:blank','',[rfReplaceAll,rfIgnoreCase]);
mLog.Lines.Add(StrLink);
// обрабатываем таблицу выводов результатов
if Companies.IsNewCompany(StrLink) then
begin
tmpComp := TCompany.Create;
tmpComp.CInfoURL := StrLink;
tmpComp.CName := DocElement.innerText;
ExtractHHCompanyInfo(StrLink, tmpComp);
Companies.Add(tmpComp);

if vStop then break;

for j := 0 to cxSpinEdit2.Value*100 do
begin
Sleep(10);
Application.ProcessMessages;
end;
end;
end;
end;
// останавливаем, если i >= cxSpinEdit3.Value
inc(i);
if (i > cxSpinEdit3.Value) and (chbTO.Checked) then
vStop := true;
end;
end;


Процедура сбора информации следующая. На входе - URL страницы с подробной информацией и объект класса TCompany, куда надо записать полученную информацию. Все уже поняли, надеюсь, что я описываю процесс парсинга так, как я привыкла его производить :) Может, кто-то привык по-другому.

procedure TMainF.ExtractHHCompanyInfo(InfoLink: string; vComp : TCompany);
var
S : string;
Doc : IHTMLDocument2;
v : OleVariant;
DocAll,
DocDIV : IHTMLElementCollection;
DocElement : IHTMLElement;
i : integer;
idHttp1 : TidHttp;
isError : boolean;
begin
// извлечение информации о компании
try
isError := false;
idHttp1 := TidHttp.Create(nil);
idHttp1.Request.UserAgent := 'Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.3) Gecko/2008092417 AdCentriaIM/1.7 Firefox/3.0.3';
idHttp1.Request.Referer := 'http://hh.ru';
idHttp1.Request.AcceptLanguage := 'ru';
idHttp1.Response.KeepAlive := true;
idHttp1.HandleRedirects := true;
try
try
// опущу проверку EIDHttpProtocolException, хотя по правилам надо сделать
S := idHttp1.Get(InfoLink);
except
sLog('log.txt','Не удалось загрузить страницу с адресом'+#13#10+InfoLink);
isError := true;
end;
S := UTF8ToAnsi(S);
finally
idHttp1.Free;
end;
if isError then exit;

Doc := coHTMLDocument.Create as IHTMLDocument2;
if Doc = nil then
begin
ShowMessage('Ошибка создания IHTMLDocument2');
exit;
end;
v := VarArrayCreate([0,0],VarVariant);
v[0] := S;
Doc.write(PSafeArray(TVarData(v).VArray));

DocAll := Doc.all;
DocDIV := DocAll.Tags('DIV') as IHTMLElementCollection;
for i := 0 to DocDIV.length-1 do
begin
DocElement := DocDIV.Item(i, 0) as IHtmlElement;

if (LowerCase(trim(DocElement.className)) = LowerCase('b-employerpage-url')) then
vComp.CURL := DocElement.innerText;

if (LowerCase(trim(DocElement.className)) = LowerCase('g-user-content b-employerpage-desc')) then
begin
vComp.CInfo := DocElement.innerText;
// почистим результат от всяких "левых" символов
vComp.CInfo := StringReplace(vComp.CInfo,'&ndash;','-',[rfReplaceAll,rfIgnoreCase]);
vComp.CInfo := StringReplace(vComp.CInfo,'&laquo;','"',[rfReplaceAll,rfIgnoreCase]);
vComp.CInfo := StringReplace(vComp.CInfo,'&raquo;','"',[rfReplaceAll,rfIgnoreCase]);
vComp.CInfo := StringReplace(vComp.CInfo,'&nbsp;',' ',[rfReplaceAll,rfIgnoreCase]);
vComp.CInfo := StringReplace(vComp.CInfo,'&bull;','-',[rfReplaceAll,rfIgnoreCase]);
end;
end;

except
end;

if not vStop then
for i := 0 to cxSpinEdit2.Value-1 do
begin
Application.ProcessMessages;
Sleep(1000);
end;
end;


Небольшие пояснения. Я здесь использовала DOM. Это быстро и удобно. Тем более, что парсить такую информацию регулярками - совсем неудобно. Ссылку на сайт компании можно легко получить, взяв innerText элемента div класса "b-employerpage-url" (он такой один на всю страницу всем нам на благо). А подробную информацию можно получить, взяв innerText элемента с классом "g-user-content b-employerpage-desc". Чистим результат от символов, встречающихся в результате. Например, заменяем '&ndash;' на '-', '&laquo;' и '&raquo;' на '"' и т.д.. Необходимость этого выявляется уже чисто на практике: пробуем парсить страничку, смотрим результаты и делаем выводы.

Вот, в принципе, и все.

Что касается меня — я вернулась из отпуска, порядком отдохнувшей, загорелой и соскучившейся по интернету :) Скоро вольюсь в привычный ритм работы и придумаю что-нибудь еще ;D

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

понедельник, 6 июля 2009 г.

Парсинг для начинающих. Практическая работа. Часть I

До начала второй части отпуска осталось 4 дня. Использую это время с пользой — напишу статью на блог. :)

Итак, долой теорию, переходим к практике парсинга. Соберем с какого-нибудь сайта структурированную информацию. Исходя из того, что для примера структура должна быть простой, я выбрала обредший небывалую популярность во время кризиса сайт по поиску работы hh.ru. Еще мой выбор обоснован тем, что с этим сайтом я довольно-таки давно и тесно "работаю" :D. Парсить в этот раз мы будем не вакансии, а информацию о предприятиях.

По ссылке http://hh.ru/employersList.do можно увидеть список компаний, имеющих на данный момент открытые вакансии. Отметив чекбокс "Показать компании, у которых нет открытых вакансий" и нажав на кнопку "Найти" получаем список всех предприятий (на момент написания статьи — 76 101). База немаленькая. Если парсить в один поток и делать перерыв между загрузками 2 секунды, то процесс займет около двух суток.

Рассмотрим структуру выдачи. Get-запрос будет следующим:

http://hh.ru/employersList.do?letter=&employerName=&areaId=113&companyWithoutVacancy=on&page=[PageNum]


[PageNum] — номер страницы. Нумерация начинается с нуля.

Необходимая нам информация о предприятии:
- название;
- ссылка на сайт;
- описание предприятия;
- ссылка на источник информации.

Последний пункт — URL страницы, с которой берется информация (ссылка вида http://hh.ru/employer/[ID]). Мы будем парсить постранично список с предприятиями, потом заходить по каждой ссылке и брать подробную информацию. "Почему такой способ, а не перебор всех ID по порядку в ссылке http://hh.ru/employer/[ID]?" — спросите вы. Отвечу: "Да потому, что это менее затратно". Идентификаторов гораздо больше, чем "действующих" ссылок (например, наберите http://hh.ru/employer/4 — увидите своими глазами, что предприятия с таким ID не существует).

Итак, алгоритм:
1) скачиваем первую страницу списка фирм;
2) находим все ссылки на предприятия;
3) если ссылки есть - заходим по каждой ссылке и получаем детальную информацию, записываем; если ссылок нет — заканчиваем работу;
4) загружаем следующую по счету страницу;
5) повторяем пункты 2 — 4.

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

Как находить ссылки на предприятия? Способов тоже несколько:
1) в содержимом страницы по регулярному выражению;
2) просмотреть весь массив ссылок, найденный через DOM, и отобрать ссылки, начинающиеся с "/employer/".

По каждой найденной ссылке зайти и найти информацию о компании + адрес сайта. Полученные результаты сохранить. Тут уж кто как привык. Можно сразу в базу. Мне, допустим, удобней и привычней записать сначала все в List, а потом уже с ним делать все, что угодно.

Для компаний я создала класс TCompany:

type
TCompany = class
N : integer;
CName,
CURL,
CInfoURL,
CInfo : string;
end;

TCompanies = class (TList)
function IsNewCompany (sLink : string) : boolean;
procedure LoadFromQuery ( Query : TpFIBDataSet );
destructor Destroy; override;
end;


TCompanies — класс для работы со списком предприятий.

Интерфейс получился такой:
parsing hh.ru

Продолжение следует.

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

Поделиться