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

织梦 DedeCMS / 帝国 CMS 迁移到 WordPress:数据映射与模板重写要点

织梦收费、帝国维护成本高,怎么把站迁到 WordPress?本文讲清 dede_archives 等核心表结构、文章与附加表的合并查询、栏目与自定义字段映射、织梦模板标签转 WordPress 的对应方法。

U
UFO_PF 2026-07-23 · 阅读 26 分钟
织梦 网站迁移
阅读提示
本文约 10,173 字,预计阅读 26 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:从织梦出走,多半是因为那一纸授权通知

2021 年之后,织梦(DedeCMS)开始对商业使用收取授权费,大量企业站收到告知函。同期帝国 CMS 虽仍免费开源,但生态活跃度下降、移动端适配老旧、找人二次开发越来越难。于是”迁到 WordPress”成了很多老站的选择。

织梦站迁移的难点在于三点:数据分表存储(主表 dede_archives 只存标题和元信息,正文在附加表 dede_addonarticle 里);模板是自己的一套标签体系({dede:arclist}、{dede:field}),与 WordPress 的模板标签完全不兼容;历史数据质量差(大量采集文章、重复标题、正文里塞满内联样式和站外图片链接)。

此外还有一个绕不开的现实问题:织梦站的安全问题。很多老织梦站已经被挂马、被植入隐藏链接,直接迁移会把垃圾数据一起带过去。迁移前必须先体检。

原理分析:织梦与帝国的数据结构

织梦(DedeCMS)核心表:

表名 作用 迁移要点
dede_archives 内容主表:id、typeid、title、pubdate、click、writer、litpic 对应 wp_posts 主体
dede_addonarticle 文章附加表:aid、body、typeid 通过 aid 与 archives.id 关联,必须 JOIN
dede_arctype 栏目表:id、typename、typedir 对应 wp_terms 分类
dede_taglist / dede_tagindex 标签与标签索引 对应 post_tag
dede_uploads 附件记录 媒体文件位置参考
dede_feedback 评论/留言 对应 wp_comments
dede_addonimages / dede_addonsoft 图片集/软件模型附加表 需按模型分别处理

关键陷阱:织梦的”内容模型”机制。 普通文章在 dede_addonarticle,图片集在 dede_addonimages,软件在 dede_addonsoft。如果站点用了多个模型,必须分别查询再合并,只查 addonarticle 会漏掉大量内容。

帝国 CMS(EmpireCMS)的结构更灵活: 采用”主表 + 分表”设计,phome_ecms_news 是新闻系统主表,phome_ecms_news_data_1(数字递增)是分表存储的正文,phome_enewsclass 是栏目表。分表数量取决于站点配置,迁移时要遍历所有分表。

两个系统的内容模型与 WordPress 的对应关系:

概念 织梦 帝国 WordPress
内容 archives + addon* ecms_news + data_* wp_posts
栏目 arctype enewsclass wp_terms (category)
标签 taglist 自带 tags post_tag
自定义字段 附加表字段 / 自定义模型 系统模型字段 wp_postmeta 或 ACF 字段组
浏览量 archives.click 需开启点击统计 wp_postmeta(配合插件)
缩略图 archives.litpic titlepic _thumbnail_id
模板 .htm + {dede:} 标签 灵动标签 [e:loop] PHP 模板 + 循环

迁移前的必要体检:清垃圾数据。 织梦站常见的问题是采集内容,表现为:正文里有大量无意义段落、隐藏的站外链接、重复的标题。这些内容迁过去只会拉低站点质量,建议在迁移时就直接过滤掉,而不是”先迁过来再说”。

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

步骤 1:导出并体检数据。 先看清楚站点里到底有什么:

-- 按栏目统计内容量,确认模型分布
SELECT t.typename, COUNT(a.id) AS cnt
FROM dede_archives a
LEFT JOIN dede_arctype t ON a.typeid = t.id
GROUP BY a.typeid
ORDER BY cnt DESC;

-- 检查有多少内容是"采集嫌疑":正文过短、标题重复
SELECT title, COUNT(*) AS c
FROM dede_archives
GROUP BY title HAVING c > 1
ORDER BY c DESC LIMIT 50;

-- 统计正文长度分布,找出异常短的内容
SELECT
  SUM(CHAR_LENGTH(body) < 500)  AS 短正文,
  SUM(CHAR_LENGTH(body) >= 500) AS 正常
FROM dede_addonarticle;

-- 找出正文里含站外链接的内容(采集站外链嫌疑)
SELECT aid FROM dede_addonarticle
WHERE body LIKE '%<a href="http%'
  AND body NOT LIKE '%mimangfei.com%'
LIMIT 100;

步骤 2:把栏目结构映射到 WordPress。 织梦的 typedir 通常是 {cmspath}/news 这样的格式,用它生成 slug:

# 先在 WordPress 建好分类骨架(按步骤 1 统计出的栏目)
wp term create category "新闻中心" --slug="news" --porcelain
wp term create category "产品中心" --slug="product" --porcelain
wp term create category "解决方案" --slug="solution" --porcelain

步骤 3:写迁移脚本,处理分表 JOIN。 这是核心步骤:

<?php
/**
 * dede2wp.php —— 织梦迁移到 WordPress
 * 用法:放在 WordPress 根目录执行 wp eval-file dede2wp.php
 */
require_once __DIR__ . '/wp-load.php';

$old = new mysqli( '127.0.0.1', 'dede_user', 'dede_pass', 'dede_db' );
$old->set_charset( 'utf8mb4' );

// 织梦 typeid → WordPress 分类 term_id
$cat_map  = [ 1 => 5, 2 => 6, 3 => 7 ];
// 只迁移这些模型,按需调整
$models   = [ 'article' => 'dede_addonarticle', 'images' => 'dede_addonimages' ];

$total = 0;
$skip  = 0;

foreach ( $models as $model => $addon_table ) {

    // 核心:archives 主表 JOIN 附加表取正文
    $sql = "SELECT a.id, a.typeid, a.title, a.pubdate, a.click, a.litpic, a.keywords,
                   a.description, a.writer, b.body
            FROM dede_archives a
            INNER JOIN {$addon_table} b ON a.id = b.aid
            WHERE a.arcrank = 0          -- 只迁移已审核内容
            ORDER BY a.id ASC";

    $res = $old->query( $sql );

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

        // 清洗:过滤过短正文(疑似采集或空内容)
        if ( mb_strlen( strip_tags( $r['body'] ) ) < 100 ) {
            $skip++;
            continue;
        }

        $content = $r['body'];

        // 织梦正文常见污染:内联样式、font 标签、站外隐藏链接
        $content = preg_replace( '/<font[^>]*>/i', '', $content );
        $content = str_ireplace( '</font>', '', $content );
        $content = preg_replace( '/\sstyle="[^"]*"/i', '', $content );
        // 移除 display:none 的隐藏容器(常见于挂马与采集站外链)
        $content = preg_replace(
            '#<div[^>]*style=["\'][^"\']*display\s*:\s*none[^"\']*["\'][^>]*>.*?</div>#is',
            '', $content
        );
        // 图片路径:织梦常见 /uploads/allimg/ 前缀
        $content = str_replace(
            [ '/uploads/allimg/', 'http://旧站.com/uploads/allimg/' ],
            '/wp-content/uploads/',
            $content
        );

        $post_id = wp_insert_post( [
            'post_title'   => html_entity_decode( $r['title'], ENT_QUOTES, 'UTF-8' ),
            'post_content' => $content,
            'post_excerpt' => $r['description'],
            'post_name'    => 'a' . $r['id'],   // 织梦 aid 作为别名,方便 URL 映射
            'post_type'    => 'post',
            'post_status'  => 'publish',
            'post_date'    => date( 'Y-m-d H:i:s', $r['pubdate'] ),
        ] );

        if ( ! $post_id ) { continue; }

        // 保留织梦原始信息
        update_post_meta( $post_id, '_dede_aid',      $r['id'] );
        update_post_meta( $post_id, '_dede_click',    (int) $r['click'] );
        update_post_meta( $post_id, '_dede_litpic',   $r['litpic'] );
        // 保留原始 URL,用于生成 301
        update_post_meta( $post_id, '_dede_url', '/plus/view.php?aid=' . $r['id'] );

        if ( isset( $cat_map[ $r['typeid'] ] ) ) {
            wp_set_post_categories( $post_id, [ $cat_map[ $r['typeid'] ] ] );
        }
        if ( ! empty( $r['keywords'] ) ) {
            wp_set_post_tags( $post_id, array_filter( array_map( 'trim', explode( ',', $r['keywords'] ) ) ), false );
        }

        $total++;
        if ( $total % 300 === 0 ) { wp_cache_flush(); echo "已导入 {$total}\n"; }
    }
}

echo "迁移完成:导入 {$total} 条,跳过 {$skip} 条\n";

帝国 CMS 的对应查询(分表需遍历)示意:

-- 帝国 CMS:查看有哪些数据分表
SHOW TABLES LIKE 'phome_ecms_news_data_%';

-- 主表与分表 JOIN(data_1 需按实际分表遍历)
SELECT n.id, n.classid, n.title, n.newstime, n.onclick, n.titlepic, d.newstext
FROM phome_ecms_news n
INNER JOIN phome_ecms_news_data_1 d ON n.id = d.id
WHERE n.checked = 1;

步骤 4:迁移图片与缩略图。 织梦的图片通常在 /uploads/allimg/,按年月分目录:

# 织梦默认图片目录
rsync -av /old_dede/uploads/allimg/ /var/www/html/wp-content/uploads/
rsync -av /old_dede/uploads/        /var/www/html/wp-content/uploads/  # 其他资源

# 帝国 CMS 默认在 /d/file/
rsync -av /old_empire/d/file/ /var/www/html/wp-content/uploads/

# 把 litpic 批量注册为特色图片(用上一步存下的 _dede_litpic)
wp eval '
$q = new WP_Query(["posts_per_page"=>-1, "meta_key"=>"_dede_litpic"]);
while ($q->have_posts()) { $q->the_post();
  $lit = get_post_meta(get_the_ID(), "_dede_litpic", true);
  if (!$lit) continue;
  $file = basename($lit);
  $path = ABSPATH . "wp-content/uploads/" . $file;
  if (!file_exists($path)) continue;
  $aid = attachment_url_to_postid(home_url("/wp-content/uploads/" . $file));
  if ($aid) set_post_thumbnail(get_the_ID(), $aid);
}'

步骤 5:模板重写——织梦标签与 WordPress 的对照。 这是工作量最大的部分,先把最常用的标签对应关系理清楚:

织梦标签 作用 WordPress 对应写法
{dede:field.title/} 当前页标题 <?php the_title(); ?>
{dede:field.body/} 正文 <?php the_content(); ?>
{dede:arclist row='10' titlelen='40'} 文章列表 WP_Query 循环
{dede:list pagesize='10'} 栏目列表分页 have_posts() 主循环
{dede:channel} 栏目导航 wp_nav_menu() 或 wp_list_categories()
{dede:pagelist} 分页导航 the_posts_pagination()
{dede:prenext} 上一篇/下一篇 previous_post_link() / next_post_link()
{dede:field.pubdate function='...'} 时间格式化 the_time('Y-m-d')
{dede:global.cfg_webname/} 站点名称 bloginfo('name')

织梦的 arclist 换成 WordPress 循环的标准写法:

<?php
// 织梦 {dede:arclist typeid='5' row='10' orderby='pubdate'} 的 WordPress 等价实现
$args = [
    'cat'            => 5,          // 对应 typeid 映射后的分类 ID
    'posts_per_page' => 10,
    'orderby'        => 'date',
    'order'          => 'DESC',
    'post_status'    => 'publish',
];
$query = new WP_Query( $args );

if ( $query->have_posts() ) :
    echo '<ul class="arc-list">';
    while ( $query->have_posts() ) : $query->the_post(); ?>
        <li>
            <a href="<?php the_permalink(); ?>" title="<?php the_title_attribute(); ?>">
                <?php echo wp_trim_words( get_the_title(), 40 ); ?>
            </a>
            <span class="date"><?php echo get_the_date( 'Y-m-d' ); ?></span>
        </li>
    <?php endwhile;
    echo '</ul>';
    wp_reset_postdata();   // 必须调用,否则影响后续主循环
else :
    echo '<p>暂无内容</p>';
endif;
?>

步骤 6:URL 与 301 映射。 织梦的默认 URL 是 /plus/view.php?aid=123,迁移后必须重定向:

# 织梦动态文章页 → 新 URL
location = /plus/view.php {
    if ($arg_aid ~ "^(\d+)$") {
        return 301 https://www.mimangfei.com/a$1.html;
    }
    return 404;
}

# 织梦栏目页 /plus/list.php?tid=5
location = /plus/list.php {
    if ($arg_tid = "5") { return 301 https://www.mimangfei.com/category/news/; }
    if ($arg_tid = "6") { return 301 https://www.mimangfei.com/category/product/; }
    return 404;
}

如果织梦开启了 HTML 静态化(/news/123.html),映射会简单很多,直接用别名 a123 对应即可。

常见踩坑

  1. 只查 dede_addonarticle 漏掉其他模型。 图片集、软件模型在各自的附加表里。迁移前先用 SHOW TABLES LIKE 'dede_addon%' 确认有哪些附加表。
  2. 字符集问题。 织梦老站常用 gbk,WordPress 用 utf8mb4。连接时 $old->set_charset('utf8mb4') 无效就改用 gbk 读取再用 mb_convert_encoding() 转码。
  3. 织梦正文里的 {dede:...} 残留标签。 部分采集内容的正文里会混入未解析的模板标签,导入后用 wp search-replace 清理。
  4. 迁移了采集垃圾内容。 迁移是清理历史包袱的最佳时机,正文 < 100 字、标题重复的应该直接丢弃,而不是照单全收。
  5. 只迁数据没迁模板,导致整站变默认外观。 织梦模板无法直接复用,必须按标签对照表重写。工作量通常在 1–3 天。
  6. click 浏览量丢失。 WordPress 原生没有浏览量字段,需要用插件(如 WP-PostViews)并导入到对应的 postmeta 键。
  7. 案例:一次”迁完被 Google 标为危险网站”。 客户的织梦站运行了 8 年,中间被入侵过,正文中被批量注入了隐藏的博彩链接(用 display:none + font-size:0 隐藏)。团队写脚本时只做了简单的字段搬运,把注入内容一起迁到了新站。上线两周后 Google Safe Browsing 判定为危险站点,Chrome 访问直接拦截。事后排查时用 SQL 搜索 display:none、visibility:hidden、font-size:0 三种特征,清理了 1400 多条被注入的内容。教训:老站迁移前必须做一次安全体检,重点查正文里的隐藏元素、站外链接、wp_postmeta 里的异常数据;迁移脚本里要带上清洗规则,正文过滤后再入库。

总结

织梦/帝国迁移的判断标准是”内容完整、URL 有去处、模板能看”。执行顺序:先体检统计(栏目分布、重复标题、正文长度)→ 清洗垃圾内容 → 建分类骨架 → 用 JOIN 分表查询导入并保留 _dede_aid / _dede_url → 迁移图片并批量设置特色图 → 按标签对照表重写模板 → 配置 301。迁移是清理历史包袱的最佳时机,不要把采集内容和挂马残留一起带过去。 URL 的 301 处理请参照 网站改版 URL 映射,数据迁移的通用映射思路可以结合 老站迁移到 WordPress 一起看。

U
UFO_PF作者

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

留下你的想法

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