nordabiz/scripts/parse_krs_pdf.py
Maciej Pienczyn 3f9273cff6 feat: Add company logos to search results, hide events section
- Add company logo display in search results cards
- Make logo clickable (links to company profile)
- Temporarily hide "Aktualności i wydarzenia" section on company profiles
- Add scripts for KRS PDF download/parsing and CEIDG API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 15:32:53 +01:00

280 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""
KRS PDF Parser - wyciąga dane zarządu i wspólników z odpisu KRS
Parsuje odpisy pełne pobrane z ekrs.ms.gov.pl i wyciąga:
- Członków zarządu (funkcja, imię, nazwisko, PESEL)
- Wspólników (imię, nazwisko, PESEL, udziały)
- Prokurentów
Usage:
python scripts/parse_krs_pdf.py --file /path/to/odpis.pdf
python scripts/parse_krs_pdf.py --dir /path/to/pdfs/
"""
import re
import json
import argparse
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import List, Optional, Dict, Any
try:
import pdfplumber
except ImportError:
print("Wymagana biblioteka pdfplumber. Zainstaluj: pip install pdfplumber")
exit(1)
@dataclass
class Person:
"""Osoba powiązana z firmą"""
nazwisko: str
imiona: str
pesel: Optional[str] = None
rola: str = "" # PREZES ZARZĄDU, CZŁONEK ZARZĄDU, WSPÓLNIK, PROKURENT
udzialy: Optional[str] = None # dla wspólników
def full_name(self) -> str:
return f"{self.imiona} {self.nazwisko}"
@dataclass
class KRSData:
"""Dane wyciągnięte z odpisu KRS"""
krs: str
nazwa: str
nip: Optional[str] = None
regon: Optional[str] = None
zarzad: List[Person] = None
wspolnicy: List[Person] = None
prokurenci: List[Person] = None
zrodlo: str = "ekrs.ms.gov.pl"
def __post_init__(self):
if self.zarzad is None:
self.zarzad = []
if self.wspolnicy is None:
self.wspolnicy = []
if self.prokurenci is None:
self.prokurenci = []
def to_dict(self) -> Dict[str, Any]:
return {
'krs': self.krs,
'nazwa': self.nazwa,
'nip': self.nip,
'regon': self.regon,
'zarzad': [asdict(p) for p in self.zarzad],
'wspolnicy': [asdict(p) for p in self.wspolnicy],
'prokurenci': [asdict(p) for p in self.prokurenci],
'zrodlo': self.zrodlo
}
def extract_text_from_pdf(pdf_path: str) -> str:
"""Wyciąga tekst z PDF"""
with pdfplumber.open(pdf_path) as pdf:
text = ""
for page in pdf.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
return text
def parse_person_block(lines: List[str], start_idx: int) -> Optional[Person]:
"""
Parsuje blok danych osoby z linii PDF
Format w PDF:
1.Nazwisko / Nazwa lub firma 1 - NAZWISKO
2.Imiona 1 - IMIĘ DRUGIE_IMIĘ
3.Numer PESEL/REGON lub data 1 - 12345678901, ------
"""
person = Person(nazwisko="", imiona="")
found_nazwisko = False
found_imiona = False
found_pesel = False
for i in range(start_idx, min(start_idx + 10, len(lines))):
line = lines[i].strip()
# Wykryj początek następnej osoby - przestań parsować
if i > start_idx and ('1.Nazwisko' in line or 'Nazwisko / Nazwa' in line):
# Początek nowej osoby - koniec bloku
break
# Nazwisko (tylko pierwsze znalezione)
if not found_nazwisko and 'Nazwisko' in line and ' - ' in line:
match = re.search(r' - ([A-ZĄĆĘŁŃÓŚŹŻ\-]+)$', line)
if match:
person.nazwisko = match.group(1)
found_nazwisko = True
# Imiona (tylko pierwsze znalezione)
if not found_imiona and 'Imiona' in line and ' - ' in line:
match = re.search(r' - ([A-ZĄĆĘŁŃÓŚŹŻ ]+)$', line)
if match:
person.imiona = match.group(1).strip()
found_imiona = True
# PESEL (tylko pierwsze znalezione)
if not found_pesel and 'PESEL' in line and ' - ' in line:
match = re.search(r' - (\d{11})', line)
if match:
person.pesel = match.group(1)
found_pesel = True
# Funkcja (dla zarządu)
if 'Funkcja' in line and ' - ' in line:
match = re.search(r' - ([A-ZĄĆĘŁŃÓŚŹŻ ]+)$', line)
if match:
person.rola = match.group(1).strip()
if person.nazwisko and person.imiona:
return person
return None
def parse_krs_pdf(pdf_path: str) -> KRSData:
"""
Parsuje odpis KRS i wyciąga dane
"""
text = extract_text_from_pdf(pdf_path)
lines = text.split('\n')
# Extract basic info
krs_match = re.search(r'Numer KRS:\s*(\d{10})', text)
krs = krs_match.group(1) if krs_match else ""
# Find company name - format: "3.Firma, pod którą spółka działa 1 - NAZWA FIRMY"
nazwa = ""
nazwa_match = re.search(r'3\.Firma,?\s+pod którą spółka działa\s+\d+\s+-\s+([^\n]+)', text)
if nazwa_match:
nazwa = nazwa_match.group(1).strip()
# NIP and REGON - format: "REGON: 369796786, NIP: 5862329746"
nip_match = re.search(r'NIP:\s*(\d{10})', text)
nip = nip_match.group(1) if nip_match else None
regon_match = re.search(r'REGON:\s*(\d{9,14})', text)
regon = regon_match.group(1) if regon_match else None
data = KRSData(krs=krs, nazwa=nazwa, nip=nip, regon=regon)
# Parse sections
in_zarzad = False
in_wspolnicy = False
in_prokurenci = False
for i, line in enumerate(lines):
line_stripped = line.strip()
# Detect sections
if 'ZARZĄD' in line_stripped.upper() and 'Nazwa organu' in line_stripped:
in_zarzad = True
in_wspolnicy = False
in_prokurenci = False
continue
# Wspólnicy - szukaj "Dane wspólników" żeby nie łapać "Wspólnik może mieć:"
if 'Dane wspólników' in line_stripped or 'WSPÓLNICY' in line_stripped.upper():
in_wspolnicy = True
in_zarzad = False
in_prokurenci = False
if 'PROKURENCI' in line_stripped.upper() or 'Prokurent' in line_stripped:
in_prokurenci = True
in_zarzad = False
in_wspolnicy = False
# Parse person data when we find "Nazwisko"
if '1.Nazwisko' in line_stripped or 'Nazwisko / Nazwa' in line_stripped:
person = parse_person_block(lines, i)
if person:
if in_zarzad:
# Look for function in nearby lines
for j in range(i, min(i + 8, len(lines))):
if 'Funkcja' in lines[j] and ' - ' in lines[j]:
func_match = re.search(r' - ([A-ZĄĆĘŁŃÓŚŹŻ ]+)$', lines[j])
if func_match:
person.rola = func_match.group(1).strip()
break
if not person.rola:
person.rola = "CZŁONEK ZARZĄDU"
data.zarzad.append(person)
elif in_wspolnicy:
person.rola = "WSPÓLNIK"
# Look for share info
for j in range(i, min(i + 10, len(lines))):
if 'udziałów' in lines[j].lower() or 'udział' in lines[j].lower():
data.wspolnicy.append(person)
break
else:
data.wspolnicy.append(person)
elif in_prokurenci:
person.rola = "PROKURENT"
data.prokurenci.append(person)
return data
def main():
parser = argparse.ArgumentParser(description="Parse KRS PDF files")
parser.add_argument("--file", type=str, help="Single PDF file to parse")
parser.add_argument("--dir", type=str, help="Directory with PDF files")
parser.add_argument("--output", type=str, help="Output JSON file")
args = parser.parse_args()
results = []
if args.file:
print(f"Parsing: {args.file}")
data = parse_krs_pdf(args.file)
results.append(data.to_dict())
# Print summary
print(f"\n=== {data.nazwa} (KRS: {data.krs}) ===")
print(f"NIP: {data.nip}, REGON: {data.regon}")
print(f"\nZarząd ({len(data.zarzad)} osób):")
for p in data.zarzad:
print(f" - {p.full_name()} - {p.rola}")
print(f"\nWspólnicy ({len(data.wspolnicy)} osób):")
for p in data.wspolnicy:
print(f" - {p.full_name()}")
if data.prokurenci:
print(f"\nProkurenci ({len(data.prokurenci)} osób):")
for p in data.prokurenci:
print(f" - {p.full_name()}")
elif args.dir:
pdf_dir = Path(args.dir)
pdf_files = list(pdf_dir.glob("*.pdf"))
print(f"Found {len(pdf_files)} PDF files")
for pdf_file in pdf_files:
print(f"Parsing: {pdf_file.name}...")
try:
data = parse_krs_pdf(str(pdf_file))
results.append(data.to_dict())
print(f" OK: {data.nazwa}")
except Exception as e:
print(f" ERROR: {e}")
# Save results
if args.output and results:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\nResults saved to: {args.output}")
elif results:
print("\n=== JSON OUTPUT ===")
print(json.dumps(results, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()