Многие считают, что парсер – нечто сложное и не понятное. Так ли это?
Я не программист, ни по образованию, ни по роду занятий. Но люблю разбираться в разных вопросах. Поэтому решил разобраться и с парсерами. Тем более, что занялся продажей автомобильных масел и фильтров. Рыться в каталогах – долго и муторно. Охота так, чтобы отжал продавец кнопку, а ему показало, что нужно подать покупателю. Поэтому задача запарсить сайт производителя фильтров, записать все в БД, чтобы было доступно локально.
Все, что будет написано ниже, применимо не только к автомобильным фильтрам, а и к компьютерным комплектующим и прочим товарам, новостям, статьям, блогам.
Процесс установки и настройки WAMPили LAMPописывать нет смысла.
И так, приступим. Для начала надо изучить объект. Открываем страницу каталога. Открываем ее исходник. Любой такой каталог начинается с выбора производителя авто. Исходник содержит производителей и их Id в списке <select>. Это и будет нашей первой таблицей в БД. Создаем ее в PHPMyAdmin, обозвав _car_brand.
Первый столбец brand_id (Primary), второй brand_name. Я эту таблицу заполнил, написав разовую программку, которую тут же стер. Она использовала simple_html_dom и брала данные со страницы каталога. Можно заполнить вручную. Цель этой статьи не в том. Мы решаем дальнейшую проблему, уткнувшись в которую, многие не знают, как поступить, и забрасывают свой парсер.
Все дело в том, что страница меняется на экране динамически путем ajax запросов, при этом ее исходный текст остается неизменным. Т.е. куда то летит запрос и возвращается JSON. Потом на клиенте свежие данные обвешиваются HTML тегами. Вот эти запросы мы и будем формировать.
Создаем файлик poisk.php и открываем его в Notepad++. И начинаем кодить:
<?php
// считываем ИД производителя авто
$brandId=$_REQUEST["selBrand"];
// Если еще не выбирали производителя (1й этап)
if ($brandId == 0){
echo 'ВЫБЕРИТЕ ПРОИЗВОДИТЕЛЯ<form method="POST" action="poisk.php"> <select name="selBrand">';
$database = 'db_6';
$link = mysql_pconnect("mysql.ru", "dbu1", "polk");
mysql_select_db($database) or die("Немогуподключитьсякбазе.");
mysql_query("set character_set_server='utf8'");
mysql_query("set names 'utf8'");
// Берем полный список производителей и выводим
$car_brand = mysql_query("SELECT * FROM _car_brand");
// Цикл по строкам. Кому что не ясно, читаем про работу с БД
while ($brand_id = mysql_fetch_array($car_brand)){
$a=$brand_id[0];
$b=$brand_id[1];
echo '<option value="'.$a.'">'.$b.'</option>';
}
echo '</select> <input type="submit" id="btnAppSearch" name="btnAppSearch" value="Go"/></form>';
}
Коротко говоря, считали из базы и вывели полный список производителей…
Дальше интересней. Нам нужна вторая таблица в БД `_car_model с моделями авто. В ней поля (`car_model_id`, `car_model_brand_id`, `car_model_name`). Эту таблицу мы будем заполнять не по циклу, а в процессе работы с программой.
// Если производитель уже выбран
else {
// Считываем прилетел ли ИД модели
$selModel=$_REQUEST["selModel"];
if ($selModel == 0){
// Производитель выбран, но не выбрана модель (2й этап)
$database = 'db_6';
$link = mysql_pconnect("mysql.ru", "dbu1", "polk");
mysql_select_db($database) or die("Немогуподключитьсякбазе.");
mysql_query("set character_set_server='utf8'");
mysql_query("setnames 'utf8'");
// Смотрим имя производителя и его ИД
$car_brand = mysql_query("SELECT * FROM `_car_brand` WHERE `car_brand_id` = '$brandId'");
$brand_id = mysql_fetch_array($car_brand);
$a=$brand_id[0];
$b=$brand_id[1];
// Выводим имя производителя без возможности его сменить, только через обновление страницы
echo '<form method="POST" action="poisk.php">ПРОИЗВОДИТЕЛЬ : <select name="selBrand"><option value="'.$a.'">'.$b.'</option></select>
<a href="poisk.php">СМЕНИТЬ</a>
<br>ВЫБЕРИТЕМОДЕЛЬ : ';
echo '<select name="selModel">';
// Лезем в локальную БД, смотрим, есть ли в ней модели этого производителя
$car_model = mysql_query("SELECT * FROM `_car_model` WHERE `car_model_brand_id` = '$brandId'");
$i=0;
// Если в локальной БД есть модели, то выводим их, счетчик их считает...
while ($model_id = mysql_fetch_array($car_model)){
$c=$model_id[0];
$d=$model_id[2];
echo '<option value="'.$c.'">'.$d.'</option>';
$i++;
}
Тут вроде пока все просто, но, как мы помним, наша таблица с моделями на сей момент пуста… Поэтому и список будет пуст. Придётся лезть за ним на сайт.
// Если счетчик не посчитал, значит локально их нет...
if ($i<1){
// Лезем тогда на сайт за моделями
Как уже говорил, в исходнике страницы мы ничего не увидим, поэтому запускаем встроенные в браузер средства отладки. Я пользовался оперой. Поэтому правый клик => посмотреть код элемента. Переходим на вкладку Network, где переходим в под-вкладку XHR. Тут то и отображаются «подпольные» сетевые действия, которые нужно внимательно изучить. Выбираем в списке автомобиль (я выбрал TOYOTA) и кликаем «дальше»… В окне браузера появляется список с моделями, а на нашей вкладке мы видим что добавилась строка get_models/. Выбрав ее, видим много интересного. Видим адрес, по которому браузер отправлял запрос. Дальше нас интересует информация, которую браузер отправлял на сайт: Request headers. И данные, которые браузер передал: FormData. Snoopy.class — нам в помощь.
include ("Snoopy.class.php");
$snoopy = new Snoopy;
// Некоторые параметры Снупи знает, их и указываем от реквеста
$snoopy->accept = "application/json, text/javascript, */*; q=0.01";
// Некоторые параметры ему не известны, поэтому указываем как RAW
$snoopy->rawheaders["Accept-Encoding"] = "gzip, deflate";
$snoopy->rawheaders["Accept-Language"] = "ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4";
$snoopy->rawheaders["Connection"] = "keep-alive";
$snoopy->rawheaders["Content-Type"] = "application/x-www-form-urlencoded";
$snoopy->cookies["PHPSESSID"] = '7b60548db34612bc5af6';
$snoopy->cookies["ci_session"] = тут очень много всего было…;
$snoopy->host = "www.filter.com";
$snoopy->rawheaders["Origin"] = "http://www.filter.com";
$snoopy->referer = "http://www.filter.com/catalogue";
$snoopy->agent = "Mozilla/5.0 (Windows NT 6.1; WOW64)";
$snoopy->rawheaders["X-Requested-With"] = "XMLHttpRequest";
// Шапка закончилась, поэтому формируем массив запроса, передаваемый Пост
$submit_vars = array();
$submit_vars["brandId"] = $brandId;
// Ну а это адрес, куда весь запрос наш полетит
$submit_url = "http://www.filter.com/application/get_models/";
Еще раз, для понимания: все эти данные здесь для примера. У каждого сайта они будут свои. У меня, почему то, при передаче Content-Length, такого же, как и в запросе браузера, приходил пустой ответ от сервера. Попробовал не передавать его – работает. Значит параметр не важный для запроса. А вот без этого : $snoopy->rawheaders[«X-Requested-With»] = «XMLHttpRequest»;
Приходил пустой результат.
С ним сайт отвечает — результат получен!!!
Шлем запрос и получаем результат:
if($snoopy->submit($submit_url,$submit_vars))
{
// Если удачно залезли
$var = $snoopy->results;
// Тут к нам как раз и прилетел JSON, декодируем в массив
$vars = json_decode($var);
//Который потом разбираем
foreach($vars as $value){
$c=$value->app_model_id;
$d=$value->app_model_name;
// Выводим список моделей
echo '<option value="'.$c.'">'.$d.'</option>
';
// И пишем его в БД
$sql = mysql_query("INSERT INTO `db_ 6`.`_car_model` (`car_model_id`, `car_model_brand_id`, `car_model_name`) VALUES ('$c', '$a', '$d')");
}
}
}
echo '</select>';
echo '<input type="submit" id="btnAppSearch" name="btnAppSearch" value="Go"/> </form>';
}
Как можно увидеть, таблица с моделями заполняется при первом выборе производителя. При повторном запросе по этому производителю модели будут прилетать из локальной БД. Ну а дальше:
else {
// Оказалось, что выбран и производитель и модель
Проделываем то же самое с get_years, get_eng_vol, дойдя до ягодки get_applications, отправив которой в запросе
$submit_vars = array();
$submit_vars["modelId"] = $selModel;
$submit_vars["year"] = $year;
$submit_vars["eng_vol"] = $eng_vol;
Получим в ответ заветный JSON c {Oil;Air;Fuel;Cabin;Trans}
http://idemitsu.to.net.ru/poisk.php — вот полученный вариант каталога, который можно встроить в сайт или в окно 1с продавца…
Всем удачи и успехов.
И сразу вопрос: а откуда мы узнали имя базы, логин и пароль ? а потом еще и структуру базы… имена таблиц…
(1) Как откуда? Из головы… Придумали… Вы считаете мне было нужно в статье про парсер описывать работу с Мускулем? Я посчитал это лишним, простите.
(2)
Получается, что вы делаете:
1) Создаем локальный сайт: база + немного кода PHP
Вот тут не хватает описания структуры БД.
2) Наполняем базу данными и выводим результаты
а) «Производители» — откуда взяли список производителей ?
б) модели — если локальная БД пуста, получаем с сайта производителя (описано в статье)
г) год выпуска + кузов — если локальная БД пуста, получаем с сайта производителя (описано в статье)
Итак, замечания:
1)
с этого нужно было начинать статью 🙂 Да, про «Еще раз…» — так и не увидел первого упоминания 🙂
2) Сайт производителя: указан только в примере кода — нигде четко не указано, с каким именно сайтом работаете. А учитывая Ваши же слова (приведены выше) — с этого нужно было начинать статью!
3)
описывать работу с мускулем не стоит, но вот описать структуру Вашей базы — нужно.
4) А где полный исходник ?
в от к этому тексту как минимум три скрина бы…
Спасибо за замечания, но будьте внимательны. Второй абзац гласит:
«Все, что будет написано ниже, применимо не только к автомобильным фильтрам, а и к компьютерным комплектующим и прочим товарам, новостям, статьям, блогам.»
Вы считаете, что здесь не хватает фразы: «Поэтому локальная база, запросы к сайту и весь программный код у каждого будет свой»?
Проблема заключается в том, что даже поисковики не умеют правильно работать с сайтами каталогов, применяющих ajax и json.
Поэтому найти в Яндексе или Гугле какое масло залить или какой фильтр поставить — великая проблема. Дает ссылки на форумы, но не производителей.
Зачем описывать структуру БД, состоящей из плоских таблиц? я описал первые две. Остальные вытекают из них…
По поводу подопытного сайта… Ни один парсер не скажет, что именно он парсит. Ибо рискует быть забаненным. Мой парсер не использует циклов, поэтому не дает нагрузку на сервер. Инфа в локальную базу пишется только после первого открытия страниц с текущими данными.
Страницы открывают поисковики, примерно по 10 — 100 в день. Это я вижу по логам, которые пишу в отдельную таблицу.
Поэтому каждый день в локальную базу добавляется 5-50 машин. Основные, ходовые модели уже давно пробиты моими менеджерами…
Для чего Вам полный исходник? Каждый следующий шаг копирует предыдущий, меняется только запрос и ответ…
Ок, только уже послезавтра. Уезжаю в командировку. Послезавтра повешу Вам скрины с какого нибудь из сайтов, на которых тренировался.
Подбор масла Мотюль пойдет? Там хорошо все видно< как изучать подпольные сетевые действия браузера,,,
(4)
(5)
Для кого пишется статья ? Если для новичка — то слишком запутано, для не новичка — бесполезна.
ИМХО, эта фраза вообще лишняя и вводит в заблуждение.
при прочтении «Все, что будет написано ниже, применимо не только к …» в голове у читающего возникает мысль: «О! мне сейчас расскажут как сделать универсальную всеядную штуковина на все случаи жизни!»
Я считаю, что не хватает фразы: «Рассмотрим на примере сайта производителя Имя_Сайта.»
И не забыть явно напомнить: «У каждого производителя/сайта своя структура данных и эта структура может изменяться».
Описание таблиц где-то спряталось в тексте…
Опять таки, если для новичка (а судя по содержанию, именно для новичка) — структура базы обязательна!
Не нужно прилагать полные таблицы с описанием полей, типов и т.д., достаточно скриншота из PHPMyAdmin.
«Полный» — это я немного погорячился, достаточно рабочего. Какая бы ни была хорошая и подробная статья, рабочий пример намного более нагляден.
И да, можно стартмани заработать 🙂
Нда… вот сижу и думаю… человек написал статью, а я придираюсь…
Вы уж извините, тяжело быть перфекционистом 🙂
(8) Эту статью я написал как раз для одного единственного новичка в парсинге, который никак не мог разобраться как парсить динамическую страницу.
https://www.google.ru/#newwindow=1&q=toyota+allion+2003+1.8+oil+filter
И он, в результате, написал весьма не плохой продукт, который по заданным параметрам находит нужный товар на нескольких заданных сайтах. С чем не справляется ни один поисковик, которому надо указать не параметры, а текст. Да и то
выдаст всякую ерунду, не ответив на главный вопрос.
Весь программный код, который я выложил, полностью рабочий. Заменены только адреса, название базы и пароль.
На форумах по РНР это весьма применимая практика. Но, к сожалению, там я не нашел статей, как получить динамическую инфу, что и описал.
Что именно запутанно в статье? Чего Вы так упорно добиваетесь имя сайта? Человек, который разбирается в теме, его легко определит, а парсеры, ибо это не вполне легально, имя сайта не скажут. И не будет меняться структура у динамического каталога. Она напрямую зависит от базы данных. Это не одно и то же, что парсить статические страницы через регулярки, чем я занимался лет этак 20 назад. В свое время был забанен на тот момент крупнейшими интернет магазинами, ибо парсил в лоб по циклу.
И скриншот из РНРМуАдмин сделать не реально, ибо в моем случае я использую не отдельную базу, а огромную базу интернет магазина, куда собирают инфу десятки парсеров. Фотки, описания, отзывы. Светить это мне не надо.
Кстати, этот парсер в итоге не только фильтра подбирает, но и дополняет базу кроссов. Я к теме выложил скриншотик кроссов. Они тоже получаются парсингом.
По поводу придирок, Вы придирайтесь, мне интересно. Я не писатель и не программист… Поэтому интересно чужое мнение.
На форумах по РНР выкладываю длинный код — налетает стая его обкакивать, типа тут можно было процедур написать, а не повторять дважды текст, а тут нафига безусловный переход поставил?…
Смешные они, однако. Использование процедур, как вы считаете, не является ли случайно двойным безусловным переходом? Двойные стандарты даже в программировании. Процедуры нужно, а безусловные переходы нельзя… Как то так…
ИМХО, после всех этих комментариев уже и добавить нечего 🙂