需從幾百 MB 甚至 GB 級文本文件(如日誌、字典、語料庫)中**不加載整文件到內存**的前提下,等概率隨機抽取一行。核心難點:行長度不固定、無法直接按字節偏移定位行邊界。
<?php
function getRandomLineFile(string $file): string|false
{
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!$lines) return false;
return $lines[array_rand($lines)];
}
// 內存佔用 = 文件大小,>100MB 易 OOM
第一遍統計總行數,第二遍讀到目標行。內存 O(1),但需兩次完整 IO。
<?php
function getRandomLineTwoPass(string $file): string|false
{
$fh = fopen($file, 'rb');
if (!$fh) return false;
// Pass 1: 計數
$lineCount = 0;
while (!feof($fh)) {
if (fgets($fh)) $lineCount++;
}
if ($lineCount === 0) { fclose($fh); return false; }
$target = random_int(0, $lineCount - 1);
// Pass 2: 定位
rewind($fh);
$current = 0;
while (!feof($fh)) {
$line = fgets($fh);
if ($line !== false && $current++ === $target) {
fclose($fh);
return rtrim($line, "\r\n");
}
}
fclose($fh);
return false;
}
推薦用於大文件。遍歷時以 1/n 概率替換當前保留行,數學證明每行最終被選中概率均為 1/N。
<?php
function getRandomLineReservoir(string $file): string|false
{
$fh = fopen($file, 'rb');
if (!$fh) return false;
$selected = '';
$count = 0;
while (($line = fgets($fh)) !== false) {
$count++;
// 以 1/count 概率保留當前行
if (random_int(1, $count) === 1) {
$selected = rtrim($line, "\r\n");
}
}
fclose($fh);
return $count > 0 ? $selected : false;
}
隨機字節偏移 → 向前讀到行首 → 返回該行。**偏向長行**,但極快(單次 seek + 少量讀取)。
<?php
function getRandomLineByteSeek(string $file): string|false
{
$size = filesize($file);
if ($size === 0) return false;
$fh = fopen($file, 'rb');
if (!$fh) return false;
// 隨機偏移
$offset = random_int(0, $size - 1);
fseek($fh, $offset);
// 丟棄不完整的行(讀到下一個換行符)
fgets($fh);
// 讀取完整下一行
$line = fgets($fh);
// 若讀到 EOF,從頭開始讀第一行(環狀)
if ($line === false) {
rewind($fh);
$line = fgets($fh);
}
fclose($fh);
return $line ? rtrim($line, "\r\n") : false;
}
| 算法 | 內存 | IO 次數 | 耗時(估) | 均勻度 | 適用場景 |
|---|---|---|---|---|---|
| file() 全讀入 | ~100MB | 1 次順序 | ~200ms | 完美 | <50MB 文件 |
| 兩次遍歷 | O(1) | 2 次順序 | ~400ms | 完美 | 中等文件、需精確 |
| 水塘抽樣 | O(1) | 1 次順序 | ~220ms | 完美 | 大文件首選 |
| 字節跳轉 | O(1) | 1 次隨機+少量 | ~5ms | 偏向長行 | 極大文件、日誌抽樣 |
<?php
function getWeightedRandomLine(string $file, callable $weightFn): string|false
{
$fh = fopen($file, 'rb');
if (!$fh) return false;
$totalWeight = 0;
$selected = '';
while (($line = fgets($fh)) !== false) {
$w = $weightFn($line);
$totalWeight += $w;
if (random_int(1, $totalWeight) <= $w) {
$selected = rtrim($line, "\r\n");
}
}
fclose($fh);
return $selected ?: false;
}
// 示例:按行長度加權
$line = getWeightedRandomLine('data.txt', fn($l) => strlen($l));
offsets[],隨機選索引 fseek(offsets[$i]) 秒級定位mb_substr 處理多字節字符