g «Новости читать бесплатно»

в 11:17, , рубрики: mysql, php, tdd, Новости, метки: , , ,

Прелюдия.

Однажды утром мне понадобилось узнать свежие новости о происходящем в одной маленькой и гордой стране. Казалось бы, что может быть проще? Зайдя на сайт корпорации Добра, я быстренько вбил название страны, добавил «news», нажал Enter, получил примерно 800 миллионов результатов и недоуменно почесал репу. Это действие вызвало в ней процессы, тихонько нашёптывающие на ухо: «покер, гейши, своё, написать...». К вечеру скромный сайт-сервис был готов и запущен, а о ключевых моментах того, как проходил процесс создания, придерживаясь принципов KISS, TDD и Rapid Development, я и хочу рассказать в этой публикации. Немного кода, немного текста — добро пожаловать под кат!

Брэнд.

Так уж получилось, что большинство не-IT населения в мире, посещает сайты следующим образом: вписывают искомое слово в первое попавшееся поле для ввода, поисковая машина, установленная по умолчанию, обрабатывает запрос и выводит результат в браузер, пользователь щелкает на первую ссылку, получает искомое, все довольны. Прекрасно осознавая, что именно таким образом многие из читателей и будут заходить к нам, в качестве бренда для проекта было придумано уникальное слово. Не длинное, не короткое, связанное с тематикой проекта, и, вроде как, хорошо запоминающееся. Забегая вперёд, скажу, что эта затея сработала на ура.

Материальная база.

Исполнители — Apache 2/PHP 5/MySQL5. Технологии — HTML5, JS(AJAX), XML.
Почему, вообще, в свете последних постов, PHP? Ответ прост — мне нужно ехать, а не шашечки, да и PHP я знаю лучше, чем другие веб-ориентированные языки. А ещё чистый PHP, даже без фреймворков, уже содержит все необходимые средства, которые мне понадобились. Я не за PHP, я не против PHP, я просто его использовал и получил желаемый результат.
Предвосхищая следующий вопрос «а почему не Nginx в качестве front-end?», отвечу заранее — уже существовала инфраструктура, которой рулит апач, а так как упор был на скоростную разработку, менять её я просто поленился. В будущем, если проекту повезёт, конечно, придётся перевести мордочку на Nginx.
Весь код был набран в замечательном быстром редакторе AkelPad, а к базе я подключался с помощью HeidiSQL, для меня она оказалась гораздо удобнее, скажем, того же PHPMyAdmin.

Паук.

Создание паука для моей задачи оказалось несложным. Бери себе RSS-ленты в цикле да добавляй в базу.
$rss = simplexml_load_file("http://source.com/rss/feed.xml");
foreach ($rss->channel->item as $item) {
ParseAndSecureRSSFields;
$query = "INSERT IGNORE INTO db ... $item->title ...";
}

Планировщик запускает скрипт сбора каждые полчаса. Скрипт берёт базу ссылок на RSS-ленты ведущих мировых новостных сайтов и в цикле вызывает функцию добавления новостей. Сперва парсер SimpleXML их разбирает, затем экранирует спецсимволы в полученном тексте, а уже INSERT IGNORE вставляет новость в базу. Что за IGNORE? Простейший способ избавиться от дублей — просто не создавать их. Если в запросе задана опция игнорирования, MySQL не будет добавлять в базу новость с уже существующим заголовком. Итак, крон теребит паука, база наполняется, едем дальше.

MySQL.

Так как в качестве движка для базы из-за индексов был выбран MyISAM, мои правки файла my.cnf ограничились, в основном, секцией [mysqld]. Был проанализирован конфигурационный файл довольно нагруженного продакшен сервера, который прислал мне друг. Увеличение размера буферов key_buffer_size, table_open_cache, sort_buffer_size, read_buffer_size, read_rnd_buffer_size привело к заметному разгону базы. На этом я решил временно остановиться и не бежать впереди паровоза.

Индексы.

О, индексы — им можно спеть целую оду. Сколько про них было прочитано-перечитано, сколько копий сломано, сколько алгоритмов написано. Большие базы без индексов — лишь полумёртвые хранилища информации. Индексам — быть! Но вернёмся к нашим новостям.
Помимо обычных KEY индексов для ключевых полей базы, я создал FULLTEXT индекс для заголовка и текста новости. Собственно, именно ради этого индекса, всё и затевалось, такой тип индекса доступен только при работе с MyISAM таблицами. Создать его, можно, например, так:
ALTER TABLE db ADD FULLTEXT(title, content);

Поиск.

Один из ключевых моментов любого сайта или сервиса — это поиск. К этому моменту паук уже успел немного наполнить базу данными, пишем форму поиска и с замиранием сердца выполняем наш первый простейший запрос к базе:
SELECT * FROM db

Опачке! Кракозяблы атакуют мой дисплей! При создании базы я установил умолчальную кодировку и сравнение в utf8, а вот php об этом сказать забыл. Добавляем в скрипт строчку:
$result = mysql_query("set names utf8 collate utf8_general_ci");

перед выполнением каких-либо действий с базой, и, наконец-то, получаем наши данные в удобочитаемом виде. Ура-ура, it works! Приступаем к поиску.
Прежде всего, стоит определить нашу реакцию, если пользователь ничего не ищет — то есть просто заходит на главную страницу. Для этого я приготовил следующий запрос:
SELECT * FROM db ORDER BY addtime DESC LIMIT 0,30

Если пользователь уже что-либо нашёл, и хочет вывести текущую новость, позволим ему это сделать, не забывая защитить запрос от самых зловредных пользователей:
$id = mysql_real_escape_string(strip_tags($id));
SELECT * FROM content WHERE id = $id

В случае, если пользователь ищет что-либо именно сейчас, начинается самое интересное. Помните, мы создавали FULLTEXT индекс? Настало время его использовать. Начиная с третьей версии, MySQL научился вести поиск в стиле а-ля Google самостоятельно, используя этот волшебный индекс, разве что семантику сам не разбирает. Поддерживаются и уточнения к запросу "+health -viagra", и даже релевантность полученных результатов. То, что надо! За детальным изучением прошу в документацию по MySQL, а мой запрос получился примерно таким:
SELECT (title,text) FROM db WHERE MATCH (title,text) AGAINST ($search* IN BOOLEAN MODE) ORDER BY time

IN BOOLEAN MODE указывает MySQL, что стоит применять модификаторы + — в тексте запроса.
Смотрим результат, да, вывод отсортирован по времени, но не по релевантности. Изучение документации привело меня к следующей модификации запроса:
SELECT title,text, MATCH (title,text) AGAINST ('$search*' IN BOOLEAN MODE) AS score FROM db WHERE MATCH (title,text) AGAINST ('$search*' IN BOOLEAN MODE) ORDER BY score DESC, time DESC LIMIT 0,30

Вуаля! В этот момент я немного забеспокоился о производительности, всё-таки два запроса, но документация и друг пояснили, что мудрый MySQL выполнит это одним запросом, но сортировка по релевантности уже будет в порядке. Так оно и оказалось. А ещё, FULLTEXT SEARCH, при интенсивном нарастании объёма данных, должен будет работать гораздо шустрее %LIKE%.
При использовании данного вида поиска проявился побочный, но очень полезный аналитический эффект: если, к примеру, задать запрос «погода в Москве» или «ГазМяс — МясГаз» то база весьма релевантно выведет именно то, что и требовалось.

Геотаргетинг.

Вот сердце проекта и готово, пора переходить к гейшам. Открываем главную страницу… и… и нас посещает мысль — А ЧТО ЭТО, вообще, такое? Новость на английском, следующая на финском, потом на русском, на иврите, вязью… Паук не халтурит, ходит по миру, собирает цидульки. Но нам бы почитать, и желательно на родном, Великом и Могучем…
Честно говоря, передо мной тут встала небольшая дилемма — способов определить, что за посетитель к нам пришёл и откуда — существует вагон и маленькая тележка, я же колебался между двумя — определением страны (и, соответственно, языка посетителя) по GeoIP, либо прямым определением языка, выставленного по умолчанию в браузере. В идеале, конечно, неплохо было бы скомбинировать оба способа для получения более точных результатов, но я решил ограничиться лишь информацией от браузера (не прогадал), а заодно и сэкономил на запросе к гео-базе. Выглядит это примерно так:
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
$lang = strtolower(substr($_SERVER['HTTP_ACCEPT_LANGUAGE'],0,2));

а в SQL-запрос надо добавить условие:
... WHERE sourcelang LIKE $lang

Смело нажимаем F5 и наслаждаться видом родных новостей. Конечно, стоит протестировать всё, заменяем в браузере предпочтительный язык, к примеру, на fi или ar, обновляем страницу, видим, что всё работает, возвращаем настройки обратно.
В процессе теста радостно понимаем, что новости по-арабски теперь увидеть-то мы увидим, а вот прочитать и осознать — это уже вряд ли (мы же не полиглоты?), поэтому к каждой новости на «ином» языке, отличающемся от нашего родного, в переменной $lang, выведем ссылку Translate using Google. Да, машинный перевод — не фонтан. Да, зачастую получается «Моя приехать эшельбе бешельме» — пусть так, зато другие языки нам уже не страшны, и нам не надо ничего копипастить в переводчики! Переходим к следующему, логично вытекающему из предыдущего, шагу:

Локализация интерфейса.

$messages = array (
'en' => array("forexample" => "For example"),
'de' => array("forexample" => "Zum Beispiel"),
'ru' => array("forexample" => "Например")
);
function msg($s) {
global $lang; global $messages;
if (isset($messages[$lang][$s])) return $messages[$lang][$s];
}

Тут особо пояснять и нечего, в нужном месте для вывода строки зовём msg(«forexample»). Сей код я честно подсмотрел в интернете и чуть допилил с учётом особенностей проекта. Задачу свою он выполняет, теперь люди будут видеть не только новости, но и интерфейс на своём родном языке.

Лента.

Хочу ещё гейш! С эффектами! Мы ведь делаем ленту новостей? Так пусть лента будет лентой! За нас уже всё сделано, обратимся к помощи JQuery:


При прокрутке страницы до конца вниз, срабатывает наш JQuery обработчик, подгружая следующие 30 записей, пока не достигнута трёхсотая.
Так как наша лента получается достаточно развёрнутой, неплохо было бы дать пользователю возможность вернуться к началу новостей. За это отвечает небольшой JQuery плагин UItoTop:

Экспорт.

Data Mining — это не просто сбор и обработка данных, это ещё и результаты этой обработки. А в таком проекте, как новостной, это ещё и неотъемлемая его часть, для этого и была придумана технология RSS, ведь если посетитель захочет стать постоянным читателем новостей, он будет в первую очередь искать именно эту оранжевую кнопку. Уведомим браузеры, что на нашем сайте есть источник XML для RSS:
/>

и сгенерируем непосредственно xml из базы. Я сделал это вручную.
После этого действа, во всех нюансах проверив работу новоиспечённой ленты, я создал несколько аккаунтов в Twitter и настроил экспорт своих RSS лент с помощью TwitterFeed. Добавил сайты в поисковые машины. Установил аналитику от Google. В твиттере появились первые подписчики, пошёл трафик. С поисковых машин тоже, помните, в самом начале статьи я упоминал про уникальный бренд — сработало, анализируя логи я увидел, что каждый третий заход на сайт был осуществлён через поисковую строку поисковых машин, которые честно выводили по запросу на первом месте искомый сайт.
Итак, интернациональный проект запущен и работает. Итоговое количество кода в главном модуле на PHP — 11 килобайт. А главное — достигнута цель — теперь я читаю только те новости, что и хотел, получилось универсально, с гейшами, так что можно и поделиться с сообществом.

Кода.

Планы. Собственно, Mining. Данные постепенно накапливаются, и уже скоро можно будет приступить к их аналитической обработке, например, применяя алгоритмы нейронных сетей. Сделать поиск похожих по содержанию (similar) новостей. Ну и всё то, о чём я сейчас, надеюсь, прочту в комментариях.
Пока шла работа над проектом, я старался покрыть все предметные области тестами. Но есть один тест, который мне неподвластен, и имя ему — Хабраэффект. Так и родилась эта статья. Протестим?

CaretFeed

Автор: CaretFeed

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js