本文约 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 对应即可。
常见踩坑
- 只查
dede_addonarticle漏掉其他模型。 图片集、软件模型在各自的附加表里。迁移前先用SHOW TABLES LIKE 'dede_addon%'确认有哪些附加表。 - 字符集问题。 织梦老站常用
gbk,WordPress 用utf8mb4。连接时$old->set_charset('utf8mb4')无效就改用gbk读取再用mb_convert_encoding()转码。 - 织梦正文里的
{dede:...}残留标签。 部分采集内容的正文里会混入未解析的模板标签,导入后用wp search-replace清理。 - 迁移了采集垃圾内容。 迁移是清理历史包袱的最佳时机,正文 < 100 字、标题重复的应该直接丢弃,而不是照单全收。
- 只迁数据没迁模板,导致整站变默认外观。 织梦模板无法直接复用,必须按标签对照表重写。工作量通常在 1–3 天。
click浏览量丢失。 WordPress 原生没有浏览量字段,需要用插件(如 WP-PostViews)并导入到对应的 postmeta 键。- 案例:一次”迁完被 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 一起看。
