Previous Entry Share
Многопоточная качалка на Erlang
what_me
Ну вот, моя первая программа на эрланге. https://github.com/onokhov/erlang_web_crawler/

Язык занятный. Параллелить и устраивать межпроцессное общение просто. Но остальное писать неудобно, думаю, что просто с непривычки. И документацию я ещё не прочитал. Осилил только getting_started, описания модулей string, list, re, httpc и ещё некоторых.
Сторонние модули использовать не хотелось, поэтому то, что в штатных модулях не нашел, делал сам.

Сравнил по производительности с перловой качалкой, эрланговская получилась немного быстрее. За минуту с форума glav.su перловая вытянула 26МБ, а эрланговская 26,7МБ.

Хотелось бы критики по коду от людей эрланг знающих.

Update: Закоммитил правки по комментариям. Заменены паттерны, ++ заменен на [|], использованы list comprehensions.
Update 2: Отказ от prespawn. И правки по стилю
Update 3: Багфикс в receive_text_data/2. Правки по стилю
Tags: ,

ja dumaju chto ty mozhesh s'ekonomitj odin argument: WorkersMaxNum
jego ne nado protaskivatj cherez vse vyzovy, dostatochno protaskivatj RAZNICU: WorkersMaxNum - WorkersNum.

WorkersMaxNum == CONSTANTA.
a WorkersNum sam po sebe ne nuzhen. dlja kriterija konca vsego izpoljzuj UrlsInProgress!

Sdelaj: SpareWorkersNum
i menjaj jego ot Max do 0 rovno v tex mestax gde ty menjajesh WorkersNum toljko v druguju storonu menjaj.

eto pozvolit tebe i "when" sokratitj siljno.
i nakonec stroki nachnut vlezatj v ekran :)
(kstati ne zloupotrbljaj dlinnymi imenami peremennyx. uchti chto SCOPE peremennyx v erlange ochenj korotkij. vprochem argumenty dostojny xoroshix imjon.)

Edited at 2017-01-11 10:06 pm (UTC)

ja by vot v etom meste:

crawler(WorkersMaxNum, WorkersNum, Host, Urls, SeenUrls, UrlsInProgress) when WorkersMaxNum == WorkersNum orelse Urls =:= [] ->

pozhertvoval skorostju v poljzu krasoty:

crawler(N, N, Host, Urls, SeenUrls, UrlsInProgress) ->
wait_messages(N, Host, Urls, SeenUrls, UrlsInProgress);
crawler(_, N, Host, [], SeenUrls, UrlsInProgress) ->
wait_messages(N, Host, [], SeenUrls, UrlsInProgress);

no ne nastaivaju, eto prosto mne lichno tak legche chitatj, chem "OR" guargy.

ja by pomenjal porjadok argumentov:
crawler(SpareWorkers, Host, Urls, SeenUrls, UrlsInProgress)
na:
crawler(Host, SpareWorkers, Urls, UrlsInProgress, SeenUrls)

konstanta kotoraja skvoznjakom proxodit, idjot vperjod -- eto takoj marker celogo domena, vizualjno jego luche prilepitj k imeni funkcii i ne meshatj jego s aktualjnoj informacijej.

SeenUrls eto accumuljator, my ix po privychke tolkajem v konec.
Vcelom vyxodit takoje nechotkoje pravilo: sleva-napravo volatiljnostj argumentov rastjot. takzhe sleva kak pravilo stojat argumenty kotoryje my matchim v golovax, a sprava argumenty kotoryje my ne matchim. (eto konechno ne zakon, no pomojemu jesli takoj porjadok xotjaby slegka oboznachen, to chitajemostj vyshe, nu jesli ty zatolkajesh Host v samyj pravyj kraj ja tozhe vozrazhatj ne budu)

A vot Urls i UrlsInProgress objazateljno postavj RJADOM: potomuchto eto kagby odna suchnostj. ty elementy tuda sjuda perekladyvajesh tipa.


{error, Reason} ->
io:format("Error fetching ~s: ~w~n", [Url, Reason]),
[];

you consider the given Url "fetched" even if it was not.

Эту недоработку я осознанно оставил. Следовало прокомментировать в коде.

{ok, RequestId} ->
{Html, UrlsToFetch} = parse_html(receive_text_data(RequestId), Url),
save_to_file(Html, path_to_index(Url)),
sets:to_list(sets:from_list(UrlsToFetch))

receive_text_data() does not always return your text. it would be nicer to deal with a failed calle BEFORE using its result. This way you will not need to handle meaningless input for two subsequent functions parse() and save()

case receive_text_data() of [] -> ....

vmesto:
receive_text_data( RequestId, [BinBodyPart|Accumulator] );

you can join binaries on the fly, thus saving yourself a list:reverse()

receive_text_data( RequestId, <<Accumulator/binary, BinBodyPart/binary>> );

P.S. jobanyj LJ, sratyj HTML. suka ubivatj pidarasov!

Edited at 2017-01-11 11:13 pm (UTC)

it will be way more justified to pass the Accumulator through all
receive_text_data() recursive calls. Even though you are convinced that httpc sends you exactly one "stream_start" message.

change:
true -> receive_text_data(RequestId);
to:
true -> receive_text_data(RequestId, Accumulator);

then tell me what are you going to do with a httpc that did not give the "stream_start" message?

P.S. "Acc" is a better name for an accumulator.

OR!!! EVEN BETTER!!!
receive the "stream_start" message separately!
do it in the receive_text_data/1
and remove it from the receive_text_data/2

perfect!

case string:right(Url,1) == "/" of true ->

zachem ty matchish "/" chtoby potom matchitj 'true'
sokratilishnij match:

case string:right(Url,1) of "/" ->

a potom sokrati string:right()

case lists:last(Url) of $/ ->

tak kak ty NE kachajesh s drugix xostov, ty mozhesh (ili dazhe dolzhen) uprostitj obrabotku urlov scylok iz teksta.

krome togo:
ja dumaju chto tvoj regexp ne opredeljajet absoljutnyje urly.
IMJA KATALOGA mozhet popastj pod etot regexp.
a vot imja xosta bez ":" ne popadjot.

nado pochetatj stondart, kak brauzer reshajet chto url otnositeljnyj -- ja mogu predpolozhitj chto on smotrit na prefix protokola (potomuchto inache imja hosta ot imjemi kataloga on ne otlichit!!)

Re: url_to_absolute()

я читал стандарт. Абсолютный uri содержит схему, которая отделяется от остальной части первым двоеточием. Иначе uri относительный, и необходим базовый uri, чтобы понять, куда указывает относительный. Для браузера базовый урл, это тот, по которому он загрузил документ. Если путь в относительном uri начинается с // - то, это почти абсолютный, ему не хватает только схемы, которую нужно взять у базового. Если путь начинается с одного слэша, то у базового берём authority часть - это схема, юзеринфо, хост, порт. В остальных случаях оставляем у базового uri путь до последнего слэша (полный путь без файла) и дописываем к нему наш относительный uri. И потом разбираемя с точками внутри пути /../ и /./
href="www.google.com" - относительный uri и, если этот документ на яндексе лежит, то полный получится http://ya.ru/www.google.com
Мне не хватало возможностей стандартного эрланговского модуля http_uri, и поначалу я сделал свой модуль uri в котором реализованы все эти to_absolute, to_relative. Но потом решил, чтоб код краулера был в одном файле, чтоб удобно было показывать, обсуждать, править. (Это учебная программа).
Оптимизировать разбор html привязываясь к хосту, думаю, неправильно. Универсальный подход, мне кажется, здесь будет лучше. Скорость тут не нужна. По производительности мы упираемся в сеть, а не в процессор.

А вот есть у меня вопрос на другую тему. По подходу "let it crash". Тут в чатике по эрлангу/эликсиру спросили, как найти различающиеся строки в двух больших файлах. Задача это прекрасно ложится на perl, но мне захотелось попрактиковаться в эрланге и я написал https://gist.github.com/onokhov/f65c179b98dcf480d0472d489241b8f0 (строки считаются совпадающими, если третья колонка integer - одинаковая)
Вопрос на который у меня нет ответа - правильно ли при работе с файлом падать, если он не открывается, или не читается? Я привык к обработке всех ошибок, а тут, вроде, говорят: "let it crash".

?

Log in

No account? Create an account