csf-scraper
Port a Python de phpcfdi/csf-scraper: obtiene los datos fiscales actuales de una persona moral o física dado su RFC y su idCIF (SAT, Constancia de Situación Fiscal).
Requisitos
- Python >= 3.11
pdftotext(paquete del sistemapoppler-utils), necesario solo para leer la CSF desde un archivo PDF (obtain_from_pdf_path).- Dependencias:
httpx,beautifulsoup4,lxml,pydantic,llama-index-core. Los clientes de cada proveedor LLM son extras opcionales (llm-gemini).
Instalación
Desde PyPI (solo lectura de CSF por RFC+idCIF y por PDF):
pip install csf-scraper-python
Para leer CSF escaneadas (JPEG/PNG) con Gemini, agrega el extra:
pip install "csf-scraper-python[llm-gemini]"
Desde el código fuente:
pip install .
Ejemplo de uso
Todo el flujo es asíncrono: Scraper envuelve un httpx.AsyncClient y todos
los métodos de lectura son corrutinas, así que se consumen con await.
import asyncio
from csf_scraper import Scraper
async def main() -> None:
# El scraper es un context manager asíncrono: cierra el AsyncClient al salir
async with Scraper.create() as scraper:
# RFC + idCIF directamente (se validan con los value objects Rfc e IdCif)
person = await scraper.obtain_from_rfc_and_cif("DIM8701081LA", "1904014102123")
# También puedes obtener los datos directamente de un archivo local PDF
# (requiere pdftotext / poppler-utils instalado)
person = await scraper.obtain_from_pdf_path("ruta/al/csf.pdf")
# Acceso a los datos de la persona (moral o física) por atributos
if person.is_fisica():
print(person.nombre)
elif person.is_moral():
print(person.razon_social)
# Regímenes: lista de Regimen, cada uno con su id ya calculado
for regimen in person.regimenes:
print(regimen.regimen, regimen.regimen_id, regimen.fecha_alta)
# Los títulos del HTML que no tienen un campo propio se guardan aquí
print(person.extra_data)
# Diccionario de datos (envoltura de model_dump) o JSON (ISO-8601 en fechas)
data = person.to_dict()
json_text = person.model_dump_json()
if __name__ == "__main__":
asyncio.run(main())
Para leer una CSF escaneada (JPEG/PNG) se inyecta el motor de lectura de imágenes.
Requiere el extra llm-gemini:
import asyncio
from csf_scraper import CsfImageExtractor, GeminiImageReader, Scraper
async def main() -> None:
reader = GeminiImageReader(api_key="…", model_name="…")
async with Scraper.create(image_extractor=CsfImageExtractor(reader)) as scraper:
person = await scraper.obtain_from_image_path("ruta/al/csf.png")
print(person.to_dict())
if __name__ == "__main__":
asyncio.run(main())
Salida de person.to_dict() para una persona moral (fechas como datetime):
{
"rfc": "DIM8701081LA",
"id_cif": "12345678",
"fecha_inicio_operaciones": datetime(2019, 2, 21),
# …
"regimenes": [
{
"regimen": "Régimen General de Ley Personas Morales",
"fecha_alta": datetime(2019, 2, 21),
"regimen_id": "601",
},
],
"extra_data": {"localidad": ""},
"razon_social": "Mi razón social",
"regimen_de_capital": "SA DE CV",
"fecha_constitucion": datetime(2019, 2, 21),
}
Modelos
Personaes la base dePersonaFisicayPersonaMoral; sus campos se exponen como atributos ensnake_case(rfc,id_cif,colonia, …).rfc: Rfc | Noneeid_cif: IdCif | Noneusan los value objects que validan el formato tanto al construir como al asignar (pydantic normaliza las cadenas automáticamente).is_fisica()eis_moral()permiten distinguir el tipo de persona sin inspeccionar el RFC por fuera de la librería.Regimense construye con el texto del régimen y, opcionalmente, la fecha de alta:Regimen(regimen="Régimen Simplificado de Confianza", fecha_alta="01-02-2001"). El atributoregimen_idse calcula a partir del texto yfecha_altaacepta cadenasdd-mm-yyyyodatetime.extra_datareúne los títulos del HTML que no corresponden a un campo del modelo. La correspondencia título → campo está enPersona.field_titles.to_dict()es una envoltura demodel_dump();model_dump_json()serializa las fechas en ISO-8601.
Value objects
| Value object | Regla |
|---|---|
Rfc |
3–4 letras (incluye Ñ y &) + 6 dígitos + 3 alfanuméricos; normaliza espacios/mayúsculas. is_fisica() (13) / is_moral() (12) |
IdCif |
solo dígitos, longitud 8–13 |
CsfImage |
imagen JPEG o PNG validada por magic bytes; expone as_bytes(), as_base64() y get_mime_type() |
Los value objects son subclases de str, por lo que se comportan como cadenas,
se serializan como texto plano y lanzan la excepción personalizada
correspondiente si el formato no es válido:
from csf_scraper import InvalidIdCifFormatException, InvalidRfcFormatException, Rfc
rfc = Rfc(" dim8701081la ") # → "DIM8701081LA"
rfc.is_moral() # → True
try:
Rfc("no-es-un-rfc")
except InvalidRfcFormatException as exception:
print(exception)
Lectura de CSF
La librería separa dos responsabilidades que es fácil confundir:
- Origen: de dónde salen los datos (una ruta de archivo). Contrato público
RfcCifExtractorPort.extract(path) -> RfcCifData. - Motor: cómo se lee una imagen ya cargada en memoria. Contrato
ImageReaderPort.read(image) -> RfcCifData.
Scraper solo habla con RfcCifExtractorPort, de modo que no distingue un PDF
(CsfPdfExtractor) de una imagen: cualquier extractor que cumpla el contrato
sirve para ambas rutas.
flowchart LR
S[Scraper] -->|obtain_from_pdf_path| P[CsfPdfExtractor]
S -->|obtain_from_image_path| B[CsfImageExtractor]
B -->|ImageReaderPort| G[GeminiImageReader]
G -->|hereda| L[LlmImageReaderService]
B -.-> T[OCR: OpenCV + Tesseract]
P --> R[RfcCifData]
B --> R
Contrato de los extractores (RfcCifExtractorPort)
class RfcCifExtractorPort(ABC):
@abstractmethod
async def extract(self, path: str) -> RfcCifData: ...
- El contrato es asíncrono:
extractes una corrutina para que cada implementación pueda delegar E/S (procesos externos, disco) sin bloquear el bucle de eventos;Scraperla consume conawait. - La ruta viaja en el método, no en el constructor: los extractores son sin estado y reutilizables entre archivos.
- El constructor queda reservado para configuración del motor (API key, modelo, binario alternativo…).
- Devuelve siempre
RfcCifDataconrfc: Rfceid_cif: IdCifya validados. - Los adaptadores de la librería reportan sus fallos con
CsfScraperException; las dependencias inyectadas pueden propagar sus propias excepciones.
Implementaciones: CsfPdfExtractor (texto de pdftotext + PdfCsfTextParser)
y CsfImageExtractor (puente entre una ruta y cualquier ImageReaderPort).
Contrato de los motores de imagen (ImageReaderPort)
class ImageReaderPort(ABC):
@abstractmethod
async def read(self, image: CsfImage) -> RfcCifData: ...
- Recibe una imagen en memoria; nunca abre archivos ni conoce rutas.
- La validación de formato (JPEG/PNG) ocurre al construir
CsfImage, así que el motor puede asumir que la imagen es válida.
Sub-contrato de los adaptadores basados en LLM
Los adaptadores de Gemini, DeepSeek, etc. heredan de LlmImageReaderService e
implementan ImageReaderPort. La base aporta el comportamiento común; el
adaptador crea su cliente de LlamaIndex y expone read() delegando en el método
plantilla.
class LlmImageReaderService(ABC):
prompt: ClassVar[str] = CSF_PROMPT
provider: str # propiedad abstracta
model: str
async def _run_program(
self, image: CsfImage, llm_provider: LLM
) -> RfcCifData: # plantilla
...
async def _image_to_schema(
self,
image_documents: Sequence[ImageBlock],
retry_prompt: str,
llm_provider: LLM,
) -> RfcCifSchema:
...
LlmImageReaderService solo tiene cuatro responsabilidades:
- Armado del prompt:
CSF_PROMPTmás la realimentación del intento previo. - Armado del mensaje con la imagen: un único
ImageBlockcon los bytes y el MIME type de laCsfImage. - Reintentos: cuando
RfcoIdCifrechazan el formato de negocio, se vuelve a interrogar al modelo pasándole el error como contexto (hastamax_retries, por omisiónMAX_RETRIES = 3). Los fallos de transporte no se reintentan: se envuelven enImageReadingException. - Parseo al DTO: un
RfcCifSchemaya tipado se mapea aRfcCifData.
| Responsabilidad | Dónde vive |
|---|---|
| Prompt canónico | constante CSF_PROMPT (+ RETRY_PROMPT) |
| Lectura del archivo | CsfImage.new_from_path, invocado por CsfImageExtractor |
| Forma de la respuesta | RfcCifSchema (Field con description para el modelo) |
| Transporte al proveedor | el adaptador crea llm_provider y lo pasa a _run_program |
| Validación de RFC/idCIF | value objects Rfc / IdCif |
| Mapeo de errores | ImageReadingException y derivadas |
Nota: la imagen se lee con asyncio.to_thread dentro de CsfImageExtractor y
pdftotext se ejecuta con asyncio.to_thread dentro de PdfToText, porque
ambas operaciones son bloqueantes; el resto del flujo (HTTP, LLM) usa APIs
asíncronas.
RfcCifSchema es un BaseModel cuyos Field llevan description; esos textos
son los que el modelo multimodal usa para saber qué aspecto tienen el RFC y el
idCIF impresos en la Constancia de Situación Fiscal. Pydantic valida la forma
(campos presentes y de tipo str) y los value objects validan el formato de
negocio, que es justo lo que dispara los reintentos.
El adaptador crea su cliente de LlamaIndex (GoogleGenAI) y lo pasa al método
plantilla; _image_to_schema construye y ejecuta el
MultiModalLLMCompletionProgram:
class GeminiImageReader(LlmImageReaderService, ImageReaderPort):
def __init__(
self, api_key: str, model_name: str, *, max_retries: int = MAX_RETRIES
) -> None:
super().__init__(model=model_name, max_retries=max_retries)
self._llm_provider = GoogleGenAI(api_key=api_key, model=model_name)
@property
def provider(self) -> str:
return "gemini"
async def read(self, image: CsfImage) -> RfcCifSchema:
return await self._run_program(
image=image, llm_provider=self._llm_provider
)
El extra llm-gemini instala llama-index-llms-google-genai; el resto de la
librería no depende de ningún proveedor.
Por omisión Scraper solo sabe leer PDFs; el extractor de imágenes es
opcional y se inyecta con Scraper.create(image_extractor=…). Si se invoca
obtain_from_image_path sin extractor configurado se lanza
ImageExtractorNotConfiguredException.
Excepciones
Todas heredan de CsfScraperException:
CifDownloadException(la petición HTTP falló; conservaurl; la excepción original queda encadenada en__cause__)CifNotFoundException(el SAT no devolvió información del CIF)RfcFromPdfNotFoundException/CifFromPdfNotFoundException(el PDF no contiene RFC / idCIF)EmptyPdfContentException/PdfToTextConvertException(problemas conpdftotext)RfcFromImageNotFoundException/CifFromImageNotFoundException(la imagen no contiene RFC / idCIF)ImageExtractorNotConfiguredException(se pidió leer una imagen sin inyectar un extractor)InvalidImageException/InvalidImageFormatException(imagen vacía, ilegible o con formato distinto de JPEG/PNG)ImageReadingException(fallo del motor de lectura de imágenes; conservaproviderymodel)InvalidRfcFormatException/InvalidIdCifFormatException(formato inválido en los value objectsRfc/IdCif)
Desarrollo
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/ruff check .
.venv/bin/mypy
.venv/bin/pytest
Los tests usan fixtures de tests/_files/ y mockean HTTP con respx:
ningún test contacta al SAT.
Documentación de la migración
docs/migration-map.md: correspondencia archivo a archivo PHP → Python.docs/differences.md: diferencias de comportamiento deliberadas.
Licencia
MIT — ver LICENSE (heredado del original) y NOTICE.md.
Metadata
Release files for csf-scraper-python 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| csf_scraper_python-0.1.1.tar.gz | 76.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| csf_scraper_python-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 118.1 kB
Release files / csf_scraper_python-0.1.1.tar.gz
| Download URL | csf_scraper_python-0.1.1.tar.gz |
|---|---|
| Size | 76.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cbb2db93bdc25b4a8a23a99d484efb815363ce2c658a6e6c7362ec8c6fe5506d
|
|
BLAKE2b-256 checksum How to use checksums |
15265fc0d8d3987bbc286bc9018d37c0604568d91a1903b80fa5cb96b7550de9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / csf_scraper_python-0.1.1-py3-none-any.whl
| Download URL | csf_scraper_python-0.1.1-py3-none-any.whl |
|---|---|
| Size | 41.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
38bc7ac57923a836e211adbf238f1e96baebd3f3bab60bed0248440b966b20b6
|
|
BLAKE2b-256 checksum How to use checksums |
61a8120a8fc59a621d25c95af68d56f0cf77b0b4d70b51504a654890c01f7f88
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for csf-scraper-python 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.