From the gazette to Markdown

Retrieving a single DOF note with dofjson and nota2md

Published

July 17, 2026

Abstract

This walkthrough shows how the LegalIA toolchain turns one note of the Diario Oficial de la Federación into clean Markdown. Using the gazette’s open-data service we download the summary of every note published on one day, select a single note from that list, and build its Markdown from the official HTML — three steps, two small packages: dofjson and nota2md. The third package of the repository, dof2md, is only needed when a note exists solely as scanned page images, so it appears here only in passing.

Open in Colab

1 Three packages, one pipeline

The LegalIA repository ships three Python packages, now published on PyPI, that cover complementary stages of the same pipeline:

dofjson

A thin client for the DOF’s open-data JSON service. It answers questions like which notes were published on this day? and what is the content of this note?

nota2md

Given the identifier of a note, produces its Markdown, choosing the best available source: official HTML when it exists, OCR of the scanned pages otherwise.

dof2md

Operates at the coarsest grain: downloads a complete edition of the gazette as PDF and converts it — OCR included — to Markdown.

It is worth stating upfront which of the three this tour actually requires. For a modern note, the DOF publishes its full text as HTML, and the path from identifier to Markdown involves only dofjson (to talk to the service) and nota2md (to convert the HTML). dof2md never enters the picture; it is the heavy artillery that nota2md borrows when a note predates the HTML era and survives only as scanned images. We therefore explain it only superficially, at the end.

The packages are available on PyPI. If you are running this notebook outside the repository — on Google Colab, for instance — uncomment the following cell:

Code
# %pip install dofjson nota2md

2 Step 1: every note of one day

The gazette’s open-data service is organized around dates and identifiers. dofjson.client.get_notas takes a date and returns the day’s summary: one list of notes per edition (morning, evening, extraordinary), each note a dictionary of metadata. We ask for July 15, 2026:

Code
import datetime as dt

from dofjson import client

day = dt.date(2026, 7, 15)
notas = client.get_notas(day)
{k: len(v) for k, v in notas.items() if isinstance(v, list)}
{'NotasExtraordinarias': 0,
 'imagenesDiario': 1,
 'NotasVespertinas': 0,
 'NotasMatutinas': 69,
 'audioSumario': 1}

The morning edition claims 69 notes, but the raw listing includes stub entries without a title that the DOF uses internally. The client provides a helper that drops them and leaves a clean index of the day:

Code
notas = client.quita_notas_sin_titulo(notas)
manana = notas["NotasMatutinas"]
len(manana)
57

Fifty-seven real notes. Each entry carries, among other fields, the note’s identifier (codNota), its title, the section of the gazette it appeared in and whether the service holds its content as HTML (existeHtml). A glance at the first few titles gives the flavor of an ordinary day in the gazette:

Code
import pandas as pd

pd.DataFrame(
    [
        {
            "codNota": n["codNota"],
            "title": n["titulo"][:80] + ("…" if len(n["titulo"]) > 80 else ""),
            "HTML": n["existeHtml"],
        }
        for n in manana[:8]
    ]
)
codNota title HTML
0 5793639 Acuerdo por el que se establecen acciones para... S
1 5793641 Norma Oficial Mexicana NOM-042-NUCL-2026, Cate... S
2 5793643 Circular por la que se comunica a las dependen... S
3 5793645 Acuerdo número 07/07/26 por el que se establec... S
4 5793647 Convenio Modificatorio al Convenio Específico ... S
5 5793648 Convenio Modificatorio al Convenio Específico ... S
6 5793650 Aviso de medición y deslinde del predio de pre... S
7 5793651 Aviso de medición y deslinde del predio de pre... S

3 Step 2: selecting one note

Any note can now be addressed by its codNota. For this example we take the agreement that sets the school calendars for the 2026–2027 school year — a document that touches practically every household in the country:

Code
nota = next(n for n in manana if n["titulo"].startswith("Acuerdo número 07/07/26"))
cod_nota = nota["codNota"]
print(f"codNota   : {cod_nota}")
print(f"section   : {nota['codSeccion']}")
print(f"page      : {nota['pagina']}")
print(f"has HTML  : {nota['existeHtml']}")
print(f"title     : {nota['titulo'][:120]}…")
codNota   : 5793645
section   : UNICA
page      : 22
has HTML  : S
title     : Acuerdo número 07/07/26 por el que se establecen los calendarios escolares para el ciclo lectivo 2026-2027, aplicables e…

Because existeHtml is "S", the service can hand us the note’s full text directly. dofjson.client.get_nota fetches that detail record; the HTML lives in its cadenaContenido field. It is official, but it is not pretty:

Code
detalle = client.get_nota(cod_nota)["Nota"]
print(detalle["cadenaContenido"][:400])
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <HTML> <head>   <title>ACUERDO número 07/07/26 por el que se establecen los calendarios escolares para el ciclo lectivo 2026-2027, aplicables en toda la República para la educación preescolar, primaria, secundaria, normal y demás para la formación de maestras y maestros de edu</title>                        

4 Step 3: from HTML to Markdown

This is where nota2md takes over. Its single entry point, build_nota_markdown, receives the codNota, an output directory and a source argument that selects the conversion path: "html" (what we use here), "image" or "pdf" for the OCR routes, or "auto" to let the package decide — it prefers HTML whenever the note has it. The function fetches whatever it needs through dofjson on its own; passing the day’s index we already downloaded merely saves a request:

Code
from pathlib import Path

from nota2md.builder import build_nota_markdown

md_path = build_nota_markdown(cod_nota, Path("output"), source="html", notas_del_dia=notas)
md_path
PosixPath('output/nota-5793645.md')

The result is a single self-contained Markdown file, with the title as a heading and the body cleaned of the gazette’s presentational markup:

Code
print(md_path.read_text(encoding="utf-8")[:1200])
# ACUERDO número 07/07/26 por el que se establecen los calendarios escolares para el ciclo lectivo 2026-2027, aplicables en toda la República para la educación preescolar, primaria, secundaria, normal y demás para la formación de maestras y maestros de educación básica.

## Al margen un sello con el Escudo Nacional, que dice: Estados Unidos Mexicanos.- Secretaría de Educación Pública.

MARIO DELGADO CARRILLO, Secretario de Educación Pública, con fundamento en los artículos 38 de la Ley Orgánica de la Administración Pública Federal; 87, primer párrafo, 88, 89, primer párrafo y 113, fracciones III y XXII de la Ley General de Educación; 5, fracción XIX del Reglamento Interior de la Secretaría de Educación Pública, y

## CONSIDERANDO

Que el artículo 3o. de la Constitución Política de los Estados Unidos Mexicanos establece que la educación inicial, preescolar, primaria y secundaria conforman la educación básica, misma que es obligatoria;

Que la Ley General de Educación (LGE) dispone que corresponde de manera exclusiva a la Secretaría de Educación Pública (SEP), establecer el Calendario Escolar aplicable a toda la República para cada ciclo lectivo de la educación preescolar, la primari

The same round trip is available from the command line, which is often more convenient for batch work — the two commands below reproduce this whole walkthrough:

dofjson 2026-07-15 --outdir output     # -> output/15072026-notas.json
nota2md 5793645 --outdir output        # -> output/nota-5793645.md

5 When there is no HTML: the scanned decades

The HTML path covers the recent decades, but the archive reaches back to 1917 — and, as the exploration of the titles showed, most of that history predates the electronic gazette. For those notes existeHtml is "N" and the service offers only the scanned images of the printed pages. nota2md handles this case with the same entry point: called with source="image" (or "pdf"), it downloads the note’s page images, determines which pages the note spans, and runs them through an OCR pipeline before cutting out the note’s own text. That OCR machinery is not its own — it is borrowed, lazily, from dof2md.

6 And dof2md?

As promised, only superficially: dof2md downloads a complete edition of the DOF as PDF and converts it to Markdown with a document-analysis OCR pipeline (MinerU), including the recovery of tables. It is the right tool when the unit of interest is the whole day rather than the individual note, and it is the dependency that gives nota2md its OCR route for the scanned decades. For the journey of this page — one modern note, from identifier to Markdown — it was, as anticipated, never needed.

Methodological note

This page is a Jupyter notebook executed against the DOF’s open-data service (https://sidof.segob.gob.mx) at render time; the outputs shown correspond to the note with codNota 5793645, published in the morning edition of July 15, 2026. The packages are documented in their respective READMEs in the repository; none of them requires the raw local archive used by the explorations.

The three packages this page describes — dofjson, nota2md and dof2md — were written by the LegalIA team together with Claude, Anthropic’s coding assistant, through Claude Code: the assistant helped design and implement the open-data client, the HTML-to-Markdown conversion and the scanned-page OCR pipeline, along with their test suites. As with the rest of the project, the authors reviewed and validated every change before it was committed.