web-dev-qa-db-fra.com

Utiliser str_replace pour qu'il n'agisse que sur le premier match?

Je veux une version de str_replace() qui ne remplace que la première occurrence de $search dans le $subject. Existe-t-il une solution simple à ce problème ou ai-je besoin d'une solution hacky?

275
Nick Heiner

Peut être fait avec preg_replace :

function str_replace_first($from, $to, $content)
{
    $from = '/'.preg_quote($from, '/').'/';

    return preg_replace($from, $to, $content, 1);
}

echo str_replace_first('abc', '123', 'abcdef abcdef abcdef'); 
// outputs '123def abcdef abcdef'

La magie est dans le quatrième paramètre facultatif [Limite]. De la documentation:

[Limite] - Le maximum possible remplacements pour chaque modèle dans chaque chaîne de sujet. La valeur par défaut est -1 (limite no ).


Cependant, voir la réponse de zombat pour une méthode plus efficace (environ 3 à 4 fois plus rapide).

299
karim79

Il n'y a pas de version de celui-ci, mais la solution n'est pas du tout hacky.

$pos = strpos($haystack, $needle);
if ($pos !== false) {
    $newstring = substr_replace($haystack, $replace, $pos, strlen($needle));
}

Assez facile, et enregistre la pénalité de performance des expressions régulières.


Bonus: Si vous souhaitez remplacer last occurrence, utilisez simplement strrpos au lieu de strpos.

507
zombat

Edit: les deux réponses ont été mises à jour et sont maintenant correctes. Je laisserai la réponse car les timings des fonctions sont toujours utiles.

Les réponses par 'zombat' et 'too much php' ne sont malheureusement pas correctes. Ceci est une révision de la réponse postée par zombat (je n'ai pas assez de réputation pour poster un commentaire):

$pos = strpos($haystack,$needle);
if ($pos !== false) {
    $newstring = substr_replace($haystack,$replace,$pos,strlen($needle));
}

Notez le strlen ($ needle) au lieu de strlen ($ replace). L'exemple de Zombat ne fonctionnera correctement que si l'aiguille et le remplacement ont la même longueur. 

Voici la même fonctionnalité dans une fonction avec la même signature que le propre str_replace de PHP:

function str_replace_first($search, $replace, $subject) {
    $pos = strpos($subject, $search);
    if ($pos !== false) {
        return substr_replace($subject, $replace, $pos, strlen($search));
    }
    return $subject;
}

Ceci est la réponse révisée de 'too much php':

implode($replace, explode($search, $subject, 2));

Notez le 2 à la fin au lieu de 1. Ou au format de la fonction:

function str_replace_first($search, $replace, $subject) {
    return implode($replace, explode($search, $subject, 2));
}

J'ai chronométré les deux fonctions et la première est deux fois plus rapide lorsqu'aucune correspondance n'est trouvée. Ils ont la même vitesse quand un match est trouvé.

90
Bas

Je me demandais lequel était le plus rapide, alors je les ai tous testés.

Vous trouverez ci-dessous:

  • Une liste complète de toutes les fonctions qui ont été ajoutées sur cette page
  • Tests comparatifs pour chaque contrution (temps d'exécution moyen supérieur à 10 000 exécutions)
  • Liens vers chaque réponse (pour le code complet)

Toutes les fonctions ont été testées avec les mêmes paramètres:

$string = 'OOO.OOO.OOO.S';
$search = 'OOO'; 
$replace = 'B';

Fonctions qui remplacent uniquement la première occurrence d'une chaîne dans une chaîne:


Fonctions qui remplacent uniquement la dernière occurrence d'une chaîne dans une chaîne:

66

Malheureusement, je ne connais aucune fonction PHP capable de le faire.
Vous pouvez rouler le vôtre assez facilement comme ceci:

function replace_first($find, $replace, $subject) {
    // stolen from the comments at PHP.net/str_replace
    // Splits $subject into an array of 2 items by $find,
    // and then joins the array with $replace
    return implode($replace, explode($find, $subject, 2));
}
53
too much php

J'ai créé cette petite fonction qui remplace chaîne sur chaîne (sensible à la casse) avec limite, sans avoir besoin de l'expression rationnelle. Ça fonctionne bien.

function str_replace_limit($search, $replace, $string, $limit = 1) {
    $pos = strpos($string, $search);

    if ($pos === false) {
        return $string;
    }

    $searchLen = strlen($search);

    for ($i = 0; $i < $limit; $i++) {
        $string = substr_replace($string, $replace, $pos, $searchLen);

        $pos = strpos($string, $search);

        if ($pos === false) {
            break;
        }
    }

    return $string;
}

Exemple d'utilisation:

$search  = 'foo';
$replace = 'bar';
$string  = 'foo wizard makes foo brew for evil foo and jack';
$limit   = 2;

$replaced = str_replace_limit($search, $replace, $string, $limit);

echo $replaced;
// bar wizard makes bar brew for evil foo and jack
7
Parziphal
function str_replace_once($search, $replace, $subject) {
    $pos = strpos($subject, $search);
    if ($pos === false) {
        return $subject;
    }

    return substr($subject, 0, $pos) . $replace . substr($subject, $pos + strlen($search));
}
3
luchaninov

Le moyen le plus simple serait d'utiliser une expression régulière.

L'autre façon est de trouver la position de la chaîne avec strpos () et ensuite un substr_replace ()

Mais je voudrais vraiment aller pour le RegExp.

3
Rufinus

CODE A ÉTÉ RÉVISÉ, pensez donc que certains commentaires sont trop anciens

Et merci à tous pour m'aider à améliorer cela 

Alors, allons-y pour:

Remplacer le premier 'o' par 'ea' par exemple:

$s='I love you';
echo str_replace_first('o','ea',$s);

//output: I leave you

La fonction:

function str_replace_first($a,$b,$s)
         {
         $w=strpos($s,$a);
         if(!$w)return $s;
         return substr($s,0,$w).$b.substr($s,$w+strlen($a));
         }
2
PYK

En complément de ce que les gens ont dit, rappelez-vous que la chaîne entière est un tableau:

$string = "Lorem ipsum lá lá lá";

$string[0] = "B";

echo $string;

"Borem ipsum lá lá lá"

2
Leandro Castro
$string = 'this is my world, not my world';
$find = 'world';
$replace = 'farm';
$result = preg_replace("/$find/",$replace,$string,1);
echo $result;
2
zack

Selon les résultats de mon test, j'aimerais voter pour le test regular_express fourni par karim79. (Je n'ai pas assez de réputation pour voter maintenant!)

La solution de zombat utilise trop d'appels de fonction, je simplifie même les codes. J'utilise PHP 5.4 pour exécuter les deux solutions 100 000 fois, et voici le résultat:

$str = 'Hello abc, have a Nice day abc! abc!';
$pos = strpos($str, 'abc');
$str = substr_replace($str, '123', $pos, 3);

==> 1,85 seconde

$str = 'Hello abc, have a Nice day abc! abc!';
$str = preg_replace('/abc/', '123', $str, 1);

==> 1,35 seconde

Comme vous pouvez le voir. La performance de preg_replace n’est pas si mauvaise que beaucoup de gens le pensent. Donc, je suggère la solution élégante si votre express régulier n'est pas compliqué.

2
Hunter Wu

Pour développer la réponse de zombat (que je crois être la meilleure), j'ai créé une version récursive de sa fonction qui prend en compte un paramètre $limit pour spécifier le nombre d'occurrences que vous souhaitez remplacer.

function str_replace_limit($haystack, $needle, $replace, $limit, $start_pos = 0) {
    if ($limit <= 0) {
        return $haystack;
    } else {
        $pos = strpos($haystack,$needle,$start_pos);
        if ($pos !== false) {
            $newstring = substr_replace($haystack, $replace, $pos, strlen($needle));
            return str_replace_limit($newstring, $needle, $replace, $limit-1, $pos+strlen($replace));
        } else {
            return $haystack;
        }
    }
}
2
jtate

Pour développer réponse de @ renocor , j'ai écrit une fonction rétro-compatible à 100% avec str_replace(). Autrement dit, vous pouvez remplacer toutes les occurrences de str_replace() par str_replace_limit() sans déranger quoi que ce soit, même ceux qui utilisent des tableaux pour les codes $search, $replace et/ou $subject.

La fonction pourrait être complètement autonome, si vous voulez remplacer l'appel de fonction par ($string===strval(intval(strval($string)))), mais je vous le déconseille, car valid_integer() est une fonction plutôt utile pour les entiers fournis sous forme de chaînes.

Remarque: Chaque fois que cela est possible, str_replace_limit() utilisera plutôt str_replace() afin que tous les appels à str_replace() puissent être remplacés par str_replace_limit() sans que vous ayez à vous soucier de la performance.

Usage

<?php
$search = 'a';
$replace = 'b';
$subject = 'abcabc';
$limit = -1; // No limit
$new_string = str_replace_limit($search, $replace, $subject, $count, $limit);
echo $count.' replacements -- '.$new_string;

2 remplacements - bbcbbc

$limit = 1; // Limit of 1
$new_string = str_replace_limit($search, $replace, $subject, $count, $limit);
echo $count.' replacements -- '.$new_string;

1 remplacements - bbcabc

$limit = 10; // Limit of 10
$new_string = str_replace_limit($search, $replace, $subject, $count, $limit);
echo $count.' replacements -- '.$new_string;

2 remplacements - bbcbbc

Une fonction

<?php

/**
 * Checks if $string is a valid integer. Integers provided as strings (e.g. '2' vs 2)
 * are also supported.
 * @param mixed $string
 * @return bool Returns boolean TRUE if string is a valid integer, or FALSE if it is not 
 */
function valid_integer($string){
    // 1. Cast as string (in case integer is provided)
    // 1. Convert the string to an integer and back to a string
    // 2. Check if identical (note: 'identical', NOT just 'equal')
    // Note: TRUE, FALSE, and NULL $string values all return FALSE
    $string = strval($string);
    return ($string===strval(intval($string)));
}

/**
 * Replace $limit occurences of the search string with the replacement string
 * @param mixed $search The value being searched for, otherwise known as the needle. An
 * array may be used to designate multiple needles.
 * @param mixed $replace The replacement value that replaces found search values. An
 * array may be used to designate multiple replacements.
 * @param mixed $subject The string or array being searched and replaced on, otherwise
 * known as the haystack. If subject is an array, then the search and replace is
 * performed with every entry of subject, and the return value is an array as well. 
 * @param string $count If passed, this will be set to the number of replacements
 * performed.
 * @param int $limit The maximum possible replacements for each pattern in each subject
 * string. Defaults to -1 (no limit).
 * @return string This function returns a string with the replaced values.
 */
function str_replace_limit(
        $search,
        $replace,
        $subject,
        &$count,
        $limit = -1
    ){

    // Set some defaults
    $count = 0;

    // Invalid $limit provided. Throw a warning.
    if(!valid_integer($limit)){
        $backtrace = debug_backtrace();
        trigger_error('Invalid $limit `'.$limit.'` provided to '.__function__.'() in '.
                '`'.$backtrace[0]['file'].'` on line '.$backtrace[0]['line'].'. Expecting an '.
                'integer', E_USER_WARNING);
        return $subject;
    }

    // Invalid $limit provided. Throw a warning.
    if($limit<-1){
        $backtrace = debug_backtrace();
        trigger_error('Invalid $limit `'.$limit.'` provided to '.__function__.'() in '.
                '`'.$backtrace[0]['file'].'` on line '.$backtrace[0]['line'].'. Expecting -1 or '.
                'a positive integer', E_USER_WARNING);
        return $subject;
    }

    // No replacements necessary. Throw a notice as this was most likely not the intended
    // use. And, if it was (e.g. part of a loop, setting $limit dynamically), it can be
    // worked around by simply checking to see if $limit===0, and if it does, skip the
    // function call (and set $count to 0, if applicable).
    if($limit===0){
        $backtrace = debug_backtrace();
        trigger_error('Invalid $limit `'.$limit.'` provided to '.__function__.'() in '.
                '`'.$backtrace[0]['file'].'` on line '.$backtrace[0]['line'].'. Expecting -1 or '.
                'a positive integer', E_USER_NOTICE);
        return $subject;
    }

    // Use str_replace() whenever possible (for performance reasons)
    if($limit===-1){
        return str_replace($search, $replace, $subject, $count);
    }

    if(is_array($subject)){

        // Loop through $subject values and call this function for each one.
        foreach($subject as $key => $this_subject){

            // Skip values that are arrays (to match str_replace()).
            if(!is_array($this_subject)){

                // Call this function again for
                $this_function = __FUNCTION__;
                $subject[$key] = $this_function(
                        $search,
                        $replace,
                        $this_subject,
                        $this_count,
                        $limit
                );

                // Adjust $count
                $count += $this_count;

                // Adjust $limit, if not -1
                if($limit!=-1){
                    $limit -= $this_count;
                }

                // Reached $limit, return $subject
                if($limit===0){
                    return $subject;
                }

            }

        }

        return $subject;

    } elseif(is_array($search)){
        // Only treat $replace as an array if $search is also an array (to match str_replace())

        // Clear keys of $search (to match str_replace()).
        $search = array_values($search);

        // Clear keys of $replace, if applicable (to match str_replace()).
        if(is_array($replace)){
            $replace = array_values($replace);
        }

        // Loop through $search array.
        foreach($search as $key => $this_search){

            // Don't support multi-dimensional arrays (to match str_replace()).
            $this_search = strval($this_search);

            // If $replace is an array, use the value of $replace[$key] as the replacement. If
            // $replace[$key] doesn't exist, just an empty string (to match str_replace()).
            if(is_array($replace)){
                if(array_key_exists($key, $replace)){
                    $this_replace = strval($replace[$key]);
                } else {
                    $this_replace = '';
                }
            } else {
                $this_replace = strval($replace);
            }

            // Call this function again for
            $this_function = __FUNCTION__;
            $subject = $this_function(
                    $this_search,
                    $this_replace,
                    $subject,
                    $this_count,
                    $limit
            );

            // Adjust $count
            $count += $this_count;

            // Adjust $limit, if not -1
            if($limit!=-1){
                $limit -= $this_count;
            }

            // Reached $limit, return $subject
            if($limit===0){
                return $subject;
            }

        }

        return $subject;

    } else {
        $search = strval($search);
        $replace = strval($replace);

        // Get position of first $search
        $pos = strpos($subject, $search);

        // Return $subject if $search cannot be found
        if($pos===false){
            return $subject;
        }

        // Get length of $search, to make proper replacement later on
        $search_len = strlen($search);

        // Loop until $search can no longer be found, or $limit is reached
        for($i=0;(($i<$limit)||($limit===-1));$i++){

            // Replace 
            $subject = substr_replace($subject, $replace, $pos, $search_len);

            // Increase $count
            $count++;

            // Get location of next $search
            $pos = strpos($subject, $search);

            // Break out of loop if $needle
            if($pos===false){
                break;
            }

        }

        // Return new $subject
        return $subject;

    }

}
2
0b10011

Pour une ficelle

$string = 'OOO.OOO.OOO.S';
$search = 'OOO';
$replace = 'B';

//replace ONLY FIRST occurance of "OOO" with "B"
    $string = substr_replace($string,$replace,0,strlen($search));
    //$string => B.OOO.OOO.S

//replace ONLY LAST occurance of "OOOO" with "B"
    $string = substr_replace($string,$replace,strrpos($string,$search),strlen($search)) 
    //$string => OOO.OOO.B.S

    //replace ONLY LAST occurance of "OOOO" with "B"
    $string = strrev(implode(strrev($replace),explode(strrev($search),strrev($string),2)))
    //$string => OOO.OOO.B.S

Pour un seul personnage

$string[strpos($string,$search)] = $replace;


//EXAMPLE

$string = 'O.O.O.O.S';
$search = 'O';
$replace = 'B';

//replace ONLY FIRST occurance of "O" with "B" 
    $string[strpos($string,$search)] = $replace;  
    //$string => B.O.O.O.S

//replace ONLY LAST occurance of "O" with "B" 
    $string[strrpos($string,$search)] = $replace; 
    // $string => B.O.O.B.S
1

Cette fonction est fortement inspirée de la réponse de @renocor . Elle sécurise la fonction multi-octets.

function str_replace_limit($search, $replace, $string, $limit)
{
    $i = 0;
    $searchLength = mb_strlen($search);

    while(($pos = mb_strpos($string, $search)) !== false && $i < $limit)
    {
        $string = mb_substr_replace($string, $replace, $pos, $searchLength);
        $i += 1;
    }

    return $string;
}

function mb_substr_replace($string, $replacement, $start, $length = null, $encoding = null)
{
    $string = (array)$string;
    $encoding = is_null($encoding) ? mb_internal_encoding() : $encoding;
    $length = is_null($length) ? mb_strlen($string) - $start : $length;

    $string = array_map(function($str) use ($replacement, $start, $length, $encoding){

        $begin = mb_substr($str, 0, $start, $encoding);
        $end = mb_substr($str, ($start + $length), mb_strlen($str), $encoding);

        return $begin . $replacement . $end;

    }, $string);

    return ( count($string) === 1 ) ? $string[0] : $string;
}
1
blablabla

Jetez un coup d'œil à Str.replaceFirst () de Laravel:

public static function replaceFirst($search, $replace, $subject)
{
    if ($search == '') {
        return $subject;
    }
    $position = strpos($subject, $search);
    if ($position !== false) {
        return substr_replace($subject, $replace, $position, strlen($search));
    }
    return $subject;
}

Laravel Str class

0
Asimov

Si votre chaîne ne contient aucun caractère multi-octets et que vous souhaitez remplacer un seul caractère, vous pouvez simplement utiliser strpos

Voici une fonction qui gère les erreurs

/**
 * Replace the first occurence of given string
 *
 * @param  string $search  a char to search in `$subject`
 * @param  string $replace a char to replace in `$subject`
 * @param  string $subject
 * @return string
 *
 * @throws InvalidArgumentException if `$search` or `$replace` are invalid or if `$subject` is a multibytes string
 */
function str_replace_first(string $search , string $replace , string $subject) : string {
    // check params
    if(strlen($replace) != 1 || strlen($search) != 1) {
        throw new InvalidArgumentException('$search & $replace must be char');
    }elseif(mb_strlen($subject) != strlen($subject)){
        throw new InvalidArgumentException('$subject is an multibytes string');
    }
    // search 
    $pos = strpos($subject, $search);
    if($pos === false) {
        // not found
        return $subject;
    }

    // replace
    $subject[$replace] = $subject;

    return $subject;
}
0

Vous pouvez utiliser ceci:

function str_replace_once($str_pattern, $str_replacement, $string){ 

        if (strpos($string, $str_pattern) !== false){ 
            $occurrence = strpos($string, $str_pattern); 
            return substr_replace($string, $str_replacement, strpos($string, $str_pattern), strlen($str_pattern)); 
        } 

        return $string; 
    } 

Trouvé cet exemple de php.net

Usage:

$string = "Thiz iz an examplz";
var_dump(str_replace_once('z','Z', $string)); 

Sortie:

ThiZ iz an examplz

Cela peut réduire les performances un peu, mais la solution la plus simple.

0
happyhardik