From 1f919eaef022b4721ecb9f72a0ea703b2ac3e89a Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Fri, 5 Feb 2021 00:10:23 +0300 Subject: initial --- README.md | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ common.php | 95 +++++++++++++++++++++++++++++++++++ composer.json | 15 ++++++ composer.lock | 96 +++++++++++++++++++++++++++++++++++ fetch-documents.php | 99 +++++++++++++++++++++++++++++++++++++ fetch-messages.php | 38 ++++++++++++++ get-attaches.php | 31 ++++++++++++ insert-api-objects.php | 44 +++++++++++++++++ replace-photos.php | 33 +++++++++++++ 9 files changed, 583 insertions(+) create mode 100644 README.md create mode 100644 common.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 fetch-documents.php create mode 100644 fetch-messages.php create mode 100644 get-attaches.php create mode 100644 insert-api-objects.php create mode 100644 replace-photos.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..d65d533 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# vk-messages-post-archive + +> **Дисклеймер:** информация актуальна на осень 2020 года. Может, во ВКонтакте уже всё поменялось и скрипт более +> не нужен, а может, поменялся формат отдаваемого архива и скрипт не сработает. Смотрите сами. + +Во ВКонтакте существует [функция](https://vk.com/data_protection?section=rules&scroll_to_archive=1) выгрузки информации +в ZIP-архиве, в т.ч. всех сообщений. Сделана она как попало: история сообщений, полученная таким образом, содержит +только текст самих сообщений (там даже нет пересланных! сложно было положить, что ли?), ссылки на фото и нерабочие (!) +ссылки на документы. Но лучше так, чем никак. + +Этот репозиторий содержит простенькие инструменты и инструкции, с помощью которых можно дополнительно выкачать все +фотографии из истории, а также получить и сохранить объекты сообщений с вложениями из API (рекомендуется как минимум +для получения рабочих ссылок на документы). + +Это проще, чем выкачивать всё через API с нуля. Пусть ВКонтактик соберёт нам свой архив, там уже есть более-менее +нормальная навигация и оформление, а мы потом просто скачаем все, чего не хватает. Все-таки простых текстовых сообщений +без вложений должно быть на порядок больше. + +# Системные требования + +- bash +- PHP >= 7.1 + - composer + - php-iconv + - php-curl + - наверное, неплохой идеей будет включить opcache для cli в `php.ini` (путь которого можно узнать через `php --ini`): + ``` + opcache.enable=1 + opcache.enable_cli=1 + opcache.file_cache=/tmp/php-opcache + ``` +- iconv +- wget +- sqlite3 +- стандартные утилиты типа cat, find, grep, xargs и т.д. + +# "Поехали", как говорил Юра + +## Скачиваем фоточки + +Склонили репозиторий, устанавливаем зависимости: +``` +git clone https://github.com/gch1p/vk-messages-post-archive +cd vk-messages-post-archive +composer install +``` + +Перейти в папку с распакованным архивом от ВК: +``` +cd path/to/Archive +``` + +Запускаем нехитрый парсер: +``` +for f in $(find messages -type f -name "*.html"); do + cat "$f" | iconv -f windows-1251 | php path/to/get-attaches.php >> to-download.txt +done +``` +В файле `to-download.txt` будут ID сообщений с аттачами вперемешку со ссылками на фотки. + +Выкачиваем фотки. `-P8` означает, что будет параллельно запущено 8 процессов wget. Можете написать другое число. +``` +cat to-download.txt | grep https | xargs -P8 wget -x -i +``` + +Заменяем ссылки (``) на изображения (``). Тут в `-P` разумно передать количество ядер. +``` +find messages -type f -name "*.html" | xargs -I{} -P4 php path/to/replace-photos.php "{}" +``` + +## Достаём остальную информацию из API + +Теперь давайте залезем в API. ВК недавно отключил сторонним приложениям доступ к сообщениям, но кого это остановит? +Скачайте [десктопный мессенджер](https://vk.com/messenger), авторизуйтесь в нём, потом закройте и зайдите в папку, где +он хранит конфиг (`~/.config/VK` на Linux, `~/Library/Application Support/VK` на маке, на винде по логике должно быть +`%APPDATA%\Roaming\VK`), там будет sqlite3 база `vk.db`. Зайдём в неё: +``` +sqlite3 vk.db +``` + +Осмотримся. +``` +sqlite> .tables +auth debug_api_fails recents users +communities friends settings +``` + +Хм, табличка `auth` выглядит многообещающе. Посмотрим структуру: +``` +sqlite> .schema auth +CREATE TABLE auth ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + ts INTEGER, + access_token TEXT +, is_encrypted INTEGER); +``` + +О! То, что нужно. Нам нужен `access_token`. +``` +sqlite> SELECT access_token FROM auth; +``` + +Скопируйте токен и можно выходить (`Ctrl+D`) отсюда. + +Теперь откройте скрипт `common.php` и пропишите этот токен в константу `ACCESS_TOKEN`, а так же путь к папке +`Archive` в константу `ARCHIVE_DIR`. + +После этого можно запустить скрипт `fetch-messages.php` для слива объектов сообщений. Они будут сохраняться в папку `api` +с именами `{dir}/{id}.txt`, где `{id}` – это ID сообщения, а `{dir}` – это остаток от деления ID сообщения на 100. + +Метод [messages.getById](https://vk.com/dev.php?method=messages.getById) вконтактовского API может вернуть до 100 +сообщений за 1 запрос, плюс нужно учитывать стандартное ограничение на 3 запроса в секунду, иначе нам прилетит капча. +`xargs` поможет запустить не более 3-х инстансов скрипта за раз и передать не более 100 идентификаторов в каждый, а +`sleep(1)` сделает сам скрипт в конце. + +``` +cat to-download.txt | grep -v https | xargs -n100 -P3 php path/to/fetch-messages.php +``` + +Теперь добавим возможность просмотра этих объектов на страничках истории. +``` +find messages -type f -name "*.html" | xargs -I{} -P4 php path/to/insert-api-objects.php "{}" +``` + +## Скачиваем документы + +``` +php fetch-documents.php +``` + +Ну вот, вроде, и всё. \ No newline at end of file diff --git a/common.php b/common.php new file mode 100644 index 0000000..e355eba --- /dev/null +++ b/common.php @@ -0,0 +1,95 @@ +find('.message'); + if (!count($nodes)) + throw new Exception('no message nodes found'); + + foreach ($nodes as $node) { + $kludges = $node->find('.kludges'); + if (empty($kludges)) + continue; + + $attachments = $kludges[0]->find('.attachment'); + if (empty($attachments)) + continue; + + $message_id = $node->getAttribute('data-id'); + if (!is_null($message_callback)) + $message_callback($doc, $message_id, $node); + + foreach ($attachments as $attachment) { + $desc = $attachment->find('.attachment__description'); + if (empty($desc) || $desc[0]->innertext != 'Фотография') + continue; + + $link_node = $attachment->find('a.attachment__link'); + if (!$link_node) + continue; + + $href = $link_node[0]->href; + if (strpos($href, 'https://vk.com/im?sel') !== false) + continue; + + if (!is_null($photo_callback)) + $photo_callback($doc, $href, $link_node[0]); + } + } + + return $doc; +} + +function httpPost(string $url, array $fields = []): array { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); + curl_setopt($ch, CURLOPT_USERAGENT, FAKE_USER_AGENT); + $body = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return [$code, $body]; +} + +function httpGet(string $url): array { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_USERAGENT, FAKE_USER_AGENT); + $body = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return [$code, $body]; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e2ca2c9 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "simplehtmldom/simplehtmldom": "^2.0@RC", + "ext-curl": "*", + "ext-json": "*", + "ext-iconv": "*" + }, + "config": { + "platform": { + "php": "7.1" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..89e9a04 --- /dev/null +++ b/composer.lock @@ -0,0 +1,96 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6e4d59c1ab45b11a7233ab1e77f9f0df", + "packages": [ + { + "name": "simplehtmldom/simplehtmldom", + "version": "2.0-RC2", + "source": { + "type": "git", + "url": "https://github.com/simplehtmldom/simplehtmldom.git", + "reference": "3c87726400e59d8e1bc4709cfe82353abeb0f4d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplehtmldom/simplehtmldom/zipball/3c87726400e59d8e1bc4709cfe82353abeb0f4d1", + "reference": "3c87726400e59d8e1bc4709cfe82353abeb0f4d1", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^6 || ^7" + }, + "suggest": { + "ext-curl": "Needed to support cURL downloads in class HtmlWeb", + "ext-mbstring": "Allows better decoding for multi-byte documents", + "ext-openssl": "Allows loading HTTPS pages when using cURL" + }, + "type": "library", + "autoload": { + "classmap": [ + "./" + ], + "exclude-from-classmap": [ + "/example/", + "/manual/", + "/testcase/", + "/tests/", + "simple_html_dom.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "S.C. Chen", + "role": "Developer" + }, + { + "name": "John Schlick", + "role": "Developer" + }, + { + "name": "logmanoriginal", + "role": "Developer" + } + ], + "description": "A fast, simple and reliable HTML document parser for PHP.", + "homepage": "https://simplehtmldom.sourceforge.io/", + "keywords": [ + "Simple", + "dom", + "html", + "parser", + "php", + "simplehtmldom" + ], + "support": { + "issues": "https://sourceforge.net/p/simplehtmldom/bugs/", + "rss": "https://sourceforge.net/p/simplehtmldom/news/feed.rss", + "source": "https://sourceforge.net/p/simplehtmldom/repository/", + "wiki": "https://simplehtmldom.sourceforge.io/docs/" + }, + "time": "2019-11-09T15:42:50+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "simplehtmldom/simplehtmldom": 5 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/fetch-documents.php b/fetch-documents.php new file mode 100644 index 0000000..315f175 --- /dev/null +++ b/fetch-documents.php @@ -0,0 +1,99 @@ + implode(',', $message_ids), + 'access_token' => ACCESS_TOKEN, + 'v' => '5.109' +]; +list($code, $body) = httpPost($url, $fields); + +if ($code != 200) + fatalError('api returned '.$code); + +$response = json_decode($body, true); +if (!empty($response['error'])) + fatalError('api error: '.$response['error']['error_msg']); + +foreach ($response['response']['items'] as $item) { + $id = (int)$item['id']; + + $dir_n = $id % 100; + $cur_dir = ARCHIVE_DIR.'/messages/api/'.$dir_n; + + if (!file_exists($cur_dir)) { + if (!mkdir($cur_dir, 0755, true)) + fatalError('failed to mkdir('.$cur_dir.')'); + } + + file_put_contents($cur_dir.'/'.$id.'.txt', json_encode($item, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); +} + +sleep(1); \ No newline at end of file diff --git a/get-attaches.php b/get-attaches.php new file mode 100644 index 0000000..36c12ee --- /dev/null +++ b/get-attaches.php @@ -0,0 +1,31 @@ +getMessage()); +} + +if (!empty($message_ids)) + echo implode("\n", $message_ids)."\n"; + +if (!empty($photo_urls)) + echo implode("\n", $photo_urls)."\n"; \ No newline at end of file diff --git a/insert-api-objects.php b/insert-api-objects.php new file mode 100644 index 0000000..f2d3823 --- /dev/null +++ b/insert-api-objects.php @@ -0,0 +1,44 @@ +createElement('a'); + $a->setAttribute('href', 'javascript:void(0)'); + $a->setAttribute('onclick', "this.nextSibling.style.display = (this.nextSibling.style.display === 'none' ? 'block' : 'none')"); + $a->appendChild($doc->createTextNode('Показать/скрыть объект API')); + + $div = $doc->createElement('div'); + $div->setAttribute('style', 'display: none; font-size: 11px; font-family: monospace; background-color: #edeef0; padding: 10px; white-space: pre; overflow: auto;'); + $div->appendChild($doc->createTextNode($obj)); + + $node->appendChild($doc->createElement('br')); + $node->appendChild($a); + $node->appendChild($div); + + $is_modified = true; + }, null); +} catch (Exception $e) { + fatalError($e->getMessage()); +} + +if ($is_modified) + file_put_contents($file, iconv('utf-8', 'windows-1251//IGNORE', $doc->outertext)); \ No newline at end of file diff --git a/replace-photos.php b/replace-photos.php new file mode 100644 index 0000000..9715932 --- /dev/null +++ b/replace-photos.php @@ -0,0 +1,33 @@ +parent(); + $link_node->remove(); + + $img = $doc->createElement('img'); + $img->setAttribute('src', $local_href); + $img->setAttribute('alt', $href); + + $parent->appendChild($doc->createElement('br')); + $parent->appendChild($img); + }); +} catch (Exception $e) { + fatalError($e->getMessage()); +} + +file_put_contents($file, iconv('utf-8', 'windows-1251//IGNORE', $doc->outertext)); \ No newline at end of file -- cgit v1.2.3