搜索全站
建站 17 分钟阅读

导航站链接数据管理与批量导入:用 CPT + CSV 维护上千条网址

上千条网址一条条手工录入不现实。本文给出用 WP-CLI 自定义命令批量导入 CSV 的完整代码,含字段映射、URL 去重、分类自动创建与失败回滚,并讲清大批量写入时的内存与超时处理。

U
UFO_PF 2019-11-23 · 阅读 17 分钟
导航 建站
阅读提示
本文约 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 数据库备份 一起做。

U
UFO_PF作者

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

留下你的想法

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