Пишу этот пост с целью поделиться опытом. Сразу предупреждаю — этот путь не из простых, есть проще, но так интереснее 🙂
Постановка задачи
Дано: блог в ЖЖ, доступ в админку можно считать утерянным, т.к. владелец блога не позволяет соглашаться с неким дополнительным соглашением ЖЖ. Суть в том, что не ответив на него согласен, доступ в админку закрыт. Замкнутый круг. Стандартными инструментами для извлечения данных (например, формирования xml) не воспользоваться. Придётся парсить html.
Блог довольно старый, записей в нём много: за 4 года около 1500 записей. Есть картинки, комментарии, теги — всё это нужно перенести в блог на WP.
Реализация
Вытягиваем ссылки на все записи.
Для того, чтобы спарсить, для начала необходимо собрать ссылки на все страницы блога. Разумеется, отдельной страницы со ссылками на все записи одного аккаунта у ЖЖ нет 🙂 Немного покликав по блогу обнаружил рубрику с архивом записей, вроде этой, а вот с неё уже можно перейти на архивы ссылок по месяцам. Отлично! То, что мне нужно. Будем парсить ссылки отсюда. Для этого я написал вот такой скриптик на php. Использована библиотека simple_html_dom.php. Документация: ссылка.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<? include_once('simple_html_dom.php'); $arrayData = array(); $handle = fopen("/home/web_user/web/domen.com/public_html/blogcopy/file.txt", "a"); //файл для сохранения ссылок $html = file_get_html('https://borisakunin.livejournal.com/2014/'); //ссылка на годовой календарь записей //выбираем ссылки со страниц foreach($html->find('table') as $table) { foreach($table->find('a[class=month]') as $element){ echo $element->href.' ->'.$element->innertext.' <br>'; $arrayData[] = $element->href; fwrite($handle, $element->href.PHP_EOL); //ссылки сохраняем в файл } } fclose($handle); //print_r($arrayData); ?> |
Этот скрипт сохраняет ссылки только на архивы за каждый месяц. Скрипт пришлось запустить несколько раз (по числу годовых архивов, страниц всего несколько — не было смысла автоматизировать). Ссылок на все записи у меня ещё нет. Для их получения я написал ещё один скрипт.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<? include_once('simple_html_dom.php'); //считываем ссылки на архивы по месяцам и отрываем файл для записи ссылок на записи $content = file('/home/web_user/web/domen.com/public_html/blogcopy/file.txt'); $handle = fopen("/home/web_user/web/domen.com/public_html/blogcopy/file_url.txt", "a"); //перебираем все ссылки и парсим содержимое foreach ($content as $string) { $string = trim($string); //var_dump($string); $html = file_get_html("$string"); foreach($html->find('div[class=entry-text]') as $div) { foreach($div->find('a') as $element) {//далее - выбираем ссылки только на нужные страницы if (preg_match("/.html/", $element->href)) { echo $element->href.' ->'.$element->innertext.' <br>'; fwrite($handle, $element->href.PHP_EOL); } } } $html->clear(); } fclose($handle); ?> |
Теперь у меня есть список на все записи этого аккаунта ЖЖ. Каждая ссылка: https://borisakunin.livejournal.com/125938.html
Если бы в блоге было больше записей, например, 10 тысяч и выше, то все записи было бы правильнее перенести себе на хостинг при помощи wget. В этом случае, можно существенно сократить время выполнения скрипта.
Парсим страницы livejournal
Итак, у меня получился файл примерно в 1500 ссылок. Скрипт должен зайти на каждую страницу, вытянуть все необходимые данные и добавить их в БД WordPress. В конечном итоге все записи в новом блоге должны выглядеть так, будто они изначально там и были. Перед работой с WP лучше сделать дамп базы.
Итак, я написал вот такой скрипт (чуть ниже полный листинг). Что делает скрипт? Открывает файл с УРЛами, переходим по ссылке и загружает страницу, выбирает необходимые данные (те, что нужно перенести в блог на wordpress). Картинки из каждой записи, вернее ссылки на картинки я помещаю в 2 файла: file_images.txt — <img src=…> в теле записи и file_bigimages.txt — ссылки на большие картинки (только на те, что были загружены пользователем в ЖЖ).
Для того, чтобы вытянуть из html-страниц нужную информацию, помимо библиотеки simple_html_dom.php я использую регулярные выражения. Графические файлы не обязательно имеют расширение jpeg|jpg, поэтому пришлось поработать над регулярным выражением. В скрипте есть строки, которые можно удалить, в основном это касается элементов вывода информации, я использовал их для отладки и убирать не стал.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
<? include_once('simple_html_dom.php'); //подключаем функции WP define('WP_USE_THEMES', false); require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php' ); $content = file('/home/web_user/web/domen.com/public_html/blogcopy/file_url3.txt'); $handle = fopen("/home/web_user/web/domen.com/public_html/blogcopy/file_result.txt", "a"); $handleImages = fopen("/home/web_user/web/domen.com/public_html/blogcopy/file_images.txt", "a"); $handleBigImages = fopen("/home/web_user/web/domen.com/public_html/blogcopy/file_bigimages.txt", "a"); foreach ($content as $string) { $string = trim($string); echo 'URL: '.$string.'<br>'; $html = file_get_html("$string"); $title = $html->find('div.entry-wrap dt.entry-title',1); echo '$title = '.$title->innertext.'<br>'; //находим title $wpTitle = $title->plaintext; $date = $html->find('div.entry-wrap abbr.updated',0); //находим дату if(isset($date)){ // echo '$date = '.$date->title.'<br>'; preg_match("/[0-9]{4}-[0-9]{2}-[0-9]{2}/", $date->title, $formDate); preg_match("/[0-9]{2}:[0-9]{2}:[0-9]{2}/", $date->title, $formTime); $date = $formDate[0].' - '.$formTime[0];// получаем дату в формате WP echo $date; } //ищем теги к записи и помещаем в массив $tags = $html->find('div.entry-wrap div.ljtags',0); $tagsArr = array(); if (isset($tags)) { echo '$tags = +'.$tags->plaintext.'+<br>'; $tagsArr = explode(",",$tags->plaintext); $tagsArr = str_replace('Tags: ','',$tagsArr); } //находим все img src из записи? записываем их в файл // и заменяем часть урл в теле записи на тот, что будет в новом блоге $i=0; $pregArr=array();$imgTemp=''; foreach($html->find('div.entry-content img') as $img) { preg_match("/\/?[0-9A-Za-z._-]{1,}$/", $img->src, $pregArr); echo 'OLD $img'.$i.' = +'.$img->src.'<b>'.$pregArr[0].'</b>+<br>'; $imgTemp = $img->src; $img->src = 'http://domen.com/images'.$pregArr[0]; echo 'NEW $img'.$i.' = '.$img->src.'<br>'; //write img urls to file if (fwrite($handleImages, $imgTemp.PHP_EOL) === FALSE) { echo "<b>Ошибка записи $imgTemp </b><br>"; } echo "URL Записан в файл $imgTemp <br>"; } //поиск ссылок на большие картинки, запись их в файл, замена урл в теле записи //некоторые файлы жж хранит без расширения jpg foreach($html->find('div.entry-content a img') as $aimg){ $bigImg = $aimg->parent()->href; preg_match("/.{1,}livejournal.{0,}[^(html|\/\|)]$/i", $bigImg, $pregImg); //выбираем все ссылки с жж не являющиеся ссылками на хтмл-файлы //write urls to file if (isset($pregImg[0])){ echo '<b> AIMG: '.$bigImg.'</b> File: '.$pregImg[0].'<br>'; $imgBigTemp = $aimg->parent()->href; preg_match("/\/?[0-9A-Za-z._-]{1,}$/", $aimg->parent()->href, $pregArr); if (fwrite($handleBigImages, $imgBigTemp.PHP_EOL) === FALSE) { echo "<b>Ошибка записи $imgBigTemp </b><br>"; } echo "URL Записан в файл $imgBigTemp <br>"; preg_match("/\/?[0-9A-Za-z._-]{1,}$/", $pregArr[0], $pregfileArr); $aimg->parent()->href = 'http://domen.com/bigimages'.$pregfileArr[0]; echo 'New big Img: '.$aimg->parent()->href.'<br>'; } } //парсим комментарии $comments = $html->find('div.entry-wrap span.comments-count',0); if (isset($comments)) { echo 'COMMENT: +'.$comments.'+'; // $commentsList = $html->find('div.entry-wrap span.ljuser b'); foreach($html->find('div.entry-wrap div.comment-wrap') as $commentsList) { $wpCommentUser = $commentsList->find('span.ljuser b',0)->innertext; $wpCommentUrl = $commentsList->find('span.ljuser a',0)->href; preg_match("/([0-9]{4}-[0-9]{2}-[0-9]{2})/", $commentsList->find('a.comment-permalink span',0)->innertext, $outputdateArray); preg_match("/[0-9]{2}:[0-9]{2}/", $commentsList->find('a.comment-permalink span',0)->innertext, $outputTimeArray); $wpCommentDate = $outputdateArray[0].' '.$outputTimeArray[0]; $wpCommentText = $commentsList->find('div.comment-text',0)->innertext; // echo '<h3>'.$wpCommentUser.' - '.$wpCommentUrl.'</h3>'; // echo '<h3>'.$wpCommentDate.'</h3>'; // echo '<h3>'.$wpCommentText.'</h3>'; //формируем массив для дальнейшей записи при помощи API функции wordpress $commentdata[] = array( 'comment_author' => $wpCommentUser, 'comment_author_url' => $wpCommentUrl, 'comment_content' => $wpCommentText, 'comment_type' => '', 'comment_parent' => 0, 'comment_date' => $wpCommentDate, 'comment_approved' => 1, ); } print_r($commentdata); } //ищем тело записи $content = $html->find('div.entry-wrap div.entry-content',0); $html->find('div.ljtags',0)->innertext= ''; echo '$content = '.$content->outertext.'<br>'; $wpContent = $content->innertext; //формируем массив для дальнейшей записи в БД wordpress $post_data = array( 'post_title' => $wpTitle, 'post_content' => $wpContent, 'post_date' => $date, 'tags_input' => $tagsArr, 'post_author' => 1, 'post_status' => 'publish', 'post_category' => array(13) ); //добавляем запись в WP $post_id = wp_insert_post($post_data, true); //print_r($post_id); // Выведет id поста или объект с массивом ошибок //если запись добавлена, то добавляем тег со старым урлом записи (мало ли пригодится в будущем) if (isset($post_id)==TRUE){ $metaPost = add_post_meta($post_id, 'old_url', $string); //если есть комменты, то добавлем их if (isset($comments)==TRUE){ foreach($commentdata as $value) { $value['comment_post_ID'] = $post_id; //print_r ($value); wp_insert_comment( $value ); } unset($commentdata); } //получаем урл добавленной записи и пишем его в файл if ($metaPost == TRUE){ $postUrl = get_permalink($post_id, false ); //write result url to file if (fwrite($handle, $postUrl.PHP_EOL) === FALSE) { echo "<b>Ошибка записи $postUrl </b><br>"; } echo "<br><b>URL Записан в файл $postUrl </b><br>"; } /* массив для записи комментариев в WP $commentdata = array( 'comment_post_ID' => $post_id, 'comment_author' => $wpCommentUser, 'comment_author_url' => $wpCommentUrl, 'comment_content' => $wpCommentText, 'comment_type' => '', 'comment_parent' => 0, 'comment_date' => $wpCommentDate, // получим current_time('mysql') 'comment_approved' => 1, ); */ } echo '<br><br>'; $html->clear(); } fclose($handle); fclose($handleImages); fclose($handleBigImages); fclose($content); ?> |
Я посчитал, что если копировать сразу 1,5тысячи страниц, то:
- скрипт может хорошо нагрузить систему
- будет долго выполняться
Рисковать не стал и принял решение обрабатывать не все страницы сразу, а только по 100 страниц за один запуск. Для этого и создан временный файл file_url3.txt Для его заполнения урлами, перед каждым запуском скрипта, я очищал файл и добавлял по 100 ссылок из основного файла. Делается это достаточно просто:
cat file_url.txt | head -n 200 | tail -n 100 > file_url3.txt
Перед каждым запуском скрипта достаточно добавлять +100 к head
Для тестового запуска лучше создать отдельную категорию в WordPress и создавать записи в ней, также можно убрать post_status=’publish’ и тогда все скопированные страницы будут иметь статус черновика и не будут отображаться на сайте.
Производительность
Скрипт не особенно нагружает систему (запуск производился на недорогом VPS с 512мб ОЗУ). Пиковая нагрузка — 13% использования cpu и пара процентов — озу.
Большую нагрузку показал wget, которым я выкачивал файлы с livejournal. Вот пример запуска wget:
1 |
[user@ih772777 bigimages]# wget -b -i ../blogcopy/file_bigimages.txt |
Первый ключ запускает wget в фоновом режиме, -i задаёт путь к файлу где лежат ссылки на изображения. Надо отметить, что с регулярными выражениями я не ошибся, в список ссылок проскочил только один html файл. Блог существовал длительное время и за это время администрация ЖЖ несколько раз изменяла свою систему хранения и именования файлов. Если с ресурсами беда, то можно понизить приоритет wget.
Вот вывод wget:
1 2 3 4 5 6 |
Работа продолжается в фоновом режиме, pid 6390. Выходные данные будут записаны в «wget-log». FINISHED --2018-05-15 21:56:38-- Total wall clock time: 3m 28s Downloaded: 790 files, 123M in 16s (7,75 MB/s) |
Собственно всё.
Пути улучшения скрипта
Мой скрипт не добавляет графические файлы в медиа библиотеку WordPress, т.е. они не будут доступны через соответствующий пункт меню в админке. Хотя они отображаются в записях через обычный html код и хранятся в специально созданных директориях. Это можно реализовать при помощи соответствующей API функции WordPress.
Свежие комментарии