Symfony 4从CSV Feed URL下载数据的实现及问题排查
嘿,作为Symfony开发者,我来帮你梳理并解决这个问题——你完全可以用Symfony 4直接实现需求,不需要jQuery(前端工具,和后端定时任务无关)或者额外Python工具,下面一步步来解决:
一、先搞定核心障碍:403 Forbidden错误
这是你当前无法获取CSV的关键,403通常是目标服务器拒绝了你的请求,常见原因和解决方法:
- 缺少User-Agent头:很多服务器会拦截没有浏览器标识的请求
- 需要额外请求头:比如Referer
- 服务器需要认证:比如Basic Auth或API密钥
给Guzzle添加请求头示例
修改你的Guzzle请求代码,加上必要的标识:
$client = new Client(); try { $response = $client->request('GET', $url, [ 'headers' => [ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 如果需要,补充Referer头,比如目标商店的主页 // 'Referer' => 'https://partner-store.com/' ], // 如果目标是HTTPS,生产环境不要禁用验证,仅测试用 // 'verify' => false // 如果需要Basic认证,添加以下配置 // 'auth' => ['your-username', 'your-password'] ]); } catch (\GuzzleHttp\Exception\GuzzleException $e) { $io->error("请求失败:{$e->getMessage()}"); return Command::FAILURE; }
用Curl排查错误(可选)
如果Guzzle还是失败,可以用Curl打印详细错误信息:
$fp = fopen($filePath, 'x+'); $ch = curl_init($url); curl_setopt($ch, CURLOPT_TIMEOUT, 50); curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'); // 打印错误信息 curl_setopt($ch, CURLOPT_VERBOSE, true); $verbose = fopen('php://temp', 'w+'); curl_setopt($ch, CURLOPT_STDERR, $verbose); $data = curl_exec($ch); if(curl_errno($ch)) { rewind($verbose); $verboseLog = stream_get_contents($verbose); $io->error("Curl错误:".curl_error($ch).",详细日志:{$verboseLog}"); } curl_close($ch); fclose($verbose);
二、关于直接解析URL还是下载文件
两种方式都可行,但推荐先下载到本地临时文件再解析,原因:
- 避免网络中断导致解析到一半失败
- 调试时可以重复使用本地文件
- 目标服务器可能限制单次请求时长,本地解析更稳定
整合下载+解析的完整逻辑
修改你的execute方法,实现“下载文件→解析→入库→清理临时文件”的流程:
protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $io->title('开始导入商店Feed数据...'); $converter = new ConverterToArray(); // 获取所有配置了Feed URL的商店 $stores = $this->em->getRepository(Store::class)->findBy(['feedUrl' => ['!=', null]]); foreach ($stores as $store) { $url = $store->getFeedUrl(); $feedColumnsMatch = $store->getFeedColumnsMatch(); // 生成唯一文件名避免冲突 $fileName = sprintf('%s-%s.txt', $store->getName(), uniqid()); $filePath = $fileUploader->getTargetDirectory() . "/" . $fileName; try { // 1. 用Guzzle获取Feed内容 $client = new Client(); $response = $client->request('GET', $url, [ 'headers' => [ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', ], ]); if ($response->getStatusCode() !== 200) { $io->error("商店{$store->getName()}请求失败:HTTP {$response->getStatusCode()}"); continue; } // 2. 保存到本地文件 file_put_contents($filePath, $response->getBody()->getContents()); $io->writeln("已下载{$store->getName()}的Feed到本地"); // 3. 解析CSV/TSV文件 $articlesArray = $converter->convert($filePath, $feedColumnsMatch); // 4. 这里添加把$articlesArray存入数据库的逻辑 // 比如循环$articlesArray,创建Article实体,persist后flush foreach ($articlesArray as $articleData) { $article = new Article(); $article->setStore($store); $article->setName($articleData[$feedColumnsMatch->getNameColumn()]); // 其他字段映射... $this->em->persist($article); } $this->em->flush(); $io->writeln("成功导入{$store->getName()}的".count($articlesArray)."条商品数据"); // 5. 清理临时文件 unlink($filePath); } catch (\Exception $e) { $io->error("处理{$store->getName()}时出错:{$e->getMessage()}"); // 出错时清理临时文件 if (file_exists($filePath)) { unlink($filePath); } continue; } } $io->success('所有Feed导入任务完成!'); return Command::SUCCESS; }
三、优化ConverterToArray类
你的解析逻辑可以做几个小优化,提升稳定性:
public function convert($filePath, FeedColumnsMatch $feedColumnsMatch) { if(!file_exists($filePath) || !is_readable($filePath)) { $io->error("文件不存在或无法读取:{$filePath}"); return []; } $articles = []; $headerRecord = []; $feedFormat = $feedColumnsMatch->getFeedFormat(); if($feedFormat === "csv" || $feedFormat === "tsv"){ $delimiter = $feedFormat === "csv" ? $feedColumnsMatch->getDelimiter() : "\t"; if (($handle = fopen($filePath, 'r')) !== FALSE) { $rowCounter = 0; while (($rowData = fgetcsv($handle, 1000, $delimiter)) !== FALSE) { // 转码为UTF-8,避免乱码(适配数据库编码) $rowData = array_map(function($value) { return mb_convert_encoding($value, 'UTF-8', 'auto'); }, $rowData); if(0 === $rowCounter){ $headerRecord = $rowData; }else{ // 避免索引越界,只匹配存在的表头 $article = []; foreach ($rowData as $key => $value){ if(isset($headerRecord[$key])){ $article[$headerRecord[$key]] = $value; } } $articles[] = $article; } $rowCounter++; } fclose($handle); } } return $articles; }
四、配置CRON定时任务
当命令能正常运行后,配置CRON定时触发,比如每天凌晨2点执行:
0 2 * * * /usr/bin/php /path/to/your/project/bin/console app:update-articles >> /path/to/your/project/var/log/feed-update.log 2>&1
注意替换成你的PHP路径和项目路径,日志文件用来记录运行情况,方便排查问题。
内容的提问来源于stack exchange,提问作者elgiorgio
相关产品推荐
相关产品推荐

