WordPress中拆分WP_Query输出生成全量产品XML Feed咨询
问题描述
尝试为约30000个产品生成XML Feed,但当前输出仅能生成7000-8000个产品就中断,需要包含全部产品。询问能否拆分生成过程或输出文件,这种方案是否需要大量代码,或者有其他解决思路。当前使用WP_Query从数据库获取数据,通过foreach循环,用XMLWriter输出XML,代码如下:
add_action( 'wp_loaded', 'product_feeds_output_writer'); function product_feeds_output_writer() { $args = array( 'post_type' => 'product', // Total product 30k. 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => '_stock_status', 'value' => 'instock', 'compare' => '=' ) ) ); $query = new WP_Query(); if (! $posts = $query->query( $args ) ) { $error = new WP_Error( '003', 'No products found.' ); wp_send_json_error( $error ); } $bloginfo = array( 'name' => get_bloginfo('name'), 'url' => get_bloginfo('url'), 'description' => get_bloginfo('description') ); $xml = new XMLWriter(); $xml->openURI( $_SERVER['DOCUMENT_ROOT'] . '/wp-content/uploads/woo-products-feed-generator/XMLWriter.xml' ); $xml->startDocument('1.0','UTF-8'); $xml->setIndent( 4 ); $xml->startElement('rss'); $xml->writeAttribute('version', '2.0'); $xml->writeAttribute('xmlns:g', 'http://base.google.com/ns/1.0'); $xml->startElement('channel'); $xml->writeElement('title', $bloginfo['name']); $xml->writeElement('link', $bloginfo['url']); $xml->writeElement('description', $bloginfo['description']); foreach ( $posts as $post ) { $product = wc_get_product( $post->ID ); $bir = random_int( 0, 9 ); $on = random_int( 10, 99 ); $yuz = random_int( 100, 999 ); $bin = random_int( 1000, 9999 ); $mil = random_int( 1000000, 9999999 ); $id = $product->get_id(); $title = $product->get_title(); $link = $product->get_permalink(); $description = $product->get_description(); $price = $product->get_regular_price() ?: $product->get_price(); $sale_price = $product->get_sale_price() ?: ''; $availability = $product->get_stock_status() == 'instock' ? 'in_stock' : 'out_of_stock'; $brand = $product->get_attribute( 'pa_marka' ); $image_link = $product->get_meta('_product_image_thumbnail') ?: wp_get_attachment_url( $product->get_image_id() ); $type = implode( '', wp_get_post_terms( $product->id, 'product_cat', array('fields' => 'names') ) ); $xml->startElement('item'); $xml->writeElement( 'g:id', $id ); $xml->writeElement( 'g:title', $title ); $xml->writeElement( 'g:description', $description ); $xml->writeElement( 'g:link', $link ); $xml->writeElement( 'g:price', $price.' '.'TRY' ); if( $sale_price ) { $xml->writeElement( 'g:sale_price', $sale_price.' '.'TRY' ); } $xml->writeElement( 'g:availability', $availability ); $xml->writeElement( 'g:brand', $brand ); //$xml->writeElement( 'g:google_product_category', $track ); $xml->writeElement( 'g:product_type', $type ); $xml->writeElement( 'g:identifier_exists', 'yes' ); $xml->writeElement( 'g:gtin', $mil.'-'.'K'.$yuz.'DK' ); $xml->writeElement( 'g:mpn', $bin.'BS'.'-'.'L'.$on.$bir.'ZP' ); $xml->writeElement( 'g:condition', 'Yeni' ); //$xml->writeElement( 'g:shipping', $track ); $xml->writeElement( 'g:image_link', $image_link ); if( $attachment_images = $product->get_gallery_image_ids() ) { $attachment_images = array_slice( $attachment_images, 0, 10 ); foreach( $attachment_images as $attachment_id ) { $image_link = wp_get_attachment_url( $attachment_id ); $xml->writeElement( 'g:additional_image_link', $image_link ); } } $xml->writeElement( 'g:material', 'Polipropilen' ); if( $value = $product->get_attribute( 'pa_renk' ) ) { $xml->startElement('g:product_detail'); $xml->writeElement('g:section_name', 'Additional information' ); $xml->writeElement('g:attribute_name', 'Renk' ); $xml->writeElement('g:attribute_value', $value ); $xml->endElement(); } if( $value = $product->get_attribute( 'pa_beden' ) ) { $xml->startElement('g:product_detail'); $xml->writeElement('g:section_name', 'Additional information' ); $xml->writeElement('g:attribute_name', 'Beden' ); $xml->writeElement('g:attribute_value', $value ); $xml->endElement(); } $xml->endElement(); } $xml->endElement(); $xml->endElement(); $xml->endDocument(); $xml->flush(); unset($xml); }
问题根源
- 内存耗尽:一次性拉取3万条产品数据到内存,远超PHP默认内存限制,直接导致脚本中断。
- 执行超时:处理大量数据的时间超过了PHP的
max_execution_time限制,脚本被强制终止。
解决方案
方案1:分批生成单XML文件(推荐,代码改动小)
通过WP_Query的offset和posts_per_page参数分批拉取数据,逐步将产品追加到同一个XML文件中,保证文件结构完整。
修改后代码示例
add_action( 'wp_loaded', 'product_feeds_output_writer'); function product_feeds_output_writer() { // 加安全验证,避免公开访问 if (!isset($_GET['feed_key']) || $_GET['feed_key'] !== 'your_secure_key_here') { wp_die('Access denied'); } $per_batch = 1000; // 每批处理1000条,可根据服务器配置调整 $current_batch = isset($_GET['batch']) ? (int)$_GET['batch'] : 1; $offset = ($current_batch - 1) * $per_batch; $args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => $per_batch, 'offset' => $offset, 'meta_query' => array( array( 'key' => '_stock_status', 'value' => 'instock', 'compare' => '=' ) ), 'no_found_rows' => true, // 关闭总条数查询,优化性能 'update_post_meta_cache' => false, // 关闭元数据缓存,节省内存 'update_post_term_cache' => false // 关闭分类缓存,节省内存 ); $query = new WP_Query($args); $posts = $query->posts; $xml_file_path = $_SERVER['DOCUMENT_ROOT'] . '/wp-content/uploads/woo-products-feed-generator/XMLWriter.xml'; $is_first_batch = $current_batch === 1; $xml = new XMLWriter(); $xml->setIndent(4); if ($is_first_batch) { // 第一批次写入完整XML头部和基础信息 $xml->openURI($xml_file_path); $xml->startDocument('1.0','UTF-8'); $xml->startElement('rss'); $xml->writeAttribute('version', '2.0'); $xml->writeAttribute('xmlns:g', 'http://base.google.com/ns/1.0'); $xml->startElement('channel'); $bloginfo = array( 'name' => get_bloginfo('name'), 'url' => get_bloginfo('url'), 'description' => get_bloginfo('description') ); $xml->writeElement('title', $bloginfo['name']); $xml->writeElement('link', $bloginfo['url']); $xml->writeElement('description', $bloginfo['description']); } else { // 后续批次生成临时XML,只包含当前批的item $xml->openMemory(); $xml->startDocument('1.0','UTF-8'); $xml->startElement('channel'); } // 产品数据处理逻辑保留原代码 foreach ($posts as $post) { $product = wc_get_product($post->ID); $bir = random_int(0, 9); $on = random_int(10, 99); $yuz = random_int(100, 999); $bin = random_int(1000, 9999); $mil = random_int(1000000, 9999999); $id = $product->get_id(); $title = $product->get_title(); $link = $product->get_permalink(); $description = $product->get_description(); $price = $product->get_regular_price() ?: $product->get_price(); $sale_price = $product->get_sale_price() ?: ''; $availability = $product->get_stock_status() == 'instock' ? 'in_stock' : 'out_of_stock'; $brand = $product->get_attribute('pa_marka'); $image_link = $product->get_meta('_product_image_thumbnail') ?: wp_get_attachment_url($product->get_image_id()); $type = implode('', wp_get_post_terms($product->id, 'product_cat', array('fields' => 'names'))); $xml->startElement('item'); $xml->writeElement('g:id', $id); $xml->writeElement('g:title', $title); $xml->writeElement('g:description', $description); $xml->writeElement('g:link', $link); $xml->writeElement('g:price', $price.' TRY'); if($sale_price) { $xml->writeElement('g:sale_price', $sale_price.' TRY'); } $xml->writeElement('g:availability', $availability); $xml->writeElement('g:brand', $brand); $xml->writeElement('g:product_type', $type); $xml->writeElement('g:identifier_exists', 'yes'); $xml->writeElement('g:gtin', $mil.'-K'.$yuz.'DK'); $xml->writeElement('g:mpn', $bin.'BS-L'.$on.$bir.'ZP'); $xml->writeElement('g:condition', 'Yeni'); $xml->writeElement('g:image_link', $image_link); if($attachment_images = $product->get_gallery_image_ids()) { $attachment_images = array_slice($attachment_images, 0, 10); foreach($attachment_images as $attachment_id) { $image_link = wp_get_attachment_url($attachment_id); $xml->writeElement('g:additional_image_link', $image_link); } } $xml->writeElement('g:material', 'Polipropilen'); if($value = $product->get_attribute('pa_renk')) { $xml->startElement('g:product_detail'); $xml->writeElement('g:section_name', 'Additional information'); $xml->writeElement('g:attribute_name', 'Renk'); $xml->writeElement('g:attribute_value', $value); $xml->endElement(); } if($value = $product->get_attribute('pa_beden')) { $xml->startElement('g:product_detail'); $xml->writeElement('g:section_name', 'Additional information'); $xml->writeElement('g:attribute_name', 'Beden'); $xml->writeElement('g:attribute_value', $value); $xml->endElement(); } $xml->endElement(); } if ($is_first_batch) { // 第一批次不关闭根标签,留待后续追加 $xml->flush(); } else { // 后续批次提取item内容,追加到主文件 $temp_xml = $xml->outputMemory(true); $item_content = substr($temp_xml, strpos($temp_xml, '<item>'), strrpos($temp_xml, '</channel>') - strpos($temp_xml, '<item>')); $main_xml = file_get_contents($xml_file_path); $main_xml = substr($main_xml, 0, strrpos($main_xml, '</channel>')) . PHP_EOL . $item_content . '</channel></rss>'; file_put_contents($xml_file_path, $main_xml); } // 计算总批次,自动跳转下一批 $total_products = count(wc_get_products(array( 'post_type' => 'product', 'post_status' => 'publish', 'meta_query' => array( array( 'key' => '_stock_status', 'value' => 'instock', 'compare' => '=' ) ), 'return' => 'ids' ))); $total_batches = ceil($total_products / $per_batch); if ($current_batch < $total_batches) { wp_redirect(add_query_arg('batch', $current_batch + 1, $_SERVER['REQUEST_URI'])); exit; } else { echo 'XML Feed生成完成,文件路径:' . $xml_file_path; } unset($xml); wp_reset_postdata(); }
方案2:拆分生成多个独立XML文件
如果不需要单个大文件,可直接每批生成一个独立的XML文件,逻辑更简单,无需处理文件追加:
// 仅修改文件路径部分 $xml_file_path = $_SERVER['DOCUMENT_ROOT'] . '/wp-content/uploads/woo-products-feed-generator/XMLWriter_batch_'.$current_batch.'.xml'; // 每个批次都写入完整的XML结构,包括头部和尾部 $xml->startDocument('1.0','UTF-8'); $xml->startElement('rss'); // ... 写入channel信息 ... // ... 写入当前批的item ... $xml->endElement(); // 关闭channel $xml->endElement(); // 关闭rss $xml->endDocument(); $xml->flush();
方案3:临时提升PHP限制(辅助方案)
在代码开头添加以下代码,临时提升内存和超时限制,可缓解小数据量问题,但对3万条数据仅为辅助手段:
ini_set('memory_limit', '256M'); ini_set('max_execution_time', 300); // 设置为5分钟
关键优化点
- 关闭WP_Query的冗余缓存:通过
no_found_rows、update_post_meta_cache、update_post_term_cache参数减少内存占用。 - 分批处理:避免一次性加载所有数据到内存,降低服务器压力。
- 安全验证:添加密钥验证,防止Feed生成接口被恶意调用。
内容的提问来源于stack exchange,提问作者ersezar
相关产品推荐
相关产品推荐

