├── .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 | }
--------------------------------------------------------------------------------