├── .gitignore ├── LICENSE ├── README.md ├── example.config.php ├── parser.class.php └── vk.class.php /.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | *.db 3 | *.log 4 | logs/ 5 | .idea/ 6 | image.jpg 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Troynin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Парсинг контента ВКонтакте 2 | 3 | PHP парсер, который позволяет брать разные посты из групп ВК и добавлять в свою группу. 4 | 5 | ## Перед началом работы 6 | Прежде всего проверьте, включен ли SQLite3 (http://php.net/manual/ru/sqlite3.installation.php) и cURL (https://stackoverflow.com/questions/1347146/how-to-enable-curl-in-php-xampp). Настройка может отличаться в зависимости от ОС, условий запуска (хостинг) и т.д. 7 | 8 | ## Настройка 9 | Для начала работы необходимо открыть example.config.php и сделать следующее: 10 | 11 | 1. Указать `VK_GROUP_ID` - ID своей группы; 12 | 2. Получить `VK_ACCESS_TOKEN`. В настройках (`example.config.php`) описана инструкция, как это сделать. Приложения создаются по ссылке [здесь](https://vk.com/apps?act=manage); 13 | 3. Перечислите группы, из которых будет забираться контент в массиве `$groups`; 14 | 4. Переименовать `example.config.php` в `config.php`; 15 | 5. Запустите скрипт через браузер или CRON (есть также опция - разрешать запуск не через CRON, или нет). Пример команды для CRON указан в том же файле настроек. 16 | 17 | Готово. Теперь посты из других групп будут добавляться в вашу. В файле настроек (`config.php`) есть ещё много различных опций с описанием их работы. Вы можете изменить их под ваши нужды. 18 | -------------------------------------------------------------------------------- /example.config.php: -------------------------------------------------------------------------------- 1 | log('Файл с настройками не найден - остановка'); 12 | 13 | die('Файл с настройками не найден. Вероятно, у вас есть example.config.php. ' . 14 | 'Заполните его своими данными и переименуйте в config.php.'); 15 | } 16 | 17 | include 'config.php'; // Конфигурация скрипта 18 | include 'vk.class.php'; // Класс для взаимодействия с API вконтакте 19 | 20 | if (!VK_ACCESS_TOKEN) { 21 | $this->log('Не указан "VK_ACCESS_TOKEN" в настройках'); 22 | 23 | exit; 24 | } 25 | 26 | if (!VK_API_VERSION) { 27 | $this->log('Не указан "VK_API_VERSION" в настройках'); 28 | 29 | exit; 30 | } 31 | 32 | if (!VK_GROUP_ID) { 33 | $this->log('Не указан "VK_GROUP_ID" в настройках'); 34 | 35 | exit; 36 | } 37 | 38 | // Открытие этого файла только cron'ом 39 | if (ONLY_CRON && (!isset($_SERVER['argv'][0]) && $_SERVER['argv'][0] != '--cron')) { 40 | $this->log('Скрипт запущен не через CRON - остановка'); 41 | 42 | exit; 43 | } 44 | 45 | // Директория для информационных логов 46 | if (!file_exists('logs/')) { 47 | mkdir('logs/', 0777, true); 48 | } 49 | 50 | // Отображаем все ошибки 51 | error_reporting(E_ALL); 52 | ini_set('display_errors', TRUE); 53 | // Логгирование 54 | ini_set('log_errors', 1); 55 | ini_set('error_log', LOG_FILE); 56 | // Лимит выполнения скрипта по времени 57 | set_time_limit(TIMEOUT); 58 | 59 | // Подключаемся к SQLite. Если БД не существует, то создаём её 60 | $this->db = new SQLite3(DB_FILE); 61 | $this->db->exec(" 62 | CREATE TABLE IF NOT EXISTS " . DB_NAME . " ( 63 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 64 | `hash` TEXT, 65 | `group` TEXT, 66 | `message` TEXT, 67 | `attachment` TEXT, 68 | `date` TEXT 69 | ) 70 | "); 71 | 72 | // Инициализируем класс работы с API 73 | $this->vk = new vk(VK_ACCESS_TOKEN, VK_API_VERSION); 74 | $this->owner = '-' . $groups[array_rand($groups)]; 75 | $this->blacklist = $blacklist; 76 | 77 | $this->log('Скрипт успешно запущен'); 78 | } 79 | 80 | /** 81 | * Получаем случайный пост из одной из доступных групп, указанных в настройках 82 | * 83 | * @return array возвращаем всю обработанную информацию о посте или же 84 | * false, если нечего не было найдено или произошла ошибка 85 | */ 86 | public function get_post() { 87 | $post = $this->vk->method('wall.get', array( 88 | //'captcha_sid' => '601516643537', 89 | //'captcha_key' => 'dqnn2h', 90 | 'owner_id' => $this->owner, // Случайная группа из списка 91 | 'offset' => rand(SEARCH_RANGE_START, SEARCH_RANGE_END), // Поиск поста в определённом диапазоне 92 | 'count' => '1' 93 | )); 94 | 95 | if ($post->response->items) { 96 | $this->log('Пост найден'); 97 | 98 | $post = $post->response->items[0]; 99 | 100 | // Если тип поста copy или в тексте есть ссылки, то 101 | // скорее всего это рекламный пост - постить не будем 102 | if ($post->post_type === 'copy' 103 | || preg_match('/(http:\/\/[^\s]+)/', $post->text) 104 | || preg_match('/\[club(.*)]/', $post->text)) 105 | { 106 | $this->log('Имеется подозрение на рекламу — пропуск'); 107 | 108 | return false; 109 | } 110 | 111 | if (count($this->blacklist) > 0 && trim($this->blacklist[0])) { 112 | foreach($this->blacklist as $word) { 113 | if (strpos(mb_strtolower($post->text, 'UTF-8'), mb_strtolower($word, 'UTF-8')) !== false) { 114 | $this->log('В тексте поста найдено слово из чёрного списка ("' . $word . '") — пропуск'); 115 | 116 | return false; 117 | } 118 | } 119 | } 120 | 121 | $this->log('Начинается обработка'); 122 | 123 | return $this->process_post($post); 124 | } else { 125 | $this->log('Пост не найден'); 126 | 127 | return false; 128 | } 129 | } 130 | 131 | /** 132 | * Обработка - убираем ненужную информацию, сохраняем изображения, накладываем 133 | * водяной знак и другие полезные процедуры 134 | * 135 | * @param $post необработанный пост 136 | * @return array обработанный пост 137 | */ 138 | private function process_post($post) { 139 | $output = new stdClass(); 140 | // Химичим с текстом, чтобы убрать все теги
141 | // Двойные кавычки не для красоты "\n" (!) 142 | $output->text = preg_replace('##i', "\n", $post->text); 143 | $output->attach = ''; 144 | $output->hash = ''; 145 | 146 | if (ADD_COPYRIGHT) { 147 | $output->copyright = 'https://vk.com/wall' . $post->owner_id . '_' . $post->id; 148 | } else { 149 | $output->copyright = false; 150 | } 151 | 152 | // Проверка на наличие прикреплений 153 | // Собираем их все в одну переменную 154 | if (isset($post->attachments)) { 155 | foreach ($post->attachments as $item) { 156 | if (isset($item->photo)) { 157 | // Сохраняем картинку локально, выбирая самую большую 158 | $this->grab_image(end($item->photo->sizes)->url); 159 | 160 | // Проверяем, была ли уже такая картинка 161 | if ($hash = $this->check_hash(DIRECTORY . 'image.jpg', true)) { 162 | $output->hash .= $hash; 163 | } else { 164 | return false; 165 | } 166 | 167 | // Накладываем водяной знак, если разрешено в настройках 168 | if (WATERMARK_ACTIVE) { 169 | $this->apply_watermark(DIRECTORY . 'image.jpg'); 170 | } 171 | 172 | // Вначале получаем адрес сервера для сохранения картинки 173 | $server = $this->vk->method( 174 | 'photos.getWallUploadServer', 175 | array('group_id' => VK_GROUP_ID) 176 | ); 177 | 178 | // Подготовка к сохранению 179 | $data['file'] = new CURLFile(DIRECTORY . 'image.jpg'); 180 | // Отправляем файл на сервер 181 | $ch = curl_init($server->response->upload_url); 182 | 183 | curl_setopt($ch, CURLOPT_POST, true); 184 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 185 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 186 | 187 | $json = json_decode(curl_exec($ch)); 188 | 189 | curl_close($ch); 190 | 191 | // Сохраняем картинку на сервер ВК и получаем её ID 192 | $saved_photo = $this->vk->method('photos.saveWallPhoto', array( 193 | 'group_id' => VK_GROUP_ID, 194 | 'server' => $json->{'server'}, 195 | 'photo' => $json->{'photo'}, 196 | 'hash' => $json->{'hash'} 197 | )); 198 | 199 | $output->attach .= 'photo' . $saved_photo->response[0]->owner_id . 200 | '_' . $saved_photo->response[0]->id . ', '; 201 | } elseif (isset($item->audio)) { 202 | $output->attach .= 'audio' . $item->audio->owner_id . 203 | '_' . $item->audio->id . ', '; 204 | } elseif (isset($item->doc)) { 205 | // Проверяем, была ли уже такая гифка 206 | if ($hash = $this->check_hash($item->doc->url, true)) { 207 | $output->hash .= $hash; 208 | } else { 209 | return false; 210 | } 211 | 212 | $output->attach .= 'doc' . $item->doc->owner_id . 213 | '_' . $item->doc->id . ', '; 214 | } elseif (isset($item->video)) { 215 | // Проверяем, было ли такое видео 216 | if ($hash = $this->check_hash($item->video->id)) { 217 | $output->hash .= $hash; 218 | } else { 219 | return false; 220 | } 221 | 222 | $output->attach .= 'video' . $item->video->owner_id . 223 | '_' . $item->video->id . ', '; 224 | } 225 | } 226 | 227 | $this->log('Пост успешно обработан'); 228 | 229 | return $output; 230 | } else { 231 | $this->log('Не найдено ни одного прикрепления'); 232 | } 233 | } 234 | 235 | /** 236 | * Отправка полностью готового поста ВК в группу по указанному VK_GROUP_ID 237 | * 238 | * @param array $data - массив с данными о посте 239 | */ 240 | public function send_post($data) { 241 | $time = time() + (rand(1, 30) * 60); 242 | 243 | $response = $this->vk->method('wall.post', array( 244 | 'owner_id' => '-' . VK_GROUP_ID, 245 | 'from_group' => 1, 246 | 'friends_only' => 0, 247 | 'message' => $data->text, 248 | 'attachments' => $data->attach, 249 | 'publish_date' => $time, 250 | 'copyright' => $data->copyright 251 | )); 252 | 253 | if (!isset($response->error)) { 254 | // Сохраняем в БД 255 | $this->db->exec(" 256 | INSERT INTO " . DB_NAME . " ('hash', 'group', 'message', 'attachment', 'date') 257 | VALUES ('$data->hash', '$this->owner', '$data->text', '$data->attach', '$time') 258 | "); 259 | 260 | $this->log('Пост успешно отправлен'); 261 | 262 | return true; 263 | } else { 264 | $this->log('Ошибка отправки поста: ' . $response->error->error_msg); 265 | 266 | return false; 267 | } 268 | } 269 | 270 | /** 271 | * Проверка документа на существование в БД. Если есть, значит такой документ уже 272 | * был добавлен ранее. Следовательно добавлять его не нужно. 273 | * 274 | * @param string строка или путь к файлу 275 | * @param boolean это файл или нет - влияет на используемую хеш-функцию 276 | * @return any 277 | */ 278 | private function check_hash($subject, $file = false) { 279 | $hash = $file ? md5_file($subject) : md5($subject); 280 | $rows = $this->db->querySingle(" 281 | SELECT COUNT(*) FROM " . DB_NAME . " WHERE hash LIKE '%$hash%' 282 | "); 283 | 284 | if ($rows !== 0) { 285 | $this->log('Такой документ уже был добавлен ранее — пропуск'); 286 | 287 | return false; 288 | } else { 289 | return $hash; 290 | } 291 | } 292 | 293 | /** 294 | * Получение документа с помощью cURL 295 | * 296 | * @param $url URL документа 297 | * @return any полученный документ 298 | */ 299 | private function grab_image($url) { 300 | $ch = curl_init($url); 301 | 302 | curl_setopt($ch, CURLOPT_HEADER, 0); 303 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 304 | curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1); 305 | 306 | $raw = curl_exec($ch); 307 | curl_close($ch); 308 | 309 | if (file_exists(DIRECTORY . 'image.jpg')) { 310 | unlink(DIRECTORY . 'image.jpg'); 311 | } 312 | 313 | $fp = fopen(DIRECTORY . 'image.jpg', 'x'); 314 | 315 | fwrite($fp, $raw); 316 | fclose($fp); 317 | } 318 | 319 | /** 320 | * Логгирование с записью в файл 321 | * 322 | * @param string сообщение для записи в лог 323 | */ 324 | public function log($message) { 325 | file_put_contents('./logs/' . date('d-m-Y') . '.txt', '[' . date('d-m-Y h:i:s') . '] ' . $message . PHP_EOL, FILE_APPEND); 326 | } 327 | 328 | /** 329 | * Наложение водяного знака на изображение 330 | * 331 | * @param $img_file используемое изображение 332 | * @param $filetype получаемое расширение на выходе 333 | * @param $watermark изображение водяного знака 334 | */ 335 | private function apply_watermark($img_file, $filetype = 'jpg', $watermark = DIRECTORY_WATERMARK) { 336 | // Размеры картинки 337 | $image = GetImageSize($img_file); 338 | $xImg = $image[0]; 339 | $yImg = $image[1]; 340 | 341 | // Размеры водяного знака 342 | $offset = GetImageSize($watermark); 343 | 344 | // Позиционирование по горизонтали 345 | if (WATERMARK_X) { 346 | $xOffset = $image[0] * (WATERMARK_X / 100) - $offset[0]/2; 347 | } else { 348 | $xOffset = $image[0]/2 - $offset[0]/2; 349 | } 350 | 351 | // Позиционирование по вертикали 352 | if (WATERMARK_Y) { 353 | $yOffset = $image[1] * (WATERMARK_Y / 100) - $offset[1]/2; 354 | } else { 355 | $yOffset = $image[1]/2 - $offset[1]/2; 356 | } 357 | 358 | // Формат картинки 359 | switch ($image[2]) { 360 | case 1: 361 | $img = imagecreatefromgif($img_file); 362 | break; 363 | 364 | case 2: 365 | $img = imagecreatefromjpeg($img_file); 366 | break; 367 | 368 | case 3: 369 | $img = imagecreatefrompng($img_file); 370 | break; 371 | } 372 | 373 | $r = imagecreatefrompng($watermark); 374 | $x = imagesx($r); 375 | $y = imagesy($r); 376 | $xDest = $xImg - ($x + $xOffset); 377 | $yDest = $yImg - ($y + $yOffset); 378 | 379 | imageAlphaBlending($img,1); 380 | imageAlphaBlending($r,1); 381 | imagesavealpha($img,1); 382 | imagesavealpha($r,1); 383 | imagecopyresampled($img, $r, $xDest, $yDest, 0, 0, $x, $y, $x, $y); 384 | 385 | switch ($filetype) { 386 | case 'jpg': 387 | case 'jpeg': 388 | imagejpeg($img, $img_file, 100); 389 | imagejpeg($img, $img_file, 100); 390 | break; 391 | 392 | case 'gif': 393 | imagegif($img, $img_file); 394 | break; 395 | 396 | case 'png': 397 | imagepng($img, $img_file); 398 | break; 399 | } 400 | 401 | imagedestroy($r); 402 | imagedestroy($img); 403 | 404 | $this->log('Водяной знак успешно добавлен'); 405 | } 406 | } 407 | 408 | $vkparser = new VKparser(); 409 | 410 | $post_info = ''; 411 | 412 | do { 413 | $post_info = $vkparser->get_post(); 414 | 415 | sleep(5); // Пауза между получением нового поста 416 | } while (!$post_info && !STOP_SEARCH_AFTER_FAILURE); 417 | 418 | if ($post_info) { 419 | $vkparser->send_post($post_info); 420 | } else { 421 | $vkparser->log('Пост не найден, включен параметр "STOP_SEARCH_AFTER_FAILURE" - завершение работы парсера'); 422 | } 423 | -------------------------------------------------------------------------------- /vk.class.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 13 | $this->api_version = $api_version; 14 | } 15 | 16 | /** 17 | * Делает запрос к API VK 18 | * @param $method 19 | * @param $params 20 | */ 21 | public function method($method, $params = null) 22 | { 23 | $p = ''; 24 | 25 | if ($params && is_array($params)) 26 | { 27 | foreach ($params as $key => $param) 28 | { 29 | $p .= ($p == '' ? '' : '&') . $key . '=' . urlencode($param); 30 | } 31 | } 32 | 33 | $curl_handle = curl_init(); 34 | curl_setopt($curl_handle, CURLOPT_URL, $this->url . $method . '?' . ($p ? $p . '&' : '') . 'access_token=' . $this->access_token . '&v=' . $this->api_version); 35 | curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 2); 36 | curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1); 37 | $response = curl_exec($curl_handle); 38 | curl_close($curl_handle); 39 | 40 | if ($response) 41 | { 42 | return json_decode($response); 43 | } 44 | 45 | return false; 46 | } 47 | } --------------------------------------------------------------------------------