2009-08-27 28 views
11

Tôi đang tải tệp CSV từ máy chủ khác dưới dạng nguồn cấp dữ liệu từ nhà cung cấp.Thao tác chuỗi dài 30 triệu ký tự

Tôi đang sử dụng curl để lấy nội dung của tệp và lưu thành biến được gọi là $contents.

Tôi có thể truy cập vào phần đó tốt, nhưng tôi đã cố gắng phát nổ bởi \r\n để nhận một dãy các dòng nhưng không thành công với lỗi 'hết bộ nhớ'.

I echo strlen($contents) và khoảng 30,5 triệu ký tự.

Tôi cần thao tác các giá trị và chèn chúng vào cơ sở dữ liệu. Tôi cần phải làm gì để tránh các lỗi phân bổ bộ nhớ?

Trả lời

17

PHP bị nghẹt thở vì nó sắp hết bộ nhớ. Thay vì có curl điền một biến PHP với nội dung của tệp, hãy sử dụng tùy chọn

CURLOPT_FILE 

để lưu tệp vào đĩa thay thế.

//pseudo, untested code to give you the idea 

$fp = fopen('path/to/save/file', 'w'); 
curl_setopt($ch, CURLOPT_FILE, $fp); 
curl_exec ($ch); 
curl_close ($ch); 
fclose($fp); 

Sau đó, khi các tập tin được lưu, thay vì sử dụng file hoặc file_get_contents chức năng (mà sẽ tải toàn bộ tập tin vào bộ nhớ, giết chết PHP một lần nữa), sử dụng fopenfgets để đọc các tập tin một dòng tại một thời gian.

+5

Câu trả lời trong http://stackoverflow.com/a/1342760/4668 là câu trả lời hay nhất của tôi. –

2

Đổ nó vào một tệp. Đừng cố gắng giữ tất cả dữ liệu đó trong bộ nhớ cùng một lúc.

3
  1. Tăng memory_limit trong php.ini.
  2. Đọc dữ liệu bằng cách sử dụng fopen()fgets().
5

Bạn có thể muốn xem xét lưu tệp vào tệp tạm thời và sau đó đọc từng dòng một bằng cách sử dụng fgets hoặc fgetcsv.

Bằng cách này bạn tránh được mảng lớn ban đầu mà bạn nhận được từ việc bùng nổ một chuỗi lớn như vậy.

47

Như câu trả lời khác nói:

  • bạn không thể có tất cả những gì trong bộ nhớ
  • một giải pháp sẽ được sử dụng CURLOPT_FILE

Tuy nhiên, bạn có thể không muốn thực sự tạo ra một tập tin ; bạn có thể muốn làm việc với dữ liệu trong bộ nhớ ... Sử dụng nó ngay sau khi nó "đến".

Một giải pháp khả thi có thể được definind bạn sở hữu dòng wrapper, và sử dụng thế này, thay vì một tập tin thực tế, với CURLOPT_FILE

Trước hết, xem:


Và bây giờ, chúng ta hãy đi với một ví dụ.

Trước tiên, hãy tạo lớp dòng wrapper của chúng tôi:

class MyStream { 
    protected $buffer; 

    function stream_open($path, $mode, $options, &$opened_path) { 
     // Has to be declared, it seems... 
     return true; 
    } 

    public function stream_write($data) { 
     // Extract the lines ; on y tests, data was 8192 bytes long ; never more 
     $lines = explode("\n", $data); 

     // The buffer contains the end of the last line from previous time 
     // => Is goes at the beginning of the first line we are getting this time 
     $lines[0] = $this->buffer . $lines[0]; 

     // And the last line os only partial 
     // => save it for next time, and remove it from the list this time 
     $nb_lines = count($lines); 
     $this->buffer = $lines[$nb_lines-1]; 
     unset($lines[$nb_lines-1]); 

     // Here, do your work with the lines you have in the buffer 
     var_dump($lines); 
     echo '<hr />'; 

     return strlen($data); 
    } 
} 

Những gì tôi làm là:

  • làm việc trên các khối dữ liệu (tôi sử dụng var_dump, nhưng bạn muốn làm công cụ thông thường của bạn thay vào đó) khi họ đến
  • Lưu ý rằng bạn không nhận được "dòng đầy đủ": kết thúc của một dòng là phần bắt đầu của một đoạn và đầu của cùng một dòng đó là ở cuối đoạn trước đó; như vậy, bạn cần phải giữ một số bộ phận của một chunck giữa các cuộc gọi đến stream_write


Tiếp theo, chúng tôi đăng ký dòng wrapper này, để được sử dụng với giả giao thức "thử nghiệm":

// Register the wrapper 
stream_wrapper_register("test", "MyStream") 
    or die("Failed to register protocol"); 


Và, bây giờ, chúng tôi yêu cầu curl của chúng tôi, như chúng ta sẽ làm gì khi writting vào một tập tin "thật", như câu trả lời khác đề nghị:

// Open the "file" 
$fp = fopen("test://MyTestVariableInMemory", "r+"); 

// Configuration of curl 
$ch = curl_init(); 
curl_setopt($ch, CURLOPT_URL, "http://www.rue89.com/"); 
curl_setopt($ch, CURLOPT_HEADER, 0); 
curl_setopt($ch, CURLOPT_BUFFERSIZE, 256); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 
curl_setopt($ch, CURLOPT_FILE, $fp); // Data will be sent to our stream ;-) 

curl_exec($ch); 

curl_close($ch); 

// Don't forget to close the "file"/stream 
fclose($fp); 

Lưu ý rằng chúng ta không làm việc với một tệp thực, nhưng với giao thức giả của chúng ta.


Bằng cách này, mỗi lần một đoạn dữ liệu đến, phương pháp MyStream::stream_write sẽ được gọi, và sẽ có thể làm việc trên một lượng nhỏ dữ liệu (khi tôi kiểm tra, tôi luôn luôn có 8192 byte, bất cứ điều gì giá trị tôi sử dụng cho CURLOPT_BUFFERSIZE)


Một vài lưu ý:

  • Bạn cần phải kiểm tra điều này hơn tôi đã làm, rõ ràng
  • việc triển khai stream_write của tôi có thể sẽ không hoạt động nếu các dòng dài hơn 8192 byte; tùy thuộc vào bạn để vá nó ;-)
  • Nó chỉ có nghĩa là một vài gợi ý, và không phải là một giải pháp làm việc đầy đủ: bạn phải kiểm tra (một lần nữa), và có lẽ mã nhiều hơn một chút!

Tuy nhiên, tôi hy vọng điều này sẽ giúp ;-)
Hãy vui vẻ!

+0

+1 cho điều này! Tôi chỉ muốn thêm rằng khi giao dịch với dữ liệu nhị phân bạn muốn xuất dữ liệu $ trực tiếp và không chạm vào nó ở tất cả vì điều đó rất có thể sẽ làm hỏng nó. – mekwall

+4

Thông minh. Kể từ khi curl 7.9.7 'CURLOPT_FILE' được đổi tên thành' CURLOPT_WRITEDATA' và tôi nghĩ bây giờ bạn có thể làm điều gì đó tương tự bằng cách sử dụng 'CURLOPT_WRITEFUNCTION', gọi lại là' stream_write ($ data) 'và lưu nhu cầu luồng vỏ bánh. Xem http://curl.haxx.se/libcurl/c/curl_easy_setopt.html –

+0

đây là giải pháp thân thiện với bộ nhớ. – Lupus

0

NB:

"Về cơ bản, nếu bạn mở một tập tin với fopen, fclose nó và sau đó bỏ liên kết nó, nó hoạt động tốt Nhưng nếu giữa fopen và fclose, bạn cung cấp cho các tập tin xử lý để curl để làm. một số văn bản vào tập tin, thì bỏ liên kết thất bại. Tại sao điều này xảy ra là ngoài tôi. tôi nghĩ rằng nó có thể liên quan đến Bug # 48.676"

http://bugs.php.net/bug.php?id=49517

Vì vậy, hãy cẩn thận nếu bạn đang ở trên một tuổi phiên bản PHP. Có một sửa chữa đơn giản trên trang này để gấp đôi đóng tài nguyên tệp:

fclose($fp); 
if (is_resource($fp)) 
    fclose($fp); 
9

Darren Cook comment to Pascal MARTIN phản hồi thực sự thú vị. Trong các phiên bản PHP + Curl hiện đại, tùy chọn CURLOPT_WRITEFUNCTION có thể được thiết lập để CURL gọi một cuộc gọi lại cho mỗi "dữ liệu" nhận được. Cụ thể, "có thể gọi" sẽ nhận được hai tham số, cái đầu tiên có đối tượng curl gọi và đối số thứ hai với đoạn dữ liệu. Funcion phải trả lại strlen($data) để cuộn tiếp tục gửi thêm dữ liệu.

Các cuộc gọi có thể là các phương thức trong PHP. Sử dụng tất cả điều này, tôi đã phát triển một giải pháp khả thi mà tôi thấy dễ đọc hơn mà trước đó (mặc dù phản ứng Pascal Martin thực sự tuyệt vời, mọi thứ đã thay đổi kể từ đó). Tôi đã sử dụng các thuộc tính công khai để đơn giản, nhưng tôi chắc rằng người đọc có thể thích ứng và cải thiện mã. Bạn thậm chí có thể hủy bỏ yêu cầu CURL khi đạt được một số dòng (hoặc byte). Tôi hy vọng điều này sẽ hữu ích cho người khác.

<? 
class SplitCurlByLines { 

    public function curlCallback($curl, $data) { 

     $this->currentLine .= $data; 
     $lines = explode("\n", $this->currentLine); 
     // The last line could be unfinished. We should not 
     // proccess it yet. 
     $numLines = count($lines) - 1; 
     $this->currentLine = $lines[$numLines]; // Save for the next callback. 

     for ($i = 0; $i < $numLines; ++$i) { 
      $this->processLine($lines[$i]); // Do whatever you want 
      ++$this->totalLineCount; // Statistics. 
      $this->totalLength += strlen($lines[$i]) + 1; 
     } 
     return strlen($data); // Ask curl for more data (!= value will stop). 

    } 

    public function processLine($str) { 
     // Do what ever you want (split CSV, ...). 
     echo $str . "\n"; 
    } 

    public $currentLine = ''; 
    public $totalLineCount = 0; 
    public $totalLength = 0; 

} // SplitCurlByLines 

// Just for testing, I will echo the content of Stackoverflow 
// main page. To avoid artifacts, I will inform the browser about 
// plain text MIME type, so the source code should be vissible. 
Header('Content-type: text/plain'); 

$splitter = new SplitCurlByLines(); 

// Configuration of curl 
$ch = curl_init(); 
curl_setopt($ch, CURLOPT_URL, "http://stackoverflow.com/"); 
curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($splitter, 'curlCallback')); 

curl_exec($ch); 

// Process the last line. 
$splitter->processLine($splitter->currentLine); 

curl_close($ch); 

error_log($splitter->totalLineCount . " lines; " . 
$splitter->totalLength . " bytes."); 
?>