<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Enodev.fr / Christophe's log</title><link>https://www.enodev.fr/</link><description>Personal weblog of Christophe Vu-Brugier</description><atom:link href="https://www.enodev.fr/rss.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><lastBuildDate>Wed, 18 Jun 2025 17:32:20 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Des robots pour visiteurs</title><link>https://www.enodev.fr/posts/des-robots-pour-visiteurs.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;&lt;link rel="stylesheet" href="https://www.enodev.fr/bots/cal-heatmap.css"&gt;&lt;/p&gt;
&lt;p&gt;Quand je lis les journaux de mon serveur Web, j'ai l'impression que la
plupart des visiteurs sont des robots.&lt;/p&gt;
&lt;div id="bot-ratio-monthly"&gt;&lt;/div&gt;
&lt;script src="https://www.enodev.fr/bots/bot-ratio-plot.js"&gt;&lt;/script&gt;

&lt;p&gt;La part des visiteurs qui s'identifient comme des robots via leur user
agent est passée de moins de 30 % début 2024 à 40 % en
décembre.&lt;/p&gt;
&lt;p&gt;L'entraînement des modèles
d&lt;a href="https://fr.wikipedia.org/wiki/Intelligence_artificielle_g%C3%A9n%C3%A9rative"&gt;'IA générative&lt;/a&gt;
tels que les
&lt;a href="https://fr.wikipedia.org/wiki/Grand_mod%C3%A8le_de_langage"&gt;LLM&lt;/a&gt;
nécessite un corpus toujours plus grand de données. Les entreprises du
secteur ont donc développé des programmes qui parcourent le Web pour
enregistrer le contenu des sites visités. Aux visiteurs de longue date
que sont
&lt;a href="http://www.google.com/bot.html"&gt;Googlebot&lt;/a&gt; et
&lt;a href="http://www.bing.com/bingbot.htm"&gt;Bingbot&lt;/a&gt; se sont ajoutés
&lt;a href="https://openai.com/gptbot"&gt;GPTBot&lt;/a&gt;,
ClaudeBot,
&lt;a href="https://developer.amazon.com/support/amazonbot"&gt;Amazonbot&lt;/a&gt;, et bien d'autres.&lt;/p&gt;
&lt;h2&gt;Comment évaluer la part des robots parmi les visiteurs à partir des journaux ?&lt;/h2&gt;
&lt;p&gt;Les serveurs Web comme Apache enregistrent des informations sur les
requêtes HTTP traitées dans un
&lt;a href="https://httpd.apache.org/docs/2.4/fr/logs.html#accesslog"&gt;fichier journal des accès&lt;/a&gt;.
Parmi ces informations figure l'en-tête
&lt;a href="https://developer.mozilla.org/fr/docs/Glossary/User_agent"&gt;user agent&lt;/a&gt;
de la requête HTTP. Il s'agit d'une chaîne de caractère qui identifie
le navigateur Web.&lt;/p&gt;
&lt;p&gt;Le user agent des robots d'indexation contient en général les termes
&lt;em&gt;bot&lt;/em&gt;, &lt;em&gt;crawler&lt;/em&gt; ou &lt;em&gt;spider&lt;/em&gt;. Toutefois, un robot peut prétendre être
un banal navigateur Web en usurpant son user agent.&lt;/p&gt;
&lt;p&gt;Je suggère d'utiliser le logiciel libre
&lt;a href="https://github.com/rcoh/angle-grinder"&gt;Angle-grinder&lt;/a&gt;
pour analyser le fichier journal des accès d'Apache et estimer le
ratio des requêtes valides envoyées par des robots.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;agrind&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;a.log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"* | apache | where status == 200 | if(contains(toLowerCase(contentlength), 'bot'), 1, 0) as bot_req | avg(bot_req) as bot_req_ratio"&lt;/span&gt;
&lt;span class="go"&gt;bot_req_ratio&lt;/span&gt;
&lt;span class="go"&gt;---------------------&lt;/span&gt;
&lt;span class="go"&gt;0.40&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Les pipelines, séparés par le caractère &lt;code&gt;|&lt;/code&gt;, sont composés ainsi :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Toutes les entrées du fichier journal sont sélectionnées avec le
   caractère &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Le parser pour le format du journal d'Apache interprète les champs.&lt;/li&gt;
&lt;li&gt;Seules les requêtes dont le statut de réponse HTTP est
   &lt;a href="https://developer.mozilla.org/fr/docs/Web/HTTP/Status/200"&gt;200&lt;/a&gt; sont conservées.
   En effet, de nombreuses requêtes sont invalides et ne nous intéressent pas.
   Par exemple, des requêtes émanant de robots à la recherche de vulnérabilités à exploiter.&lt;/li&gt;
&lt;li&gt;De façon étrange, le user agent est contenu dans le champ &lt;code&gt;contentlength&lt;/code&gt;.
   Si le champ contient la chaîne de caractères &lt;code&gt;"bot"&lt;/code&gt;, on définit le champ &lt;code&gt;bot_req&lt;/code&gt; à 1, sinon à 0.
   On peut étendre cette condition pour inclure les termes moins fréquents &lt;code&gt;"crawler"&lt;/code&gt; et &lt;code&gt;"spider"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enfin, la valeur moyenne du champ &lt;code&gt;bot_req&lt;/code&gt; est calculée.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Quels sont les robots les plus actifs ?&lt;/h2&gt;
&lt;p&gt;Voici la liste des cinq robots les plus actifs sur ce site en 2024.&lt;/p&gt;
&lt;table class="table table-striped"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rang&lt;/th&gt;
&lt;th&gt;Nom du robot&lt;/th&gt;
&lt;th&gt;Propriétaire&lt;/th&gt;
&lt;th&gt;Part des requêtes HTTP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Googlebot&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;4.72 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;2.82 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;bingbot&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;1.50 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;YandexBot&lt;/td&gt;
&lt;td&gt;Yandex&lt;/td&gt;
&lt;td&gt;1.34 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Bytespider&lt;/td&gt;
&lt;td&gt;ByteDance&lt;/td&gt;
&lt;td&gt;1.01 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;Comment a évolué l'activité des robots au cours de l'année ?&lt;/h2&gt;
&lt;p&gt;Les graphiques qui suivent représentent l'activité des robots les plus
connus sous la forme d'une
&lt;a href="https://fr.wikipedia.org/wiki/Carte_thermique"&gt;carte thermique&lt;/a&gt;
du calendrier.&lt;/p&gt;
&lt;p&gt;Chaque case symbolise un jour de l'année. Les jours se suivent
verticalement du lundi au dimanche et sont groupés par mois. La
couleur d'une case varie du jaune pâle au rouge foncé en fonction du
nombre de requêtes HTTP reçues ce jour là.&lt;/p&gt;
&lt;p&gt;Les graphiques sont affichés par la bibliothèque JavaScript
&lt;a href="https://cal-heatmap.com/"&gt;Cal-Heatmap&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Googlebot&lt;/h3&gt;
&lt;p&gt;Très actifs, les robots de Google ont visité le site tous les jours en 2024.&lt;/p&gt;
&lt;div id="calendar-googlebot"&gt;&lt;/div&gt;

&lt;h3&gt;GPTBot&lt;/h3&gt;
&lt;p&gt;Les robots d'OpenAI ont visité le site 212 jours en 2024.
Rares en début d'année, ils ont été très présents en octobre.&lt;/p&gt;
&lt;div id="calendar-gptbot"&gt;&lt;/div&gt;

&lt;h3&gt;Bingbot&lt;/h3&gt;
&lt;p&gt;Les robots de Microsoft ont visité le site tous les jours en 2024.&lt;/p&gt;
&lt;div id="calendar-bingbot"&gt;&lt;/div&gt;

&lt;h3&gt;YandexBot&lt;/h3&gt;
&lt;p&gt;Les robots de Yandex ont visité le site 342 jours en 2024 mais ont été
plus discrets à partir du mois de septembre.&lt;/p&gt;
&lt;div id="calendar-yandexbot"&gt;&lt;/div&gt;

&lt;h3&gt;Bytespider&lt;/h3&gt;
&lt;p&gt;Les robots de ByteDance ont visité le site 254 jours en 2024 et ont
été plus actifs de juillet à septembre.&lt;/p&gt;
&lt;div id="calendar-bytespider"&gt;&lt;/div&gt;

&lt;h3&gt;AmazonBot&lt;/h3&gt;
&lt;p&gt;Les robots d'Amazon apparaissent au septième rang et se distinguent
dans leurs accès : ils indexent l'ensemble du site une fois par mois.&lt;/p&gt;
&lt;div id="calendar-amazonbot"&gt;&lt;/div&gt;

&lt;h3&gt;ClaudeBot&lt;/h3&gt;
&lt;p&gt;Classés au neuvième rang par leur activité, les robots d'Anthropic ont
visité le site 50 jours en 2024, principalement en avril et en mai.&lt;/p&gt;
&lt;div id="calendar-claudebot"&gt;&lt;/div&gt;

&lt;script&gt;
function drawCalendar(divId, dataSource) {
    const cal = new CalHeatmap();
    cal.paint({
        itemSelector: `#${divId}`,
        date: {locale: {weekStart: 1}, start: new Date('2024-01-01')},
        data: {source: dataSource, x: 'd', y: 'c', defaultValue: 0},
        range: 12,
        domain: {type: 'month'},
        subDomain: {type: 'day'},
        scale: {
            color: {
                'scheme': 'YlOrRd',
                'type': 'sqrt',
                'domain': [0, 200]
            }
        }
    });
    render(`&lt;div id="${divId}"&gt;&lt;/div&gt;`);
}

const botnames = ["googlebot", "gptbot", "bingbot", "yandexbot", "bytespider", "amazonbot", "claudebot"];
botnames.forEach(function (botname) {
    const divId = `calendar-${botname}`;
    const dataSource = `/bots/${botname}-2024.json`;
    window.addEventListener("load", function() {
        drawCalendar(divId, dataSource);
    });
});
&lt;/script&gt;

&lt;h2&gt;IA quelqu'un ?&lt;/h2&gt;
&lt;p&gt;Les robots représentent une part croissante des visiteurs de ce
site. Ils deviendront peut-être majoritaires en 2025.&lt;/p&gt;
&lt;p&gt;Toutefois, j'aimerais qu'ils butinent plus sobrement le contenu du
site pour ne pas consommer inutilement ses ressources informatiques.&lt;/p&gt;
&lt;p&gt;Ils pourraient s'inspirer de l'efficacité discrète de ce butineur
insaisissable.&lt;/p&gt;
&lt;div class="caption"&gt;
  &lt;img src="https://www.enodev.fr/images/moro-sphinx.jpg" class="img-fluid rounded" alt="Un Moro-sphinx (Macroglossum stellatarum) butine du Chèvrefeuille."&gt;
  &lt;p&gt;Un &lt;a href="https://fr.wikipedia.org/wiki/Moro-sphinx"&gt;Moro-sphinx&lt;/a&gt; (&lt;em&gt;Macroglossum stellatarum&lt;/em&gt;) butine du Chèvrefeuille.&lt;/p&gt;
&lt;/div&gt;</description><category>Angle-grinder</category><category>Apache</category><category>DataViz</category><category>Heatmap</category><category>Internet</category><category>Robots</category><guid>https://www.enodev.fr/posts/des-robots-pour-visiteurs.html</guid><pubDate>Sun, 26 Jan 2025 15:00:00 GMT</pubDate></item><item><title>XZ backdoor lessons: reproducing target-isns release tarballs</title><link>https://www.enodev.fr/posts/xz-backdoor-lessons-reproducing-target-isns-release-tarballs.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;Andres Freund detected a backdoor in
&lt;a href="https://github.com/tukaani-project/xz"&gt;XZ&lt;/a&gt;,
a data compression library, and
&lt;a href="https://lwn.net/ml/oss-security/20240329155126.kjjfduxw2yrlxgzm@awork3.anarazel.de/"&gt;disclosed&lt;/a&gt;
it as
&lt;a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3094"&gt;CVE-2024-3094&lt;/a&gt;
at the end of March 2024.&lt;/p&gt;
&lt;p&gt;Many good articles describe how the attackers implanted the backdoor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LWN: &lt;a href="https://lwn.net/Articles/967866/"&gt;Free software's not-so-eXZellent adventure&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;LWN: &lt;a href="https://lwn.net/Articles/967192/"&gt;How the XZ backdoor works&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Russ Cox: &lt;a href="https://research.swtch.com/xz-timeline"&gt;Timeline of the XZ open source attack&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The attackers prepared the backdoor for more than two years and
completed their work when they became maintainers. They published a
release tarball with a malicious build script not present in the
source code. The attackers uploaded the release tarball to GitHub.&lt;/p&gt;
&lt;p&gt;I maintain &lt;a href="https://github.com/open-iscsi/target-isns"&gt;target-isns&lt;/a&gt;, a
niche free software project packaged in several Linux
distributions. There is not much activity in the project because it is
mostly done. The last release of target-isns – version
&lt;a href="https://github.com/open-iscsi/target-isns/releases/tag/v0.6.8"&gt;v0.6.8&lt;/a&gt;
– happened in May 2020. I built the release tarball on my
machine and uploaded it to GitHub.&lt;/p&gt;
&lt;p&gt;I believe that one of the lessons of the XZ backdoor episode is
&lt;a href="https://en.wikipedia.org/wiki/Trust,_but_verify"&gt;"Trust, but verify"&lt;/a&gt;.
Applied to target-isns releases I published, this could mean:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How can users verify that the release tarball of target-isns matches the source code?&lt;/li&gt;
&lt;li&gt;How can we improve the release process of target-isns to ease that verification?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How to verify a release tarball built with git-archive&lt;/h2&gt;
&lt;p&gt;target-isns is written in C and built with
&lt;a href="https://cmake.org"&gt;CMake&lt;/a&gt;. A custom target, named &lt;em&gt;dist&lt;/em&gt;, invokes
&lt;a href="https://git-scm.com/docs/git-archive"&gt;git-archive&lt;/a&gt; to generate a
tarball of the source code. Ironically, the tarball is compressed with XZ.&lt;/p&gt;
&lt;p&gt;To verify that the release tarball matches the source code, we must
compare the checksum of the published release tarball with one built
locally from the source code.&lt;/p&gt;
&lt;h3&gt;How to compute the checksum of the published release tarball&lt;/h3&gt;
&lt;p&gt;Let's download the release tarball for target-isns v0.6.8 from GitHub
and compute its SHA256 checksum:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-OL&lt;span class="w"&gt; &lt;/span&gt;--silent&lt;span class="w"&gt; &lt;/span&gt;https://github.com/open-iscsi/target-isns/releases/download/v0.6.8/target-isns-0.6.8.tar.xz
&lt;span class="gp"&gt;$ &lt;/span&gt;sha256sum&lt;span class="w"&gt; &lt;/span&gt;target-isns-0.6.8.tar.xz
&lt;span class="go"&gt;544de09a2073242b21f6859841a5016c79c4006a53435a79b5cfc6602a59db97  target-isns-0.6.8.tar.xz&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;How to reproduce the release tarball from the source code&lt;/h3&gt;
&lt;p&gt;We clone the repository with &lt;code&gt;git clone&lt;/code&gt; and checkout the tag we want
with &lt;code&gt;--branch v0.6.8&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;--branch&lt;span class="w"&gt; &lt;/span&gt;v0.6.8&lt;span class="w"&gt; &lt;/span&gt;https://github.com/open-iscsi/target-isns.git
&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;target-isns
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The command &lt;code&gt;git show v0.6.8&lt;/code&gt; reports that the tagger (it's me) signed
the tag with &lt;a href="https://www.gnupg.org"&gt;GnuPG&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://www.enodev.fr/stories/about-cvubrugier.html"&gt;about&lt;/a&gt; page mentions the
fingerprint of my public GnuPG key. We import that key &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://www.enodev.fr/posts/xz-backdoor-lessons-reproducing-target-isns-release-tarballs.html#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;--keyserver&lt;span class="w"&gt; &lt;/span&gt;keyserver.ubuntu.com&lt;span class="w"&gt; &lt;/span&gt;--search-key&lt;span class="w"&gt; &lt;/span&gt;81D01C4E399FFEDEDBD93D7F08390B7DF2FC1876
&lt;span class="go"&gt;gpg: data source: http://185.125.188.27:11371&lt;/span&gt;
&lt;span class="gp gp-VirtualEnv"&gt;(1)&lt;/span&gt;     &lt;span class="go"&gt;Christophe Vu-Brugier &amp;lt;cvubrugier@example.org&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;          4096 bit RSA key 08390B7DF2FC1876, created: 2011-06-16&lt;/span&gt;
&lt;span class="go"&gt;Keys 1-1 of 1 for "81D01C4E399FFEDEDBD93D7F08390B7DF2FC1876".  Enter number(s), N)ext, or Q)uit &amp;gt; 1&lt;/span&gt;
&lt;span class="go"&gt;gpg: key 08390B7DF2FC1876: public key "Christophe Vu-Brugier &amp;lt;cvubrugier@example.org&amp;gt;" imported&lt;/span&gt;
&lt;span class="go"&gt;gpg: Total number processed: 1&lt;/span&gt;
&lt;span class="go"&gt;gpg:               imported: 1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After importing my public key, we verify the tag with &lt;code&gt;git tag --verify&lt;/code&gt; &lt;sup id="fnref2:1"&gt;&lt;a class="footnote-ref" href="https://www.enodev.fr/posts/xz-backdoor-lessons-reproducing-target-isns-release-tarballs.html#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;tag&lt;span class="w"&gt; &lt;/span&gt;--verify&lt;span class="w"&gt; &lt;/span&gt;v0.6.8
&lt;span class="go"&gt;object 52e4fd427b1aff902ef4e7bce9a9c2f6b358a5eb&lt;/span&gt;
&lt;span class="go"&gt;type commit&lt;/span&gt;
&lt;span class="go"&gt;tag v0.6.8&lt;/span&gt;
&lt;span class="go"&gt;tagger Christophe Vu-Brugier &amp;lt;cvubrugier@example.org&amp;gt; 1588947171 +0200&lt;/span&gt;

&lt;span class="go"&gt;target-isns v0.6.8&lt;/span&gt;
&lt;span class="go"&gt;gpg: Signature made Fri May  8 14:13:16 2020 UTC&lt;/span&gt;
&lt;span class="go"&gt;gpg:                using RSA key 81D01C4E399FFEDEDBD93D7F08390B7DF2FC1876&lt;/span&gt;
&lt;span class="go"&gt;gpg:                issuer "cvubrugier@example.org"&lt;/span&gt;
&lt;span class="go"&gt;gpg: Good signature from "Christophe Vu-Brugier &amp;lt;cvubrugier@example.org&amp;gt;" [unknown]&lt;/span&gt;
&lt;span class="go"&gt;gpg: WARNING: This key is not certified with a trusted signature!&lt;/span&gt;
&lt;span class="go"&gt;gpg:          There is no indication that the signature belongs to the owner.&lt;/span&gt;
&lt;span class="go"&gt;Primary key fingerprint: 81D0 1C4E 399F FEDE DBD9  3D7F 0839 0B7D F2FC 1876&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The tag signature is correct. We build a release tarball and compare
its checksum with the published release tarball.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;# &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;build
&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build/
&lt;span class="gp"&gt;$ &lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;..
&lt;span class="go"&gt;-- The C compiler identification is GNU 12.2.0&lt;/span&gt;
&lt;span class="go"&gt;-- Detecting C compiler ABI info&lt;/span&gt;
&lt;span class="go"&gt;-- Detecting C compiler ABI info - done&lt;/span&gt;
&lt;span class="go"&gt;-- Check for working C compiler: /usr/bin/cc - skipped&lt;/span&gt;
&lt;span class="go"&gt;-- Detecting C compile features&lt;/span&gt;
&lt;span class="go"&gt;-- Detecting C compile features - done&lt;/span&gt;
&lt;span class="go"&gt;-- Configuring done&lt;/span&gt;
&lt;span class="go"&gt;-- Generating done&lt;/span&gt;
&lt;span class="go"&gt;-- Build files have been written to: /target-isns/build&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;dist
&lt;span class="go"&gt;Built target dist&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;sha256sum&lt;span class="w"&gt; &lt;/span&gt;target-isns-0.6.8.tar.xz
&lt;span class="go"&gt;544de09a2073242b21f6859841a5016c79c4006a53435a79b5cfc6602a59db97  target-isns-0.6.8.tar.xz&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The checksum (&lt;code&gt;544de09a&lt;/code&gt;) of the generated tarball matches the
checksum of the published tarball: we reproduce the release tarball.&lt;/p&gt;
&lt;p&gt;I reproduced the release tarball on Debian 12 (bookworm). Does that
work with other Linux distributions?&lt;/p&gt;
&lt;h2&gt;Reproducing the release tarball from other Linux distributions&lt;/h2&gt;
&lt;p&gt;I use &lt;a href="https://podman.io"&gt;Podman&lt;/a&gt; to build the release tarball in several
Linux containers.&lt;/p&gt;
&lt;table class="table table-striped"&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Linux distribution&lt;/th&gt;
&lt;th&gt;Release tarball reproduced&lt;/th&gt;
&lt;th&gt;Git version&lt;/th&gt;
&lt;th&gt;XZ version&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alpine 3.19.1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.43.0&lt;/td&gt;
&lt;td&gt;5.4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class="table-danger"&gt;
&lt;td&gt;Arch Linux&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;2.44.0&lt;/td&gt;
&lt;td&gt;5.6.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debian 11 (bullseye)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.30.2&lt;/td&gt;
&lt;td&gt;5.2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debian 12 (bookworm)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.39.2&lt;/td&gt;
&lt;td&gt;5.4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fedora 39&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.44.0&lt;/td&gt;
&lt;td&gt;5.4.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu 22.04 (jammy)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.34.1&lt;/td&gt;
&lt;td&gt;5.2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu 24.04 (noble, pre-release)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.43.0&lt;/td&gt;
&lt;td&gt;5.4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rocky Linux 8&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.39.3&lt;/td&gt;
&lt;td&gt;5.2.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rocky Linux 9&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;2.39.3&lt;/td&gt;
&lt;td&gt;5.2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The release tarball is reproducible everywhere except on Arch
Linux. The output of &lt;code&gt;git archive&lt;/code&gt; is the same but XZ compresses the
tarball differently. A binary search with &lt;code&gt;git bisect&lt;/code&gt; finds the
commit that introduce the change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tukaani-project/xz/commit/6daa4d0ea46a8441f21f609149f3633158bf4704"&gt;XZ: Use threaded mode by default (as if --threads=0 was used).&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;XZ (version 5.6.1) on Arch Linux compresses data with several threads
by default. With &lt;code&gt;--threads=1&lt;/code&gt;, we can force XZ to compress data with
just one thread. With that, we can reproduce the release tarball of
target-isns on Arch Linux.&lt;/p&gt;
&lt;p&gt;Usually, there are different ways for a compressor to represent the
original data. That's why a compressor output may differ when its algorithm
or parameters change. To reduce the risk of compressed tarballs
varying across platforms, target-isns releases could switch to a
compressor less likely to change such as
&lt;a href="https://www.gnu.org/software/gzip/"&gt;gzip&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Future work&lt;/h2&gt;
&lt;p&gt;While existing release tarballs of target-isns are reproducible, we
should improve the release process.&lt;/p&gt;
&lt;dl&gt;
&lt;dt&gt;Document the release process.&lt;/dt&gt;
&lt;dd&gt;A document should describe how releases are made and how to
  reproduce them.&lt;/dd&gt;
&lt;dt&gt;Setup a continuous integration pipeline to generate the release tarball from a tag.&lt;/dt&gt;
&lt;dd&gt;A continuous integration job should automatically generate the
  release tarball from a tag. The job should run in a container (for
  example a &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt; image). With that, users
  could reproduce the release tarballs by running the same container
  image on their machine.&lt;/dd&gt;
&lt;dt&gt;Sign the release tarball with GnuPG.&lt;/dt&gt;
&lt;dd&gt;The maintainer should download the tarball generated by the release
  pipeline, reproduce it, and test it. Then, they should publish the
  release tarball, the SHA256 checksum file, and sign these files with
  their private GnuPG key.&lt;/dd&gt;
&lt;/dl&gt;
&lt;div class="caption"&gt;
  &lt;img src="https://www.enodev.fr/images/araignee-crabe.jpg" class="img-fluid rounded-circle" alt="A crab spider (Misumena vatia) caught a bee."&gt;
  &lt;p&gt;A crab spider (&lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Misumena_vatia"&gt;Misumena vatia&lt;/a&gt;&lt;/em&gt;) caught a bee.&lt;/p&gt;
&lt;/div&gt;

&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I removed my email from the command output. &lt;a class="footnote-backref" href="https://www.enodev.fr/posts/xz-backdoor-lessons-reproducing-target-isns-release-tarballs.html#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;a class="footnote-backref" href="https://www.enodev.fr/posts/xz-backdoor-lessons-reproducing-target-isns-release-tarballs.html#fnref2:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>Git</category><category>GitHub</category><category>GnuPG</category><category>release</category><category>security</category><category>XZ</category><guid>https://www.enodev.fr/posts/xz-backdoor-lessons-reproducing-target-isns-release-tarballs.html</guid><pubDate>Sat, 13 Apr 2024 09:00:00 GMT</pubDate></item><item><title>Statistiques d'accès au site en IPv6</title><link>https://www.enodev.fr/posts/statistiques-acces-au-site-en-ipv6.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;Le mois dernier, j'ai observé que bien que mon serveur Web possédât
une adresse &lt;a href="https://fr.wikipedia.org/wiki/IPv6"&gt;IPv6&lt;/a&gt;, aucun
navigateur Web ne s'y connectait via ce protocole. J'avais tout
simplement oublié d'associer un enregistrement &lt;em&gt;IPv6 address record&lt;/em&gt;
au nom de mon domaine dans la zone
&lt;a href="https://fr.wikipedia.org/wiki/Domain_Name_System"&gt;DNS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Un mois plus tard, la lecture des logs de mon serveur Web confirme
qu'une partie des navigateurs Web s'y connectent en IPv6 et je peux
mesurer dans quelle proportion.&lt;/p&gt;
&lt;h2&gt;Dites AAAA&lt;/h2&gt;
&lt;p&gt;Sur Internet, le service DNS associe les noms de domaine Internet avec
leurs adresses IP.&lt;/p&gt;
&lt;p&gt;La plupart des sites Internet déclarent des adresses IPv4 dans leur
zone DNS à travers des enregistrements DNS A (&lt;em&gt;A&lt;/em&gt; pour &lt;em&gt;Adresse&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Conscients de
&lt;a href="https://fr.wikipedia.org/wiki/%C3%89puisement_des_adresses_IPv4"&gt;l'épuisement des adresses IPv4&lt;/a&gt;,
de plus en plus de sites Internet sont aussi accessibles en IPv6. Pour permettre aux
navigateurs Web de résoudre leur nom de domaine en adresses IPv6,
ils ajoutent des enregistrements DNS AAAA dans leur zone DNS.
Les quatre "A" dénotent le fait qu'une adresse IPv6 (128 bits) est
quatre fois plus longue qu'une adresse IPv4 (32 bits).&lt;/p&gt;
&lt;p&gt;Sous Linux, l'outil &lt;a href="https://www.mankier.com/1/dig"&gt;dig&lt;/a&gt; permet
d'effectuer des requêtes DNS en ligne de commande et donc de vérifier
si un site Internet déclare être accessible en IPv4 &lt;em&gt;et&lt;/em&gt; en IPv6.&lt;/p&gt;
&lt;p&gt;Pour afficher l'adresse IPv4 associée à un nom de domaine :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;dig&lt;span class="w"&gt; &lt;/span&gt;+short&lt;span class="w"&gt; &lt;/span&gt;enodev.fr&lt;span class="w"&gt; &lt;/span&gt;A
&lt;span class="go"&gt;159.69.222.76&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Et pour obtenir l'adresse IPv6 éventuellement associée :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;dig&lt;span class="w"&gt; &lt;/span&gt;+short&lt;span class="w"&gt; &lt;/span&gt;enodev.fr&lt;span class="w"&gt; &lt;/span&gt;AAAA
&lt;span class="go"&gt;2a01:4f8:1c1e:b2d0::1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Qui AAAAccède ?&lt;/h2&gt;
&lt;p&gt;Le
&lt;a href="https://httpd.apache.org/docs/2.4/fr/logs.html#accesslog"&gt;fichier journal des accès&lt;/a&gt;
du serveur HTTP Apache enregistre les requêtes qu'il a traitées.&lt;/p&gt;
&lt;p&gt;Dans le format par défaut du fichier journal des accès, l'adresse IP
du client figure au début de chaque ligne.&lt;/p&gt;
&lt;p&gt;L'extrait de journal suivant présente une requête HTTP effectuée par
un navigateur Web connecté en IPv4 suivie d'une requête formulée
par un autre navigateur Web connecté en IPv6 :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;24.35.90.123 - - [01/Oct/2023:00:03:20 +0200] "GET /posts/rusticity-convert-an-integer-to-an-enum.html HTTP/2.0" 200 6966 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
2a02:8308:285:e800:9807:3eea:1a93:1234 - - [01/Oct/2023:00:03:30 +0200] "GET /posts/rusticity-convert-an-integer-to-an-enum.html HTTP/2.0" 200 6966 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.43"
&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Analyse des accès avec Angle-grinder&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/rcoh/angle-grinder"&gt;Angle-grinder&lt;/a&gt; est un outil en
ligne de commande qui permet d'interpréter des données en effectuant
des opérations de tri, de sélection, de groupement, ou des
calculs. Angle-grinder est notamment capable d'interpréter directement
un fichier journal des accès d'Apache. Ceci va nous aider à mesurer
dans quelle proportion les navigateurs Web se connectent au site en
IPv6.&lt;/p&gt;
&lt;p&gt;Voici une façon de compter le nombre d'adresse IP différentes qui se
sont connectées au serveur HTTP avec Angle-grinder:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;agrind&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;apache.log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"* | apache | count_distinct(ip) as unique_ipaddrs"&lt;/span&gt;
&lt;span class="go"&gt;unique_ipaddrs&lt;/span&gt;
&lt;span class="go"&gt;----------------------&lt;/span&gt;
&lt;span class="go"&gt;5326&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Les pipelines, séparés par le caractère &lt;code&gt;|&lt;/code&gt;, sont composés ainsi :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Toutes les entrées du fichier journal sont sélectionnées avec le
   caractère &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Le parser pour le format du journal d'Apache interprète les champs.&lt;/li&gt;
&lt;li&gt;L'opérateur d'agrégation &lt;code&gt;count_distinct()&lt;/code&gt; est appelé sur le champ
   nommé &lt;code&gt;ip&lt;/code&gt;. La colonne de résultat est nommée &lt;code&gt;unique_ipaddrs&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pour dénombrer le nombre d'adresse &lt;em&gt;IPv6&lt;/em&gt; différentes, il suffit
d'ajouter un filtre qui sélectionne les adresses IP qui contiennent le
séparateur &lt;code&gt;:&lt;/code&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;agrind&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;apache.log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"* | apache | contains(ip, ':') as is_ipv6 | where is_ipv6 | count_distinct(ip) as unique_ipv6addrs"&lt;/span&gt;
&lt;span class="go"&gt;unique_ipv6addrs&lt;/span&gt;
&lt;span class="go"&gt;------------------------&lt;/span&gt;
&lt;span class="go"&gt;500&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;9,4 % des navigateurs Web se connectent donc au site en IPv6. La
part des &lt;em&gt;requêtes&lt;/em&gt; HTTP effectuées en IPv6 est assez proche –
10 % – comme on peut le constater avec la requête suivante :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;agrind&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;apache.log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"* | apache | contains(ip, ':') as is_ipv6 | if(is_ipv6, 1, 0) as ipv6_req | avg(ipv6_req) as ipv6_req_ratio"&lt;/span&gt;
&lt;span class="go"&gt;ipv6_req_ratio&lt;/span&gt;
&lt;span class="go"&gt;----------------------&lt;/span&gt;
&lt;span class="go"&gt;0.10&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Il sera intéressant de suivre la progression du ratio des accès
réalisés en IPv6 dans le temps.&lt;/p&gt;
&lt;p&gt;Note: en décembre 2024, 14 % des requêtes HTTP sont effectuées en IPv6.&lt;/p&gt;
&lt;div class="caption"&gt;
  &lt;img src="https://www.enodev.fr/images/araignee-toile-ipv6.jpg" class="img-fluid rounded" alt="Une épeire fasciée sur sa toile. Connectée en IPv6 ?"&gt;
  &lt;p&gt;Une &lt;a href="https://fr.wikipedia.org/wiki/Argiope_frelon"&gt;épeire fasciée&lt;/a&gt; sur sa toile. Connectée en IPv6 ?&lt;/p&gt;
&lt;/div&gt;</description><category>Angle-grinder</category><category>Apache</category><category>DNS</category><category>IPv6</category><guid>https://www.enodev.fr/posts/statistiques-acces-au-site-en-ipv6.html</guid><pubDate>Sat, 30 Sep 2023 20:55:42 GMT</pubDate></item><item><title>Réalisation d'un nichoir bûche</title><link>https://www.enodev.fr/posts/realisation-d-un-nichoir-buche.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;Au printemps 2018, j'ai construit un nichoir pour mésange charbonnière
à partir d'une grosse bûche. Cet abri a depuis accueilli plusieurs
nichées, signe que les mésanges s'y plaisent. Puisque la période de
nidification approche, voyons comment réaliser ce type de nichoir
simple et rustique.&lt;/p&gt;
&lt;h2&gt;Matériel&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Une bûche. Dans mon cas, les dimensions étaient les suivantes :
  longueur = 34 cm, diamètre = 18 cm.&lt;/li&gt;
&lt;li&gt;Une hache à fendre.&lt;/li&gt;
&lt;li&gt;Une scie à bûche.&lt;/li&gt;
&lt;li&gt;Une scie à cloche.&lt;/li&gt;
&lt;li&gt;Un marteau.&lt;/li&gt;
&lt;li&gt;Des clous.&lt;/li&gt;
&lt;li&gt;Des vis à bois.&lt;/li&gt;
&lt;li&gt;Des pattes d'assemblage.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Étapes de construction&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Scier deux rondelles aux extrémités. Elle constitueront la base et le toit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fendre la bûche en quatre dans le sens de la longueur.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fendre chaque quartier en biais pour constituer la pièce à vivre.&lt;/p&gt;
&lt;p&gt;Pour une mésange charbonnière, la surface au sol minimale est un
&lt;a href="https://fr.wikipedia.org/wiki/Nichoir#Nichoir_de_type_ferm%C3%A9"&gt;carré de 10 cm de côté&lt;/a&gt;
(0.01 m&lt;sup&gt;2&lt;/sup&gt; loi Carrez). La hauteur sous plafond conseillée est d'environ 25 cm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dégrossir les bords intérieurs sans chercher à les rendre lisses :
   les aspérités permettent aux oiseaux de grimper.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Percer le trou d'envol. Le diamètre conseillé pour une mésange
   charbonnière est de 32 à 34 mm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Clouer les murs à la base.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Visser les pattes de fixation pour finir d'assembler les murs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Caler le toit. Le toit doit être amovible pour permettre de
   nettoyer l'intérieur du nichoir mais il ne doit pas s'envoler au
   moindre coup de vent.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="row" style="margin-bottom: 1.5em;"&gt;
  &lt;div class="col-md-4"&gt;
    &lt;img src="https://www.enodev.fr/images/nichoir-01-buche-fendue.jpg" class="img-fluid rounded" alt="Découpe de la loge"&gt;
  &lt;/div&gt;
  &lt;div class="col-md-4"&gt;
    &lt;img src="https://www.enodev.fr/images/nichoir-02-assemblage.jpg" class="img-fluid rounded" alt="Assemblage des parois"&gt;
  &lt;/div&gt;
  &lt;div class="col-md-4"&gt;
    &lt;img src="https://www.enodev.fr/images/nichoir-03-final.jpg" class="img-fluid rounded" alt="Nichoir bûche achevé"&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;Place à l'observation&lt;/h2&gt;
&lt;p&gt;La construction de votre nichoir bûche est achevée. Une fois
solidement fixé à un arbre, nul doute qu'un couple de mésanges ne
tardera pas à le remarquer et à y faire son nid douillet. À bonne
distance – pour ne pas les déranger – vous aurez tout
loisir d'observer leur va-et-vient incessant. Bonne observation !&lt;/p&gt;
&lt;div class="caption"&gt;
  &lt;img src="https://www.enodev.fr/images/nichoir-04-mesange.jpg" class="img-fluid rounded" alt="Mésange charbonnière au nichoir"&gt;
  &lt;p&gt;Mésange charbonnière au nichoir&lt;/p&gt;
&lt;/div&gt;</description><category>Bricolage</category><category>Nature</category><category>Nichoir</category><category>Ornithologie</category><guid>https://www.enodev.fr/posts/realisation-d-un-nichoir-buche.html</guid><pubDate>Sat, 20 Mar 2021 15:00:00 GMT</pubDate></item><item><title>Fin du QIF à la Société Générale</title><link>https://www.enodev.fr/posts/fin-du-qif-a-la-societe-generale.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;div class="alert alert-info" role="alert"&gt;
  &lt;strong&gt;Mise à jour :&lt;/strong&gt;
    le QIF est de retour à la Société Générale depuis
    février 2020. Mais cet article reste pertinent si vous préférez
    manipuler le relevé de vos opérations au format CSV.
&lt;/div&gt;

&lt;p&gt;J'utilise &lt;a href="https://gnucash.org/"&gt;GnuCash&lt;/a&gt; depuis une dizaine d'années
pour tenir mes comptes. J'ai l'habitude d'importer le relevé des
opérations au format
&lt;a href="http://www.respmech.com/mym2qifw/qif_new.htm"&gt;QIF&lt;/a&gt; dans GnuCash. Or
depuis la refonte du site Internet destiné aux particuliers, la
Société Générale ne propose plus l'export du relevé des opération au
format QIF. À la place, la banque au carré rouge et noir propose un
fichier CSV contenant les opérations réalisées au cours des six
derniers mois.&lt;/p&gt;
&lt;p&gt;Contactée, la Société Générale m'a informé &lt;em&gt;« ne pas
pouvoir changer le format proposé par le site »&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Je constate qu'à l'image des &lt;acronym title="Google, Apple, Facebook,
Amazon et Microsoft"&gt;GAFAM&lt;/acronym&gt;, la Société Générale cherche à
capter les données de ses clients pour les exploiter et néglige leur
partage avec ces mêmes clients qui en sont pourtant les
propriétaires. Ainsi, le site Internet de la Société Générale propose
d'importer des comptes &lt;em&gt;externes&lt;/em&gt;, c'est-à-dire tenus dans &lt;em&gt;d'autres&lt;/em&gt;
établissements bancaires. Mais quiconque veut rapatrier &lt;em&gt;ses données&lt;/em&gt;
doit se contenter d'un fichier CSV réduit à la portion congrue.&lt;/p&gt;
&lt;p&gt;Nous allons étudier les problèmes liés au fichier CSV fourni par la
Société Générale et développer ensemble &lt;del&gt;l'esprit d'équipe&lt;/del&gt; une
solution pour faciliter son import dans GnuCash.&lt;/p&gt;
&lt;h2&gt;Du CSV à la française&lt;/h2&gt;
&lt;p&gt;Après avoir téléchargé le fichier CSV, observons ses propriétés :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;Export_17052019_16112019.csv
Export_17052019_16112019.csv:&lt;span class="w"&gt; &lt;/span&gt;ISO-8859&lt;span class="w"&gt; &lt;/span&gt;text,&lt;span class="w"&gt; &lt;/span&gt;with&lt;span class="w"&gt; &lt;/span&gt;CRLF&lt;span class="w"&gt; &lt;/span&gt;line&lt;span class="w"&gt; &lt;/span&gt;terminators
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Le fichier est encodé en &lt;a href="https://fr.wikipedia.org/wiki/ISO/CEI_8859"&gt;ISO
8859&lt;/a&gt; et les retours à la
ligne sont matérialisés par deux octets
&lt;a href="https://fr.wikipedia.org/wiki/Carriage_Return_Line_Feed"&gt;CR+LF&lt;/a&gt; comme
il est d'usage sous Windows.&lt;/p&gt;
&lt;p&gt;Que contiennent donc &lt;em&gt;nos&lt;/em&gt; données ?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$ head Export_17052019_16112019.csv
="0201900016400270";17/05/2019;16/11/2019;
date_comptabilisation;libellé_complet_operation;montant_operation;devise;
15/11/2019;CARTE X7527 15/11 METRO    ;-14,90;EUR;
13/11/2019;VIR RECU    8527975563S DE: Boulot SAS MOTIF: BOULOT OCTOBRE 2019 33 REF: BOULOT OCTOBRE 2019 33;2188,28;EUR;
11/11/2019;PRELEVEMENT EUROPEEN 7997325297 DE: DODO GESTION ID: FR65ZZZ233162 MOTIF: 081 LOYER DODO;-614,50;EUR;
10/11/2019;CARTE X7527 10/11 RESTO    ;-11,00;EUR;
09/11/2019;CARTE X7527 09/11 BRICO    ;-14,50;EUR;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Nous avons affaire à la variante française du
&lt;a href="https://fr.wikipedia.org/wiki/Comma-separated_values"&gt;CSV&lt;/a&gt;
qui utilise le &lt;em&gt;point-virgule&lt;/em&gt; pour délimiter les colonnes et la
&lt;em&gt;virgule&lt;/em&gt; pour partager la partie entière et la partie décimale d'un
nombre.&lt;/p&gt;
&lt;p&gt;La première ligne débute par le numéro de compte. La notation
&lt;code&gt;="0123"&lt;/code&gt; est une astuce pour indiquer à Microsoft Excel de
&lt;a href="https://stackoverflow.com/questions/34595812/is-a-csv-with-equal-sign-valid"&gt;préserver le zéro&lt;/a&gt;
au début d'un nombre. Suivent les dates de début et de fin des
données exportées. Les dates sont exprimées au format français
&lt;em&gt;JJ/MM/AAAA&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;La deuxième ligne contient les intitulés de colonnes qui sont donc la
&lt;em&gt;date de comptabilisation&lt;/em&gt;, le &lt;em&gt;libellé de l'opération&lt;/em&gt;, son &lt;em&gt;montant&lt;/em&gt;
et la &lt;em&gt;devise&lt;/em&gt;. Nous remarquons que la ligne se termine par un
séparateur &lt;em&gt;point-virgule&lt;/em&gt; inutile.&lt;/p&gt;
&lt;h2&gt;Nettoyer le fichier CSV avec Pandas&lt;/h2&gt;
&lt;p&gt;Après avoir observé les données, nous ouvrons notre boîte à outils
d'apprenti
&lt;a href="https://fr.wikipedia.org/wiki/Science_des_donn%C3%A9es"&gt;Data Scientist&lt;/a&gt;.
Nous choisissons Python et Pandas.
&lt;a href="https://www.python.org/"&gt;Python&lt;/a&gt; est un langage de programmation
qu'on ne présente plus : il est même enseigné au Lycée.
&lt;a href="https://pandas.pydata.org/"&gt;Pandas&lt;/a&gt; est une bibliothèque Python
destinée à la fouille de données.
Python et Pandas, voilà une excellente compagnie pour le paresseux que
je suis.&lt;/p&gt;
&lt;h3&gt;Charger&lt;/h3&gt;
&lt;p&gt;La bibliothèque Pandas fournit la fonction
&lt;a href="https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html"&gt;&lt;code&gt;read_csv()&lt;/code&gt;&lt;/a&gt;
avec de nombreux paramètres permettant de nettoyer le fichier CSV.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Export_17052019_16112019.csv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;parse_dates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dayfirst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;usecols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;index_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'currency'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                 &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ISO-8859-1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Parmi les paramètres :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sep&lt;/code&gt; et &lt;code&gt;decimal&lt;/code&gt; adaptent le comportement de &lt;code&gt;read_csv()&lt;/code&gt; pour la
  variante française du CSV.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;header&lt;/code&gt; indique que la ligne des entêtes est la deuxième ligne (la
  première ligne ayant pour indice zéro).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dayfirst&lt;/code&gt; sélectionne le format de date européen où le jour
  apparaît avant le mois (les États-Unis fêtent Noël le 12/25).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index_col&lt;/code&gt; précise que la première colonne – la date –
  sert d'index.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La fonction &lt;code&gt;read_csv()&lt;/code&gt; retourne un objet de type
&lt;a href="https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html"&gt;&lt;code&gt;DataFrame&lt;/code&gt;&lt;/a&gt;.
Voici son contenu affiché par Python :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&amp;gt;&amp;gt;&amp;gt; df
                                                  description   amount currency
date
2019-11-15                        CARTE X7527 15/11 METRO       -14.90      EUR
2019-11-13  VIR RECU    8527975563S DE: Boulot SAS MOTIF: ...  2188.28      EUR
2019-11-11  PRELEVEMENT EUROPEEN 7997325297 DE: DODO GESTI...  -614.50      EUR
2019-11-10                        CARTE X7527 10/11 RESTO       -11.00      EUR
2019-11-09                        CARTE X7527 09/11 BRICO       -14.50      EUR
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Filtrer&lt;/h3&gt;
&lt;p&gt;La version précédente du site Internet de la Société Générale
proposait d'exporter les transactions au format QIF &lt;em&gt;depuis le dernier
téléchargement&lt;/em&gt;. La nouvelle mouture propose un export au format CSV
des transactions des six derniers mois.&lt;/p&gt;
&lt;p&gt;Par conséquent, nous sommes désormais contraint de relever dans
GnuCash la date de la dernière transaction déjà importée afin de
filtrer les données CSV à importer et limiter les doublons.&lt;/p&gt;
&lt;p&gt;La première colonne – la date – ayant été choisie comme
index, l'opération se fait simplement avec un &lt;em&gt;slice&lt;/em&gt; :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&amp;gt;&amp;gt;&amp;gt; df = df['2019-11-15':'2019-11-13']
&amp;gt;&amp;gt;&amp;gt; df
                                                  description   amount currency
date
2019-11-15                        CARTE X7527 15/11 METRO       -14.90      EUR
2019-11-13  VIR RECU    8527975563S DE: Boulot SAS MOTIF: ...  2188.28      EUR
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Distinguer les dépôts et les retraits&lt;/h3&gt;
&lt;p&gt;Le CSV exporté depuis le site Internet de la Société Générale comprend
une seule colonne &lt;em&gt;montant&lt;/em&gt;. Or GnuCash exige de distinguer &lt;em&gt;dépôt&lt;/em&gt; et
&lt;em&gt;retrait&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Nous devons donc ajouter deux colonnes au tableau et les remplir à
partir du montant.&lt;/p&gt;
&lt;p&gt;Pour une transaction donnée :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Si le montant est &lt;em&gt;positif&lt;/em&gt;, il s'agit d'un dépôt. Nous copions la
  valeur du montant dans la colonne dépôt et nous écrivons zéro dans
  la colonne débit.&lt;/li&gt;
&lt;li&gt;Si le montant est &lt;em&gt;négatif&lt;/em&gt;, il s'agit d'un retrait. Nous copions la
  valeur absolue du montant dans la colonne retrait et nous écrivons
  zéro dans la colonne dépôt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avec Pandas, les colonnes &lt;em&gt;dépôt&lt;/em&gt; et &lt;em&gt;retrait&lt;/em&gt; peuvent être calculées
à partir du &lt;em&gt;montant&lt;/em&gt; de la manière suivante :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'deposit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'withdraw'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;En Python, le mot clé &lt;em&gt;lambda&lt;/em&gt; définit une fonction &lt;em&gt;anonyme&lt;/em&gt;. Dans la
première instruction, la colonne dépôt est remplie en appliquant au
montant la fonction qui pour &lt;em&gt;x&lt;/em&gt; retourne &lt;em&gt;x&lt;/em&gt; s'il est positif et 0
dans le cas contraire.&lt;/p&gt;
&lt;p&gt;Voici à quoi ressemble notre tableau suite à l'ajout des colonnes
dépôt et retrait :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;                                                  description   amount currency  deposit  withdraw
date
2019-11-15                        CARTE X7527 15/11 METRO       -14.90      EUR     0.00      14.9
2019-11-13  VIR RECU    8527975563S DE: Boulot SAS MOTIF: ...  2188.28      EUR  2188.28       0.0
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Exporter à destination de Gnucash&lt;/h3&gt;
&lt;p&gt;La méthode
&lt;a href="https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_csv.html#pandas.DataFrame.to_csv"&gt;&lt;code&gt;to_csv()&lt;/code&gt;&lt;/a&gt;
permet de sauvegarder un &lt;code&gt;DataFrame&lt;/code&gt; dans un
fichier au format CSV.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gnucash.csv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deposit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'withdraw'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Seules sont exportées les colonnes utiles à GnuCash : date (implicite
car il s'agit de l'index), description, dépôt, et retrait.&lt;/p&gt;
&lt;p&gt;Notre fichier CSV &lt;em&gt;« nettoyé »&lt;/em&gt; est prêt à être
importé dans GnuCash.&lt;/p&gt;
&lt;h2&gt;Importer le fichier CSV final dans GnuCash&lt;/h2&gt;
&lt;p&gt;Dans l'interface graphique de GnuCash, nous cliquons sur : &lt;em&gt;fichier&lt;/em&gt;
→ &lt;em&gt;importer&lt;/em&gt; → &lt;em&gt;importer des transactions depuis un CSV&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Après avoir sélectionné notre fichier CSV, GnuCash nous présente la
fenêtre d'aperçu de l'import.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Aperçu de l'import dans GnuCash" class="rounded" src="https://www.enodev.fr/images/gnucash-import-preview.png"&gt;&lt;/p&gt;
&lt;p&gt;Nous appliquons les réglages suivants :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Le point-virgule est défini comme séparateur de colonnes.&lt;/li&gt;
&lt;li&gt;La première ligne qui correspond à l'entête est ignorée.&lt;/li&gt;
&lt;li&gt;Le type des données de chaque colonne est précisé.&lt;/li&gt;
&lt;li&gt;Un compte cible est renseigné pour l'ensemble des transactions (champ &lt;em&gt;account&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le wiki de GnuCash décrit plus en détail comment
&lt;a href="https://wiki.gnucash.org/wiki/CSV_Import/Export#Importing_Transactions"&gt;importer des transactions au format CSV&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Pour aller plus loin&lt;/h2&gt;
&lt;p&gt;Le script &lt;a href="https://www.enodev.fr/csv-socgen/csv-socgen.py"&gt;csv-socgen.py&lt;/a&gt; reprend
l'ensemble du code présenté ici. Il ne s'agit que d'un premier jet.&lt;/p&gt;
&lt;p&gt;Le script mérite d'être amélioré pour remplir automatiquement le
compte destinataire et modifier la description si elle correspond à un
motif donné. Ainsi, le compte destinataire de la transaction ayant
pour description &lt;em&gt;METRO&lt;/em&gt; serait automatiquement renseigné en
&lt;em&gt;Dépenses:Transports publics:Métro&lt;/em&gt;. Remarquons que la banque affiche
une catégorie pour chaque transaction sur son site Internet mais que
cette information ne figure pas dans le fichier CSV. À nous de ruser.&lt;/p&gt;
&lt;p&gt;Un ancien slogan de la Société Générale clamait &lt;em&gt;« On est
là pour vous aider »&lt;/em&gt;. Cette ambition a disparu en 2019
avec un slogan devenu &lt;em&gt;« C'est vous
l'avenir »&lt;/em&gt;. Alors, ne comptez que sur vous-même pour tenir
vos comptes !&lt;/p&gt;</description><category>Banque</category><category>CSV</category><category>GnuCash</category><category>Pandas</category><category>Python</category><category>QIF</category><guid>https://www.enodev.fr/posts/fin-du-qif-a-la-societe-generale.html</guid><pubDate>Fri, 22 Nov 2019 18:00:00 GMT</pubDate></item><item><title>Rénovo X240 Full HD</title><link>https://www.enodev.fr/posts/renovo-x240-full-hd.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;Après avoir réparé &lt;a href="https://www.enodev.fr/posts/renovo-x240.html"&gt;le clavier&lt;/a&gt;, voici venu le
moment d'améliorer l'écran de mon Lenovo ThinkPad X240 pour le confort
de mes petits yeux.&lt;/p&gt;
&lt;p&gt;La lecture de ce &lt;a href="https://www.reddit.com/r/thinkpad/comments/844nr7/upgraded_my_x240_with_x250_trackpad_ips_fhd"&gt;fil de
discussion&lt;/a&gt;
sur Reddit m'a convaincu de remplacer l'écran LCD d'origine d'une
résolution de 1366×768 pixels par un modèle Full HD plus
récent atteignant 1920×1080 pixels.&lt;/p&gt;
&lt;p&gt;J'ai opté pour le modèle d'écran LCD suivant: 00HN899. Je l'ai acheté
sur eBay pour un peu moins de 80 euros et je l'ai reçu chez moi
deux jours plus tard en provenance de Tallinn (Estonie).&lt;/p&gt;
&lt;p&gt;Je me suis assuré que le câble qui relie la carte mère à l'écran est
compatible Full HD : la mention FHD figure bien sur le câble et
je n'ai donc pas eu besoin de changer le câble.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Câble LCD compatible Full HD (FHD)" class="img-fluid rounded-circle" src="https://www.enodev.fr/images/lenovo-X240-FHD-cable.jpg"&gt;&lt;/p&gt;
&lt;p&gt;L'écran LCD est maintenu par des clips sur ses bords et son démontage
ne nécessite aucun outil particulier comme illustré dans &lt;a href="https://www.youtube.com/watch?v=QIkCFLwoghU&amp;amp;t=13"&gt;cette
vidéo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Sous Debian, j'ai constaté que l'affichage était plus fin mais que les
polices de caractères étaient bien trop petites pour être lisible. Par
défaut, le serveur graphique X définit la résolution à
96×96 &lt;acronym title="dots per inch"&gt;DPI&lt;/acronym&gt;. J'ai
corrigé le problème en &lt;a href="https://wiki.archlinux.org/index.php/HiDPI#X_Resources"&gt;augmentant la
résolution&lt;/a&gt; à
144×144 &lt;acronym title="dots per inch"&gt;DPI&lt;/acronym&gt; dans
le fichier &lt;code&gt;~/.Xresources&lt;/code&gt;. Il faut définir le paramètre
&lt;code&gt;Xft.dpi&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;dpi&lt;span class="w"&gt; &lt;/span&gt;~/.Xresources
&lt;span class="go"&gt;Xft.dpi: 144&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;</description><category>hardware</category><category>Lenovo</category><category>repair</category><guid>https://www.enodev.fr/posts/renovo-x240-full-hd.html</guid><pubDate>Sat, 31 Mar 2018 17:12:24 GMT</pubDate></item><item><title>Rénovo X240</title><link>https://www.enodev.fr/posts/renovo-x240.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;J'ai récupéré un ordinateur portable Lenovo ThinkPad X240 pour lui
donner une seconde jeunesse&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://www.enodev.fr/posts/renovo-x240.html#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;. Au départ, il s'agissait simplement de
changer le clavier qui était défectueux depuis qu'il avait été aspergé
d'une tasse de café. Et puis la loi de Murphy a frappé une deuxième
fois cette machine.&lt;/p&gt;
&lt;p&gt;La gamme des ordinateurs ThinkPad est très répandue en entreprise ce
qui explique qu'on trouve aisément des pièces de rechange. Après avoir
suivi les instructions de cet excellent &lt;a href="https://www.youtube.com/watch?v=HTN-CO_flVs"&gt;tutoriel
vidéo&lt;/a&gt; pour changer le
clavier, je démarrais l'ordinateur déjà fier de ma
réparation. Aussitôt, j'entendis un léger claquement – &lt;em&gt;Plop&lt;/em&gt;
– accompagné par l'odeur âcre d'un composant électronique qui
venait de brûler. Le message d'erreur suivant s'affichait à l'écran :
&lt;em&gt;Fan error&lt;/em&gt;. Ça ne sentait pas bon…&lt;/p&gt;
&lt;p&gt;Effectivement, le ventilateur ne tournait plus ce qui interrompait la
séquence de démarrage. J'essayais alors une astuce trouvée sur
Internet : lors du démarrage, je soufflais fortement à travers la
sortie d'aération du ventilateur pour le mettre en mouvement et faire
croire au BIOS que le ventilateur fonctionnait. L'ordinateur démarrait
bien mais le ventilateur demeurait immobile sous Linux ; je
décidais sagement d'éteindre la machine pour éviter que son processeur
ne surchauffe.&lt;/p&gt;
&lt;p&gt;Le lendemain, je démontais le ventilateur pour le tester
séparément. Ne disposant pas d'une source de courant continu de 5
volts, j'en bricolais une en coupant un câble USB. En effet, entre
&lt;a href="https://fr.wikipedia.org/wiki/Universal_Serial_Bus#Types_A_et_B"&gt;le fil rouge et le fil noir&lt;/a&gt;,
la tension est de 5 volts. Ainsi alimenté, le ventilateur brassait
l'air à plein régime.&lt;/p&gt;
&lt;p&gt;En observant avec plus d'attention la carte mère, je remarquais une
tâche noire à côté du connecteur du ventilateur. Je tenais le
coupable : un composant électronique avait bel et bien grillé.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Fusible grillé en amont du ventilateur" class="img-fluid rounded-circle" src="https://www.enodev.fr/images/lenovo-X240-fuse.jpg"&gt;&lt;/p&gt;
&lt;p&gt;À ce stade, je ne connaissais ni la nature ni les caractéristiques du
composant qui avait grillé. Heureusement, un collègue trouvait sur ce
&lt;a href="https://forum.thinkpads.com/viewtopic.php?t=121522"&gt;forum&lt;/a&gt; les
schémas électroniques du Lenovo X240&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://www.enodev.fr/posts/renovo-x240.html#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;. À la page 50 du document, se
trouvait la réponse à ma question : le défunt composant était un
fusible qui pouvait supporter un courant d'une intensité de
2 ampères. Il a eu très chaud !&lt;/p&gt;
&lt;p&gt;&lt;img alt="Schéma du connecteur du ventilateur" class="img-fluid" src="https://www.enodev.fr/images/lenovo-X240-schematics.png"&gt;&lt;/p&gt;
&lt;p&gt;J'ai la chance de travailler dans l'électronique grand public et
d'avoir des collègues électroniciens qui savent souder des composants
montés en surface. Ils sont très adroits parce que les composants
montés en surface sont minuscules (à titre d'exemple le &lt;a href="https://industrial.panasonic.com/ww/products/emc-circuit-protection/circuit-protection/fuses/micro-chip-fuse/ERBRD2R00X"&gt;fusible
mentionné&lt;/a&gt;
mesure 1 mm × 0.5 mm). Il n'aura fallu que cinq
minutes à l'un de mes collègues pour remplacer le regretté fusible par
un fil, faute de disposer d'un remplaçant&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://www.enodev.fr/posts/renovo-x240.html#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt;. Voici le résultat :&lt;/p&gt;
&lt;p&gt;&lt;img alt="Remplacement du fusible grillé par un fil" class="img-fluid rounded-circle" src="https://www.enodev.fr/images/lenovo-X240-repair.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Et le patient dans tout ça ? Il se porte bien puisque c'est
depuis le Rénovo X240 que j'écris ces lignes.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Merci Guillaume. &lt;a class="footnote-backref" href="https://www.enodev.fr/posts/renovo-x240.html#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Merci Rémi. &lt;a class="footnote-backref" href="https://www.enodev.fr/posts/renovo-x240.html#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Merci Jean-Luc. &lt;a class="footnote-backref" href="https://www.enodev.fr/posts/renovo-x240.html#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>hardware</category><category>Lenovo</category><category>repair</category><guid>https://www.enodev.fr/posts/renovo-x240.html</guid><pubDate>Sun, 29 Oct 2017 14:18:25 GMT</pubDate></item><item><title>Rusticity: convert an integer to an enum</title><link>https://www.enodev.fr/posts/rusticity-convert-an-integer-to-an-enum.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;I am learning how to program in
&lt;a href="https://www.rust-lang.org/"&gt;Rust&lt;/a&gt;. Thinking in Rust is a delightful
experience and the more I practice Rust the more I feel how it
empowers developers to solve complex problems with confidence.&lt;/p&gt;
&lt;p&gt;However, I sometimes get frustrated on my way to rusticity. For
instance, when a programming task easily done in C or Python requires
more work in Rust. I experienced this when trying to convert an
integer to an enum. This blog post compares how this is usually done
in C and how to do it safely in Rust.&lt;/p&gt;
&lt;h2&gt;Converting an integer to an enum in C&lt;/h2&gt;
&lt;p&gt;In C, the enumeration constants have the type &lt;code&gt;int&lt;/code&gt;. Thus, an integer
value can be directly assigned to an enum.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;atomic_number&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HYDROGEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HELIUM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;IRON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;atomic_number&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IRON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Beware of Rust!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;While it is easy to assign an integer value to an enum, the C compiler
performs no bounds checking. Nothing prevents us from assigning an
impossible value to an &lt;code&gt;atomic_number&lt;/code&gt; enum.&lt;/p&gt;
&lt;h2&gt;Converting an integer to an enum in Rust, the naïve way&lt;/h2&gt;
&lt;p&gt;Let's write a similar program in Rust:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AtomicNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HYDROGEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HELIUM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;IRON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AtomicNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When we try to compile and run the program with &lt;code&gt;cargo run&lt;/code&gt;, the Rust
compiler reports a &lt;em&gt;mismatched types&lt;/em&gt; error:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="go"&gt;error[E0308]: mismatched types&lt;/span&gt;
&lt;span class="go"&gt; --&amp;gt; src/main.rs:9:33&lt;/span&gt;
&lt;span class="go"&gt;  |&lt;/span&gt;
&lt;span class="go"&gt;9 |     let element: AtomicNumber = 26;&lt;/span&gt;
&lt;span class="go"&gt;  |                  ------------   ^^ expected enum `AtomicNumber`, found integer&lt;/span&gt;
&lt;span class="go"&gt;  |                  |&lt;/span&gt;
&lt;span class="go"&gt;  |                  expected due to this&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The compiler error clearly indicates that &lt;code&gt;AtomicNumber&lt;/code&gt; and &lt;code&gt;integer&lt;/code&gt;
are two different types.&lt;/p&gt;
&lt;p&gt;To explicitly convert an integer to our &lt;code&gt;AtomicNumber&lt;/code&gt; enum, we can
write a conversion function that takes an unsigned 32-bits integer as
parameter and returns an &lt;code&gt;AtomicNumber&lt;/code&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AtomicNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HYDROGEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HELIUM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;IRON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AtomicNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;from_u32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AtomicNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AtomicNumber&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HYDROGEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AtomicNumber&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HELIUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AtomicNumber&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;IRON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;panic!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unknown value: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AtomicNumber&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;from_u32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;from_u32()&lt;/code&gt; function is an &lt;em&gt;associated function&lt;/em&gt; of the
&lt;code&gt;AtomicNumber&lt;/code&gt; type because it is associated with that type,
and unlike a method does not take a first parameter named &lt;code&gt;self&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There are several issues in &lt;code&gt;from_u32()&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When the given value does not match any variant in the enumeration,
  &lt;code&gt;panic!()&lt;/code&gt; terminates the program.&lt;/li&gt;
&lt;li&gt;Both the enumeration definition and the conversion function match
  enumeration variants with integers.
  This duplication is error-prone.&lt;/li&gt;
&lt;li&gt;The conversion function grows with the number of variants.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The periodic table contains more than 100 elements. Implementing a
conversion function for atomic numbers is boring and
error-prone. There is a better way.&lt;/p&gt;
&lt;h2&gt;Converting an integer to an enum in Rust with num-derive&lt;/h2&gt;
&lt;p&gt;A more elegant solution is to use the &lt;a href="https://docs.rs/num/0.4.0/num/trait.FromPrimitive.html"&gt;FromPrimitive
trait&lt;/a&gt;
from the &lt;a href="https://crates.io/crates/num"&gt;num&lt;/a&gt; crate coupled with syntax
extensions from the &lt;a href="https://crates.io/crates/num-derive"&gt;num-derive&lt;/a&gt;
crate.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;Cargo.toml&lt;/code&gt;, add dependencies for
&lt;a href="https://crates.io/crates/num"&gt;num&lt;/a&gt;,
&lt;a href="https://crates.io/crates/num-derive"&gt;num-derive&lt;/a&gt;, and
&lt;a href="https://crates.io/crates/num-traits"&gt;num-traits&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;[dependencies]&lt;/span&gt;
&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.4"&lt;/span&gt;
&lt;span class="n"&gt;num-derive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.4"&lt;/span&gt;
&lt;span class="n"&gt;num-traits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.2"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, use the &lt;code&gt;#[derive]&lt;/code&gt; attribute:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;extern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#[macro_use]&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num_derive&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;#[derive(FromPrimitive)]&lt;/span&gt;
&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AtomicNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HYDROGEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HELIUM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;IRON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FromPrimitive&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;from_u32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AtomicNumber&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;IRON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Beware of Rust!"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unknown atomic number"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;#[derive(FromPrimitive)]&lt;/code&gt; attribute instructs the Rust compiler
to generate a basic implementation of the &lt;code&gt;FromPrimitive&lt;/code&gt; trait for
the &lt;code&gt;AtomicNumber&lt;/code&gt; enumeration. Unlike our handwritten &lt;code&gt;from_u32()&lt;/code&gt;
function, the conversion function generated by the Rust compiler
returns an &lt;a href="https://doc.rust-lang.org/std/option/"&gt;&lt;code&gt;Option&lt;/code&gt;&lt;/a&gt; which is
either &lt;code&gt;Some&lt;/code&gt; atomic number or &lt;code&gt;None&lt;/code&gt; if the given integer does not
match any known atomic number. This is much safer than calling
&lt;code&gt;panic!()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With the &lt;code&gt;#[derive(FromPrimitive)]&lt;/code&gt; attribute our Rust program is
nearly as concise as the same program written in C with the bonus
of being safer.&lt;/p&gt;
&lt;h2&gt;Harder, better, safer, rustier&lt;/h2&gt;
&lt;p&gt;While I had some hard time figuring how to convert an integer value to
an enum variant in Rust, I feel reassured by its type safety and pleased
by its ecosystem of &lt;a href="https://www.crates.io/"&gt;crates&lt;/a&gt;.&lt;/p&gt;
&lt;div class="caption"&gt;
  &lt;img src="https://www.enodev.fr/images/rust-pont-bir-hakeim.jpg" class="img-fluid rounded" alt="Rust on the pont de Bir-Hakeim"&gt;
  &lt;p&gt;Rust on the &lt;a href="https://en.wikipedia.org/wiki/Pont_de_Bir-Hakeim"&gt;pont de Bir-Hakeim&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</description><category>Programming</category><category>Rust</category><guid>https://www.enodev.fr/posts/rusticity-convert-an-integer-to-an-enum.html</guid><pubDate>Wed, 06 Sep 2017 18:00:00 GMT</pubDate></item><item><title>Le paquet targetcli-fb intègre Debian</title><link>https://www.enodev.fr/posts/targetcli-fb-integre-debian.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;Depuis quelques mois,
le &lt;a href="https://tracker.debian.org/pkg/targetcli-fb"&gt;paquet targetcli-fb&lt;/a&gt;
qui permet de configurer une target iSCSI avec LIO fait partie de
Debian Testing (la version en cours de développement
de &lt;a href="https://wiki.debian.org/fr/DebianStretch"&gt;Debian 9 "Stretch"&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;L'intégration de targetcli-fb vient combler un manque dans Debian. En
effet, aucun utilitaire n'est fourni dans l'actuelle Debian Stable
(Debian 8 "Jessie") pour configurer une target iSCSI avec LIO.&lt;/p&gt;
&lt;p&gt;Je participe à la maintenance du paquet targetcli-fb dans Debian avec
deux contributeurs plus expérimentés : Christian Seiler
et &lt;a href="https://www.researchut.com/"&gt;Ritesh Raj Sarraf&lt;/a&gt;. J'utilise
Debian depuis de nombreuses années et c'est un juste retour des choses
que de contribuer modestement (et librement) au projet.&lt;/p&gt;
&lt;p&gt;L'iSCSI sous Linux étant un sujet rarement traité sur le web
francophone, j'évoquerai comment s'en servir dans un futur article.&lt;/p&gt;</description><category>Debian</category><category>iSCSI</category><category>targetcli-fb</category><guid>https://www.enodev.fr/posts/targetcli-fb-integre-debian.html</guid><pubDate>Tue, 20 Dec 2016 19:00:00 GMT</pubDate></item><item><title>Time-lapse de fleurs de safran</title><link>https://www.enodev.fr/posts/time-lapse-de-fleurs-de-safran.html</link><dc:creator>Christophe Vu-Brugier</dc:creator><description>&lt;p&gt;Mi octobre, j'ai eu le plaisir d'assister à l'ouverture des fleurs de
safran
(&lt;em&gt;&lt;a href="https://fr.wikipedia.org/wiki/Crocus_sativus"&gt;Crocus sativus&lt;/a&gt;&lt;/em&gt;)
dans le potager de mes parents. L'occasion était belle de fixer sur la
&lt;del&gt;pellicule&lt;/del&gt; carte SD les étapes de la floraison pour en faire un
time-lapse.&lt;/p&gt;
&lt;video id="crocus-video" controls&gt;
  &lt;source src="https://www.enodev.fr/videos/fleurs-safran-timelapse-HD.mp4" type="video/mp4"&gt;
&lt;/source&gt;&lt;/video&gt;

&lt;p&gt;Cette vidéo accélérée 1500 fois a été réalisée en prenant une image
toutes les 5 minutes puis en générant une vidéo qui fait défiler 5
images par seconde.&lt;/p&gt;
&lt;p&gt;Pour réaliser la vidéo, j'ai employé deux logiciels en ligne de
commande : &lt;a href="https://www.imagemagick.org/"&gt;ImageMagick&lt;/a&gt;
et &lt;a href="https://www.ffmpeg.org/"&gt;ffmpeg&lt;/a&gt;. ImageMagick a permis de
recadrer, redimensionner et légender chaque image tandis que ffmpeg a
été utilisé pour combiner les images en une vidéo au format H.264.&lt;/p&gt;
&lt;h2&gt;Recadrage et redimensionnement&lt;/h2&gt;
&lt;p&gt;Au moment de la prise de vue, j'ai oublié de configurer mon appareil
photo pour prendre des clichés aux dimensions de la vidéo finale :
1920×1080 pixels. J'ai donc enregistré des images dont les
dimensions étaient trop grandes : 4608×3456 pixels
(ratio &lt;sup&gt;4&lt;/sup&gt;⁄&lt;sub&gt;3&lt;/sub&gt;). Il a fallu recadrer et
réduire chaque image à sa dimension finale : 1920×1080 pixels
(ratio &lt;sup&gt;16&lt;/sup&gt;⁄&lt;sub&gt;9&lt;/sub&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img alt="Recadrage et redimensionnement" class="img-fluid" src="https://www.enodev.fr/images/safran-recadrage-redimensionnement.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Les deux étapes de recadrage et de redimensionnement ont été réalisées
par une seule commande &lt;code&gt;convert&lt;/code&gt; en utilisant les options &lt;code&gt;-crop&lt;/code&gt; et
&lt;code&gt;-resize&lt;/code&gt; :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;convert&lt;span class="w"&gt; &lt;/span&gt;-crop&lt;span class="w"&gt; &lt;/span&gt;3840x2160+0+1130&lt;span class="w"&gt; &lt;/span&gt;-resize&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;source.jpg&lt;span class="w"&gt; &lt;/span&gt;destination.jpg
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Cet appel à la commande &lt;code&gt;convert&lt;/code&gt; recadre d'abord l'image aux
dimensions 3840×2160 en décalant le cadre de 1130 pixels vers le
bas (et 0 pixel vers la droite) puis divise ses dimensions par deux
pour aboutir à une image de 1920×1080 pixels.&lt;/p&gt;
&lt;h2&gt;Extraction de l'heure de prise de vue et ajout de la légende&lt;/h2&gt;
&lt;p&gt;J'ai extrait la date et l'heure de la prise de vue pour chaque image
avec le programme &lt;code&gt;exiftool&lt;/code&gt; :&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;exiftool&lt;span class="w"&gt; &lt;/span&gt;-DateTimeOriginal&lt;span class="w"&gt; &lt;/span&gt;image.jpg
&lt;span class="go"&gt;Date/Time Original              : 2016:10:17 08:37:09&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ensuite, j'ai utilisé la commande &lt;code&gt;convert&lt;/code&gt; pour ajouter une
&lt;em&gt;undercolor box&lt;/em&gt; qui contient la légende.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Ajout d'un légende" class="img-fluid" src="https://www.enodev.fr/images/safran-legende.jpg"&gt;&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;convert&lt;span class="w"&gt; &lt;/span&gt;source.jpg&lt;span class="w"&gt; &lt;/span&gt;-pointsize&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-font&lt;span class="w"&gt; &lt;/span&gt;Inconsolata&lt;span class="w"&gt; &lt;/span&gt;-fill&lt;span class="w"&gt; &lt;/span&gt;white&lt;span class="w"&gt; &lt;/span&gt;-undercolor&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'#00000080'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-annotate&lt;span class="w"&gt; &lt;/span&gt;+8+50&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;" Légende "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;destination.jpg
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;La légende est écrite en caractères blancs (&lt;code&gt;-fill white&lt;/code&gt;) de 50
points de hauteur (&lt;code&gt;-pointsize 50&lt;/code&gt;), dans une police à chasse fixe
(&lt;code&gt;-font Inconsolata&lt;/code&gt;). Un fond noir en semi transparence (&lt;code&gt;-undercolor
'#00000080'&lt;/code&gt;) augmente la lisibilité.&lt;/p&gt;
&lt;h2&gt;Création de la vidéo au format H.264 à partir des images&lt;/h2&gt;
&lt;p&gt;La dernière étape a consisté à combiner les images en une vidéo
avec
&lt;a href="https://trac.ffmpeg.org/wiki/Slideshow"&gt;ffmpeg&lt;/a&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;ffmpeg&lt;span class="w"&gt; &lt;/span&gt;-framerate&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;/1&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;image-%03d.jpg&lt;span class="w"&gt; &lt;/span&gt;-c:v&lt;span class="w"&gt; &lt;/span&gt;libx264&lt;span class="w"&gt; &lt;/span&gt;-pix_fmt&lt;span class="w"&gt; &lt;/span&gt;yuv420p&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;video.mp4
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Les images défilent au rythme de cinq par seconde (&lt;code&gt;-framerate 5/1&lt;/code&gt;)
et le codec vidéo choisi est x264 pour coder un flux vidéo H.264
(&lt;code&gt;-c:v libx264&lt;/code&gt;). J'ai du contraindre le &lt;em&gt;pixel format&lt;/em&gt; avec &lt;code&gt;-pix_fmt
yuv420p&lt;/code&gt; parce que Firefox 45 considère que la vidéo est corrompue
quand le &lt;em&gt;pixel format&lt;/em&gt; par défaut (yuv422p) est employé.&lt;/p&gt;</description><category>ffmpeg</category><category>ImageMagick</category><category>photo</category><category>time-lapse</category><guid>https://www.enodev.fr/posts/time-lapse-de-fleurs-de-safran.html</guid><pubDate>Tue, 01 Nov 2016 15:00:00 GMT</pubDate></item></channel></rss>