本文约 6,700 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:第一条链接是手动加的,第一千条怎么办
建导航站的人往往从手动录入开始:后台新建、填标题、填网址、选分类,一条 20 秒。录到第五十条开始烦躁,第两百条基本放弃,最后站点停在 300 条不再更新。而一个有价值的导航站,链接量至少要上千。
批量导入的难点不在”读文件”,而在数据质量:CSV 里网址格式五花八门(带不带 https、有没有尾斜杠、含不含 utm_ 参数),分类名称大小写不一致,还有大量重复项。如果直接 wp_insert_post() 一把梭,跑完会发现库里多了几百条重复数据,而且没法回滚。因此导入脚本必须做三件事:规范化、去重、可回滚。
原理分析:批量写入的三个性能瓶颈
第一个瓶颈是单次请求的内存与执行时间。WordPress 默认 PHP memory_limit 常见 128M,max_execution_time 30 秒。用命令行跑 WP-CLI 不受 max_execution_time 限制,但内存仍受限。解决办法是分批提交 + wp_cache_flush() 释放对象缓存。
第二个瓶颈是 wp_insert_post() 每次都会触发大量钩子与元数据写入。上千条时可以考虑用 $wpdb->insert() 直接写 wp_posts,但会绕过钩子导致 term 关系、slug 等需手动处理,通常只在万级数据量时才值得。常规做法是分批 wp_insert_post() 并在每批之间清理缓存。
第三个瓶颈是固定链接冲突。站点多了以后 sanitize_title() 生成的 slug 可能重复,WordPress 会自动加 -2 后缀,导致你以为的 URL 和实际不一致。导入时应当显式指定 post_name。
| 数据量 | 建议方式 | 注意点 |
|---|---|---|
| < 200 条 | 后台手工 + 分类批量 | 无需脚本 |
| 200–3000 条 | WP-CLI 分批导入 | 每批 100 条 + 清缓存 |
| > 3000 条 | 直写 $wpdb 或拆文件 | 需手动维护 term 关系 |
保姆级实操
步骤 1:准备标准 CSV。 第一行为表头,字段固定为 name,url,desc,category。
name,url,desc,category
WordPress 官方插件目录,https://wordpress.org/plugins/,查找与下载官方插件,开发资源
WordPress 官方文档,https://developer.wordpress.org/,开发者参考手册,开发资源
迷忙飞,https://mimangfei.com/,WordPress 实战笔记,博客推荐
步骤 2:写 WP-CLI 自定义命令。 存为 wp-content/mu-plugins/import-links.php,mu-plugins 下的文件会自动加载。
<?php
/**
* Plugin Name: 导航链接批量导入
* Description: 通过 WP-CLI 命令从 CSV 批量导入网址
*/
if ( defined( 'WP_CLI' ) && WP_CLI ) {
class Mimangfei_Import_Links_Command extends WP_CLI_Command {
/**
* 从 CSV 导入网址
*
* ## OPTIONS
*
* <file>
* : CSV 文件路径
*
* ## EXAMPLES
*
* wp nav-import import /tmp/links.csv
*/
public function import( $args, $assoc_args ) {
$file = $args[0];
if ( ! is_readable( $file ) ) {
WP_CLI::error( '文件不存在或不可读:' . $file );
}
$handle = fopen( $file, 'r' );
$header = fgetcsv( $handle );
$batch = 0;
$ok = 0;
while ( ( $row = fgetcsv( $handle ) ) !== false ) {
$data = array_combine( $header, $row );
$url = $this->normalize_url( $data['url'] );
if ( ! $url ) {
WP_CLI::warning( '跳过无效网址:' . $data['url'] );
continue;
}
if ( $this->url_exists( $url ) ) {
continue;
}
$post_id = wp_insert_post( array(
'post_type' => 'nav_site',
'post_title' => sanitize_text_field( $data['name'] ),
'post_name' => sanitize_title( $data['name'] ),
'post_status' => 'publish',
), true );
if ( is_wp_error( $post_id ) ) {
WP_CLI::warning( $post_id->get_error_message() );
continue;
}
update_post_meta( $post_id, '_site_url', esc_url_raw( $url ) );
update_post_meta( $post_id, '_site_desc', sanitize_text_field( $data['desc'] ) );
$term = term_exists( $data['category'], 'nav_cat' );
if ( ! $term ) {
$term = wp_insert_term( $data['category'], 'nav_cat' );
}
if ( ! is_wp_error( $term ) ) {
wp_set_object_terms( $post_id, (int) $term['term_id'], 'nav_cat' );
}
$ok++;
$batch++;
if ( $batch >= 100 ) {
$batch = 0;
wp_cache_flush();
WP_CLI::log( "已导入 {$ok} 条,缓存已清理…" );
}
}
fclose( $handle );
WP_CLI::success( "导入完成,共 {$ok} 条" );
}
private function normalize_url( $url ) {
$url = trim( $url );
if ( '' === $url ) {
return '';
}
if ( ! preg_match( '#^https?://#i', $url ) ) {
$url = 'https://' . ltrim( $url, '/' );
}
$url = preg_replace( '#[?&]utm_[^&]*#', '', $url );
return filter_var( $url, FILTER_VALIDATE_URL ) ? $url : '';
}
private function url_exists( $url ) {
global $wpdb;
return (bool) $wpdb->get_var(
$wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_site_url' AND meta_value = %s LIMIT 1",
esc_url_raw( $url )
)
);
}
}
WP_CLI::add_command( 'nav-import', 'Mimangfei_Import_Links_Command' );
}
步骤 2 补充:命令行执行与内存控制。
cd /var/www/mimangfei
# 先跑小样本验证字段映射(只留前 5 行)
head -n 6 /tmp/links.csv > /tmp/links-sample.csv
wp nav-import import /tmp/links-sample.csv --path=/var/www/mimangfei
# 确认无误后跑全量,必要时提高内存上限
wp nav-import import /tmp/links.csv --path=/var/www/mimangfei
php -d memory_limit=512M /usr/local/bin/wp nav-import import /tmp/links.csv --path=/var/www/mimangfei
步骤 3:导入前先备份,出错能回滚。
wp db export /tmp/before-import.sql --path=/var/www/mimangfei
# 需要回滚时:
# wp db import /tmp/before-import.sql --path=/var/www/mimangfei
步骤 4:批量设置排序权重。 新导入的链接默认权重为 0,按分类统一初始化,避免排序混乱。
wp post list --post_type=nav_site --format=ids --path=/var/www/mimangfei \
| tr ' ' '\n' | while read id; do
[ -n "$id" ] && wp post meta update "$id" _site_order 10 --path=/var/www/mimangfei
done
步骤 5:定期跑死链检查。 用 curl -I 批量探测,把非 200 的导出来人工复核。
while read -r url; do
code=$(curl -s -o /dev/null -w '%{http_code}' -m 10 -A 'Mozilla/5.0' "$url")
[ "$code" != "200" ] && echo "$code $url"
done < <(wp post list --post_type=nav_site --post_status=publish \
--field=meta_value --meta_key=_site_url --path=/var/www/mimangfei)
常见踩坑
- CSV 用 Excel 另存为 UTF-8 不带 BOM,否则第一行表头会带上 BOM 导致字段映射错位。
- 中文 CSV 在 Linux 下用
fgetcsv读取乱码,因为文件是 GBK 编码(应先iconv -f GBK -t UTF-8)。 - 忘记
wp_cache_flush(),跑到第 800 条时报 “Fatal error: Allowed memory size of 134217728 bytes exhausted”。 - 重复导入产生大量同名条目,因为去重用到了未规范化的 URL(带
utm_参数)。 - 导入时没指定
post_name,slug 自动加后缀,与预期 URL 不符。 - 案例:某导航站一次导入 2000 条链接,脚本没做分批也没清缓存,跑到第 1200 条时内存耗尽退出,此时已写入 1200 条但没有记录断点。第二次运行时因为 URL 去重生效,重复的 1200 条被跳过,剩下 800 条正常导入,最终数据看似完整。但因为第一次导入的条目缺少排序权重,前台排序全乱,花了两天才手工修正。教训是先备份、再分批、最后统一初始化字段。
- 分类名大小写不一致,生成了”开发工具”和”开发工具 “(带空格)两个分类。
term_exists()对同名不同层级的分类判断不准,导致子分类挂到了错误的父级。
总结
批量导入的验收标准:导入后条数与原文件行数一致(去重剔除的要有日志)、随机抽查 10 条 URL 可访问、每个分类下条数与预期吻合。行动清单:① CSV 统一 UTF-8 无 BOM;② 导入前 wp db export 备份;③ 先跑小样本再全量;④ 每 100 条清一次缓存;⑤ URL 规范化后再去重;⑥ 导入后统一初始化排序字段。链接维护好之后,还需要配合 导航站收录与 SEO、网址导航站搭建教程 与 WordPress 数据库备份 一起做。
