- 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>
280 lines
9.1 KiB
Python
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()
|