Iterar sobre cada línea en una cadena en PHP

130

Tengo un formulario que permite al usuario cargar un archivo de texto o copiar / pegar el contenido del archivo en un área de texto. Puedo diferenciar fácilmente entre los dos y poner el que ingresaron en una variable de cadena, pero ¿a dónde voy desde allí?

Necesito iterar sobre cada línea de la cadena (preferiblemente no preocuparme por las nuevas líneas en diferentes máquinas), asegurarme de que tenga exactamente un token (sin espacios, tabulaciones, comas, etc.), desinfectar los datos y luego generar una consulta SQL basado en todas las líneas.

Soy un programador bastante bueno, así que conozco la idea general sobre cómo hacerlo, pero hace tanto tiempo que trabajé con PHP que siento que estoy buscando las cosas incorrectas y, por lo tanto, obtengo información inútil. El problema clave que tengo es que quiero leer el contenido de la cadena línea por línea. Si fuera un archivo, sería fácil.

Principalmente busco funciones PHP útiles, no un algoritmo de cómo hacerlo. ¿Alguna sugerencia?

Topher Fangio
fuente
Es posible que desee normalizar las nuevas líneas primero. El método s($myString)->normalizeLineEndings()está disponible con github.com/delight-im/PHP-Str (biblioteca bajo licencia MIT) que tiene muchos otros ayudantes de cadena útiles. Es posible que desee echar un vistazo al código fuente.
graznar

Respuestas:

190

preg_split la variable que contiene el texto e iterar sobre la matriz devuelta:

foreach(preg_split("/((\r?\n)|(\r\n?))/", $subject) as $line){
    // do stuff with $line
} 
Kyril
fuente
¿Esto manejará ^ M además de \ n \ r?
Topher Fangio
No estoy seguro de si el retorno de carro ascii se convierte a \ r una vez que se coloca dentro de una variable. Si no, siempre puede usar un split () / exlope () con el valor ascii en su lugar - ch (13)
Kyril
12
Una mejor expresión regular es /((\r?\n)|(\r\n?))/.
Félix Saparelli
3
Para que coincida con Unix LF (\ n), MacOS <9 CR (\ r), Windows CR + LF (\ r \ n) y raro LF + CR (\ n \ r) debería ser:/((\r?\n)|(\n?\r))/
Esperando Dev ...
2
Es probable que esto bombardee catastróficamente los datos de varios bytes.
pguardiario
158

Me gustaría proponer una alternativa significativamente más rápida (y eficiente en memoria): en strtoklugar de preg_split.

$separator = "\r\n";
$line = strtok($subject, $separator);

while ($line !== false) {
    # do something with $line
    $line = strtok( $separator );
}

Al probar el rendimiento, iteré 100 veces sobre un archivo de prueba con 17 mil líneas: preg_splittomó 27.7 segundos, mientras que strtoktomó 1.4 segundos.

Tenga en cuenta que aunque $separatorse define como "\r\n", strtokse separará en cualquiera de los caracteres y, a partir de PHP4.1.0, omita las líneas / tokens vacíos.

Consulte la entrada manual de strtok: http://php.net/strtok

Erwin Wessels
fuente
21
+1 para consideraciones de rendimiento cuando se trata de grandes conjuntos de líneas.
CodeAngry
44
Aunque esta API de función es un desastre total (llamada con diferentes parámetros), esta es la mejor solución. Ni prey_splittampoco explodedeben usarse para producir fragmentos de cadena estructurados. Es como apuntar a una mosca con una bazuca .
Maciej Sz
1
Si comprueba el uso de la memoria mientras la aplicación se está ejecutando, verá la magia. En realidad, extrae el archivo que está leyendo en la memoria en caso de que recorra cada una de las líneas, y mantiene la ubicación de su token. Querrá eliminar eso para que sea realmente eficiente en memoria. php.net/strtok#103051
AbsoluteƵERØ
2
nota rápida, usar strtok()algo más dentro de ese whileciclo romperá las cosas. También lo estaba usando para agarrar todo en una cadena hasta el primer espacio ( stackoverflow.com/a/2477411/1767412 ) y me tomó un minuto darme cuenta de por qué las cosas no iban según lo planeado
billynoah
1
debería ser la respuesta aceptada, probablemente la solución más rápida de todas las opciones.
John
94

Si necesita manejar nuevas líneas en diferentes sistemas, simplemente puede usar la constante PHP_EOL constante de PHP (http://php.net/manual/en/reserved.constants.php) y simplemente usar explotar para evitar la sobrecarga del motor de expresión regular .

$lines = explode(PHP_EOL, $subject);
FerCa
fuente
30
Cuidado: funcionará en diferentes sistemas, pero no funcionará bien con cadenas de diferentes sistemas . El Manual de PHP establece que PHP_EOL (string)es el símbolo correcto 'Fin de línea' para esta plataforma.
wadim
@wadim tiene razón! Si está procesando un archivo de texto de Windows en un servidor Unix, fallará.
javsmo
1
Tenga en cuenta que, dependiendo de la longitud de sus líneas, esto puede consumir grandes cantidades de memoria para cadenas grandes.
Synchro
Tenga en cuenta que si la última línea contiene un terminador de línea, esto también devolverá otra cadena vacía después de eso.
derecha
20

Es demasiado complicado y feo, pero en mi opinión, este es el camino a seguir:

$fp = fopen("php://memory", 'r+');
fputs($fp, $data);
rewind($fp);
while($line = fgets($fp)){
  // deal with $line
}
fclose($fp);
pguardiario
fuente
1
+1 y también puede usar php://temppara almacenar datos más grandes en un archivo de disco temporal.
CodeAngry
44
Cabe señalar que esto le permite detectar líneas vacías, a diferencia de la solución strtok (). La documentación está en php.net/manual/en/…
Josip Rodin
7
foreach(preg_split('~[\r\n]+~', $text) as $line){
    if(empty($line) or ctype_space($line)) continue; // skip only spaces
    // if(!strlen($line = trim($line))) continue; // or trim by force and skip empty
    // $line is trimmed and nice here so use it
}

^ así es como se rompen las líneas correctamente , multiplataforma compatible con Regexp:)

CodeAngry
fuente
6

Posibles problemas de memoria con strtok:

Dado que una de las soluciones sugeridas usa strtok, desafortunadamente no señala un posible problema de memoria (aunque afirma ser eficiente en la memoria). Cuando se usa de strtokacuerdo con el manual , el:

Tenga en cuenta que solo la primera llamada a strtok usa el argumento de cadena. Cada llamada posterior a strtok solo necesita el token para usar, ya que realiza un seguimiento de dónde está en la cadena actual.

Lo hace cargando el archivo en la memoria. Si está utilizando archivos grandes, debe vaciarlos si ha terminado de recorrer el archivo.

<?php
function process($str) {
    $line = strtok($str, PHP_EOL);

    /*do something with the first line here...*/

    while ($line !== FALSE) {
        // get the next line
        $line = strtok(PHP_EOL);

        /*do something with the rest of the lines here...*/

    }
    //the bit that frees up memory
    strtok('', '');
}

Si solo le preocupan los archivos físicos (por ejemplo, minería de datos):

Según el manual , para la parte de carga de archivos puede usar el filecomando:

 //Create the array
 $lines = file( $some_file );

 foreach ( $lines as $line ) {
   //do something here.
 }
Cero absoluto
fuente
4

La respuesta de Kyril es mejor teniendo en cuenta que necesita poder manejar nuevas líneas en diferentes máquinas.

"Estoy buscando principalmente funciones útiles de PHP, no un algoritmo sobre cómo hacerlo. ¿Alguna sugerencia?"

Los uso mucho:

  • explotar () se puede usar para dividir una cadena en una matriz, dado un único delimitador.
  • implode () es la contraparte de explotar, para volver de la matriz a la cadena.
Joe Kiley
fuente