28 votes

Recherchez 2 phrases en HTML (en ignorant toutes les balises) et supprimez tout le reste

J'ai du code html stockées dans une chaîne de caractères, par exemple:

$html = '
        <html>
        <body>
        <p>Hello <em>進撃の巨人</em>!</p>
        random code
        random code
        <p>Lorem <span>ipsum<span>.</p>
        </body>
        </html>
        ';

Alors j'ai deux phrases stockées dans des variables:

$begin = 'Hello 進撃の巨人!';
$end = 'Lorem ipsum.';

Je recherche $html pour ces deux phrases, et la bande de tout ce qui est avant et après eux. Donc, $html deviendra:

$html = 'Hello <em>進撃の巨人</em>!</p>
        random code
        random code
        <p>Lorem <span>ipsum<span>.';

Comment puis-je y parvenir? Notez que l' $begin et $end variables n'ont pas de balises html, mais les phrases en $html très probablement avoir des balises, comme indiqué ci-dessus.

Peut-être une regex approche?

Ce que j'ai essayé jusqu'à présent

  • Un strpos() approche. Le problème est qu' $html contient des balises dans la peine, faisant de l' $begin et $end phrases correspondent pas. J'ai peut - strip_tags($html) avant l'exécution d' strpos(), mais ensuite, je vais évidemment à la fin avec l' $html sans les balises.

  • Partie recherche de la variable, comme Hello, mais c'est jamais à l'abri et vous donnera beaucoup de matches.

12voto

Wiktor Stribiżew Points 100073

Voici une courte, mais - je crois - solution de travail basée sur un point paresseux mise en correspondance (qui peut être amélioré par la création d'un plus, déroulé regex, mais devrait être suffisant, sauf si vous avez vraiment de gros morceaux de texte).

$html = "<html>\n<body>\n<p><p>H<div>ello</div><script></script> <em>進&nbsp;&nbsp;&nbsp;撃の巨人</em>!</p>\nrandom code\nrandom code\n<p>Lorem <span>ipsum<span>.</p>\n</body>\n </html>";
$begin = 'Hello     進撃の巨人!';
$end = 'Lorem ipsum.';
$begin = preg_replace_callback('~\s++(?!\z)|(\s++\z)~u', function ($m) { return !empty($m[1]) ? '' : ' '; }, $begin);
$end = preg_replace_callback('~\s++(?!\z)|(\s++\z)~u', function ($m) { return !empty($m[1]) ? '' : ' '; }, $end);
$begin_arr = preg_split('~(?=\X)~u', $begin, -1, PREG_SPLIT_NO_EMPTY);
$end_arr = preg_split('~(?=\X)~u', $end, -1, PREG_SPLIT_NO_EMPTY);
$reg = "(?s)(?:<[^<>]+>)?(?:&#?\\w+;)*\\s*" .  implode("", array_map(function($x, $k) use ($begin_arr) { return ($k < count($begin_arr) - 1 ? preg_quote($x, "~") . "(?:\s*(?:<[^<>]+>|&#?\\w+;))*" : preg_quote($x, "~"));}, $begin_arr, array_keys($begin_arr)))
        . "(.*?)" . 
        implode("", array_map(function($x, $k) use ($end_arr) { return ($k < count($end_arr) - 1 ? preg_quote($x, "~") . "(?:\s*(?:<[^<>]+>|&#?\\w+;))*" : preg_quote($x, "~"));}, $end_arr, array_keys($end_arr))); 
echo $reg .PHP_EOL;
preg_match('~' . $reg . '~u', $html, $m);
print_r($m[0]);

Voir la IDEONE démo

Algorithme:

  • Créer une dynamique regex modèle en divisant les délimiteurs de chaînes de caractères en un seul graphèmes (puisque ceux-ci peuvent être des caractères Unicode, je vous suggère d'utiliser preg_split('~(?<!^)(?=\X)~u', $end)) et l'implosion de retour par l'ajout d'une balise facultative qui correspondent au motif (?:<[^<>]+>)?.
  • Ensuite, (?s) permet une DOTALL mode lors de l' . correspond à n'importe quel caractère, y compris un retour à la ligne, et .*? correspondra 0+ caractères à partir de la pointe de fuite délimiteur.

Regex détails:

  • '~(?<!^)(?=\X)~u correspond à tout autre endroit qu'au début de la chaîne avant chaque graphème
  • (échantillon final regex) (?s)(?:<[^<>]+>)?(?:&#?\w+;)*\s*H(?:\s*(?:<[^<>]+>|&#?\w+;))*e(?:\s*(?:<[^<>]+>|&#?\w+;))*l(?:\s*(?:<[^<>]+>|&#?\w+;))*l(?:\s*(?:<[^<>]+>|&#?\w+;))*o(?:\s*(?:<[^<>]+>|&#?\w+;))* (?:\s*(?:<[^<>]+>|&#?\w+;))*進(?:\s*(?:<[^<>]+>|&#?\w+;))*撃(?:\s*(?:<[^<>]+>|&#?\w+;))*の(?:\s*(?:<[^<>]+>|&#?\w+;))*巨(?:\s*(?:<[^<>]+>|&#?\w+;))*人(?:\s*(?:<[^<>]+>|&#?\w+;))*\!(?:\s*(?:<[^<>]+>|&#?\w+;))* + (.*?) + L(?:\s*(?:<[^<>]+>|&#?\w+;))*o(?:\s*(?:<[^<>]+>|&#?\w+;))*r(?:\s*(?:<[^<>]+>|&#?\w+;))*e(?:\s*(?:<[^<>]+>|&#?\w+;))*m(?:\s*(?:<[^<>]+>|&#?\w+;))* (?:\s*(?:<[^<>]+>|&#?\w+;))*i(?:\s*(?:<[^<>]+>|&#?\w+;))*p(?:\s*(?:<[^<>]+>|&#?\w+;))*s(?:\s*(?:<[^<>]+>|&#?\w+;))*u(?:\s*(?:<[^<>]+>|&#?\w+;))*m(?:\s*(?:<[^<>]+>|&#?\w+;))*\. - l'attaque et de fuite des délimiteurs avec option de sous-masques pour la balise correspondant et un (.*?) (capture pourrait ne pas être nécessaire) à l'intérieur.
  • ~u modificateur est nécessaire puisque les chaînes Unicode sont à traiter.
  • Mise à JOUR: pour tenir compte De 1+ espaces, tous les espaces dans l' begin et end modèles peuvent être remplacés par \s+ sous-modèle pour correspondre à tout type d'1+ espaces dans la chaîne d'entrée.
  • Mise à JOUR 2: L'auxiliaire $begin = preg_replace('~\s+~u', ' ', $begin); et $end = preg_replace('~\s+~u', ' ', $end); sont nécessaires pour tenir compte, pour 1+ espaces dans la chaîne d'entrée.
  • Pour tenir compte des entités HTML, ajouter un autre masque pour les pièces en option: &#?\\w+;, il correspond aussi à la &nbsp; et &#123; d'entités similaires. Il est également précédées \s* pour correspondre à des espaces facultatifs, et quantifiés * (peut être zéro ou plus).

8voto

Dávid Horváth Points 1077

Je voulais vraiment écrire une regex solution. Mais je suis précédé avec quelques belles et à des solutions complexes. Donc, ici, est un non-regex solution.

Petite explication: Le problème majeur est de garder les balises HTML. On pouvait facilement le texte de la recherche, si les balises HTML ont été dépouillés. Donc: la bande de l'un de ces! Nous pouvons facilement rechercher dans le dépouillé de contenu, et de produire une sous-chaîne nous voulons couper. Ensuite, essayez de couper cette sous-chaîne à partir du HTML tout en conservant les tags.

Avantages:

  • La recherche est facile et indépendant de l'HTML, vous pouvez rechercher avec la regex trop si vous avez besoin d'
  • Les exigences sont évolutives: vous pouvez facilement ajouter complète multi-octets, le soutien pour les entités et blanc-espace de l'effondrement, et ainsi de suite
  • Relativement rapide (il est possible, qu'un direct regex peut être plus rapide)
  • Ne pas toucher HTML d'origine et adaptable à d'autres langages de balisage

Une statique de la classe utilitaire pour ce scénario:

class HtmlExtractUtil
{

    const FAKE_MARKUP = '<>';
    const MARKUP_PATTERN = '#<[^>]+>#u';

    static public function extractBetween($html, $startTextToFind, $endTextToFind)
    {
        $strippedHtml = preg_replace(self::MARKUP_PATTERN, '', $html);
        $startPos = strpos($strippedHtml, $startTextToFind);
        $lastPos = strrpos($strippedHtml, $endTextToFind);

        if ($startPos === false || $lastPos === false) {
            return "";
        }

        $endPos = $lastPos + strlen($endTextToFind);
        if ($endPos <= $startPos) {
            return "";
        }

        return self::extractSubstring($html, $startPos, $endPos);
    }

    static public function extractSubstring($html, $startPos, $endPos)
    {
        preg_match_all(self::MARKUP_PATTERN, $html, $matches, PREG_OFFSET_CAPTURE);
        $start = -1;
        $end = -1;
        $previousEnd = 0;
        $stripPos = 0;
        $matchArray = $matches[0];
        $matchArray[] = [self::FAKE_MARKUP, strlen($html)];
        foreach ($matchArray as $match) {
            $diff = $previousEnd - $stripPos;
            $textLength = $match[1] - $previousEnd;
            if ($start == (-1)) {
                if ($startPos >= $stripPos && $startPos < $stripPos + $textLength) {
                    $start = $startPos + $diff;
                }
            }
            if ($end == (-1)) {
                if ($endPos > $stripPos && $endPos <= $stripPos + $textLength) {
                    $end = $endPos + $diff;
                    break;
                }
            }
            $tagLength = strlen($match[0]);
            $previousEnd = $match[1] + $tagLength;
            $stripPos += $textLength;
        }

        if ($start == (-1)) {
            return "";
        } elseif ($end == (-1)) {
            return substr($html, $start);
        } else {
            return substr($html, $start, $end - $start);
        }
    }

}

Utilisation:

$html = '
<html>
<body>
<p>Any string before</p>
<p>Hello <em>進撃の巨人</em>!</p>
random code
random code
<p>Lorem <span>ipsum<span>.</p>
<p>Any string after</p>
</body>
</html>
';
$startTextToFind = 'Hello 進撃の巨人!';
$endTextToFind = 'Lorem ipsum.';

$extractedText = HtmlExtractUtil::extractBetween($html, $startTextToFind, $endTextToFind);

header("Content-type: text/plain; charset=utf-8");
echo $extractedText . "\n";

7voto

trincot Points 10112

Les expressions régulières ont leurs limites quand il s'agit de l'analyse HTML. Comme beaucoup l'ont fait avant moi, je ferai référence à cette célèbre réponse.

Les Problèmes potentiels lorsque s'appuyant sur les Expressions Régulières

Par exemple, imaginez cette balise s'affiche dans le HTML avant de la partie qui doit être extraits:

<p attr="Hello 進撃の巨人!">This comes before the match</p>

De nombreux regexp solutions de trébucher sur ce, et de retourner une chaîne de caractères qui commence au milieu de cette ouverture en p balise.

Ou envisager un commentaire à l'intérieur de la section HTML qui doit être assortie:

<!-- Next paragraph will display "Lorem ipsum." -->

Ou, certains lâche inférieur et supérieur signes apparaissent (disons dans un commentaire, ou une valeur d'attribut):

<!-- Next paragraph will display >-> << Lorem ipsum. >> -->
<p data-attr="->->->" class="myclass">

Ce que les regexes faire?

Ce sont juste des exemples... il y a d'innombrables autres situations qui posent des problèmes à l'expression régulière en fonction des solutions.

Il y a plus de moyens fiables pour analyser le code HTML.

Charger le code HTML dans un DOM

Je propose ici une solution basée sur le DOMDocument de l'interface, à l'aide de cet algorithme:

  1. Obtenir le contenu du texte du document HTML et d'identifier les deux décalages, les deux sous-chaînes (début/fin) sont situés.

  2. Ensuite, passez par le DOM nœuds de texte de garder la trace des décalages de ces nœuds de s'intégrer. Dans les nœuds où l'un des deux de délimitation des décalages sont croisés, un prédéfini délimiteur (|) est inséré. Ce délimiteur ne devraient pas être présents dans la chaîne HTML. Par conséquent, il est doublé (||, ||||, ...) jusqu'à ce que cette condition est remplie;

  3. Enfin diviser la représentation HTML par ce séparateur et d'en extraire la partie du milieu comme résultat.

Voici le code:

function extractBetween($html, $begin, $end) {
    $dom = new DOMDocument();
    // Load HTML in DOM, making sure it supports UTF-8; double HTML tags are no problem
    $dom->loadHTML('<html><head>
            <meta http-equiv="content-type" content="text/html; charset=utf-8">
        </head></html>' . $html);
    // Get complete text content
    $text = $dom->textContent;
    // Get positions of the beginning/ending text; exit if not found.
    if (($from = strpos($text, $begin)) === false) return false;
    if (($to = strpos($text, $end, $from + strlen($begin))) === false) return false;
    $to += strlen($end);
    // Define a non-occurring delimiter by repeating `|` enough times:
    for ($delim = '|'; strpos($html, $delim) !== false; $delim .= $delim);
    // Use XPath to traverse the DOM
    $xpath = new DOMXPath($dom);
    // Go through the text nodes keeping track of total text length.
    // When exceeding one of the two offsets, inject a delimiter at that position.
    $pos = 0;
    foreach($xpath->evaluate("//text()") as $node) {
        // Add length of node's text content to total length
        $newpos = $pos + strlen($node->nodeValue);
        while ($newpos > $from || ($from === $to && $newpos === $from)) {
            // The beginning/ending text starts/ends somewhere in this text node.
            // Inject the delimiter at that position:
            $node->nodeValue = substr_replace($node->nodeValue, $delim, $from - $pos, 0);
            // If a delimiter was inserted at both beginning and ending texts,
            // then get the HTML and return the part between the delimiters
            if ($from === $to) return explode($delim, $dom->saveHTML())[1];
            // Delimiter was inserted at beginning text. Now search for ending text
            $from = $to;
        }
        $pos = $newpos;
    }
}

Vous serait-il appeler comme ceci:

// Sample input data
$html = '
        <html>
        <body>
        <p>This comes before the match</p>
        <p>Hey! Hello <em>進撃の巨人</em>!</p>
        random code
        random code
        <p>Lorem <span>ipsum<span>. la la la</p>
        <p>This comes after the match</p>
        </body>
        </html>
        ';

$begin = 'Hello 進撃の巨人!';
$end = 'Lorem ipsum.';

// Call
$html = extractBetween($html, $begin, $end);

// Output result
echo $html;

Sortie:

Hello <em>進撃の巨人</em>!</p>
        random code
        random code
        <p>Lorem <span>ipsum<span>.

Vous trouverez ce code est également plus facile à entretenir que les regex alternatives.

Voir courir sur eval.dans.

5voto

Paul Points 2325

Cela peut de loin pas la meilleure solution, mais j'aime la fissuration ma tête à propos de ces "énigmes", voici donc mon approche.

<?php
$subject = ' <html> 
<body> 
<p>He<i>l</i>lo <em>Lydia</em>!</p> 
random code 
random code 
<p>Lorem <span>ipsum</span>.</p> 
</body> 
</html>';

$begin = 'Hello Lydia!';
$end = 'Lorem ipsum.';

$begin_chars = str_split($begin);
$end_chars = str_split($end);

$begin_re = '';
$end_re = '';

foreach ($begin_chars as $c) {
    if ($c == ' ') {
        $begin_re .= '(\s|(<[a-z/]+>))+';
    }
    else {
        $begin_re .= $c . '(<[a-z/]+>)?';
    }
}
foreach ($end_chars as $c) {
    if ($c == ' ') {
        $end_re .= '(\s|(<[a-z/]+>))+';
    }
    else {
        $end_re .= $c . '(<[a-z/]+>)?';
    }
}

$re = '~(.*)((' . $begin_re . ')(.*)(' . $end_re . '))(.*)~ms';

$result = preg_match( $re, $subject , $matches );
$start_tag = preg_match( '~(<[a-z/]+>)$~', $matches[1] , $stmatches );

echo $stmatches[1] . $matches[2];

Ce sorties:

<p>He<i>l</i>lo <em>Lydia</em>!</p> 
random code 
random code 
<p>Lorem <span>ipsum</span>.</p>

Cela correspond à ce cas, mais je pense qu'il aurait besoin d'un peu plus de logique pour échapper à la regex de caractères spéciaux comme les périodes.

En général, ce que cet extrait n':

  • Le fractionnement des chaînes de caractères dans un tableau, chaque tableau représentant un caractère unique. Ce qui doit être fait, car Hello doit correspondre Hel<i>l</i>o ainsi.
  • Pour ce faire, pour la regex partie supplémentaire (<[a-z/]+>)? est inséré après chaque personnage avec un cas particulier pour le caractère espace.

4voto

Druzion Points 3611

Vous pouvez essayer ce RegEx:

 (.*?)  # Data before sentences (to be removed)
(      # Capture Both sentences and text in between
  H.*?e.*?l.*?l.*?o.*?\s    # Hello[space]
  (<.*?>)*                  # Optional Opening Tag(s)
  進.*?撃.*?の.*?巨.*?人.*?   # 進撃の巨人
  (<\/.*?>)*                # Optional Closing Tag(s)
  (.*?)                     # Optional Data in between sentences
  (<.*?>)*                  # Optional Opening Tag(s)
  L.*?o.*?r.*?e.*?m.*?\s    # Lorem[space]
  (<.*?>)*                  # Optional Opening Tag(s)
  i.*?p.*?s.*?u.*?m.*?      # ipsum
)
(.*)   # Data after sentences (to be removed)
 

Substitution par le groupe de capture 2nd

Démo en direct sur Regex101

Le Regex peut être raccourci pour:

 (.*?)(H.*?e.*?l.*?l.*?o.*?\s(<.*?>)*進.*?撃.*?の.*?巨.*?人.*?(<\/.*?>)*(.*?)(<.*?>)*L.*?o.*?r.*?e.*?m.*?\s(<.*?>)*i.*?p.*?s.*?u.*?m.*?)(.*)
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X