Сегодня мы будем писать капчу уровня радио анонимус!
Конечно, у вас такое может не сработать и выдать что-то вроде
PHP Fatal error: Call to undefined function imagecreatetruecolor()
На дешевых серверах такое бывает, достаточно ввести
apt-get install php5-gd
И ваша проблема будет солвед.
Если и это не помогает, вы можете вставить в начало скрипта строчку
phpinfo()
И посмотреть информацию о php, хотя проще купить нормальный хостинг.
Осторожнее со сжатием, формат PNG достаточно молод и не может позволить себе сжатия со степенью компрессии 100, как это позволяет себе JPEG, если вы попытаетесь также сильно сжать картинку, то увидите просто ошибку:
gd-png: fatal libpng error: zlib failed to initialize compressor -- stream error gd-png error: setjmp returns error condition
Ничего страшного, просто запомните, что PNG сильнее чем в 9 степень сжимать нельзя и будет все хорошо, главное не выходите за эти рамки. И вы увидите такую вот картинку:
Но что это с нашей картинкой??!!!
Оказалось, что PHP делали в америке, где всем насрать на русский язык, поэтому и вместо русских букв вот такая окалесица. Оказалось, что в шрифтах, которые использует PHP, просто нету кириллицы! Чего только не придумают, как бы русского человека унизить, даже многовековые наборы символов могут спокойно проигнорировать.
Но не беда! Мы можем все спасти! Для этого скачиваем нормальные шрифты:
http://narod.ru/disk/17653364000/ARIALUNI.TTF.html
Весит немного, всего 22 мегабайта, что в современных условиях не представляет каких-либо проблем, кладем его на сервер рядом с нашим скриптом, и прописываем путь к файлу в скрипте, все должно заработать.
Надо сказать, что и тут меня ждала маленькая проблема. Маленькое отступление: у меня 2 сервера, один для рабочих проектов под windows, а другой для мелких экспериментов на freebsd. Так вот, на рабочем сервере все заработало сразу, никаких проблем не возникло, а другой сервер ну никак не хотел видеть этот файл. Скорее всего это произошло из-за операционной системы, так как все остальное на серверах идентично. Из своего опыта я понял, что не стоит экономить пару процентов цены хостинга, время стоит дороже, качественный продукт всегда стоит своих денег. В скором времени я отказался от сервера под freebsd и взял в аренду еще один сервер на windows, что решило все мои проблемы со стабильностью и разными "глупыми" ошибками.
Добавляем в наш код строчку:
imagettftext($canvas,30,0,0,50,imagecolorallocate($canvas,255,0,0),"c:/php-captcha/ArialUni.TTF","Теперь и на русском!");
И все! В общем, в итоге у меня получилось вот так:
Как видите, родные русские буквы теперь отображаются корректно, все как нужно!
Итак, у нас все готово для создания капчи нашей мечты!
Что делает капча:
1. Создает строчку-задание, которое она будет отображать и которую надо будет ввести
2. Записывает строчку в базу, дабы потом узнать, правильно ли она была введена
3. Создает картинку с созданной ранее строчкой
4. Накладывает геометрические искажения, дабы было сложно автоматизировать ввод текста
5. И на последок просто отдается картинка пользователю, с запретом кеширования
1. Как создать текст
Можно конечно сгенерировать случайный набор символов, но такая капча не будет интересна, она не сможет говорить с пользователем и намекать ему, а следовательно аудиторию будет сложнее держать в лояльном состоянии.
Я предлагаю использовать следующий алгоритм: берем какое-то начало слова, к примеру "электро", из такого начала можно сделать много других слов: электромясорубка, электробритва, электростимуляция, электросекс, электроэякуляция. Конечно, мы не будем хранить словарь всех возможных продолжений, это было бы слишком затратно, да и не нужно это. Вместо этого будем продолжать по следующему принципу: возьмем последние 2 буквы, в данном случае будет "ро" и посмотрим, какие буквы в словах русского языка используются чаще всего. К примеру, после "ро" можно ожидать букву "м" с большей вероятностью, чем "о", так как "роо" используется в словах гораздо реже (гласные редко идут друг за другом). И так буква за буквой мы будем строить наше слово. Окончание слова подбираем по этому же принципу: к примеру, если есть слово "рот", а у нас есть слово оканчивающееся на "ро", то просто добавляем букву "т" и слово будет завершено. Конечно, все зависит от словаря, если в нем есть слово РОССИЯ, то слово дополняется буквами "ССИЯ" и тем самым завершается. Все очень просто.
Для начала создадим базу данных, где будем хранить все-все наши словари и данные.
Откройте приглашение командной строки и введите
> mysql -p
Утилита сама подключится к локальному серверу с именем root, поэтому ничего такого сложного указывать не надо. В появившемся приглашении введите:
mysql> create database captcha;
mysql> use captcha
этим самым мы создаем базу данных, которую будем использовать в дальнейшем. Вторая строка выбирает эту базу для дальнейшей работы.
Теперь нам надо создать таблицы. К сожалению, MySQL поддерживает только табличный формат и мы вынуждены заранее определить, какие именно данные мы будем использовать. Это крайне сложный этап проектирования, здесь очень легко сделать ошибку и эти ошибки могут быть достаточно дороги в будущем.
Мы создадим 5 таблиц для следующих целей:
1. Для префиксов слов, из чего будем строить дальше
2. Для суффиксов слов, которые будут окончаниями
3. Для информации, какую букву использовать в середине слов
4. Для хранения заданий
5. Для статистики угадывания и мониторинга возможных проблем
Примерно вот такие таблицы у нас получаются:
CREATE TABLE Prefixes ( text varchar(255) ); CREATE TABLE Suffixes ( prev varchar(255), next varchar(255) ); CREATE TABLE Middle ( prev varchar(255), next varchar(255) ); CREATE TABLE Tasks ( ip varchar(255), zadanie varchar(255), timeout varchar(255) ); CREATE TABLE Stat ( text varchar(255), uspeh varchar(255), time varchar(255) );
Введите это в терминал, вы увидете примерно следующее:
mysql> CREATE TABLE Prefixes -> ( -> text varchar(255) -> ); CREATE TABLE Suffixes ( Query OK, 0 rows affected (0.03 sec) mysql> mysql> CREATE TABLE Suffixes -> ( -> prev varchar(255), -> next varchar(255) -> ); Query OK, 0 rows affected (0.00 sec) mysql> mysql> CREATE TABLE Middle -> ( -> prev varchar(255), -> next varchar(255) -> ); Query OK, 0 rows affected (0.00 sec) mysql> mysql> CREATE TABLE Tasks -> ( -> ip varchar(255), -> zadanie varchar(255), -> timeout varchar(255) -> ); Query OK, 0 rows affected (0.00 sec) mysql> mysql> CREATE TABLE Stat -> ( -> text varchar(255), -> uspeh varchar(255), -> time varchar(255) -> ); Query OK, 0 rows affected (0.01 sec) mysql>
Это значит, что все прошло успешно, мы только что создали таблицы для хранения наших данных. Теперь осталось только загрузить данные, которые у меня в текстовых файлах с символами табуляции между полями. Способов много. Если бы мы использовали нормальную базу данных, то могли бы использовать команду BULK INSERT, но вместо базы данных у нас будет MySQL, поэтому можно поставить myPhpAdmin и залить через него, ну или выполнить что-то вроде:
LOAD DATA INFILE 'c:/php-captcha/Prefixes.txt' INTO TABLE Prefixes FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n';Увидим примерно вот такое:
mysql> LOAD DATA INFILE 'c:/php-captcha/Prefixes.txt' INTO TABLE Prefixes FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'; Query OK, 1467 rows affected (0.02 sec) Records: 1467 Deleted: 0 Skipped: 0 Warnings: 0 mysql> LOAD DATA INFILE 'c:/php-captcha/Suffixes.txt' INTO TABLE Suffixes FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'; Query OK, 261 rows affected (0.00 sec) Records: 261 Deleted: 0 Skipped: 0 Warnings: 0 mysql> LOAD DATA INFILE 'c:/php-captcha/Middle.txt' INTO TABLE Middle FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'; Query OK, 5000 rows affected (0.00 sec) Records: 5000 Deleted: 0 Skipped: 0 Warnings: 0 mysql>
Все, наши префиксы/суффиксы/серединки записаны, теперь мы можем удостовериться что все работает корректно, давайте запросим у базы 10 случайных кусочков слова, с которых будет начинаться наша капча:
mysql> SELECT * FROM Prefixes ORDER BY rand() LIMIT 10; +------------+ | text | +------------+ | перво | | благ | | лес | | тере | | ав | | шер | | фре | | гра | | вит | | зан | +------------+ 10 rows in set (0.02 sec)mysql>
Как видите, все работает успешно, а теперь выберем окончание к слову "перво":
mysql> SELECT * FROM Suffixes WHERE prev LIKE 'во' ORDER BY rand() LIMIT 10; +------+------+ | prev | next | +------+------+ | во | е | | во | й | | во | м | +------+------+ 3 rows in set (0.00 sec)mysql>
Следовательно, мы можем получить капчи: "первое", "первой" и "первом". Если капча слишком коротка, то мы выбираем следующую букву из таблицы Middle, а только когда капча будет достаточно длинной, мы можем выбрать хвост для нее. На самом деле тут есть тонкость, смотрите код далее.
Пора применить все это на практике, давайте писать код!
<?php // Определим пару констант const CAPTCHA_LEN_MIN=7; const CAPTCHA_LEN_MAX=15; // Устанавливаем соединение с сервером баз данных $baza=mysql_connect('127.0.0.1','root','qwerty') or die('Не могу подключиться: '.mysql_error()); echo 'Соединение установлено успешно'; // Выбираем базу, с которой будем работать mysql_select_db('captcha') or die('Не могу выбрать нужную базу данных'); // Надо сказать, что создание капчи - сложный // процесс, можно ведь создать такую капчу, для // которой и нет окончания в русском языке. Ведь // язык могуч и силен, поэтому возможно нам // придется сделать несколько каптч, но об этом ниже $weDone=0; while(!$weDone){ // Крутим цикл, пока не получится // Выбираем какое-то начало слова $Prefixes=mysql_fetch_row(mysql_query("SELECT * FROM Prefixes ORDER BY rand() LIMIT 1")); // Наше начало слова используем как слово $word=$Prefixes[0]; // Пока капча маленькая, ее можно растить, крутим еще цикл while(strlen($word)/2<CAPTCHA_LEN_MAX){ $tail=substr($word,-4); // отрежем хвостик // А вот и хитрость: ищем хвостик сначала в таблице окончаний // Дело в том, что окончаний всегда меньше чем нужно, на все // варианты никак не хватит, поэтому при первой возможности // завершаем капчу. Конечно, если она достаточно подросла. if(strlen($word)/2>=CAPTCHA_LEN_MIN){ $SuffixRequest=mysql_query("SELECT * FROM Suffixes WHERE prev LIKE '".$tail."' ORDER BY rand() LIMIT 1"); if(mysql_num_rows($SuffixRequest)){ $Suffixes=mysql_fetch_row($SuffixRequest); $word.=$Suffixes[1]; $weDone=1; // ДАДА, мы закончили! break; } } $MiddleRequest=mysql_query("SELECT * FROM Middle WHERE prev LIKE '".$tail."' ORDER BY rand() LIMIT 1"); if(mysql_num_rows($MiddleRequest)){ $Middle=mysql_fetch_row($MiddleRequest); $word.=$Middle[1]; } else { // Ситуация, когда мы создали такое слово, которое даже продолжить не можем. // Можно было бы отрезать 1-2 символа с конца и попробовать снова, но... // Теория эволюции говорит, и мой код создания капчи это подтверждает, что в // жопу можно катится очень долго и это не будет заметно, поэтому лучше // начать сначала, иначе мы рискуем очень долго крутить циклы, а ведь у нас // хайлоад (высокопроизводительное приложение для веба с массовым // обслуживанием множества клиентов), поэтому подвисание капчи для нас // недопустимо. break; } } }
Вот в общем-то и все. Теперь капча содержится в переменной $word и можем идти далее
В общем-то тут все просто, нам нужно IP, наше задание (оно у нас уже в переменной $word) и таймаут, дабы наша капча могла протухать.
if(!mysql_query("INSERT INTO Tasks (ip,zadanie,timeout) VALUES (".$IP.",'".$word."',".(time()+3600).");")){ die("Произошла ошибка при сохранении задания в базе данных"); }
Один из наших кастомеров имел проблем с получением сообщения:
PHP Fatal error: Call to undefined function mysql_connect()
apt-get install php5-mysql
Это пожалуй самая интересная часть при создании нашей капчи.
// ЧАСТЬ 3 // Делаем картинку и делаем на ней нашу капчу // Забегая вперед скажу, что картинку надо сделать побольше // С одной стороны, потому что капчи у нас могут быть длинными // С другой стороны, тем самым мы сделаем супер-семплинг и повысим качество $text=imagecreatetruecolor(4000,280); $razmery=imagettftext($text,200,0,0,220,imagecolorallocate($text,255,255,255),"C:/php-captcha/ARIALUNI.TTF",$word); // Узнаем размеры нашей картинки $shirina=$razmery[4]; $visota=$razmery[3];
// Часть 4 // Теперь определим функцию для искажения. Потом поймете почему function iskazi($text,$shirina,$visota){ // Определим параметры функции, которая будет нам выдавать искажения $chastota=rand(100,300); $amplituda=rand(20,50); $faza=rand(1,100000); // Cоздадим еще одну картинку, на которой будем рисовать $work=imagecreatetruecolor($shirina,$visota+$amplituda*2); for($h=0;$h<$visota;$h++){ for($w=0;$w<$shirina;$w++){ // Берем точку $tochka=imagecolorat($text,$w,$h); // Переносим эту точку с учетом искажения imagesetpixel($work, $w, $h+sin(($w+$faza)/$chastota)*$amplituda+$amplituda, $tochka); } } return($work); }
И вот почему мы оформили это все в виде функции
// Исказим же! $text=iskazi($text,$shirina,$visota);$shirina=imagesx($text);$visota=imagesy($text); $text=iskazi($text,$shirina,$visota);$shirina=imagesx($text);$visota=imagesy($text); $text=iskazi($text,$shirina,$visota);$shirina=imagesx($text);$visota=imagesy($text); $text=iskazi($text,$shirina,$visota);$shirina=imagesx($text);$visota=imagesy($text);
// Повернем на 90 градусов (так быстре, у нас же хайлоад) $text=imagerotate($text,90,0); // И сделаем еще искажений $shirina=imagesx($text);$visota=imagesy($text);$text=iskazi($text,$shirina,$visota); $shirina=imagesx($text);$visota=imagesy($text);$text=iskazi($text,$shirina,$visota); $shirina=imagesx($text);$visota=imagesy($text);$text=iskazi($text,$shirina,$visota); $shirina=imagesx($text);$visota=imagesy($text);$text=iskazi($text,$shirina,$visota); // Повернем обратно $text=imagerotate($text,-90,0);
Код очень быстрый, анроллы работают быстрее циклов, поворот на 90 градусов тоже быстрый
Новички могли бы поддаться на соблазн поворачивать картинку на 10-20 градусов, да еще и в цикле, но при этом бы сильно проиграли в производительности.
// Часть 5 // Осталось только отресайзить картинку под размер обычной капчи, при этом уйдут искажения $resultat=imagecreatetruecolor(128,28); imagecopyresampled($resultat,$text,0,0,0,0,128,28,imagesx($text),imagesy($text)); imagealphablending($resultat,false); imagesavealpha($resultat,true); // Теперь удалим лишние цвета, дабы картинка лучше сжималась for($h=0;$h<28;$h++){ for($w=0;$w<128;$w++){ imagesetpixel($resultat,$w,$h,imagecolorat($resultat,$w,$h)>0?imagecolorallocatealpha($resultat,0,0,0,0):imagecolorallocatealpha($resultat,0,0,0,127)); } } // Установим запрет кеширования (строчки из мануала php) header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Дата в прошлом // Начнем буферизацию и получим нашу картинку в виде PNG ob_start(); imagepng($resultat,NULL,9); $kartinka=ob_get_contents(); ob_end_clean(); // будем умницами и почистим память наших канвасов, это важно imagedestroy($resultat); imagedestroy($text); // Отдадим картинку пользователю, пусть он будет счастлив echo $kartinka; // А теперь сохраним нашу картинку, для выдачи, к примеру, другим пользователям $fh=fopen("captcha.png","w+"); fwrite($fh,$kartinka); fclose($fh);
Ну вот и все.
О производительности
Надо сказать, что 1 капча генерируется у меня на компьютере несколько секунд - это нормально. Во-первых, сервер всегда будет быстрее средней персоналки, так и в моем случае - на сервере генерация капчи занимает в среднем 1 секунду. Но это не значит, что я могу обслужить только 1 запрос в секунду, сервера сейчас с множеством ядер, как минимум на своем сервере я могу обслужить 16 запросов в секунду, что уже является хайлоадом. Можно и дальше масштабировать решение, просто добавляя сервера для генерации. Поверьте, если у вас такие нагрузки, то денег с монетизации должно всяко хватить на дополнительные сервера, это не проблема.
==26168== HEAP SUMMARY: ==26168== in use at exit: 26,211 bytes in 1,600 blocks ==26168== total heap usage: 29,646 allocs, 28,046 frees, 36,045,912 bytes allocated ==26168== ==26168== LEAK SUMMARY: ==26168== definitely lost: 258 bytes in 2 blocks ==26168== indirectly lost: 0 bytes in 0 blocks ==26168== possibly lost: 0 bytes in 0 blocks ==26168== still reachable: 25,953 bytes in 1,598 blocks ==26168== suppressed: 0 bytes in 0 blocks ==26168== Rerun with --leak-check=full to see details of leaked memory ==26168== ==26168== For counts of detected and suppressed errors, rerun with: -v ==26168== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 116 from 16)
Как видите, расход памяти минимален и вполне подходит для любых серверов
На Радио Анонимус есть немного более оптимальная версия с использованием memcached, она работает сносно и время выдачи капчи там немного меньше. Вы можете приобрести полную версию с оптимизацией всего за $2000, для проектов со слабой монетизацией это может быть выгоднее покупки облачных ресурсов. Вы конечно можете попробовать реализовать это самостоятельно, но у вас уйдет больше времени, чем стоимость этой капчи, разработка себя не окупит, гораздо выгоднее купить готовое решение.