2009-08-27 15 views
11

Pobieram plik CSV z innego serwera jako plik danych od dostawcy.Manipulowanie łańcuchem o długości 30 milionów znaków

Używam curl, aby uzyskać zawartość pliku i zapisać go w zmiennej o nazwie $contents.

Mogę dostać się do tej części, ale próbowałem eksplodować przez \r i \n, aby uzyskać tablicę linii, ale kończy się ona błędem "brak pamięci".

I echo strlen($contents) i około 30,5 miliona znaków.

Potrzebuję manipulować wartościami i wstawić je do bazy danych. Co muszę zrobić, aby uniknąć błędów przydziału pamięci?

Odpowiedz

17

PHP się dławi, bo wyczerpuje się pamięć. Zamiast zawijać zmienną PHP z zawartością pliku, użyj opcji, aby zapisać plik na dysku.

//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); 

Następnie, po zapisaniu pliku, zamiast przy użyciu file lub file_get_contents funkcji (co wczytać cały plik do pamięci, ponownie zabijania PHP), użyj fopen i fgets odczytać pliku wiersz po wierszu czas.

+5

Odpowiedź w dół pod adresem http://stackoverflow.com/a/1342760/4668 jest lepsza niż moja. –

2

Przeprowadź buforowanie do pliku. Nie próbuj jednocześnie przechowywać wszystkich danych w pamięci.

3
  1. Zwiększenie memory_limit w php.ini.
  2. Odczytywanie danych przy użyciu fopen() i fgets().
5

Być może warto rozważyć zapisanie pliku tymczasowego, a następnie odczytanie go w jednym wierszu na raz za pomocą fgets lub fgetcsv.

W ten sposób unikasz początkowej dużej szyku, którą otrzymujesz po eksplodowaniu tak dużego ciągu znaków.

47

W innych odpowiedzi powiedział:

  • nie można mieć to wszystko w pamięci
  • rozwiązaniem byłoby wykorzystanie CURLOPT_FILE

Ale możesz nie chcieć, aby stworzyć naprawdę plik; możesz chcieć pracować z danymi w pamięci ... Używając jej tak szybko, jak tylko "dotrze".

Jednym z możliwych rozwiązań może być definind jesteś właścicielem strumienia otoki i używać ten jeden, zamiast rzeczywistego pliku, z CURLOPT_FILE

Przede wszystkim zobacz:


A teraz, zróbmy przykład.

Najpierw stwórzmy naszą klasę strumień Wrapper:

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); 
    } 
} 

Co mogę zrobić, to:

  • prace nad kawałkami danych (używam var_dump, ale trzeba robić swoje zwykłe rzeczy zamiast tego) po ich otrzymaniu:
  • Zwróć uwagę, że nie otrzymujesz "pełnych linii": koniec linii jest początkiem fragmentu, a początek tej samej linii znajdował się na końcu poprzedniego fragmentu; tak, trzeba zachować niektóre części chunck między połączeniami do stream_write


Dalej, możemy zarejestrować ten strumień opakowanie, które mają być używane z pseudo-protocol „test”:

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


a teraz robimy curl wniosek, jak będziemy to robić, gdy pisanie do "prawdziwego" pliku, podobnie jak inne odpowiedzi zasugerował:

// 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); 

Uwaga nie pracujemy z prawdziwym plikiem, ale z naszym pseudo-protokołem.


W ten sposób, za każdym razem porcja danych przybywa, MyStream::stream_write metoda będzie sprawdzony i będzie w stanie pracować na małej ilości danych (kiedy testowane, zawsze dostaje 8192 bajtów, niezależnie od wartości I wykorzystywane do CURLOPT_BUFFERSIZE)


kilka uwag:

  • trzeba to sprawdzić więcej niż ja, oczywiście
  • moja implementacja stream_write prawdopodobnie nie zadziała, jeśli linie są dłuższe niż 8192 bajty; do ciebie, aby to załatać ;-)
  • Jest to tylko kilka wskazówek, a nie w pełni działające rozwiązanie: musisz przetestować (ponownie) i prawdopodobnie trochę więcej kodu!

Nadal mam nadzieję, że to pomoże ;-)
Miłej zabawy!

+0

+1 za to! Chciałbym tylko dodać, że podczas pracy z danymi binarnymi chciałbyś bezpośrednio wysyłać dane $ i wcale tego nie dotykać, ponieważ najprawdopodobniej to zepsuje. – mekwall

+4

Sprytny. Ponieważ curl 7.9.7 'CURLOPT_FILE' został przemianowany na' CURLOPT_WRITEDATA', i myślę, że możesz teraz zrobić coś podobnego używając 'CURLOPT_WRITEFUNCTION', który jest wywołaniem zwrotnym takim jak twoje' stream_write ($ dane) 'i oszczędza potrzebę strumienia obwoluta. Zobacz http://curl.haxx.se/libcurl/c/curl_easy_setopt.html –

+0

to dobre, przyjazne pamięci rozwiązanie. – Lupus

0

NB:

„Zasadniczo, jeśli użytkownik otworzy plik z fopen, fclose go, a następnie odłączyć go to działa dobrze, ale jeśli między fopen i fclose, dajesz plik obsługiwać zwijają zrobić. niektóre pisanie do pliku, a następnie odłączenie się niepowodzeniem. Dlaczego to dzieje się poza mną. Myślę, że może to być związane z Bug # 48676"

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

więc być ostrożnym, jeśli jesteś na starszą wersja PHP. Istnieje prosty fix na tej stronie podwójnie bliskiej zasobu pliku:

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

Darren Gotować komentarz do odpowiedzi Pascal MARTIN jest naprawdę interesująca. W nowoczesnych wersjach PHP + Curl można ustawić opcję CURLOPT_WRITEFUNCTION, aby CURL wywoływał wywołanie zwrotne dla każdego otrzymanego "kawałka" danych. W szczególności "callable" otrzyma dwa parametry, pierwszy z wywołującym obiektem zwijania, a drugi z porcją danych. Funkcja powinna powrócić strlen($data), aby zwijać i dalej wysyłać więcej danych.

Callables mogą być metodami w PHP. Wykorzystując to wszystko, opracowałem możliwe rozwiązanie, które uważam za bardziej czytelne niż poprzednie (chociaż reakcja Pascala Martina jest naprawdę świetna, od tego czasu sytuacja się zmieniła). Użyłem atrybutów publicznych dla uproszczenia, ale jestem pewien, że czytelnicy mogliby dostosować i ulepszyć kod. Możesz nawet przerwać żądanie CURL po osiągnięciu pewnej liczby linii (lub bajtów). Mam nadzieję, że byłoby to przydatne dla innych.

<? 
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."); 
?> 
Powiązane problemy