Блог Александра Денисюка

Программирование на PHP и вёрстка сайтов

Вконтакте Твиттер Хабрахабр

Любые предложения или замечания принимаю на
почту a@denisyuk.by или на номер +375 (29) 203-79-48.

Закладки разработчика сайтов — выпуск #3

С начала этого года активно писал код для сервиса, у которого вся бизнес-логика завязана на работу с изображениями: загрузкахранениеобработкаотдача. Конечно же в браузере накопилось много закладок на эту тему. Самое вкусное публикую в этом выпуске. Здесь вы найдёте различные библиотеки на PHP и несколько крутых веб-сервисов. Для меня же наибольшую ценность представляет сам исходный код всего этого добра, потому как в них я нашёл много чего интересного для себя. Если ниже ничего не отображается, тогда воспользуйтесь запасной ссылкой.

Ещё ссылки по теме:

Cloudinary — облачное хранилище изображений
Uploadcare — конкурент Cloudinary от наших разработчиков
Imgix — набор инструментов для работы с картинками по URL
S-shot — сервис для создания скриншотов сайтов

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

Забирайте подборку в свои социальные сети. Сохраняйте
в закладки и делитесь с друзьями.

13 января   php   дайджест   закладки

Полное руководство по загрузке изображений на PHP

В этой статье подробно разберём механизм загрузки изображений на сервер с помощью PHP не прибегая к сторонним компонентам и фреймворкам. Научимся безопасно загружать изображения не только с локальной машины пользователя, но и удалённые файлы по ссылке. Все примеры кода я буду писать в процедурном стиле, дабы вы быстрее могли читать код, а не перескакивать с одного метода на другой. Руководство полностью авторское и не претендует на какую-либо академичность изложения.

§1. Общие принципы
§2. Правила безопасности
§3. Конфигурация php.ini
§4. Загрузка картинки из формы
§5. Загрузка изображения по ссылке
§6. Настройка выбора нескольких файлов

§1. Общие принципы

Всю последовательность загрузки изображения на сервер можно отобразить следующим образом: настройка php.iniполучение файлапроверка безопасностивалидация данныхсохранение на диск. Процесс загрузки картинки с компьютера пользователя или по URL ничем не отличаются, за исключением способа получения изображения и его сохранения. Общая схема загрузки картинки на сервер выглядит следующим образом:

Для валидации картинки по URL мы будем использовать функцию getimagesizefromstring(), т. к. cURL скачает её в переменную для дальнейших манипуляций.

Поскольку мы загружаем изображения на сервер, то хорошо было бы проверять их определённые параметры: ширину, высоту, тип картинки, размер файла в байтах. Это зависит от логики вашего приложения, но для наглядности в этом руководстве мы проверим все вышеописанные параметры.

§2. Правила безопасности

Безопасность загрузки изображений сводится к недопущению попадания на сервер чужеродного кода и его выполнения. На практике загрузка картинок наиболее уязвимое место в PHP-приложениях: попадание shell-скриптов, запись вредоносного кода в бинарные файлы, подмена EXIF-данных. Для того, чтобы избежать большинства методов взлома нужно придерживаться следующих правил:

а не доверять данным из $_FILES;
б не проверять MIME-тип картинки из функции getimagesize();
в загружаемому файлу генерировать новое имя и расширение;
г запретить выполнение PHP-скриптов в папке с картинками;
д не вставлять пользовательские данные через require и include;
е для $_FILES использовать is_uploaded_file() и move_uploaded_file().
Если есть чем дополнить «Правила безопасности», тогда оставляйте свои замечания или ссылки на статьи по безопасности в комментариях к этому руководству, а я опубликую их в этом параграфе.

Безопасная загрузка изображений на сервер
PHP check if file is an image
How to Securely Allow Users to Upload Files
Безопасная загрузка файлов на сервер
Надёжная защита веб-сайта от большинства видов атак
Ваш сайт тоже позволяет заливать всё подряд?
PHP image upload security check list
Unrestricted File Upload
Опасный getimagesize() или Zip Bomb для PHP
Почему надо пользоваться стандартными функциями при загрузке файлов

§3. Конфигурация php.ini

PHP позволяет внести определённые конфигурационные значения в процесс загрузки любых файлов. Для этого необходимо в файле php.ini найти блоки «Resource Limits», «Data Handling» и «File Uploads», а затем отредактировать, по необходимости, следующие значения:

[Resource Limits]
; Максимальное время выполнения скрипта в секундах
max_execution_time = 60

; Максимальное потребление памяти одним скриптом
memory_limit = 64M

[Data Handling]
; Максимально допустимый размер данных отправляемых методом POST
post_max_size = 5M

[File Uploads]
; Разрешение на загрузку файлов
file_uploads = On

; Папка для хранения файлов во время загрузки
upload_tmp_dir = home/user/temp

; Максимальный размер загружаемого файла
upload_max_filesize = 5M

; Максимально разрешённое количество одновременно загружаемых файлов
max_file_uploads = 10

Исходя из указанных значений, пользователь не сможет за один раз загрузить больше десяти файлов, причём каждый файл не должен превышать 5 Мбайт. Параметры из блока «Resource Limits» больше нужны для загрузки удалённого файла, т. к. с помощью cURL мы будем скачивать содержимое в переменную и проверять её по нужным нам критериям, а для этого необходимо дополнительное время и память.

Конфигурационный файл php.ini всегда необходимо настраивать согласно бизнес-логики разрабатываемого веб-приложения. Например, мы планируем загружать не более десяти файлов до 5 Мбайт, а это значит нам понадобиться ~50 Мбайт памяти. Кроме того, нам нужно знать максимальное время загрузки одного файла с локальной машины и по ссылке, дабы установить достаточное время выполнения скрипта в max_execution_time и не пугать пользователей ошибками.

§4. Загрузка картинок из формы

Сейчас мы не будем рассматривать загрузку нескольких файлов на сервер, а разберём лишь саму механику загрузки на примере одного файла. Итак, для загрузки картинки с компьютера пользователя необходимо с помощью HTML-формы отправить файл PHP-скрипту методом POST и указать способ кодирования данных enctype="multipart/form-data" (в данном случае данные не кодируются и это значение применяется только для отправки бинарных файлов). С формой ниже мы будем работать дальше:

<form action="file-handler.php" method="post" enctype="multipart/form-data">
  <input type="file" name="upload">
  <button>Загрузить</button>
</form>

Для поля выбора файла мы используем имя name="upload" в нашей HTML-форме, хотя оно может быть любым. После отправки файла PHP-скрипту file-handler.php его можно перехватить с помощью суперглобальной переменной $_FILES['upload'] с таким же именем, которая в массиве содержит информацию о файле:

Array
(
    [name]     => picture.jpg                // оригинальное имя файла
    [type]     => image/jpeg                 // MIME-тип файла
    [tmp_name] => home\user\temp\phpD07E.tmp // бинарный файл
    [error]    => 0                          // код ошибки
    [size]     => 17170                      // размер файла в байтах
)

Не всем данным из $_FILES можно доверять: MIME-тип и размер файла можно подделать, т. к. они формируются из HTTP-ответа, а расширению в имени файла не стоит доверять в силу того, что за ним может скрываться совершенно другой файл. Тем не менее, дальше нам нужно проверить корректно ли загрузился наш файл и загрузился ли он вообще. Для этого необходимо проверить ошибки в $_FILES['upload']['error'] и удостовериться, что файл загружен методом POST с помощью функции is_uploaded_file(). Если что-то идёт не по плану, значит выводим ошибку на экран.

<?php

// Перезапишем переменные для удобства
$filePath  = $_FILES['upload']['tmp_name'];
$errorCode = $_FILES['upload']['error'];

// Проверим на ошибки
if ($errorCode !== UPLOAD_ERR_OK || !is_uploaded_file($filePath)) {

    // Массив с названиями ошибок
    $errorMessages = [
        UPLOAD_ERR_INI_SIZE   => 'Размер файла превысил значение upload_max_filesize в конфигурации PHP.',
        UPLOAD_ERR_FORM_SIZE  => 'Размер загружаемого файла превысил значение MAX_FILE_SIZE в HTML-форме.',
        UPLOAD_ERR_PARTIAL    => 'Загружаемый файл был получен только частично.',
        UPLOAD_ERR_NO_FILE    => 'Файл не был загружен.',
        UPLOAD_ERR_NO_TMP_DIR => 'Отсутствует временная папка.',
        UPLOAD_ERR_CANT_WRITE => 'Не удалось записать файл на диск.',
        UPLOAD_ERR_EXTENSION  => 'PHP-расширение остановило загрузку файла.',
    ];

    // Зададим неизвестную ошибку
    $unknownMessage = 'При загрузке файла произошла неизвестная ошибка.';

    // Если в массиве нет кода ошибки, скажем, что ошибка неизвестна
    $outputMessage = isset($errorMessages[$errorCode]) ? $errorMessages[$errorCode] : $unknownMessage;

    // Выведем название ошибки
    die($outputMessage);
}

Для того, чтобы злоумышленник не загрузил вредоносный код встроенный в изображение, нельзя доверять функции getimagesize(), которая также возвращает MIME-тип. Функция ожидает, что первый аргумент является ссылкой на корректный файл изображения. Определить настоящий MIME-тип картинки можно через расширение FileInfo. Код ниже проверит наличие ключевого слова image в типе нашего загружаемого файла и если его не окажется, выдаст ошибку:

// Создадим ресурс FileInfo
$fi = finfo_open(FILEINFO_MIME_TYPE);

// Получим MIME-тип
$mime = (string) finfo_file($fi, $filePath);

// Проверим ключевое слово image (image/jpeg, image/png и т. д.)
if (strpos($mime, 'image') === false) die('Можно загружать только изображения.');

На данном этапе мы уже можем загружать абсолютно любые картинки на наш сервер, прошедшие проверку на MIME-тип, но для загрузки изображений по определённым характеристикам нам необходимо валидировать их с помощью функции getimagesize(), которой скормим сам бинарный файл $_FILES['upload']['tmp_name']. В результате мы получим массив максимум из 7 элементов:

Array
(
    [0]        => 1280                      // ширина
    [1]        => 768                       // высота
    [2]        => 2                         // тип
    [3]        => width="1280" height="768" // аттрибуты для HTML
    [bits]     => 8                         // глубина цвета
    [channels] => 3                         // цветовая модель
    [mime]     => image/jpeg                // MIME-тип
)

Для дальнейшей валидации изображения и работы над ним нам необходиом знать только 3 значения: ширину, высоту и размер файла (для вычисления размера применим функцию filesize() для бинарного файла из временной папки).

// Результат функции запишем в переменную
$image = getimagesize($filePath);

// Зададим ограничения для картинок
$limitBytes  = 1024 * 1024 * 5;
$limitWidth  = 1280;
$limitHeight = 768;

// Проверим нужные параметры
if (filesize($filePath) > $limitBytes) die('Размер изображения не должен превышать 5 Мбайт.');
if ($image[1] > $limitHeight)          die('Высота изображения не должна превышать 768 точек.');
if ($image[0] > $limitWidth)           die('Ширина изображения не должна превышать 1280 точек.');

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

// Сгенерируем новое имя файла на основе MD5-хеша
$name = md5_file($filePath);

// Сгенерируем расширение файла на основе типа картинки
$extension = image_type_to_extension($image[2]);

// Сократим .jpeg до .jpg
$format = str_replace('jpeg', 'jpg', $extension);

// Переместим картинку с новым именем и расширением в папку /pics
if (!move_uploaded_file($filePath, __DIR__ . '/pics/' . $name . $format)) {
    die('При записи изображения на диск произошла ошибка.');
}

На этом загрузка изображения завершена. Для более удобной загрузки файлов можете использовать класс UploadedFile из пакета Symfony HttpFoundation, который является обёрткой для $_FILES и также сохраняет файл через move_uploaded_file().

§5. Загрузка изображения по ссылке

Для загрузки изображения по ссылке нам понадобиться библиотека cURL, которая работает с удалёнными ресурсами. С помощью неё мы скачаем контент в переменную. С одной стороны может показаться, что для этих целей подойдёт file_get_contents(), но на самом деле мы не сможем контролировать объём скачиваемых данных и нормально обрабатывать все возникшие ошибки. Для того, чтобы cURL корректно скачал данные нам нужно: разрешить следовать перенаправлениям, включить проверку сертификата, указать максимальное время работы cURL (формируется за счёт объёма скачиваемых данных и средней скорости работы с ресурсом). Как правильно скачать файл в переменную показано ниже с необходимыми параметрами:

<?php

// Каким-то образом получим ссылку
$url = 'https://site.ru/picture.jpg';

// Проверим HTTP в адресе ссылки
if (!preg_match("/^https?:/i", $url) && filter_var($url, FILTER_VALIDATE_URL)) {
    die('Укажите корректную ссылку на удалённый файл.');
}

// Запустим cURL с нашей ссылкой
$ch = curl_init($url);

// Укажем настройки для cURL
curl_setopt_array($ch, [

    // Укажем максимальное время работы cURL
    CURLOPT_TIMEOUT => 60,

    // Разрешим следовать перенаправлениям
    CURLOPT_FOLLOWLOCATION => 1,

    // Разрешим результат писать в переменную
    CURLOPT_RETURNTRANSFER => 1,

    // Включим индикатор загрузки данных
    CURLOPT_NOPROGRESS => 0,

    // Укажем размер буфера 1 Кбайт
    CURLOPT_BUFFERSIZE => 1024,

    // Напишем функцию для подсчёта скачанных данных
    // Подробнее: http://stackoverflow.com/a/17642638
    CURLOPT_PROGRESSFUNCTION => function ($ch, $dwnldSize, $dwnld, $upldSize, $upld) {

        // Когда будет скачано больше 5 Мбайт, cURL прервёт работу
        if ($dwnld > 1024 * 1024 * 5) {
            return -1;
        }
    },

    // Включим проверку сертификата (по умолчанию)
    CURLOPT_SSL_VERIFYPEER => 1,

    // Проверим имя сертификата и его совпадение с указанным хостом (по умолчанию)
    CURLOPT_SSL_VERIFYHOST => 2,

    // Укажем сертификат проверки
    // Скачать: https://curl.haxx.se/docs/caextract.html
    CURLOPT_CAINFO => __DIR__ . '/cacert.pem',
]);

$raw   = curl_exec($ch);    // Скачаем данные в переменную
$info  = curl_getinfo($ch); // Получим информацию об операции
$error = curl_errno($ch);   // Запишем код последней ошибки

// Завершим сеанс cURL
curl_close($ch);

Если всё прошло успешно и cURL уложился в 60 секунд, тогда содержимое по ссылке будет скачано в переменную $raw. Кроме того, функция curl_getinfo() вернёт информацию о проделанном запросе, откуда мы можем получить дополнительную информацию для анализа работы с удалёнными ресурсами:

Array
(
    [content_type]            => image/jpeg // MIME-тип из Content-Type
    [http_code]               => 200        // последний HTTP-код
    [redirect_count]          => 0          // количество перенаправлений
    [total_time]              => 0.656      // общее время работы cURL
    [connect_time]            => 0.188      // время на соединение с хостом
    [size_download]           => 4504       // реальный размер полученных данных
    [download_content_length] => 4504       // размер данных из Content-Length
    /* ... */
)

Дальше нам нужно проверить нет ли ошибок в curl_errno() и удостовериться, что ресурс отдаёт HTTP-код равный 200, иначе мы скажем, что по такому-то URL ничего не найдено. После всех проверок переменную $raw передаём в getimagesizefromstring() и работаем уже по отработанной схеме как в случае с загрузкой картинок из формы.

Обратите внимание, что мы валидируем размер файла в момент получения данных, т. к. мы не можем на 100% доверять curl_getinfo(), поскольку значения content_type, http_code, download_content_length формируются на основе полученных HTTP-заголовков. Скачивать файл полностью, а потом проверять количество байт потребует много времени и памяти. Поэтому мы контролировали размер получаемых данных с помощью опции CURLOPT_PROGRESSFUNCTION: как только cURL получит больше данных, чем наш лимит, он прекратит работу и выдаст ошибку CURLE_ABORTED_BY_CALLBACK.
// Проверим ошибки cURL и доступность файла
if ($error === CURLE_OPERATION_TIMEDOUT)  die('Превышен лимит ожидания.');
if ($error === CURLE_ABORTED_BY_CALLBACK) die('Размер не должен превышать 5 Мбайт.');
if ($info['http_code'] !== 200)           die('Файл не доступен.');

// Создадим ресурс FileInfo
$fi = finfo_open(FILEINFO_MIME_TYPE);

// Получим MIME-тип используя содержимое $raw
$mime = (string) finfo_buffer($fi, $raw);

// Закроем ресурс FileInfo
finfo_close($fi);

// Проверим ключевое слово image (image/jpeg, image/png и т. д.)
if (strpos($mime, 'image') === false) die('Можно загружать только изображения.');

// Возьмём данные изображения из его содержимого
$image = getimagesizefromstring($raw);

// Зададим ограничения для картинок
$limitWidth  = 1280;
$limitHeight = 768;

// Проверим нужные параметры
if ($image[1] > $limitHeight) die('Высота изображения не должна превышать 768 точек.');
if ($image[0] > $limitWidth)  die('Ширина изображения не должна превышать 1280 точек.');

// Сгенерируем новое имя из MD5-хеша изображения
$name = md5($raw);

// Сгенерируем расширение файла на основе типа картинки
$extension = image_type_to_extension($image[2]);

// Сократим .jpeg до .jpg
$format = str_replace('jpeg', 'jpg', $extension);

// Сохраним картинку с новым именем и расширением в папку /pics
if (!file_put_contents(__DIR__ . '/pics/' . $name . $format, $raw)) {
    die('При сохранении изображения на диск произошла ошибка.');
}

Для сохранения изображения на диск можно воспользоваться file_put_contents(), которая запишет контент в файл. Новое имя файла мы создадим через функцию md5(), а расширение сделаем из image_type_to_extension(). Теперь мы можем загружать любые картинки по ссылке.

§6. Настройка выбора нескольких файлов

В этом параграфе разберём способы загрузки нескольких изображений за один раз с локальной машины пользователя и по удалённым ссылкам. Для отправки ссылок мы задействуем $_POST и передадим ей все данные с помощью тега textarea. Для загрузки файлов из формы мы продолжим дальше работать с $_FILES. Наша новая HTML-форма будет немного отличаться от старой.

<form action="handler.php" method="post" enctype="multipart/form-data">
  <textarea name="upload"></textarea>
  <input type="file" name="upload[]" multiple>
  <button>Загрузить</button>
</form>

В конец имени поля выбора файла name="upload[]" добавились фигурные скобки и аттрибут multiple, который разрешает браузеру выбрать несколько файлов. Все файлы снова загрузятся во временную папку, если не будет никаких ошибок в php.ini. Перехватить их можно в $_FILES, но на этот раз суперглобальная переменная будет иметь неудобную структуру для обработки данных в массиве. Решается эта задача небольшими манипуляциями с массивом:

<?php

// Изменим структуру $_FILES
foreach($_FILES['upload'] as $key => $value) {
    foreach($value as $k => $v) {
        $_FILES['upload'][$k][$key] = $v;
    }

    // Удалим старые ключи
    unset($_FILES['upload'][$key]);
}

// Загружаем все картинки по порядку
foreach ($_FILES['upload'] as $k => $v) {

    // Загружаем по одному файлу
    $_FILES['upload'][$k]['tmp_name'];
    $_FILES['upload'][$k]['error'];
}

Для загрузки нескольких картинок по URL передадим наши ссылки через textarea с именем name="upload", где их можно указать через пробел или с новой строки. Функция preg_split разберёт все данные из $_POST['upload'] и сформирует массив, по которому нужно пройтись циклом и каждый валидный URL отправить в обработчик.

$data = preg_split("/\s+/", $_POST['upload'], -1, PREG_SPLIT_NO_EMPTY);

foreach ($data as $url) {
    // Валидируем и загружаем картинку по URL
}

Вы можете улучшить форму для загрузки изображений, например, воспользоваться библиотекой FineUploader или jQuery FileUpload, чтобы настроить выбор картинок с определённым расширением.

Важные замечания:

(1) в файле php.ini значения post_max_size и upload_max_filesize должны совпадать;
(2) до валидации изображения стоит говорить, что мы работаем с файлом;
(3) используйте класс UploadedFile для $_FILES в боевых условиях;
(4) ошибку UPLOAD_ERR_FORM_SIZE можно словить, если в форме указать MAX_FILE_SIZE;
(5) библиотека FastImage работает быстрее getimagesize();
(6) для имитации браузера в cURL нужно установить CURLOPT_USERAGENT;
(7) ограничить редиректы можно через CURLOPT_MAXREDIRS.

Дополнительное чтиво:

Как загрузить картинку по ссылке
Загрузка и хранение фотографий в Web приложениях
Отдаем файлы эффективно с помощью PHP
Хранение файлов
Теория хранения файлов на сервере
Можно ли полагаться на HTTP-заголовки при загрузке файла?

Найди в этом посте себя

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

— Добавить себя в список можно, если написать мне в личку или на почту.
25 декабря   блог   подписки

Поиск клиентов

Этим постом хочу обратиться к аудитории Бабло.клик и попросить бывалых рассказать как они ищут клиентов. Для меня сфера деятельности не имеет значения, а только сам подход к делу. Возможно клиенты теперь сами к вам приходят, а быть может вы работаете по распространённой схеме: клепаете лендингзапускаете контекстную рекламупринимаете заказы. Мне интересны все современные способы. Перед тем как ответить на вопрос «Как искать клиентов?» я поделюсь своим нестандартным опытом.

1 Очень много дизайнеров, редакторов, верстальщиков и просто хороших людей ведут блоги для души или с какой-то целью. Все, естественно, хотят засветиться и возможно стать популярными блоггерами как Артемий Лебедев. Поэтому большую популярность набирают посты, в которых перечисляются блоггеры и их профессиональные качества. Они могут называться так: «Давайте знакомиться», «На кого подписаться». Такие посты хорошо прокликиваются и если засветиться в самом начале, то можно получить большое количество релевантного трафика. Таким образом мне сами написали несколько заказчиков.

2 На сайте одной веб-студии можно было задать вопрос по дизайну, так сказать одна из форм коммуникации со своими подписчиками. В форме можно было прикрепить картинку. Конечно же валидации никакой не было и я загрузил самописный shell-скрипт. Оказалось у них на хостинге размещено больше десятка действующих клиентских сайтов. Я походил по папкам и файлам, а потом отправил письмо в студии о найденной уязвимости. Через пару часов они мне ответили и предложили разработать один клиентский сайт.

3 В Твиттере проскакивает довольно таки много предложений на небольшие заказы или вакансии на удалённую работу. Здесь главное подписаться на большое количество людей в своей сфере и каждый день читать свою ленту. В моём Твиттере около ~200 подписок и каждый месяц кто-то публикует твиты «ищу программиста», «нужен PHP-проггер». В начале ноября Дмитрий Шахов таким путём искал PHP-программиста на разработку сайта с нуля. Мне довелось ознакомиться с ТЗ и как следствие я отправил Дмитрию своё коммерческое предложение.

— Эй, бро! Как ты ищешь клиентов?
18 декабря   бизнес   маркетинг   фриланс

Карта позиционирования веб-студий

Этим летом проводил небольшое маркетинговое исследование позиционирования белорусских веб-студий по качеству и цене. Данные брал на TAM.BY и CMSmagazine. Под микроскоп попали студии, которые только программируют, делают один лишь дизайн, а также студии, которые покрывают весь спектр услуг, включая продвижение. Все результаты заносил в Excel, а затем отобразил их в графическом виде. Получилась интересная карта позиционирования:

Все веб-студии по результатам исследования были разделены на группы по общему представлению и получилось три эшелона, которые в шутку можно назвать так: «школота», «профи», «боги». Кстати говоря, такое представление как на графике можно экстраполировать на любую деятельность в сфере услуг. Везде будут лидеры и догоняющие с одинаковым процентным соотношением. Считайте, что на графике изображён паттерн позиционирования по качеству и цене чего угодно на любом рынке.

Чем ниже цена, тем и ниже качество предоставляемых услуг. Я не встретил ни одной веб-студии, которая бы предоставляла хороший сайт за небольшие деньги и наоборот. Поэтому на графике в верхнем левом и нижнем правом углах никого нет. Веб-студии распределены линейно и чем выше качество с ценой, тем меньше в этой области участников. Дальше приведу краткую характеристику по каждому эшелону со своей точки зрения и уже без учёта статистики:

«Школота». Это третий эшелон, где создают говносайты на файлах или визитки на WordPress. Здесь, как правило, изменяют готовые шаблоны сайтов под нужды заказчиков и в итоге не решают задач бизнеса. Большинство исполнителей без опыта работы.

«Профи». Второй эшелон закрывает потребности малого и среднего бизнеса. Тут понимают как устроен бизнес и могут сказать, какие технологии лучше выбрать. У них не так хорошо с маркетингом и их легко обойти за счёт сильного коммерческого предложения.

«Боги». Первый эшелон это всегда лидеры рынка, которые борются за большие деньги на тендерах. Имеют госзаказы. Тратят часть своей прибыли на маркетинг, проводят конференции и капитализируют свой бренд. Рекламу этих веб-студий можно увидеть на любой крупной конференции.

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


Создание сайта: от идеи до реализации
Как выжить региональной веб-студии
Памятка владельцу бизнеса, который хочет клиентов из интернета
Веб-студии стали меньше получать за сайты, но за клиентов почему-то не борются
Где заказчики ищут подрядчиков для разработки сайта

Ctrl + ↓ Ранее