Уявіть: ви переїжджаєте з WordPress на Headless CMS, і сотні коментарів з багаторічними обговореннями просто зникають. Або клієнти телефонують зі скаргами, що їхні відгуки з рейтингом 5 зірок перетворилися на порожні рядки. Ми стикалися з цим десятки разів. Наш досвід — понад 150 000 перенесених коментарів на 50+ проектах — дозволяє уникнути таких катастроф. Ми переносимо UGC з гарантією цілісності: авторство, дати, вкладеність відповідей та оцінки — все залишається на місцях. При цьому обробляємо до 20 000 записів на годину з точністю верифікації 99.9%, що в 3 рази швидше за ручне копіювання. Зв'яжіться з нами для консультації — оцінимо проект безкоштовно.
Проблеми, які вирішуємо
Конфлікти ID. При міграції ID записів та користувачів у новій системі відрізняються. Потрібен маппінг старих ідентифікаторів на нові, інакше коментарі прив'яжуться до невірних постів або втратять авторство. Несумісність форматів. Різні CMS використовують різні схеми зберігання: WordPress зберігає коментарі в wp_comments, Disqus — через зовнішній сервіс, кастомні рішення — у власних таблицях. Без універсального експорту дані спотворюються. Втрата рейтингів. Відгуки з оцінками часто зберігаються в мета-полях (wp_commentmeta). При прямому копіюванні таблиць рейтинг може не перенестися. Порушення дерева відповідей. Якщо parent_id не співставлений, відповіді перетворюються на плоскі повідомлення.
Як зберегти дерево відповідей?
Ми використовуємо сортування при експорті: ORDER BY comment_parent ASC, comment_ID ASC. Батьківські коментарі обробляються раніше дочірніх. У процесі міграції ведемо маппінг comment_id_map: старий ID → новий ID. Якщо батьківський коментар не знайдено (наприклад, видалений), дочірній переноситься зі статусом сирітського, але не втрачається. Приклад коду на Python — функція migrate_comments.
Що робити з рейтингами відгуків?
Рейтинг відгуку в WooCommerce зберігається в wp_commentmeta з meta_key = 'rating', value — від 1 до 5. Ми експортуємо його через LEFT JOIN wp_commentmeta і передаємо в цільову систему як окреме поле. Для e-commerce платформ, які не підтримують рейтинг, створюємо кастомну таксономію або мета-поле. Гарантуємо, що жодна оцінка не загубиться.
Як ми це робимо
Стек: WordPress (wp-cli, SQL), WooCommerce, Disqus (REST API), кастомні PHP/Node.js скрипти. Для великих обсягів (10k+ коментарів) використовуємо пакетну вставку з транзакціями.
Експорт з WordPress
def export_comments(wp_db):
cursor = wp_db.cursor(dictionary=True)
cursor.execute("""
SELECT
c.comment_ID as id,
c.comment_post_ID as post_legacy_id,
c.comment_author as author_name,
c.comment_author_email as author_email,
c.comment_date_gmt as created_at,
c.comment_content as content,
c.comment_approved as status,
c.comment_parent as parent_id,
c.user_id as wp_user_id,
c.comment_type as type,
cm.meta_value as rating
FROM wp_comments c
LEFT JOIN wp_commentmeta cm ON c.comment_ID = cm.comment_id
AND cm.meta_key = 'rating'
WHERE c.comment_type IN ('comment', 'review', '')
ORDER BY c.comment_parent ASC, c.comment_ID ASC
""")
return cursor.fetchall()
Перенесення зі збереженням дерева відповідей
def migrate_comments(wp_db, new_db, post_id_map, user_id_map):
comments = export_comments(wp_db)
# Маппинг старых comment_ID → новых
comment_id_map = {}
for comment in comments:
# Найти новый post ID
new_post_id = post_id_map.get(comment['post_legacy_id'])
if not new_post_id:
continue # пост не перенесён — пропустить
# Найти новый parent ID
new_parent_id = None
if comment['parent_id']:
new_parent_id = comment_id_map.get(comment['parent_id'])
if not new_parent_id:
print(f"Parent comment {comment['parent_id']} not found, skipping child")
continue
# Определить статус
status_map = {'1': 'approved', '0': 'pending', 'spam': 'spam', 'trash': 'deleted'}
status = status_map.get(str(comment['status']), 'pending')
new_comment = {
'post_id': new_post_id,
'author_name': comment['author_name'],
'author_email': comment['author_email'],
'content': comment['content'],
'status': status,
'parent_id': new_parent_id,
'user_id': user_id_map.get(comment['wp_user_id']),
'created_at': comment['created_at'],
'rating': int(comment['rating']) if comment['rating'] else None,
'legacy_id': comment['id'],
}
new_id = new_db.create_comment(new_comment)
comment_id_map[comment['id']] = new_id
print(f"Migrated {len(comment_id_map)} comments")
return comment_id_map
Відгуки з Disqus
def export_disqus_comments(disqus_api_key, forum_shortname):
"""Экспорт комментариев из Disqus через API"""
import requests
all_posts = []
cursor = None
while True:
params = {
'api_key': disqus_api_key,
'forum': forum_shortname,
'limit': 100,
}
if cursor:
params['cursor'] = cursor
resp = requests.get('https://disqus.com/api/3.0/posts/list.json', params=params)
data = resp.json()
all_posts.extend(data['response'])
if not data['cursor']['hasNext']:
break
cursor = data['cursor']['next']
return all_posts
Disqus також надає XML-експорт через admin-панель (Settings → Export). Формат WXEP сумісний з WordPress XML-імпортом.
Перенесення рейтингів/відгуків (e-commerce)
def migrate_product_reviews(wp_db, new_db, product_id_map, user_id_map):
cursor = wp_db.cursor(dictionary=True)
cursor.execute("""
SELECT
c.comment_ID, c.comment_post_ID, c.comment_author,
c.comment_author_email, c.comment_content, c.comment_date_gmt,
c.comment_approved,
MAX(CASE WHEN cm.meta_key = 'rating' THEN cm.meta_value END) as rating,
MAX(CASE WHEN cm.meta_key = 'verified' THEN cm.meta_value END) as verified
FROM wp_comments c
JOIN wp_commentmeta cm ON c.comment_ID = cm.comment_id
WHERE c.comment_type = 'review'
GROUP BY c.comment_ID
""")
for review in cursor.fetchall():
new_product_id = product_id_map.get(review['comment_post_ID'])
if not new_product_id:
continue
new_db.create_review({
'product_id': new_product_id,
'author_name': review['comment_author'],
'author_email': review['comment_author_email'],
'content': review['comment_content'],
'rating': int(review['rating'] or 5),
'is_verified': review['verified'] == '1',
'status': 'approved' if review['comment_approved'] == '1' else 'pending',
'created_at': review['comment_date_gmt'],
})
Порівняння методів міграції
| Метод | Швидкість | Ризик втрати даних | Необхідність доробок |
|---|---|---|---|
| Ручне копіювання | Низька (100 записів/год) | Високий | Немає |
| SQL-дамп | Середня (500 записів/год) | Середній (конфлікти ID) | Немає |
| Наш скрипт | Висока (20 000 записів/год) | Низький (верифікація) | Є (один раз) |
Типи коментарів та їх зберігання
| Тип | Сховище | Особливості |
|---|---|---|
| Звичайні коментарі | wp_comments |
parent_id, comment_approved |
| Відгуки з рейтингом | wp_comments + wp_commentmeta |
meta_key='rating' |
| Disqus | Зовнішній API | Експорт через REST або XML |
Верифікація після перенесення: навіщо вона потрібна?
Навіть при ідеальному скрипті можуть виникнути невідповідності через дублікати або видалені записи. Ми запускаємо автоматичну перевірку: порівнюємо кількість коментарів по кожному посту до та після міграції. У разі розбіжностей формується детальний звіт, і ми вручну коригуємо дані. Такий підхід виключає втрату навіть одного коментаря.
Процес роботи
- Аналітика. Вивчаємо поточну структуру даних: таблиці, типи коментарів, зв'язки з постами та користувачами. Складаємо карту полів.
- Експорт. Вивантажуємо дані з джерела: SQL-дампами, через WP-CLI, REST API або XML. Сортуємо за parent_id.
- Маппінг. Створюємо словники: post_id_map (старий ID → новий ID), user_id_map, comment_id_map. Враховуємо можливі колізії.
- Імпорт. Переносимо коментарі пакетами, дотримуючись батьківсько-дочірніх зв'язків. Для відгуків окремо створюємо рейтинги.
- Верифікація. Скрипт порівнює кількість коментарів на пост до та після. При розбіжностях — звіт та ручне коригування.
Верифікація після перенесення
def verify_comments_migration(wp_db, new_db, post_id_map):
cursor = wp_db.cursor()
cursor.execute("""
SELECT comment_post_ID, COUNT(*) as count
FROM wp_comments
WHERE comment_approved = '1'
GROUP BY comment_post_ID
""")
wp_counts = dict(cursor.fetchall())
mismatches = []
for wp_post_id, expected_count in wp_counts.items():
new_post_id = post_id_map.get(wp_post_id)
if not new_post_id:
continue
actual_count = new_db.count_comments(new_post_id)
if actual_count != expected_count:
mismatches.append((wp_post_id, expected_count, actual_count))
if mismatches:
print("Comment count mismatches:")
for wp_id, expected, actual in mismatches:
print(f" Post {wp_id}: expected {expected}, got {actual}")
return len(mismatches) == 0
Приклад логу міграції
``` Migrating comments for post 123... Exported 45 comments from WordPress Mapped parent IDs: 10 -> 101, 11 -> 102, ... Imported 45 comments to target Verification: expected 45, got 45 — OK ```Що входить у роботу
- Повний аудит структури коментарів вихідного сайту.
- Написання скрипта для експорту з урахуванням усіх типів (звичайні, review, Disqus).
- Маппінг ID постів та користувачів.
- Тестове перенесення на копії (5-10% даних) для виявлення проблем.
- Основний імпорт з логами кожного кроку.
- Фінальна верифікація: по кожному посту звірка кількості та вибірка випадкових записів.
- Документація: опис маппінгів, інструкція з відкату при необхідності.
- Гарантія 30 днів на цілісність даних.
Терміни та оцінка
Перенесення коментарів зі збереженням дерева відповідей, авторства та рейтингів — від 2 до 5 робочих днів залежно від обсягу (до 1000 — 2 дні, 1000-10 000 — 3-4 дні, більше — 5+). Вартість розраховується індивідуально. Оцінимо проект безкоштовно — напишіть нам, і ми підготуємо план міграції з точними термінами.
Для довідки: структура коментарів у WordPress описана в документації.







