<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <link href="https://friendlybit.com/feed/" rel="self" type="application/atom+xml" />
    <link href="https://friendlybit.com/" rel="alternate" type="text/html" />
    <updated>2025-12-28T12:00:00+01:00</updated>
    <id>https://friendlybit.com</id>
    <title type="html">Friendly Bit - Web development blog</title>
    <subtitle>Friendly Bit is a blog by Emil Stenström, a Swedish web developer that occasionally gets ideas of how to improve the internet.</subtitle>
    
        <entry>
            <title type="html">JustHTML is now safe-by-default</title>
            <link href="http://friendlybit.com/python/justhtml-sanitization/" rel="alternate" type="text/html" title="JustHTML is now safe-by-default" />
            <published>2025-12-28T12:00:00+01:00</published>
            <updated>2025-12-28T12:00:00+01:00</updated>
            <id>http://friendlybit.com/python/justhtml-sanitization/</id>
            <author>
                <name>Emil Stenström</name>
            </author>
            <summary type="text">If you accept HTML from users (comments, profiles, CMS fields), you eventually hit the same problem: You want to keep some markup. You really don’t want to...</summary>
            <content type="html" xml:base="http://friendlybit.com/python/justhtml-sanitization/">
                &lt;p&gt;If you accept HTML from users (comments, profiles, CMS fields), you eventually hit the same problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want to keep &lt;em&gt;some&lt;/em&gt; markup.&lt;/li&gt;
&lt;li&gt;You really don’t want to ship an &lt;a href=&#34;https://learn.snyk.io/lesson/xss/?ecosystem=javascript&#34;&gt;XSS&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s why JustHTML now includes a built-in, policy-driven HTML sanitizer, and why serialization is &lt;strong&gt;safe-by-default&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;safe-by-default-serialization&#34;&gt;Safe-by-default serialization&lt;a href=&#34;#safe-by-default-serialization&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;JustHTML sanitizes when you serialize to HTML or Markdown:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;justhtml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JustHTML&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;user_html&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
    &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;lt;p&amp;gt;Hello &amp;lt;b&amp;gt;world&amp;lt;/b&amp;gt; &amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt; &amp;#39;&lt;/span&gt;
    &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;lt;a href=&amp;quot;javascript:alert(1)&amp;quot;&amp;gt;bad&amp;lt;/a&amp;gt; &amp;#39;&lt;/span&gt;
    &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;lt;a href=&amp;quot;https://example.com/?a=1&amp;amp;b=2&amp;quot;&amp;gt;ok&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&amp;#39;&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JustHTML&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fragment&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to_markdown&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This drops &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; and strips dangerous URLs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;HTML&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;Hello &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;world&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;bad&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;a&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;href&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;quot;https://example.com/?a=1&amp;amp;amp;b=2&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;ok&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;MARKDOWN&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Hello &lt;span class=&#34;gs&#34;&gt;**world**&lt;/span&gt; [bad] [&lt;span class=&#34;nt&#34;&gt;ok&lt;/span&gt;](&lt;span class=&#34;na&#34;&gt;https://example.com/?a=1&amp;amp;b=2&lt;/span&gt;)
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;turning-it-off-trusted-input-only&#34;&gt;Turning it off (trusted input only)&lt;a href=&#34;#turning-it-off-trusted-input-only&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If the input is trusted and you want raw output, you can opt out:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;safe&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;False&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to_markdown&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;safe&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;False&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;custom-allowlist-policies&#34;&gt;Custom allowlist policies&lt;a href=&#34;#custom-allowlist-policies&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The default policy is intentionally conservative, but you can provide your own &lt;code&gt;SanitizationPolicy&lt;/code&gt;.
Here’s a small example that only allows &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, and &lt;code&gt;a[href]&lt;/code&gt;, and only allows &lt;code&gt;https&lt;/code&gt; links:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;justhtml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JustHTML&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SanitizationPolicy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UrlRule&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;policy&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SanitizationPolicy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;allowed_tags&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;p&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;b&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;a&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;allowed_attributes&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[],&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;a&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;href&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]},&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;url_rules&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;a&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;href&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UrlRule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;allowed_schemes&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;https&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]),&lt;/span&gt;
    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JustHTML&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fragment&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;policy&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;policy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you’re sanitizing a full document, safe serialization keeps &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; wrappers.
For snippets, pass &lt;code&gt;fragment=True&lt;/code&gt; to avoid implicit document wrappers.&lt;/p&gt;
&lt;p&gt;There are also a couple of knobs that tend to show up in real systems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;URL proxying (for example, rewriting &lt;code&gt;https://example.com/…&lt;/code&gt; to &lt;code&gt;/proxy?url=…&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Optional inline styles, with an allowlist of CSS properties and conservative value checks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;why-i-built-codejusthtml-xss-benchcode&#34;&gt;Why I built &lt;code&gt;justhtml-xss-bench&lt;/code&gt;&lt;a href=&#34;#why-i-built-codejusthtml-xss-benchcode&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you’ve worked on sanitizers before, you know the hard part isn’t writing a policy — it’s knowing what the browser will actually do with the result.&lt;/p&gt;
&lt;p&gt;So I built a tiny benchmark harness: &lt;code&gt;[justhtml-xss-bench](https://github.com/EmilStenstrom/justhtml-xss-bench/)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What it does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Takes a payload vector and a sanitizer.&lt;/li&gt;
&lt;li&gt;Sanitizes the payload.&lt;/li&gt;
&lt;li&gt;Embeds the sanitized output into the initial HTML page (&amp;quot;server-side&amp;quot; style).&lt;/li&gt;
&lt;li&gt;Loads it in a real Playwright browser engine.&lt;/li&gt;
&lt;li&gt;Fails the case if JavaScript executes (including signals like dialogs or attempted external script fetches).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It ships with &lt;strong&gt;7,000+ real-world XSS vectors&lt;/strong&gt; and can be used to compare JustHTML’s output with other sanitizers.&lt;/p&gt;
&lt;p&gt;If you want to explore it locally, the CLI looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;BASH&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;# Run all vector files in ./vectors against the default sanitizer set&lt;/span&gt;
xssbench

&lt;span class=&#34;c1&#34;&gt;# Limit to one engine&lt;/span&gt;
xssbench&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;--browser&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;chromium

&lt;span class=&#34;c1&#34;&gt;# List available sanitizers&lt;/span&gt;
xssbench&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;--list-sanitizers

&lt;span class=&#34;c1&#34;&gt;# Run a subset&lt;/span&gt;
xssbench&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;--vectors&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;vectors/bleach.json&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;--sanitizers&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;noop
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;threat-model-what-safe-means&#34;&gt;Threat model (what “safe” means)&lt;a href=&#34;#threat-model-what-safe-means&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;JustHTML’s sanitizer aims to prevent script execution when you sanitize untrusted HTML and embed the result into an HTML document as markup.&lt;/p&gt;
&lt;p&gt;It does &lt;em&gt;not&lt;/em&gt; make it safe to drop the output into JavaScript string contexts, CSS contexts, URL contexts, or other non-HTML contexts — those need their own escaping/handling.&lt;/p&gt;
&lt;p&gt;If you want the details, see the JustHTML sanitization documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/EmilStenstrom/justhtml/blob/master/docs/sanitization.md&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the benchmark harness repo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/EmilStenstrom/justhtml-xss-bench&lt;/li&gt;
&lt;/ul&gt;

            </content>
        </entry>
    
        <entry>
            <title type="html">JustHTML: Addressing some questions</title>
            <link href="http://friendlybit.com/python/justhtml-faq/" rel="alternate" type="text/html" title="JustHTML: Addressing some questions" />
            <published>2025-12-19T12:00:00+01:00</published>
            <updated>2025-12-19T12:00:00+01:00</updated>
            <id>http://friendlybit.com/python/justhtml-faq/</id>
            <author>
                <name>Emil Stenström</name>
            </author>
            <summary type="text">When Simon Willison wrote about JustHTML [1] [2], suddenly everyone was interested in giving their view. After reading through (what I think is) all of...</summary>
            <content type="html" xml:base="http://friendlybit.com/python/justhtml-faq/">
                &lt;p&gt;When Simon Willison wrote about JustHTML &lt;a href=&#34;https://simonwillison.net/2025/Dec/14/justhtml/&#34;&gt;[1]&lt;/a&gt; &lt;a href=&#34;https://simonwillison.net/2025/Dec/15/porting-justhtml/&#34;&gt;[2]&lt;/a&gt;, suddenly everyone was interested in giving their view. After reading through (what I think is) all of them, I thought I&#39;d address some questions that have arisen.&lt;/p&gt;
&lt;h2 id=&#34;quotthis-is-a-copy-this-is-derived-workquot&#34;&gt;&amp;quot;This is a copy / this is derived work!&amp;quot;&lt;a href=&#34;#quotthis-is-a-copy-this-is-derived-workquot&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It is very unclear if JustHTML is a derived work. About halfway in I did tell the LLM to &amp;quot;port html5ever&amp;quot;, but I don&#39;t think that&#39;s what the LLM did. It started from the code structure of html5ever, but much of the code was trial and error against the html5lib-tests suite. In later versions I asked it to refactor much of the code, so I don&#39;t think even the structure is there any more.&lt;/p&gt;
&lt;p&gt;I asked an agent to try to find cross references between the two projects that still remain, but all it found were things that were also in the WHATWG HTML5 specification. This doesn&#39;t say it&#39;s &lt;strong&gt;not&lt;/strong&gt; derivative work, but highlights that it&#39;s far from clear.&lt;/p&gt;
&lt;p&gt;If you can find cases where the code is very similar (and not specifically required by spec), I would be happy to see it!&lt;/p&gt;
&lt;h2 id=&#34;quothe-stripped-the-authorship-laundered-the-l&#34;&gt;&amp;quot;He stripped the authorship / laundered the license!&amp;quot;&lt;a href=&#34;#quothe-stripped-the-authorship-laundered-the-l&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This wording assumes an active attempt to strip it, which is opposite of me adding the html5ever &lt;a href=&#34;https://github.com/EmilStenstrom/justhtml/blob/master/README.md#acknowledgments&#34;&gt;acknowledgement&lt;/a&gt; to the JustHTML README. Just stop that nonsense. I have nothing but love for the html5ever developers. To put that to rest, I&#39;ve decided to 1) add their copyright block to my license anyway and 2) &lt;a href=&#34;https://github.com/servo/html5ever/issues/701&#34;&gt;ask them specifically for guidance&lt;/a&gt;. I&#39;m looking forward to hearing their view.&lt;/p&gt;
&lt;p&gt;For reference, this is the current &lt;code&gt;LICENSE&lt;/code&gt; file in JustHTML:&lt;/p&gt;
&lt;pre&gt;MIT License

Copyright (c) 2025 Emil Stenström
Copyright (c) 2014-2017, The html5ever Project Developers

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &amp;quot;Software&amp;quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
...
&lt;/pre&gt;&lt;h2 id=&#34;quothe-doesnt-understand-the-codequot&#34;&gt;&amp;quot;He doesn&#39;t understand the code&amp;quot;&lt;a href=&#34;#quothe-doesnt-understand-the-codequot&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Many commenters were angry that I didn&#39;t &lt;em&gt;understand the code&lt;/em&gt;. This is such an interesting take, especially in a society where not understanding is seen as weakness. We should be certain! We should know everything! But we don&#39;t. We&#39;re all fallible and walk around trying to figure things out.&lt;/p&gt;
&lt;p&gt;In the specific case of the HTML5 specification, there are very few people—in the world—who understand it. HTML5 is an intricate web of algorithms, that interact in difficult-to-understand ways (see &lt;a href=&#34;https://htmlparser.info/&#34;&gt;htmlparser.info&lt;/a&gt; for a great guided tour). Did you know that the tokenizer and treebuilder affect each other?&lt;/p&gt;
&lt;p&gt;Luckily for us, the authors decided to &lt;s&gt;shame&lt;/s&gt; help browsers interoperate by publishing the fantastic html5lib-tests suite. It&#39;s an incredible feat of engineering, with thousands of integration tests that (almost) completely test the specification.&lt;/p&gt;
&lt;p&gt;Here is a representative example of the kind of input those tests test for:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;HTML&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;One &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;two &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;three&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; four&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; five
&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;table&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;tr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;td&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;cell&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;table&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;svg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;foreignObject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;inside svg&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;foreignObject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;svg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;small&gt;That single snippet touches multiple &amp;quot;special&amp;quot; parts of the spec: mis-nested formatting elements (the adoption agency algorithm), implied end tags, table insertion mode oddities, and foreign content integration points.&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;What&#39;s fantastic about html5lib-tests is that it gives us a way to look at our code from the outside and see if it works or not, &lt;strong&gt;without us having to understand it&lt;/strong&gt;. If this feels extreme, think of low-level code—assembler—if you will. Do you understand how it flips the transistors in your computer? I don&#39;t. And that&#39;s fine, because you have other ways to know that your code works. You don&#39;t have to go into the details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLMs are quickly becoming a new layer on top of the code we write. If we can find a way to prove that it works, we don&#39;t need to understand it. That opens up whole new possibilities!&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;quotits-not-high-quality-code-because-it-wont&#34;&gt;&amp;quot;It&#39;s not high quality code, because it won&#39;t be maintained&amp;quot;&lt;a href=&#34;#quotits-not-high-quality-code-because-it-wont&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&#39;m very sure that this code is maintainable, because I have been maintaining it for a while already. As I was approaching 100% test coverage, a new HTML5 feature was added to the test suite: &lt;code&gt;&amp;lt;selectedcontent&amp;gt;&lt;/code&gt;. This was easily supported with a couple of queries to the LLM.&lt;/p&gt;
&lt;p&gt;I am planning to maintain it. The PRs are rolling in, and I have quite a clear image of where I want to take it. I think the API I&#39;ve put on top of the parser is really attractive, with a very low learning curve. That&#39;s worth something, and it&#39;s missing from all the other libraries.&lt;/p&gt;
&lt;p&gt;The first versions of the library were very hard to maintain, even with LLM help. When I looked under the hood there were messy nested if blocks, that mirrored some of the test data exactly. The LLM was cheating! I have not seen signs of this in the later versions of the code, and especially since LLM models got better.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I&#39;m happy that my little experiment triggered so many discussions. Overall, I think (and you are free to disagree) that having this library is a big net positive for the Python community.&lt;/p&gt;

            </content>
        </entry>
    
        <entry>
            <title type="html">How I wrote JustHTML using coding agents</title>
            <link href="http://friendlybit.com/python/writing-justhtml-with-coding-agents/" rel="alternate" type="text/html" title="How I wrote JustHTML using coding agents" />
            <published>2025-12-03T12:00:00+01:00</published>
            <updated>2025-12-03T12:00:00+01:00</updated>
            <id>http://friendlybit.com/python/writing-justhtml-with-coding-agents/</id>
            <author>
                <name>Emil Stenström</name>
            </author>
            <summary type="text">I recently released JustHTML, a python-based HTML5 parser. It passes 100% of the html5lib test suite, has zero dependencies, and includes a CSS selector...</summary>
            <content type="html" xml:base="http://friendlybit.com/python/writing-justhtml-with-coding-agents/">
                &lt;p&gt;I recently released &lt;a href=&#34;https://github.com/EmilStenstrom/justhtml&#34;&gt;JustHTML&lt;/a&gt;, a python-based HTML5 parser. It passes 100% of the html5lib test suite, has zero dependencies, and includes a CSS selector query API. Writing it taught me a lot about how to work with coding agents effectively.&lt;/p&gt;
&lt;p&gt;I thought I knew HTML going into this project, but it turns out I know nothing when it comes to parsing broken HTML5 code. That&#39;s the majority of the algorithm.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://hsivonen.fi/&#34;&gt;Henri Sivonen&lt;/a&gt;, who implemented the HTML5 parser for Firefox, called the &amp;quot;&lt;a href=&#34;https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm&#34;&gt;adoption agency algorithm&lt;/a&gt;&amp;quot; (which handles misnested formatting elements) &amp;quot;the most complicated part of the tree builder&amp;quot;. It involves a &amp;quot;&lt;a href=&#34;https://html.spec.whatwg.org/multipage/parsing.html#list-of-active-formatting-elements&#34;&gt;Noah&#39;s Ark&lt;/a&gt;&amp;quot; clause (limiting identical elements to 3) and complex stack manipulation that breaks the standard stack model.&lt;/p&gt;
&lt;p&gt;I still don&#39;t know how to solve those problems. But I still have a parser that solves those problems better than the reference implementation &lt;a href=&#34;https://github.com/html5lib/html5lib-python&#34;&gt;html5lib&lt;/a&gt;. Power of AI! :)&lt;/p&gt;
&lt;h2 id=&#34;why-html5&#34;&gt;Why HTML5?&lt;a href=&#34;#why-html5&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When picking a project to build with coding agents, choosing one that already has a lot of tests is a great idea. HTML5 is extremely well-specified, with a long specification and thousands of treebuilder and tokenizer tests available in the &lt;a href=&#34;https://github.com/html5lib/html5lib-tests&#34;&gt;&lt;code&gt;html5lib-tests&lt;/code&gt;&lt;/a&gt; repository.&lt;/p&gt;
&lt;p&gt;When using coding agents autonomously, you need a way for them to understand their own progress. A complete test suite is perfect for that. The agent can run the tests, see what failed, and iterate until they pass.&lt;/p&gt;
&lt;h2 id=&#34;building-the-parser-iterations-restarts-and-per&#34;&gt;Building the parser (iterations, restarts, and performance work)&lt;a href=&#34;#building-the-parser-iterations-restarts-and-per&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Writing a full HTML5 parser is not a short one-shot problem. I have been working on this project for a couple of months on off-hours.&lt;/p&gt;
&lt;p&gt;Tooling: I used plain VS Code with Github Copilot in Agent mode. I enabled automatic approval of all commands, and then added a blacklist of commands that I always wanted to approve manually. I wrote an &lt;a href=&#34;https://github.com/EmilStenstrom/justhtml/blob/main/.github/copilot-instructions.md&#34;&gt;agent instruction&lt;/a&gt; that told it to keep working, and don&#39;t stop to ask questions. Worked well!&lt;/p&gt;
&lt;p&gt;Here is the process it took to get here:&lt;/p&gt;
&lt;h3 id=&#34;a-one-shot-html5-parser-as-a-baseline&#34;&gt;A one-shot HTML5 parser (as a baseline)&lt;a href=&#34;#a-one-shot-html5-parser-as-a-baseline&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To begin, I asked the agent to write a super-basic one-shot HTML5 parser. It didn&#39;t work very well, but it was a start.&lt;/p&gt;
&lt;h3 id=&#34;wiring-up-codehtml5lib-testscode-lt1-pass&#34;&gt;Wiring up &lt;code&gt;html5lib-tests&lt;/code&gt; (&amp;lt;1% pass rate)&lt;a href=&#34;#wiring-up-codehtml5lib-testscode-lt1-pass&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Next, I wired up the &lt;code&gt;html5lib-tests&lt;/code&gt; and saw that we had a &amp;lt;1% pass rate. Yes, those tests are hard. They are the gold standard for HTML5 parsing, containing thousands of edge cases like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;HTML&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;iterating-to-30-coverage-refactors-and-bugfixes&#34;&gt;Iterating to ~30% coverage (refactors and bugfixes)&lt;a href=&#34;#iterating-to-30-coverage-refactors-and-bugfixes&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After that, we started iterating, slowly climbing to about 30% pass rate. This involved a lot of refactoring and fixing small bugs.&lt;/p&gt;
&lt;h3 id=&#34;refactoring-into-per-tag-handlers&#34;&gt;Refactoring into per-tag handlers&lt;a href=&#34;#refactoring-into-per-tag-handlers&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Once I could see the shape of the problem, I decided I liked a handler-based structure, where each tag gets its own handler. Modular structure ftw! I asked the agent to refactor and it did.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;TagHandler&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&amp;quot;&amp;quot;&amp;quot;Base class for all tag handlers.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;handle_start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;token&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;pass&lt;/span&gt;

&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;UnifiedCommentHandler&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TagHandler&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&amp;quot;&amp;quot;&amp;quot;Handles comments in all states.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;handle_start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;token&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;insert_comment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;token&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;reaching-100-test-coverage-with-better-models&#34;&gt;Reaching 100% test coverage (with better models)&lt;a href=&#34;#reaching-100-test-coverage-with-better-models&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;From there, we continued iterating to 100% test coverage. This took a long time, and the &lt;a href=&#34;https://www.anthropic.com/news/claude-3-7-sonnet&#34;&gt;Claude Sonnet 3.7&lt;/a&gt; release was the reason we got anywhere at all.&lt;/p&gt;
&lt;h3 id=&#34;benchmarking-and-discovering-we-were-3x-slower&#34;&gt;Benchmarking and discovering we were 3x slower&lt;a href=&#34;#benchmarking-and-discovering-we-were-3x-slower&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;With correctness handled, I set up a &lt;a href=&#34;https://github.com/EmilStenstrom/justhtml/blob/master/benchmarks/performance.py&#34;&gt;benchmark&lt;/a&gt; to test how fast my parser was. I saw that I was 3x slower than &lt;code&gt;html5lib&lt;/code&gt;, which is already considered slow.&lt;/p&gt;
&lt;h3 id=&#34;rewriting-the-tokenizer-in-rust-and-barely-matchi&#34;&gt;Rewriting the tokenizer in Rust (and barely matching &lt;code&gt;html5lib&lt;/code&gt;)&lt;a href=&#34;#rewriting-the-tokenizer-in-rust-and-barely-matchi&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So I tried the obvious next move: I let an agent rewrite the tokenizer in Rust to speed things up (note: I don&#39;t know Rust). It worked, and the speed barely passed &lt;code&gt;html5lib&lt;/code&gt;. It created a whole &lt;code&gt;rust_tokenizer&lt;/code&gt; crate with 690 lines of Rust code in &lt;code&gt;lib.rs&lt;/code&gt; that I couldn&#39;t read, but it passed the tests.&lt;/p&gt;
&lt;h3 id=&#34;discovering-codehtml5evercode-fast-correct&#34;&gt;Discovering &lt;code&gt;html5ever&lt;/code&gt; (fast, correct, Rust)&lt;a href=&#34;#discovering-codehtml5evercode-fast-correct&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;While looking for alternatives, I found &lt;a href=&#34;https://github.com/servo/html5ever&#34;&gt;&lt;code&gt;html5ever&lt;/code&gt;&lt;/a&gt;, &lt;a href=&#34;https://servo.org/&#34;&gt;Servo&lt;/a&gt;&#39;s parsing engine. It is very correct and written from scratch in Rust to be fast.&lt;/p&gt;
&lt;h3 id=&#34;asking-why-build-this-at-all&#34;&gt;Asking: why build this at all?&lt;a href=&#34;#asking-why-build-this-at-all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;At that point I had the uncomfortable thought: why would the world need a slower version of &lt;code&gt;html5ever&lt;/code&gt; in partial Python? What is the meaning of it all?! I almost just deleted the whole project.&lt;/p&gt;
&lt;h3 id=&#34;pivoting-to-porting-codehtml5evercode-logic-t&#34;&gt;Pivoting to porting &lt;code&gt;html5ever&lt;/code&gt; logic to Python&lt;a href=&#34;#pivoting-to-porting-codehtml5evercode-logic-t&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Instead of quitting, I considered writing a Python interface against &lt;code&gt;html5ever&lt;/code&gt;, but decided I didn&#39;t like the hassle of a library requiring installing binary files. So I went pure Python again, but with a faster approach: what if I port the &lt;code&gt;html5ever&lt;/code&gt; logic to Python? Shouldn&#39;t that be faster than the existing Python libraries? I decided to throw all previous work away.&lt;/p&gt;
&lt;h3 id=&#34;restarting-from-scratch-again&#34;&gt;Restarting from scratch (again)&lt;a href=&#34;#restarting-from-scratch-again&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So I started over from &amp;lt;1% test coverage and iterated with the same set of tests all the way up to 100%. This time I asked it to cross reference the Rust codebase in the beginning. It was tedious work, doing the same thing over again.&lt;/p&gt;
&lt;h3 id=&#34;still-slower-than-codehtml5libcode&#34;&gt;Still slower than &lt;code&gt;html5lib&lt;/code&gt;&lt;a href=&#34;#still-slower-than-codehtml5libcode&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Unfortunately, I ran the benchmark on the new codebase and found that it was &lt;em&gt;still&lt;/em&gt; slower than &lt;code&gt;html5lib&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&#34;profiling-real-world-benchmarks-and-micro-optimi&#34;&gt;Profiling, real-world benchmarks, and micro-optimizations&lt;a href=&#34;#profiling-real-world-benchmarks-and-micro-optimi&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So I switched to performance work: I wrote some new tools for the agents to use, a simple profiler and a scraper that built a dataset of 100k popular webpages for real-world benchmarking. I managed to get the speed down below the target with Python micro-optimizations, but only when using the just-released Gemini 3 Pro (which is incredible) to run the benchmark and profiler iteratively. No other model made any progress on the benchmarks.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;_append_text_chunk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;chunk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ends_with_cr&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;False&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;chunk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ignore_lf&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ends_with_cr&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ignore_lf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;chunk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
            &lt;span class=&#34;n&#34;&gt;chunk&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;chunk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:]&lt;/span&gt;
            &lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;deleting-untested-code-coverage-as-a-scalpel&#34;&gt;Deleting untested code (coverage as a scalpel)&lt;a href=&#34;#deleting-untested-code-coverage-as-a-scalpel&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Later, on a whim I ran &lt;a href=&#34;https://coverage.readthedocs.io/&#34;&gt;&lt;code&gt;coverage&lt;/code&gt;&lt;/a&gt; on the codebase and found that large parts of the code were &amp;quot;untested&amp;quot;. But this was backwards, because I already knew that the tests were covering everything important. So lines with no test coverage could be removed! I told the agent to start removing code to reach 100% test coverage, which was an interesting reversal of roles. These removals actually sped up the code as much as the microoptimizations.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;# Before: 786 lines of treebuilder code&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# After: 453 lines of treebuilder code&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# Result: Faster and cleaner&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;fuzzing-to-find-crashes-and-harden-the-parser&#34;&gt;Fuzzing to find crashes and harden the parser&lt;a href=&#34;#fuzzing-to-find-crashes-and-harden-the-parser&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After removing code, I got worried that I had removed too much and missed corner cases. So I asked the agent to write a &lt;a href=&#34;https://github.com/EmilStenstrom/justhtml/blob/master/benchmarks/fuzz.py&#34;&gt;html5 fuzzer&lt;/a&gt; that tried really hard to generate HTML that broke the parser.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;generate_fuzzed_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&amp;quot;&amp;quot;&amp;quot;Generate a complete fuzzed HTML document.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;parts&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;random&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;random&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;parts&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fuzz_doctype&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;
    &lt;span class=&#34;c1&#34;&gt;# Generate random mix of elements&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;num_elements&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;random&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;randint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It did break the parser, and for each breaking case I asked it to fix it, and write a new test for the test suite. Passed 3 million generated webpages without any crashes, and hardened the codebase again.&lt;/p&gt;
&lt;h3 id=&#34;comparing-against-other-parsers-how-rare-100-is&#34;&gt;Comparing against other parsers (how rare 100% is)&lt;a href=&#34;#comparing-against-other-parsers-how-rare-100-is&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To sanity-check where 100% landed, I ran the &lt;code&gt;html5lib&lt;/code&gt; tests against the other parsers. I found that &lt;strong&gt;no other parser passes 90% coverage&lt;/strong&gt;, and that &lt;code&gt;lxml&lt;/code&gt;, one of the most popular Python parsers, is at &lt;strong&gt;1%&lt;/strong&gt;. The reference implementation, html5lib itself, is at 88%. Maybe this is a hard problem after all?&lt;/p&gt;
&lt;h3 id=&#34;shipping-it-as-a-library-ci-releases-selector-a&#34;&gt;Shipping it as a library (CI, releases, selector API)&lt;a href=&#34;#shipping-it-as-a-library-ci-releases-selector-a&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Finally, to make this a good library I asked the agent to set up CI, releases via GitHub, a &lt;a href=&#34;https://github.com/EmilStenstrom/justhtml/blob/master/src/justhtml/selector.py&#34;&gt;query API&lt;/a&gt;, write READMEs, and so on.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;justhtml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JustHTML&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;query&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JustHTML&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;&amp;lt;div&amp;gt;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;elements&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;query&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;div &amp;gt; p&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Decided to rename the library from turbohtml to justhtml, to not fool anyone that it&#39;s the fastest library, and instead focus on the feeling of everything just working.&lt;/p&gt;
&lt;h2 id=&#34;what-the-agent-did-vs-what-i-did&#34;&gt;What the agent did vs. what I did&lt;a href=&#34;#what-the-agent-did-vs-what-i-did&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After writing the parser, I still don&#39;t know HTML5 properly. The agent wrote it for me. I guided it when it came to API design and corrected bad decisions at the high level, but it did ALL of the gruntwork and wrote all of the code.&lt;/p&gt;
&lt;p&gt;I handled all git commits myself, reviewing code as it went in. I didn&#39;t understand all the algorithmic choices, but I understood when it didn&#39;t do the right thing.&lt;/p&gt;
&lt;p&gt;As models have gotten better, I&#39;ve seen steady increases in test coverage. &lt;strong&gt;Gemini is the smartest model from a one-shot perspective, while Claude Opus is best at iterating its way to a good solution.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;practical-tips-for-working-with-coding-agents&#34;&gt;Practical tips for working with coding agents&lt;a href=&#34;#practical-tips-for-working-with-coding-agents&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with a clear, measurable goal.&lt;/strong&gt; &amp;quot;Make the tests pass&amp;quot; is better than &amp;quot;improve the code.&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review the changes.&lt;/strong&gt; The agent writes a lot of code. Read it. You&#39;ll catch issues and learn things.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push back.&lt;/strong&gt; If something feels wrong, say so. &amp;quot;I don&#39;t like that&amp;quot; is a valid response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use version control.&lt;/strong&gt; If the agent goes in the wrong direction, you can always revert.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let it fail.&lt;/strong&gt; Running a command that fails teaches the agent something. Don&#39;t try to prevent all errors upfront.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;was-it-worth-it-and-what-quickly-meant&#34;&gt;Was it worth it (and what “quickly” meant)?&lt;a href=&#34;#was-it-worth-it-and-what-quickly-meant&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Yes. &lt;a href=&#34;https://github.com/EmilStenstrom/justhtml&#34;&gt;JustHTML&lt;/a&gt; is about 3,000 lines of Python with 8,500+ tests passing. I couldn&#39;t have written it this quickly without the agent.&lt;/p&gt;
&lt;p&gt;But &amp;quot;quickly&amp;quot; doesn&#39;t mean &amp;quot;without thinking.&amp;quot; I spent a lot of time reviewing code, making design decisions, and steering the agent in the right direction. The agent did the typing; I did the thinking.&lt;/p&gt;
&lt;p&gt;That&#39;s probably the right division of labor.&lt;/p&gt;

            </content>
        </entry>
    
        <entry>
            <title type="html">What movies on Piratebay will you like the most?</title>
            <link href="http://friendlybit.com/python/what-movies-on-piratebay-will-you-like-the-most/" rel="alternate" type="text/html" title="What movies on Piratebay will you like the most?" />
            <published>2012-01-08T22:07:29+01:00</published>
            <updated>2012-01-08T22:07:29+01:00</updated>
            <id>http://friendlybit.com/python/what-movies-on-piratebay-will-you-like-the-most/</id>
            <author>
                <name>Emil Stenström</name>
            </author>
            <summary type="text">Christmas, and the weeks thereafter, are times for coding. And I&#39;ve been playing around with piratebay and filmtipset (a Swedish movie recommendation) a...</summary>
            <content type="html" xml:base="http://friendlybit.com/python/what-movies-on-piratebay-will-you-like-the-most/">
                &lt;p&gt;Christmas, and the weeks thereafter, are times for coding. And I&#39;ve been playing around with &lt;a href=&#34;http://thepiratebay.org&#34;&gt;piratebay&lt;/a&gt; and &lt;a href=&#34;http://filmtipset.se&#34;&gt;filmtipset&lt;/a&gt; (a Swedish movie recommendation) a little bit. I just pushed it to the &lt;a href=&#34;https://github.com/EmilStenstrom/filmtipset-piratebay&#34;&gt;filmtipset-piratebay project on GitHub&lt;/a&gt;, if you want to take a look.&lt;/p&gt;
&lt;h2 id=&#34;css-for-screen-scraping&#34;&gt;CSS for screen scraping&lt;a href=&#34;#css-for-screen-scraping&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The script is using &lt;strong&gt;CSS for screen scraping&lt;/strong&gt;; something that works extremely well:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-language=&#34;PYTHON&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;lxml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;html&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;requests&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;response&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;requests&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;http://thepiratebay.org/browse/207/0/7&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;document&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;html&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;document_fromstring&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;response&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;content&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;links&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;document&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cssselect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;.detLink&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;link&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text_content&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;link&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;links&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note: You need &lt;a href=&#34;http://lxml.de/&#34;&gt;lxml&lt;/a&gt; and &lt;a href=&#34;http://docs.python-requests.org&#34;&gt;requests&lt;/a&gt; to run the above example.&lt;/p&gt;
&lt;p&gt;Saving the above snippet to a py-file and running it will give you a list of all torrents on the given url. Play around with the CSS selector to get some other data from the page.&lt;/p&gt;
&lt;h2 id=&#34;extracting-movie-titles-from-torrent-names&#34;&gt;Extracting movie titles from torrent names&lt;a href=&#34;#extracting-movie-titles-from-torrent-names&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It&#39;s surprisingly easy to convert torrent names to movie titles. Just follow this simple algorithm:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Split the torrent name into words by treating all non-alphanumeric characters as space.&lt;/li&gt;
&lt;li&gt;Loop over the remaining words, and look for a predefined set of &amp;quot;torrent endings&amp;quot;.&lt;/li&gt;
&lt;li&gt;When you find an ending, cut the name from there&lt;/li&gt;
&lt;li&gt;(Optional) Remove the year if there is one at the end of the remaining string&lt;/li&gt;
&lt;li&gt;(Optional) Remove all movies which really are bundles of movies, and not single movies. This is easily done by looking for a set of common strongs such as &amp;quot;trilogy&amp;quot; and &amp;quot;series&amp;quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Result: &amp;quot;Real.Steel.2011.720p.BluRay.x264-REFiNED&amp;quot; -&amp;gt; &amp;quot;Real Steel&amp;quot;&lt;/p&gt;
&lt;p&gt;You can find my movie title finder implementation in parse.py on GitHub.&lt;/p&gt;
&lt;h2 id=&#34;cache-all-http-request&#34;&gt;Cache all HTTP Request&lt;a href=&#34;#cache-all-http-request&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To both save time, and be nice to the services we&#39;re querying, the script caches all HTTP requests for a number of days. I do this by simply saving the returned HTML/JSON to a file, and checking the file system for that file before making a new request. Saving the HTML/JSON, and not the processed result, makes it possible to experiment with the parsing, without having to wait for new requests from the server.&lt;/p&gt;
&lt;p&gt;My caching implementation is of course also on GitHub.&lt;/p&gt;
&lt;p&gt;***&lt;/p&gt;
&lt;p&gt;All and all, this has been a fun little project, and I&#39;ve learned a lot. But I&#39;m sure we can make this even better. Feel free to send pull requests!&lt;/p&gt;

            </content>
        </entry>
    
</feed>