Миграция MovableType -> Drupal. День 2: миграция контента и URL

Предуведомление

Описанная ниже методика предназначена для заливки пустого сайта на Drupal. Задача доливки контента на сайт, где уже что-то есть - не ставилась. Более того, на стадии импорта комментариев все старые комментарии точно будут стерты.

Если вам нужно пополнение имеющегося сайта, то описанные ниже скрипты нужно взять за основу и допилить.

Кроме того, никакими enterprise-features, вроде транзакций или обработки ошибок я категорически не заморачивался. Предполагается, какбэ, что импортом данных мы занимаемся тихо в уголочке, поступлением новых данных на старый сайт можем управлять, а после завершения импорта просто подменим сайт на скаку.

Импорт записей

Задача: вытащить записи (посты) из БД MovableType и запихать их в БД Drupal в виде объектов типа Story. Создание Drupal-объекта связано с заполнением нескольких таблиц (node, node_revisions и прочие node_*, url_aliases), пополнением таблицы тегов, другими словами эту работу не хочется делать вручную (SQL-запросами), а хочется перевесить на внутреннюю механику Drupal (ведь при создании записи оно как-то само все делается...).

План работ тривиален и прост:

  • Ставим модули Table Wizard и Migrate.
  • Добавляем нужные поля в структуру данных записи Story (не вручную, включением готовых модулей).
  • Запускаем скрипт, который перенесет нам данные постов в БД Drupal.
  • Импортируем образованную таблицу с постами в Table Wizard.
  • Делаем импорт через Migrate.
  • Полируем результат.
Первый пункт особых вопросов вызвать не должен, обычные модули. За собой потянут Views и Schema, их тоже надо выкачать и поставить, до кучи полезен и Views UI.

Поля в структуре данных Story

У исходного сайта имеются:

  • URL записей (постов), которые хочется сохранить.
  • Теги у записей.
  • Иерархическая структура рубрик.

Для сохранения URL нужно включить модуль Path (входит в core в Drupal 6.x). Прочие модули (особенно Pathauto) пока не включаем.

Для сохранения тегов и рубрик поступим следующим образом:

  1. Заведем две таксономии (словаря), Tags и Categories. Для каждой из них:
    • Поставим галку (тип) Tags, дабы не набивать значения руками (для Categories - потом поменяем тип на Multiple Select/Required и переупорядочим их).
    • Разрешим эти таксономии для типа данных, который собираемся использовать (Story)
  2. Кроме того, заведем новый Input Format (Administer -> Site Configuration -> Input Formats -> Add inpit format) у которого все фильтры выключены. Этот формат нам нужен т.к. на исходном сайте большинство текстов уже HTML-форматировано и повторное форматирование Друпалом не нужно. Если вы проводите все упражнения на новом (свежеустановленном) сайте, то этот формат получит номер 3, на что и рассчитан скрипт пре-миграции (см. ниже).

Скрипт пре-миграции данных

Прилагаемый скрипт (качать отсюда) вынимает все полезные данные из БД Movable Type и помещает их в табличку mt_posts в destintation БД. Запускать так:

./mt2drupal.pl source-db destination-db [blog-id]
Параметры
  • source-db - БД MovableType (PostgreSQL). Для переключения на MySQL нужно две строчки после слов Source DB закомментировать, а следующие две - расскомментировать.
  • destination-db - БД Drupal. В прилагаемом скрипте настроено на PostgreSQL, для работы с MySQL нужно две строчки после слов Destination DB закомментировать, а следующие две - раскомментировать.
  • blog-id - номер блога в БД MovableType. Если параметр не задан, то работа ведется с ID 1.
Для работы скрипта нужен Perl 5.x (пробовал с 5.8.9), DBI-модули для работы с вашей БД и модуль Time::ParseDate (входит в Time::modules).

Необходимые комментарии:

  • Скрипт создает таблицу mt_posts, содержащую всю необходимую информацию для импорта.
  • Так как мы импортируем блог (с одним автором), то информацией об авторе я не заморачивался, присваиваю константу (userid) на этапе импорта.
  • Формирование анонса доверено Drupal, точка по которой анонс отделяется от основного текста помечена стандартным Drupal placeholder (<!--break-->).
  • Теги и категории преобразуются в разделенные запятыми списки, преобразование списка категорий в иерархический будет делаться вручную после импорта.
  • Скрипт всасывает все данные одним запросом (select * from table), для импорта действительно больших (сотни тысяч и более записей) наборов данных лучше это делать кусочками. На тысяче все происходит быстро, на десятке тысяч должно занять приемлемое время.
  • URL-ы формируются по правилам MT: /yyyy/dd/mm/${entry_basename}.html, но если на вашем исходном сайте правила формирования URL были другими, то придется поправить в скрипте строчку-другую.

Превращение таблицы mt_posts в Drupal View

В меню Drupal: Administer -> Content Management -> Table Wizard

Выбираем Add existing table, выбираем в списке таблицу mt_posts, жмем Add tables.

Важно: если работаем с PostgreSQL, то ставим галку Skip Full Analyze, если ее оставить, то получим кучу ругани. Анализ нам собственно не нужен, все данные уже готовы для импорта (подготовлены мегаскриптом на предыдущей стадии).

Импорт данных

В меню Drupal: Administer -> Content Management -> Migrate

Делаем новый content set с такими параметрами:

  • Destination: Node: Story.
  • Source view: созданный на предыдущем шаге View (он будет называться, если все по-умолчанию, tw: mt_posts (mt_posts))

migrate-field-mapping.png Жмем Add и попадаем в картинку мэппинга полей таблицы mt_posts в объект Story (картинка слева, кликабельно). Тут все достаточно очевидно кроме двух моментов:

  1. Автор всегда uid 1 (друпаловский) т.е. суперпользователь.
  2. Поле teaser мы не заполняем (но в поле body на предыдущем этапе записан правильный разделитель) и оно заполнится автоматически на импорте.

После создания mapping, выбираем наш content set, ставим галку Import, выбираем Execute и запускаем.. Если все сделано правильно, то будут импортированы данные из mt_posts и записи появятся в списке статей Drupal.

Помимо этого, будет заполнена таблица migrate_map_1 (или с каким-то другим номером, если у вас есть другие импорты), сопоставляющая идентификаторы таблицы mt_posts (куда они попали из исходного MovableType) и node ID Друпала. Она понадобится нам в дальнейшем для:

  • Импорта комментариев
  • Импорта файла мэппинга LiveJournal - MovableType

Надо сказать, что без Migrate я бы посмотрел на задачу с пяти разных сторон, да и бросил бы нафиг.....

Импорт комментариев

Модуль Migrate: EPIC FAIL

В первой части я заметил, что у меня PostgreSQL, а не MySQL, что может вызвать проблемы. Оно и вызвало: импорт комментариев в PostgreSQL не заработал, причем даже до стадии импорта, при попытке импортировать тип комментариев во внутренних таблицах не инициализируется поле NOT NULL, постгрес кричит и все ломается сразу.

С MySQL все начинается лучше, криков нет (похоже что тамошний NOT NULL не работает нормально, но проверять не стал), но кончается так же: комментарии не импортированы, причем без какой-либо диагностики.

Остается второй путь - прямой перелив в базу данных.

По счастью, при вставке комментария нужно обновлять только две таблицы: comments и node_comment_statistics, отчего задача становится относительно несложной.

Скрипт импорта комментариев

Скрипт качаем тут. Для работы нужен тот же набор модулей, что и для скрипта пре-импорта записей.

Параметры запуска

./mt2comments.pl src-db dest-db [post-map-table]
  • src-db - исходная база MovableType (настроено на PostgreSQL, перенастройка заменой двух строк).
  • dest-db - база данных Drupal. Используется PostgreSQL-specific функция установки секвенсора (setval(seq_name)), для использования с MySQL понадобится пару строк дописать.
  • post-map-table - таблица в dest-db, задающая соответствие "ID записи MT" - "ID ноды Drupal". Эта таблица создается модулем Migrate на этапе импорта записей. Умолчание - migrate_map_1.

Скрипт сотрет все комментарии

Скрипт перенесет комментарии только для тех записей, которые есть в таблице мэппинга. Какой-либо дополнительной рихтовки после скрипта не требуется, все комментарии должны оказаться на месте и с нужным порядком в тредах.

Наведение марафета

URL записей и комментариев

URL записей должны были остаться прежними, если это не так, то нужно править скрипт пре-импорта записей и повторять импорт.

URL комментариев чудесным образом остались прежними (URL#comment-NNN, а ID комментариев мы сохранили). Если быть точным, то они остались прежними только для тех комментариев, которые влезают на одну страницу с основным текстом. Максимальное количество комментариев к записи у меня - 131, поэтому идем в Administer-Content Management-Content Types-Story-Comments settings и ставим количество комментариев на странице в 200, должно хватить.

Если по какой-то причине у страниц были дополнительные алиасы (у меня они были и были сделаны через симлинки) - добавляем эти алиасы в таблицу URL Aliases (Site Building - URL Aliases)

Теги и разделы сайта

В моем случае:
  • Тегов много (~400), разделов немного (16)
  • Тегам назначаем новый URL (/tags/русское-название), разделам - старый (/transliterated).
  • Чтобы не мучаться, не жалко с разделами поработать вручную один раз.

Методика:

  1. Ставим модуль Pathauto (и модуль Token, который у него в требованиях). Файл i18n-ascii.example.txt в каталоге modules/pathauto переименовываем в i18-ascii.txt, для русского языка он сойдет (а вот лежащий на вебе большой файл со многими языками - кривой, там мягкого знака нет).
  2. Ставим мой патч к Pathauto.
  3. Идем в настройки Pathauto (URL Aliases - Automated alias settings) и там:
    • Меняем (если хотим) разделитель на подчеркивание (в этом случае нужно сходить еще в раздел Punctuation и убрать удаление underscore).
    • Увеличиваем максимумы длины и, главное, максимальное количество алиасов при bulk update (до количества, большего чем число тегов-рубрик, скажем до 5000).
    • Включаем транслитерацию, на следующем шаге отключим ее для тегов (а для всего прочего она нужна).
    • Выключаем перегенерацию существующих алиасов, а то будем иметь сюрпризы при смене заголовков статей.
    • Taxonomy term path settings: ставим Pattern for all Tags: tags/no-transliterate-me[catpath-raw], паттерн для Category ставим как-тов в духе category/[catpath-raw] (несущественно, на следующем шаге будем править руками).
    • Ставим галку Bulk generate aliases for terms that are not aliased.
    • И жмем OK.
    • Заодно, можно сразу поменять правило вывода URL для nodes на что-то в духе [yyyy]/[mm]/[dd]/[title-raw].html
  4. В этой точке мы нагенерировали русских алиасов /tags/ для тегов и английских /category/... для разделов. Идем в список алиасов, отбираем по подстроке category и правим вручную на правильные URL категорий (т.к. правила транслитерации несколько разные в MT и в Drupal, от правки отмотаться не получится).

Показ разделов (категорий)

Меняем тип словаря Categories на Multiple Select/Required (вместо Tags), он перестает быть автопополняемым.

Переупорядочиваем разделы в нем в нужном порядке и в нужной иерархии.

Ставим модуль Taxonomy Block, подсовываем ему таксономию Category, размещаем в колонке навигации.

Показ облака тегов

Tagadelic полностью закрывает тему облака тегов, настройки тривиальны, не описываю.

Проба пера

Создаем запись с русским заголовком и русскими тегами (новыми, ранее не существовавшими). Она должна создаться с транслитерированным URL и нетранслитерированными тегами.

Архивные URL

Архивные URL исходно казались гораздо страшнее, чем оказалось на самом деле:

  • 95% работы делается через Views (и это занимает минут 5)
  • Остальные 5% - модулем URL Alter (и это занимает гораздо больше времени, ибо хождение по минному полю).
Но по порядку:

Список месяцев с записями

Модуль Views содержит готовый View archive, который и делает всю работу:

  • Определенная в нем страница archive/ при обращении без аргументов - показывает список месяцев, которые являются ссылкой на archive/YYYYMM. При обращении к archive/YYYYMM - показывается список записей, с листалкой и все такое.
  • Определенный в рассматриваемом View блок - тоже выводит список месяцев.
В-общем, нужно поправить настройки блока (почему-то там сортировка от старого к новому и листалка) и разместить его в колонке навигации.

URL вида /yyyy/mm/dd

Опять Views, но придется руками:

  • Делаем новый View, допустим bydate.
  • Делаем в нем Page с URL bydate
  • Определяем три аргумента: год, месяц, день
  • Определяем Row style (Node), сортировку (descending по дате), настраиваем листалку.
В результате получаем работающие URL:
  • bydate/YYYY - список записей за год.
  • bydate/YYYY/MM - список за месяц.
  • bydate/YYYY/MM/DD - список за день.
Увы, но bydate и archive - функциональность Views (да, думаю, и Друпала вообще) не позволяет растить описанный функционал от корня сайта. Поэтому URL-ы надо переписать.

Переписывание URL

Я собирался переписать URL-и средствами nginx, но обнаружил более родное средство: модуль URL ALter. Точнее, сначала я нашел Subpath Alias, тот потребовал URL Alter, а в последнем оказалась вся нужная функциональность.

Ставим модуль, в его настройках пишем.

Inbound filter:

// Every ^\d\d\d\d$ should go to bydate/$1
$result=preg_replace("/^(\d\d\d\d)$/","bydate/$1",$result);
#same for month
$result=preg_replace("/^(\d\d\d\d\/\d\d)$/","bydate/$1",$result);
# and same for day
$result=preg_replace("/^(\d\d\d\d\/\d\d\/\d\d)$/","bydate/$1",$result);

Outbound filter

# remove leading bydate and archive in outbound urls
$path=preg_replace("/^bydate\//","",$path);
$path=preg_replace("/^archive\/(\d\d\d\d)(\d\d)$/","$1/$2",$path);

Собственно, все. Профит.

Важное замечание: правя настройки URL Alter вы ходите по минному полю. Но мы не саперы, поэтому можно/нужно открыть url_alter.module в редакторе и быть готовым закомментировать оба eval(), которые там есть. Без этого - вы скорее всего получите просто неработающий сайт и будет вам плохо.

Недоделки

К сожалению, исправить листалки с Друпаловских ?page=N на MT-шные indexN.html не представляется возможным средствами Drupal. Наилучшим решением, похоже, будет nginx_substitution_filter или что-то подобное на уровне http-сервера, но это выходит за рамки данного текста. Если будет нужно (читай - поисковики не выкинут эти indexN быстро из индекса) - буду делать.

Comments

В RSS ридере смотрится забавно, мне тоже на второй день знакомства хотелось от друпала убежать куда-нибудь подальше ;)

"Миграция MovableType -> Drupal. День 1: постановка задачи "
"Миграция Drupal -> MovableType. День 2: миграция контента и URL"

Тьфу, пропасть.

Спасибо.