利用 PHP 陣列「鍵值唯一」特性進行去重,是處理百萬級 ID、手機號、Email 等純量資料的經典高效方案。本文深度解析底層機制、提供生產級代碼、分析記憶體佔用、給出分塊處理與替代方案建議。
PHP 陣列底層是有序 HashTable,插入操作 $arr[$key] = $val 會:
$key 雜湊時間複雜度 O(1) 平均,空間複雜度 O(N)。
<?php
/**
* 檔案去重:每行一個 ID,輸出去重後結果
* @param string $inputFile 輸入檔案路徑
* @param string $outputFile 輸出檔案路徑
* @return int 去重後數量
*/
function deduplicateFile(string $inputFile, string $outputFile): int {
$unique = []; // 鍵為 ID,值固定為 1(佔用最小)
$fp = fopen($inputFile, 'r');
if (!$fp) throw new RuntimeException("無法開啟檔案: $inputFile");
while (($line = fgets($fp)) !== false) {
$id = trim($line, "\r\n\t ");
if ($id === '') continue;
// 關鍵:以 ID 為鍵,自動去重
$unique[$id] = 1;
}
fclose($fp);
// 寫入結果
$outFp = fopen($outputFile, 'w');
foreach ($unique as $id => $_) {
fwrite($outFp, $id . "\n");
}
fclose($outFp);
return count($unique);
}
// 使用
$count = deduplicateFile('ids.txt', 'ids_unique.txt');
echo "去重完成,唯一 ID 數: $count";
<?php
function mergeDeduplicate(string $file1, string $file2, string $outputFile): int {
$unique = [];
foreach ([$file1, $file2] as $file) {
$fp = fopen($file, 'r');
while (($line = fgets($fp)) !== false) {
$id = trim($line, "\r\n\t ");
if ($id !== '') $unique[$id] = 1;
}
fclose($fp);
}
$outFp = fopen($outputFile, 'w');
foreach ($unique as $id => $_) {
fwrite($outFp, $id . "\n");
}
fclose($outFp);
return count($unique);
}
| 資料量 | 預估記憶體 (64位 PHP) | 備註 |
|---|---|---|
| 100 萬 | ~48 MB | 陣列開銷 ~48 bytes/元素 |
| 500 萬 | ~240 MB | 接近預設 memory_limit 256M |
| 1000 萬 | ~480 MB | 需調大 memory_limit 或分塊處理 |
$unique[$id] = 1 比字串/布林佔用更小yield 流式處理,但去重仍需全量鍵在記憶體INSERT IGNORE INTO tmp_ids (id) VALUES ... + SELECT id FROM tmp_ids,利用 DB 索引去重,無記憶體限制SADD unique_ids $id 分散式去重,支援跨機器<?php
function externalDeduplicate(string $inputFile, string $outputFile, int $chunkSize = 1000000): void {
$tempFiles = [];
$chunk = [];
$chunkId = 0;
// Phase 1: 分塊讀取、去重、排序、寫入暫存檔
$fp = fopen($inputFile, 'r');
while (($line = fgets($fp)) !== false) {
$id = trim($line, "\r\n\t ");
if ($id === '') continue;
$chunk[$id] = 1;
if (count($chunk) >= $chunkSize) {
ksort($chunk); // 排序便於歸併
$tempFile = sys_get_temp_dir() . "/dedup_chunk_{$chunkId}.tmp";
file_put_contents($tempFile, implode("\n", array_keys($chunk)) . "\n");
$tempFiles[] = $tempFile;
$chunk = [];
$chunkId++;
}
}
// 最後一塊
if ($chunk) {
ksort($chunk);
$tempFile = sys_get_temp_dir() . "/dedup_chunk_{$chunkId}.tmp";
file_put_contents($tempFile, implode("\n", array_keys($chunk)) . "\n");
$tempFiles[] = $tempFile;
}
fclose($fp);
// Phase 2: K-way 歸併去重(簡化版:依次讀入記憶體,實際應用優先隊列)
$merged = [];
foreach ($tempFiles as $tf) {
$lines = file($tf, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $id) {
$merged[$id] = 1;
}
unlink($tf); // 清理暫存
}
file_put_contents($outputFile, implode("\n", array_keys($merged)) . "\n");
echo "外部去重完成,唯一數: " . count($merged);
}
| 方案 | 100萬級耗時 | 記憶體 | 適用場景 |
|---|---|---|---|
| PHP 陣列鍵值 | ~0.3-0.5 秒 | ~48 MB | 單機、記憶體充足、一次性任務 |
| MySQL INSERT IGNORE | ~2-5 秒 | 無限制 | 持久化需求、並發寫入、資料已在 DB |
| Redis SADD | ~0.5-1 秒 | Redis 記憶體 | 分散式、多服務共享、需持久化 |
| sort + uniq (Linux) | ~0.2 秒 | 檔案系統 | 純文字檔案、無 PHP 環境、極大檔案 |
| Awk 去重 | ~0.3 秒 | 流式 | awk '!seen[$0]++' big.txt > uniq.txt |
sort -u / Redis / MySQLPHP 陣列去重是「瑞士軍刀」:簡單、快速、零依賴。認清記憶體邊界,超出即切換外部排序或資料庫方案,才是工程師的務實態度。