yaml_parse : récupérer la partie non yaml

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je développe un moteur de blog statique, utilisant PHP et Pandoc (qui est le seul gérant le Markdown + bibtext convenablement).

À un moment, je cherche à parser les entêtes1. J’utilise donc Yaml_Parse(). si j’ai réussi à récupérer mes entêtes, je veux également récupérer la partie markdown, donc ce qui vient après mon entête : en soi, tout ce qui est après la chaine de caractères ---\n. Pour y arriver, j’utilise ce code :

$matches = explode('---', file_get_contents('source/content/'.$post));
$matches[2];

Mais c’est terriblement lent lorsque je génère les fichiers. Sans l’explode, je suis à 2s. Avec, je suis à 12s.

J’ai pensé à un regex :

preg_match('/^-{3}\s?(\w*)\r?\n(.*)\r?\n-{3}\r?\n(.*)/s', file_get_contents('source/content/'.$post), $matches);

Mais ça n’arrive pas à chercher le contenu idoine. Avez vous une idée pour optimiser tout ça ?


  1. je génère un YAML que je converti d’une part en un fichier listant mes articles de blogs et d’autre part mon fichier atom.
+0 -0

Salut,

Je connais pas PHP donc c’est peut être un coup dans l’eau. En allant voir la documentation, je vois qu’il y a un argument limit, qui te permettrait d’arrêter la recherche à la première occurrence de ---, séparant l’en-tête du reste. À moins que explode soit implémentée vraiment avec les pieds, ça devrait améliorer les performances.

Après test, pas de grosses differences de performance avec limit. Le problème vient d’explode. Je me demande si si on utilise preg_match avec des regex ça serait plus rapide ?

+0 -0

Sans voir le reste du code, ça va être difficile. Qu'explode seule prenne 10 secondes pour découper quelque chaines me parait complètement absurde (en 10 secondes, on peut découper beaucoup de chaines de caractères !). À mon avis il y a autre chose qui coince.

Salut !

Est-ce qu’une solution ne serait pas de parcourir le fichier ligne par ligne (c’est très rapide !), en regardant à chaque fois si la ligne contient uniquement ---, et en s’arrêtant à la première occurrence ? Cela aurait l’avantage d’être super rapide, sans avoir besoin d’expression régulière ou autre, et sans traiter la chaîne entière d’un coup.

En plus, ce serait plus résiliant : en l’état, le code avec explode pourrait couper au milieu de l’en-tête s’il y a une ligne dans le YAML qui se termine par ---.

Ça revient à faire une sorte de machine à états très simple pour parser le fichier en d’un côté le YAML d’en-tête, et d’un autre le Markdown.

Quelque chose dans ce genre (non testé), qui pourrait être complexifié au besoin (si, par exemple, on veut que le séparateur soit sur une ligne entre deux lignes vides).

<?php
$f = fopen('source/content/' . $post, 'r');

$state = 'frontmatter';
$frontmatter = $markdown = '';

while($line = stream_get_line($f, 65535))
{
    // Separator between YAML frontmatter and markdown content
    if ($line == '---') {
        $state = 'markdown';
        continue;
    }
    
    switch ($state) {
        case 'frontmatter':
            $frontmatter += $line + '\n';
            break;
        case 'markdown':
            $markdown += $line + '\n';
            break;
    }
}

fclose($f);
+1 -0

Sans voir le reste du code, ça va être difficile. Qu'explode seule prenne 10 secondes pour découper quelque chaines me parait complètement absurde (en 10 secondes, on peut découper beaucoup de chaines de caractères !). À mon avis il y a autre chose qui coince.

adri1

Cela met une dizaine de secondes à générer le site, contre 4 sans. J’impute la différence à explode.

@Amaury :

le problème est que mon frontend commence et se finit par trois tirets. Je sais pas comment faire pour que ça soit le 2e. Mais je note l’idée du ligne par ligne !

Je regarde du côté de pandoc (vu que c’est le converteur), si c’est possible de faire quelque chose.

+0 -0

Sans voir le reste du code, ça va être difficile. Qu'explode seule prenne 10 secondes pour découper quelque chaines me parait complètement absurde (en 10 secondes, on peut découper beaucoup de chaines de caractères !). À mon avis il y a autre chose qui coince.

adri1

Cela met une dizaine de secondes à générer le site, contre 4 sans. J’impute la différence à explode.

qwerty

Ça répond pas vraiment à la question, 10 secondes pour splitter quelque fichiers sur un pattern de 3 caractères est une durée complètement absurde. Si 4 secondes suffisent pour générer le site entier en parsant du markdown, c’est pas un malheureux split (une opération beaucoup plus simple que parser du markdown et tout le reste nécessaire pour générer un site!) qui va multiplier le temps par 2 ou 3. Sans voir le code ni les fichiers en question, ça va être difficile de diagnostiquer quoique ce soit. Scanner ligne par ligne ne fera pas une différence énorme (c’est plus robuste par contre), faut essentiellement faire un split sur les \n ce qui va prendre une durée de temps similaire à faire un split sur les ---.

Les deux seules explications sont soit que explode est vraiment codé n’importe comment, soit il y a un truc pathologique qui se passe dans ton code. Si j’en crois cette note dans la doc de preg_split, il y a pas vraiment de raison de penser que explode est connu pour sa lenteur :

If you don’t need the power of regular expressions, you can choose faster (albeit simpler) alternatives like explode() or str_split().

+0 -0

@Amaury :

le problème est que mon frontend commence et se finit par trois tirets. Je sais pas comment faire pour que ça soit le 2e. Mais je note l’idée du ligne par ligne !

qwerty

Il suffirait de compter le nombre de ligne ne contenant que ---, et de s’arrêter au second (s’il y a toujours trois tirets avant et trois après1). On ajouterait un compteur de lignes séparatrices, ou on se baserait sur l’état actuel (si on n’a pas d’état et qu’on voit un séparateur, c’est qu’on passe au frontmatter, et si on est en train de lire le frontmatter, c’est qu’on passe au contenu Markdown)  — et voilà !

En terme de machine à états (cf. exemple en dessous), on serait à aucun état avant le premier --- (lignes ignorées), puis l’état frontmatter entre le premier et le second (lignes stockées comme frontmatter), puis l’état markdown après le second (lignes restantes stockées comme contenu Markdown).

Exemple d’implémentation

Cet exemple ignore tout ce qui est écrit avant la première ligne séparatrice contenant ---. Code non testé.

<?php
$f = fopen('source/content/' . $post, 'r');

$state = '';  // '' or 'frontmatter' or 'markdown'
$frontmatter = $markdown = '';

while($line = stream_get_line($f, 65535))
{
    if (trim($line) == '---') {
        // S'il n'y a pas encore d'état (on commence à peine à lire le fichier,
        // et c'est la première fois qu'on voit un séparateur), on commence à
        // lire l'en-tête YAML (le frontmatter).
        // Si on est en train de lire l'en-tête YAML, alors ce séparateur marque
        // sa fin, et on commence à lire le contenu Markdown.
        $state = $state == '' ? 'frontmatter' : 'markdown';
        continue;
    }
    
    switch ($state) {
        case 'frontmatter':
            $frontmatter += $line + '\n';
            break;
        case 'markdown':
            $markdown += $line + '\n';
            break;
    }
}

fclose($f);

  1. Et si ce n’est pas le cas, on peut adapter pour que si la première ligne non-vide est ---, alors on attend la seconde ligne ne contenant que ---, et sinon, on attend la première comme dans mon premier code. L’avantage des machines à états, c’est que c’est assez facile à adapter à des cas pourtant complexes.
+0 -0

Merci pour vos retours, je vais tester ton code @Amaury.

Voilà un extrait du code, pour information :

function browse() {
	global $config;
	$posts = scandir('source/content');
	$listposts = array();
	foreach($posts as $post) {
		if(strrchr($post, '.md') == '.md') {
			$metadata = yaml_parse_file('source/content/'.$post);
			//preg_match('/^-{3}\s?(\w*)\r?\n(.*)\r?\n-{3}\r?\n(.*)/s', file_get_contents('source/content/'.$post), $matches);
			$matches = explode('---', file_get_contents('source/content/'.$post), 2);
			$filename = basename($post, '.md');
			$listposts[$post]['file'] = $filename;
			$listposts[$post]['title'] = !empty($metadata['title']) ? $metadata['title'] : 'No title';
			$listposts[$post]['canonical'] = !empty($metadata['canonical']) ? $metadata['canonical'] : null;
			$listposts[$post]['author'] = !empty($metadata['author']) ? $metadata['author'] : 'Author';
			$listposts[$post]['type'] = !empty($metadata['type']) ? $metadata['type'] : 'article';
			$listposts[$post]['alert'] = !empty($metadata['alert']) ? $metadata['alert']: null;
			$listposts[$post]['date'] = !empty($metadata['date']) ? $metadata['date'] : null;
			$listposts[$post]['lastupdate'] = !empty($metadata['lastupdate']) ? $metadata['lastupdate'] : null;			
			$listposts[$post]['toc'] = !empty($metadata['toc']) ? $metadata['toc'] : false;
			$listposts[$post]['abstract'] = !empty($metadata['abstract']) ? $metadata['abstract'] : 'no abstract';
			$listposts[$post]['content'] = !empty($matches[2]) ? $matches[2] : null;
		}
		usort($listposts, function ($a, $b): int {return $a['date'] < $b['date'];});
	}
	return $listposts;
}
function generate_website() {
	global $config;
	$yaml = array (
		'title' 		=> $config['name'],
		'bloglistname' 	=> $config['bloglistname'],
		'root' 			=> $config['root'],
		'lastupdate' 	=> date(DATE_ATOM, time())
	);
	foreach(browse() as $entry) {
		pandoc($entry['file'], $config['root'].'/'.$entry['file'].'.html');
		print $config['root'].'/'.$entry['file'].'.html'.PHP_EOL;

		if($entry['type'] != 'page' AND $entry['type'] != 'draft') {
			$yaml['entry'][] = array (
				'title' 		=> $entry['title'],
				'slug' 			=> $entry['file'],
				'url' 			=> $config['root'].'/'.$entry['file'].'.html',
				'date'			=> $entry['date'],
				'lastupdate' 	=> $entry['lastupdate'],
				'date-atom' 	=> date(DATE_ATOM,strtotime($entry['date'])),
				'abstract'		=> $entry['abstract'],
				'author'		=> $entry['author'],
				//'content'		=> $entry['content']
			);
		}
	}
	yaml_emit_file('output/feed.yml', $yaml);
	if($config['blog'] == true) {
		exec('pandoc -f markdown -t markdown -i output/feed.yml -o source/content/carnet.md --template source/templates/bloglist.md');
		pandoc($config['bloglist'],  $config['root'].'/'.$config['bloglist'].'.html');
		print $config['root'].'/'.$config['bloglist'].'.html'.PHP_EOL;
		exec('pandoc -f markdown -t html -i output/feed.yml -o output/feed.xml --template source/templates/feed.xml'); 
		print $config['root'].'/feed.xml'.PHP_EOL;
		//unlink('output/feed.yml');
	}
}
+1 -0

Attention, avec explode() tu vas foirer parce-que la chaine va tôt ou tard apparaitre n’importe où dans ton markdown (par exemple les filets) ou même ton entête YAML. J’aurais préféré, s’il faut aller dans ce sens, utiliser preg_split().

J’impute la différence à explode.

Il n’y a pas de raison que ce soit lent, ça se saurait. Par ailleurs, tu utilises file_get_contents() pour créer une chaine binaire en mémoire de ton fichier (qu’on espère raisonnable.) Ce même fichier, tu l’ouvres déjà avec yaml_parse_file() : tu fais plusieurs fois des entrées-sorties disques qui sont connues pour plomber les perfs.
Normalement, tu récupères ton fichier dans une chaîne une bonne fois pour toute, puis tu fais tes opérations sur cette chaîne (donc en utilisant yaml_parse() plutôt.)

+0 -0

Attention, avec explode() tu vas foirer parce-que la chaine va tôt ou tard apparaitre n’importe où dans ton markdown (par exemple les filets) ou même ton entête YAML. J’aurais préféré, s’il faut aller dans ce sens, utiliser preg_split().

J’impute la différence à explode.

Il n’y a pas de raison que ce soit lent, ça se saurait. Par ailleurs, tu utilises file_get_contents() pour créer une chaine binaire en mémoire de ton fichier (qu’on espère raisonnable.) Ce même fichier, tu l’ouvres déjà avec yaml_parse_file() : tu fais plusieurs fois des entrées-sorties disques qui sont connues pour plomber les perfs.
Normalement, tu récupères ton fichier dans une chaîne une bonne fois pour toute, puis tu fais tes opérations sur cette chaîne (donc en utilisant yaml_parse() plutôt.)

Gil Cot

J’ai suivi ton conseil de mettre dans un seul file_get_contents en mémoire (le fichier est raisonnable, c’est des articles de blogs), ce qui fait un gain de temps.

J’avais tenté d’utiliser preg_split, mais je n’arrive pas à trouver la bonne regex pour cela. En effet, ca semble plus compliqué que /---/, mais il y a une subtilité qui m’échappe.

+0 -0

Je ne comprends pas trop cette obsession pour preg_split dans ce topic. Les regex sont une solution utile pour faire du pattern matching arbitraire (mais relativement simple) contre des chaînes, mais ne sont pas une bonne solution pour faire du matching contre des chaînes complètement connues (ici une ligne qui contient exactement ---). C’est pas pour rien que la doc de preg_split redirige vers explode ou str_split si on n’a pas besoin de ce qu’apportent les regex. Utiliser des regex dans le cas présent, c’est sortir un marteau pour enfoncer une punaise dans un panneau en liège.

La solution simple et efficace pour découper sur les lignes qui contiennent des --- a déjà été présentée : scanner ligne par ligne et les comparer à ---. Il n’y a pas de subtilités ou de pièges particuliers.

Ça m’amène à la question suivante : quel est le problème que tu cherches à régler en écrivant ton propre moteur de blog statique ? Je demande parce que tu bloques sur un problème très simple (relativement à la tâche de développer un moteur), qui va jusqu’à te faire reconsidérer de changer de langage juste pour ce micro-problème. Si tu fais ça pour apprendre comment les moteurs de blogs statiques fonctionnent, je serais toi je m’inquiéterai pas des performances alors que tu sembles encore en être à poser les fondations. Ton design va avoir le temps de changer 15 fois au-fur-et-à-mesure que ta compréhension du domaine va progresser.

Au départ je voulais un simple site statique avec pandoc. Mais on m’a réclamé des flux rss avec des articles complets et pas qu’un résumé, ce qui complexifie l’ensemble.

+0 -0

Si ton but est de générer un site statique plutôt que d’écrire un moteur, tu as autant d’utiliser un moteur existant qui gère les flux. Changer quelques en-têtes pour passer d’un moteur à l’autre est probablement beaucoup moins d’efforts que d’écrire un moteur de site, surtout si tu débutes en programmation. Par exemple, il semblerait que le moteur que j’utilise (zola) en est capable. Il y a des tonnes de moteurs qui existent déjà avec plein de fonctionnalités, autant ne pas réinventer la roue. Il y a pandoc-rss qui pourrait t’être utile, mais ça n’a pas l’air maintenu.

+0 -0

Je ne comprends pas trop cette obsession pour preg_split dans ce topic. Les regex sont une solution utile pour faire du pattern matching arbitraire (mais relativement simple) contre des chaînes, mais ne sont pas une bonne solution pour faire du matching contre des chaînes complètement connues (ici une ligne qui contient exactement ---). C’est pas pour rien que la doc de preg_split redirige vers explode ou str_split si on n’a pas besoin de ce qu’apportent les regex. Utiliser des regex dans le cas présent, c’est sortir un marteau pour enfoncer une punaise dans un panneau en liège.

adri1

En ce qui concerne la remarque de mon message, c’est justement que ça ne cherche pas « une ligne qui contient exactement » mais « la sous-chaine exactement égale » ;)

tu vas foirer parce-que la chaine va tôt ou tard apparaitre n’importe où dans ton markdown (par exemple les filets) ou même ton entête YAML.

Gil Cot

La suite montre bien

J’avais tenté d’utiliser preg_split, mais je n’arrive pas à trouver la bonne regex pour cela. En effet, ca semble plus compliqué que /---/, mais il y a une subtilité qui m’échappe.

qwerty

Comme dit adrien, tu cherches une ligne… Ça fait une regexp qui serait ressemblerait plutôt à /^---$/ nonobstant les soucis de fin de ligne…

J’ai suivi ton conseil de mettre dans un seul file_get_contents en mémoire (le fichier est raisonnable, c’est des articles de blogs), ce qui fait un gain de temps.

qwerty

Tu as aussi la possibilité de récupérer le fichier dans un tableau de lignes avec file() si c’est plus simple pour toi.
Et aussi parmi les solutions existantes, si tu veux avoir le contrôle du code, il y a la bibliothèque League\CommonMark que je trouve bien (par contre ça ne fait pas de bibtex nativement, à ma connaissance.)

+0 -1
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte