搜索全站
网站迁移 21 分钟阅读

老站迁移到 WordPress 的完整方案:数据映射思路与分步落地

把运行多年的老站迁到 WordPress,难点不在装程序而在数据映射。本文讲清字段映射表设计、RSS 与数据库直连两种导入路径、附件与图片处理、URL 保持方案与迁移后验证清单。

U
UFO_PF 2024-11-14 · 阅读 21 分钟
老站 网站迁移
阅读提示
本文约 8,265 字,预计阅读 21 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:迁站真正的难点从来不是”装 WordPress”

装一个 WordPress 十分钟就能完成,真正卡住项目的是后面这些事:老系统里的 3000 篇文章怎么搬过来?文章里的图片路径怎么批量替换?分类和标签的对应关系怎么设计?老站积累的百度收录怎么保住?自定义字段(比如”产品价格””下载链接”)迁移后怎么在新站继续用?

更现实的问题是,很多老站跑的是十年前的程序,文档缺失、数据库结构混乱、还有一堆历史遗留的脏数据(重复文章、乱码正文、失效图片)。如果一上来就写脚本导数据,做到一半发现字段对不上,返工成本极高。

正确的做法是先画映射表,再选导入路径,最后才是写脚本。映射表就是迁移项目的需求文档。

原理分析:数据模型差异决定迁移难度

所有 CMS 的数据模型本质上都是”内容 + 分类 + 用户 + 附件 + 关系”,但表达方式差别很大。以 WordPress 为例,核心是这几张表:

WordPress 表 作用 迁移时对应的老系统概念
wp_posts 文章、页面、附件、菜单、修订版本都存在这里 内容主表
wp_postmeta 文章自定义字段(键值对) 扩展属性表
wp_terms 分类/标签的名称与别名 分类主表
wp_term_taxonomy 分类法(category / post_tag / 自定义) 分类类型
wp_term_relationships 内容与分类的多对多关系 关系表
wp_users / wp_usermeta 用户与用户元数据 作者表 + 扩展信息
wp_comments / wp_commentmeta 评论与评论元数据 评论表

迁移的通用映射思路(以从任意 CMS 迁到 WordPress 为例):

老系统字段 WordPress 目标 处理方式
内容 ID wp_posts.ID 保留旧 ID 写入 wp_postmeta 的 _old_id,用于后续 301 映射
标题 post_title 直接映射,注意 HTML 实体解码
正文 post_content 直接映射,需处理绝对路径图片
摘要 post_excerpt 老系统无摘要时留空,由主题自动截取
别名/slug post_name 为空时用标题拼音转写,需保证唯一
发布时间 post_date / post_date_gmt 注意时区转换
作者 post_author 先建用户,再映射 ID
栏目/分类 wp_terms + 关系表 先建分类,再建立关系
标签 wp_term_taxonomy (post_tag) 逗号分隔拆分后逐条建立关系
浏览量 wp_postmeta 的自定义键 需要插件(如 WP-PostViews)识别
SEO 标题/描述 wp_postmeta,键名取决于 SEO 插件 Rank Math 用 rank_math_title,Yoast 用 _yoast_wpseo_title

两条导入路径的选择标准:

  • RSS / WXR 导入(WordPress 自带的导入器): 适合内容量 < 2000 条、结构标准、不需要保留自定义字段的场景。优点是零代码、安全、自动处理附件;缺点是 RSS 有条目数量上限,且自定义字段、评论、浏览量都会丢失。
  • 数据库直连脚本导入: 适合内容量大、有自定义字段、需要精确保留 ID 与关系的场景。优点是完全可控;缺点是要自己处理字符编码、HTML 清洗、图片迁移。

保姆级实操:六步完成迁移

步骤 1:盘点老站数据,摸清家底。 先做一次全量统计,别边做边猜:

-- 以典型老 CMS 为例,统计各栏目内容量(表名按实际替换)
SELECT typeid, COUNT(*) AS cnt
FROM dede_archives
GROUP BY typeid
ORDER BY cnt DESC;

-- 统计总内容量与正文平均长度,判断导入耗时
SELECT COUNT(*) AS total, AVG(CHAR_LENGTH(body)) AS avg_len FROM dede_addonarticle;

-- 找出正文中的图片引用,确认绝对路径格式
SELECT body FROM dede_addonarticle WHERE body LIKE '%<img%' LIMIT 5;

-- 检查是否存在乱码或异常数据
SELECT aid, title FROM dede_archives WHERE title LIKE '%?%' LIMIT 20;

同时导出一份”旧 URL 全量列表”,来源包括 sitemap、Nginx 日志、搜索引擎已收录列表,这一步和 网站改版 URL 映射 里的方法一致。

步骤 2:在新站准备好分类体系与用户。 不要等导入时再自动建分类,先手动把栏目结构搭好,并记录新旧 ID 对照:

# 用 WP-CLI 批量创建分类,并记下返回的 term_id
wp term create category "WordPress 教程" --slug=wordpress-tutorial --porcelain
wp term create category "建站运维" --slug=hosting --porcelain

# 批量创建作者账号
wp user create editor01 editor01@mimangfei.com --role=author --porcelain

步骤 3:路径 A —— 用内置导入器导入 RSS。 适合标准结构的中小站点。先在老站生成包含全部内容的 RSS(很多 CMS 支持 rss.php?limit=全部),然后在 WordPress 后台「工具 → 导入 → RSS」上传。如果 RSS 文件过大,用命令行方式更稳:

# 安装官方导入器插件(WordPress Importer)
wp plugin install wordpress-importer --activate

# 用 WP-CLI 导入 WXR 文件,指定作者映射
wp import /tmp/old_site.xml --authors=create --skip=attachment

# 大文件建议分批,先测试前 100 条
head -c 5000000 old_site.xml > part1.xml

路径 B —— 写数据库直连脚本。 这是内容量大时的唯一选择。思路是读取老库、清洗、写入 WordPress 标准表:

<?php
/**
 * migrate.php —— 老 CMS 数据迁移脚本(命令行执行:php migrate.php)
 * 放在 WordPress 根目录,执行前先备份!
 */
require_once __DIR__ . '/wp-load.php';

// 连接老数据库
$old = new mysqli( '127.0.0.1', 'old_user', 'old_pass', 'old_db', 3306 );
$old->set_charset( 'utf8mb4' );
if ( $old->connect_errno ) {
    exit( "老库连接失败: {$old->connect_error}\n" );
}

// 新旧分类 ID 映射,第 2 步里准备好的
$cat_map = [ 1 => 5, 2 => 6, 3 => 7 ];
// 新旧作者 ID 映射
$user_map = [ 1 => 2, 2 => 3 ];

$res = $old->query( "SELECT a.id, a.typeid, a.title, a.pubdate, a.writer, b.body
                     FROM old_archives a
                     LEFT JOIN old_addonarticle b ON a.id = b.aid
                     ORDER BY a.id ASC" );

$ok = 0;
$fail = 0;

while ( $row = $res->fetch_assoc() ) {

    // 1. 清洗正文:把老站的绝对图片路径换成新站路径
    $content = $row['body'];
    $content = str_replace( 'https://旧站.com/uploads/', '/wp-content/uploads/', $content );
    // 移除老系统内联的样式与无用标签
    $content = preg_replace( '/\s(style|onclick)="[^"]*"/i', '', $content );

    // 2. 生成唯一的 slug,中文标题转拼音后去重
    $slug = sanitize_title( $row['title'] );
    if ( empty( $slug ) || get_page_by_path( $slug, OBJECT, 'post' ) ) {
        $slug = 'post-' . $row['id'];
    }

    // 3. 写入文章
    $post_id = wp_insert_post( [
        'post_title'    => $row['title'],
        'post_content'  => $content,
        'post_status'   => 'publish',
        'post_type'     => 'post',
        'post_date'     => date( 'Y-m-d H:i:s', $row['pubdate'] ),
        'post_author'   => $user_map[ $row['writer'] ] ?? 1,
        'post_name'     => $slug,
        'import_id'     => 0,
    ], true );

    if ( is_wp_error( $post_id ) ) {
        echo "失败 ID {$row['id']}: " . $post_id->get_error_message() . "\n";
        $fail++;
        continue;
    }

    // 4. 记录旧 ID,供后续生成 301 映射表
    update_post_meta( $post_id, '_old_id', $row['id'] );
    update_post_meta( $post_id, '_old_url', '/plus/view.php?aid=' . $row['id'] );

    // 5. 建立分类关系
    if ( isset( $cat_map[ $row['typeid'] ] ) ) {
        wp_set_post_categories( $post_id, [ $cat_map[ $row['typeid'] ] ] );
    }

    $ok++;
    if ( $ok % 200 === 0 ) {
        echo "已导入 {$ok} 条\n";
        wp_cache_flush();
    }
}

echo "迁移完成:成功 {$ok} 条,失败 {$fail} 条\n";

步骤 4:迁移图片附件。 图片是迁移里最容易出岔子的部分。两种方式:

# 方式一:直接拷贝目录(老站图片目录结构规整时最省事)
rsync -av --progress root@老站IP:/var/www/html/uploads/ \
  /var/www/html/wp-content/uploads/

# 修正权限与属主
chown -R www-data:www-data /var/www/html/wp-content/uploads
find /var/www/html/wp-content/uploads -type d -exec chmod 755 {} \;
find /var/www/html/wp-content/uploads -type f -exec chmod 644 {} \;
# 方式二:批量下载正文里引用的远程图片并本地化(图片散落在老站各处时)
wp media import https://旧站.com/uploads/2020/01/pic.jpg --title="pic"

图片本地化之后,用 WP-CLI 批量替换正文里的旧路径,并重建媒体库元数据:

wp search-replace 'https://旧站.com/uploads/' 'https://新站.com/wp-content/uploads/' --precise
wp media regenerate --yes

步骤 5:生成 301 映射表并配置重定向。 利用第 3 步写入的 _old_url 元数据,直接生成 Nginx 或 Redirection 可用的映射文件:

# 导出旧 URL → 新 URL 的映射(借助 wp postmeta 查询)
wp eval '
$posts = get_posts(["numberposts" => -1, "post_type" => "post", "meta_key" => "_old_url"]);
foreach ($posts as $p) {
    $old = get_post_meta($p->ID, "_old_url", true);
    printf("%s,%s,0,301,迁移映射\n", $old, str_replace(home_url(), "", get_permalink($p->ID)));
}' > /tmp/redirect_map.csv

head -5 /tmp/redirect_map.csv

把生成的 CSV 导入 Redirection 插件,或转成 Nginx 的 map 规则(方法见 网站改版 URL 映射)。

步骤 6:迁移后验证清单。 逐项核对,别只看首页:

# 1. 内容数量是否与老站一致
wp post list --post_type=post --post_status=publish --format=count

# 2. 检查是否有空正文或乱码文章
wp post list --post_type=post --format=csv --fields=ID,post_title | head -20

# 3. 检查固定链接结构并刷新
wp option get permalink_structure
wp rewrite flush --hard

# 4. 检查是否有文章缺少分类(未被归类)
wp eval 'echo "未分类文章数: " . wp_count_posts()->publish . "\n";'

# 5. 检查媒体库文件数量与磁盘实际文件是否匹配
wp eval 'echo "媒体条目: " . wp_count_posts("attachment")->inherit . "\n";'
find /var/www/html/wp-content/uploads -type f | wc -l

# 6. 批量检查关键页面返回码
for u in /about/ /contact/ /sitemap.xml; do
  printf "%s -> %s\n" "$u" "$(curl -s -o /dev/null -w '%{http_code}' https://www.mimangfei.com$u)"
done

常见踩坑

  1. 字符集不一致导致乱码。 老库是 utf8(MySQL 的 3 字节 utf8)或 gbk,新库用 utf8mb4。连接时必须显式 set_charset('utf8mb4'),且导出时用 --default-character-set=utf8mb4。
  2. wp_insert_post 的 post_date 没转时区。 老系统存的是时间戳,直接 date() 会按服务器时区解释,导致发布时间整体偏移 8 小时。
  3. slug 重复导致 URL 不确定。 中文标题 sanitize_title() 后可能为空,此时 WordPress 会用 post-{ID},导致 URL 与预期不符。导入时显式指定 post_name。
  4. 导入时自动创建分类导致结构失控。 上千篇文章如果每篇都带新标签,会生成几千个 term,严重影响后台和性能。导入前先清理或限制标签数量。
  5. 图片本地化后 URL 变了但正文没替换。 表现为所有图片裂图。用 wp search-replace 全量替换,注意 --precise 参数。
  6. 一次跑完 5000 条导致内存溢出。 脚本里用 numberposts 分批,或者加上 wp_cache_flush() 定期清理对象缓存,必要时 ini_set('memory_limit', '512M')。
  7. 案例:一次”成功”但收录全丢的迁移。 客户从某商业 CMS 迁到 WordPress,数据导入顺利,3000 篇文章一条不少,团队验收通过。但一个月后百度收录掉了 85%。排查发现:新站的固定链接设置成了 /%post_id%.html,而老站是 /{分类拼音}/{文章ID}.html,虽然数字一样,但路径结构完全不同,导致所有旧 URL 全部 404,且团队忘了做 301。教训:迁移项目的验收标准必须包含”旧 URL 抽样 20 条,返回码必须是 301 且 Location 指向正确的新页面”。 数据在库里不等于流量保住了,URL 才是收录的载体。

总结

老站迁移的判断标准是”内容、URL、附件、SEO 元数据四样全部落地”。执行顺序:先做数据盘点与映射表 → 搭好分类与用户骨架 → 选导入路径(< 2000 条用导入器,更多写脚本)→ 迁移附件并批量替换路径 → 用 _old_url 元数据自动生成 301 映射 → 逐项验证。整个过程中,保留旧 ID 到 _old_id 这一步一定要做,它是后续所有映射、排错、回滚的依据。迁移前务必完整备份老站的数据库与文件(网站数据备份策略),迁移完成后的 90 天要持续监控收录变化。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。