<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: George K.</title>
    <description>The latest articles on DEV Community by George K. (@george_k_09db0948571db2c).</description>
    <link>https://dev.to/george_k_09db0948571db2c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1748455%2Fcbbac19b-72b8-4aab-a5c9-4895de56a14e.jpg</url>
      <title>DEV Community: George K.</title>
      <link>https://dev.to/george_k_09db0948571db2c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/george_k_09db0948571db2c"/>
    <language>en</language>
    <item>
      <title>You Don't Need to Scrape BORME: Spain's Company Registry Has an Open-Data API</title>
      <dc:creator>George K.</dc:creator>
      <pubDate>Thu, 09 Jul 2026 19:01:09 +0000</pubDate>
      <link>https://dev.to/george_k_09db0948571db2c/you-dont-need-to-scrape-borme-spains-company-registry-has-an-open-data-api-9n8</link>
      <guid>https://dev.to/george_k_09db0948571db2c/you-dont-need-to-scrape-borme-spains-company-registry-has-an-open-data-api-9n8</guid>
      <description>&lt;p&gt;Spain publishes every commercial-registry act — incorporations, director changes, insolvencies — in an official gazette called &lt;strong&gt;BORME&lt;/strong&gt; (Boletín Oficial del Registro Mercantil). Most tools we found scrape its PDFs. It turns out you don't need to: since 2009 there is an official open-data API on &lt;code&gt;boe.es&lt;/code&gt; that almost nobody seems to use. We just finished backfilling all of it — &lt;strong&gt;9.5 million company events, 2009 → today&lt;/strong&gt; — and these are the notes we wish we'd had at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API nobody talks about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Daily summary: &lt;code&gt;https://www.boe.es/datosabiertos/api/borme/sumario/{YYYYMMDD}&lt;/code&gt; with &lt;code&gt;Accept: application/json&lt;/code&gt; → lists ~50 provincial XML files per business day, each with a direct &lt;code&gt;url_xml&lt;/code&gt;. No auth, no keys, no rate-limit headers.&lt;/li&gt;
&lt;li&gt;The XML is clean, structured, and 1:1 with the PDFs. If you are parsing BORME PDFs today: stop. The XML makes an entire class of problems (page breaks mid-record!) disappear.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Six traps that cost us real time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The 404 lies about its content type.&lt;/strong&gt; Non-publication days (weekends, holidays) return 404 — with an XML body, even when you asked for &lt;code&gt;application/json&lt;/code&gt;. Check the status code before parsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing documents return HTTP 200.&lt;/strong&gt; A province that didn't publish gives you &lt;code&gt;200 OK&lt;/code&gt; with &lt;code&gt;&amp;lt;error&amp;gt;&amp;lt;descripcion&amp;gt;No se encontró el documento original&amp;lt;/descripcion&amp;gt;&amp;lt;/error&amp;gt;&lt;/code&gt; inside. If you only check status codes, you will cache garbage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trailing dot is a minefield.&lt;/strong&gt; &lt;code&gt;312077 - DAMI DELUSION S.L.&lt;/code&gt; — is the final dot a sentence terminator or part of "S.L."? A naive &lt;code&gt;rstrip('.')&lt;/code&gt; corrupts thousands of company names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One paragraph = a chain of acts.&lt;/strong&gt; A single entry routinely packs 3–6 acts: &lt;code&gt;Ceses/Dimisiones. Adm. Unico: X. Nombramientos. Liquidador: X. Disolución. Extinción. Datos registrales…&lt;/code&gt; You need an ordered-vocabulary scanner, not a per-line regex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provincial dialects are real.&lt;/strong&gt; Barcelona SHOUTS IN UPPERCASE, cooperatives have a &lt;code&gt;Consejo Rector&lt;/code&gt; instead of directors, associations a &lt;code&gt;Junta Directiva&lt;/code&gt;, and province names carry accents that never match what users type (&lt;code&gt;ARABA/ÁLAVA&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates arrive in at least five formats&lt;/strong&gt; (&lt;code&gt;26.06.26&lt;/code&gt;, &lt;code&gt;4.05.09&lt;/code&gt;, &lt;code&gt;15/11/16&lt;/code&gt;, &lt;code&gt;2-10-2009&lt;/code&gt;, &lt;code&gt;21 DE FEBRERO DE 2006&lt;/code&gt;) — and pre-euro records still quote &lt;code&gt;Ptas&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What worked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A deterministic parser over a closed vocabulary — no LLM in the hot path.&lt;/strong&gt; The gazette's shorthand is a regular language over ~50 act-type labels and ~80 role labels. A scanner plus per-act regexes parses the entire 17-year corpus in minutes and is reproducible byte-for-byte. (LLMs were still useful — for drafting test fixtures.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure what you &lt;em&gt;didn't&lt;/em&gt; parse.&lt;/strong&gt; The single best QA metric we found: consumed characters ÷ total characters, per file. Anything unconsumed ships in the output verbatim as &lt;code&gt;unparsed_spans&lt;/code&gt;, so format drift shows up as a number going down — not as silent data loss. Long-run average: &lt;strong&gt;99.9%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Politeness scales fine.&lt;/strong&gt; ≤1 request/second with an identifying User-Agent and retries with backoff. The full 2009→today backfill is ~160k requests — about two days of wall-clock, fully resumable (idempotent by file, month checkpoints).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Capacity-planning numbers: ~2,000–2,500 events per business day currently; ~6 GB of raw XML for the full history; 9.5M parsed events across 3.2M companies in Postgres.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The subtle part: three format epochs
&lt;/h2&gt;

&lt;p&gt;The XML layout differs subtly between 2009, 2015 and 2026. Two examples we hit in production: a handful of files carry a number-less &lt;code&gt;articulo&lt;/code&gt; holding an erratum note (&lt;code&gt;-&lt;/code&gt; or &lt;code&gt;- COMPANY NAME&lt;/code&gt; before a "Fe de erratas" / "Corrección de errores" paragraph) — reject the pair, not the file; and one 2024 day shipped registral coordinates with the &lt;code&gt;T / F / S&lt;/code&gt; markers missing entirely. A strict parser that refuses to guess, plus a metric for what it skipped, catches every one of these as a number — 17 years of gazette yielded only a handful of such cases. The format is remarkably stable; BOE deserves more credit for it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;If you'd rather not build all this yourself&lt;/strong&gt;: we packaged the parsed feed as a typed English JSON dataset — 52 act-type enums, officer roles with names, registry coordinates — with a &lt;a href="https://api.bormeapi.com/docs" rel="noopener noreferrer"&gt;REST API&lt;/a&gt;, webhooks and an &lt;a href="https://github.com/george-kozlitin/borme-mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; for AI agents. The &lt;a href="https://apify.com/zon4a/spain-company-registry-borme-events" rel="noopener noreferrer"&gt;Apify actor&lt;/a&gt; is $1 per 1,000 events.&lt;/p&gt;

&lt;p&gt;Everything above applies if you build your own instead — the source is official, open, and genuinely pleasant to work with once you know the traps.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>ai</category>
      <category>opendata</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
