Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """
- ESPI Scraper - biznes.pap.pl
- Pobiera komunikaty ESPI dla wybranej spolki i wypluwa w formacie do wklejenia
- do wskaznika ESPI Front-Runner w Pine Script.
- Uzycie:
- python3 espi_scraper.py 521 2026 # Polimex, rok 2026
- python3 espi_scraper.py 521 2026 2025 # Polimex, 2026 i 2025
- python3 espi_scraper.py 521 2026 --csv out.csv
- python3 espi_scraper.py --list-companies "polimex" # szukaj ID spolki
- python3 espi_scraper.py --link <URL> # pojedynczy artykul -> linia paste
- Format URL na biznes.pap.pl:
- https://biznes.pap.pl/espi/espi/{ROK}?company={ID}&selectCompany={ID}&limit=100&page={N}
- https://biznes.pap.pl/wiadomosci/firmy/{slug} <- single article (--link)
- Timezone:
- Strona podaje czas w Europe/Warsaw. Unix timestamp = UTC.
- Pine Script wskaznika ESPI Front-Runner oczekuje Unix sekund UTC.
- """
- from __future__ import annotations
- import argparse
- import csv
- import re
- import sys
- import time
- from dataclasses import dataclass, asdict
- from datetime import datetime
- from pathlib import Path
- from urllib.parse import urlencode
- from zoneinfo import ZoneInfo
- import requests
- from bs4 import BeautifulSoup
- WARSAW = ZoneInfo("Europe/Warsaw")
- UTC = ZoneInfo("UTC")
- BASE = "https://biznes.pap.pl"
- UA = "Mozilla/5.0 (compatible; ESPI-Scraper/1.0)"
- @dataclass
- class EspiEvent:
- date: str # YYYY-MM-DD (Europe/Warsaw)
- time: str # HH:MM (Europe/Warsaw)
- unix_utc: int # Unix seconds UTC
- number: str # "12/2026"
- company: str
- title: str
- url: str
- sentiment: int = 0 # -1 negative, 0 neutral, +1 positive
- # ============== SENTIMENT KEYWORD RULES ==============
- # Prosta heurystyka po tytule. Priorytet: negatywne > pozytywne > neutralne.
- POSITIVE_KW = [
- "zawarcie umowy", "istotn", "znaczac", "znaczn", "umow",
- "kontrakt", "wygran", "zysk", "dywidend", "skup akcji wlasnych",
- "skup wlasnych", "buyback", "rekomendacja", "nabyci", "zakup",
- "intencyjn", "porozumieni", "aneks", "wzrost", "rozszerzeni",
- "przedluzeni", "przyznan", "certyfikat", "zamowieni",
- "podpisani", "zatwierdzeni ugod", "strateg",
- ]
- NEGATIVE_KW = [
- "strat", "rezygnacj", "odwolanie", "odwolan",
- "zmniejszeni udzialu", "zbyci akcji", "sprzedaz akcji",
- "emisja akcji", "wydanie akcji", "nowa emisja", "emisja obligacji",
- "zamiana obligacji", "rozwodnieni", "obnizeni", "spadek",
- "upadlosc", "restrukturyzacj", "postepowani", "pozew",
- "zawiadomieni o zmianie udzialu", "zmiana udzialu w ogolnej",
- "wezwanie", "wycofanie", "delisting", "zawieszenie",
- "ostrzezenie", "kara", "sankcj", "wykreslenie",
- ]
- NEUTRAL_KW = [
- "terminy publikacji", "raport okresowy", "raport biezacy",
- "kalendarium", "qsr", " ps ", " q ",
- "zawiadomieni o transakcj",
- ]
- def _normalize(s: str) -> str:
- # Remove Polish diacritics for matching robustness
- table = str.maketrans("ąćęłńóśźżĄĆĘŁŃÓŚŹŻ", "acelnoszzACELNOSZZ")
- return s.translate(table).lower()
- def classify_sentiment(title: str) -> int:
- t = _normalize(title)
- # Neutrals first (they'd otherwise match some positive keywords)
- for kw in NEUTRAL_KW:
- if kw in t:
- return 0
- neg = any(kw in t for kw in NEGATIVE_KW)
- pos = any(kw in t for kw in POSITIVE_KW)
- if neg and not pos:
- return -1
- if pos and not neg:
- return 1
- if neg and pos:
- # Both found - weigh by which keyword appears first
- neg_pos = min((t.find(kw) for kw in NEGATIVE_KW if kw in t), default=1_000_000)
- pos_pos = min((t.find(kw) for kw in POSITIVE_KW if kw in t), default=1_000_000)
- return -1 if neg_pos < pos_pos else 1
- return 0
- def fetch(url: str) -> str:
- r = requests.get(url, headers={"User-Agent": UA}, timeout=20)
- r.raise_for_status()
- return r.text
- def parse_page(html: str, path: str) -> list[EspiEvent]:
- """
- Struktura: tabela gdzie tr.day zawiera date, a kolejne tr zawieraja
- komunikaty az do nastepnego tr.day.
- """
- soup = BeautifulSoup(html, "html.parser")
- table = soup.find("table")
- if not table:
- return []
- events: list[EspiEvent] = []
- current_date: str | None = None # YYYY-MM-DD
- for tr in table.find_all("tr"):
- day_td = tr.find("td", class_="day")
- if day_td:
- # format: "2026.04.15 - Środa"
- m = re.search(r"(\d{4})\.(\d{2})\.(\d{2})", day_td.get_text(" ", strip=True))
- if m:
- current_date = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
- continue
- tds = tr.find_all("td")
- if len(tds) < 4 or current_date is None:
- continue
- time_txt = tds[0].get_text(strip=True)
- if not re.match(r"^\d{1,2}:\d{2}$", time_txt):
- continue
- number = tds[1].get_text(strip=True)
- company = tds[2].get_text(strip=True)
- title_a = tds[3].find("a")
- title = title_a.get_text(" ", strip=True) if title_a else tds[3].get_text(" ", strip=True)
- href = title_a["href"] if title_a and title_a.has_attr("href") else ""
- if href.startswith("/"):
- href = BASE + href
- # Warsaw -> UTC
- hh, mm = map(int, time_txt.split(":"))
- y, mo, d = map(int, current_date.split("-"))
- dt_warsaw = datetime(y, mo, d, hh, mm, tzinfo=WARSAW)
- unix_utc = int(dt_warsaw.astimezone(UTC).timestamp())
- events.append(EspiEvent(
- date=current_date,
- time=time_txt,
- unix_utc=unix_utc,
- number=number,
- company=company,
- title=title,
- url=href,
- sentiment=classify_sentiment(title),
- ))
- return events
- def parse_article(url: str) -> EspiEvent:
- """
- Parsuje pojedynczy artykul z biznes.pap.pl/wiadomosci/firmy/...
- Wypluwa EspiEvent gotowy do format_paste().
- Selektory weryfikowane na realnym artykule:
- - <h1 class="articleTitle"> ... </h1> <- pelny tytul z numerem
- - <div class="publicationDate">Publikacja: 2026-04-30 12:51</div>
- - fallback tytulu z <title>...| Biznes PAP</title>
- - fallback numeru z <p class="field--name-field-lead">Raport biezacy 13/2026</p>
- """
- html = fetch(url)
- soup = BeautifulSoup(html, "html.parser")
- # ----- title -----
- title = ""
- h1 = soup.find("h1", class_="articleTitle")
- if h1:
- title = h1.get_text(" ", strip=True)
- if not title:
- t = soup.find("title")
- if t:
- # zwykle: "TYTUL | Biznes PAP" — odetnij sufiks
- title = t.get_text(strip=True).rsplit("|", 1)[0].strip()
- if not title:
- raise ValueError(f"nie znalazlem tytulu w {url}")
- # ----- publikacja -----
- pub_div = soup.find("div", class_="publicationDate")
- if not pub_div:
- raise ValueError(f"nie znalazlem <div class=publicationDate> w {url}")
- pub_text = pub_div.get_text(" ", strip=True)
- m = re.search(r"(\d{4})-(\d{2})-(\d{2})\s+(\d{1,2}):(\d{2})", pub_text)
- if not m:
- raise ValueError(f"nie sparsowalem daty z '{pub_text}'")
- y, mo, d, hh, mm = (int(g) for g in m.groups())
- date_str = f"{y:04d}-{mo:02d}-{d:02d}"
- time_str = f"{hh:02d}:{mm:02d}"
- # ----- numer raportu -----
- number = ""
- nm = re.search(r"\((\d+/\d{4})\)", title)
- if nm:
- number = nm.group(1)
- else:
- lead = soup.find("p", class_="field--name-field-lead")
- if lead:
- ln = re.search(r"(\d+/\d{4})", lead.get_text(" ", strip=True))
- if ln:
- number = ln.group(1)
- # ----- spolka -----
- company = ""
- cm = re.match(r"^([^(]+)\(", title)
- if cm:
- company = cm.group(1).strip()
- # ----- unix UTC -----
- dt_warsaw = datetime(y, mo, d, hh, mm, tzinfo=WARSAW)
- unix_utc = int(dt_warsaw.astimezone(UTC).timestamp())
- return EspiEvent(
- date=date_str,
- time=time_str,
- unix_utc=unix_utc,
- number=number,
- company=company,
- title=title,
- url=url,
- sentiment=classify_sentiment(title),
- )
- def scrape(company_id: int, year: int, limit: int = 100, max_pages: int = 20) -> list[EspiEvent]:
- all_events: list[EspiEvent] = []
- seen: set[tuple[str, str]] = set()
- for page in range(max_pages):
- params = {
- "company": company_id,
- "selectCompany": company_id,
- "limit": limit,
- "page": page,
- }
- url = f"{BASE}/espi/espi/{year}?{urlencode(params)}"
- html = fetch(url)
- page_events = parse_page(html, url)
- new_count = 0
- for e in page_events:
- key = (e.date + e.time, e.number)
- if key not in seen:
- seen.add(key)
- all_events.append(e)
- new_count += 1
- if new_count == 0:
- break
- time.sleep(0.5) # Be nice
- return all_events
- def search_companies(query: str) -> list[tuple[str, str]]:
- """Pobiera liste spolek ESPI (wszystkie strony) i zwraca pasujace do query."""
- out = []
- q = query.lower()
- page = 0
- while True:
- url = f"{BASE}/espi/company" if page == 0 else f"{BASE}/espi/company?page={page}"
- html = fetch(url)
- soup = BeautifulSoup(html, "html.parser")
- links = soup.select("a[href*='company=']")
- if not links:
- break
- for a in links:
- name = a.get_text(strip=True)
- m = re.search(r"company=(\d+)", a["href"])
- if m and name and q in name.lower():
- out.append((m.group(1), name))
- # sprawdz czy jest nastepna strona
- next_link = soup.select_one("a[rel='next']")
- if not next_link:
- break
- page += 1
- time.sleep(0.3)
- return sorted(set(out), key=lambda x: int(x[0]))
- def format_pine_inputs(events: list[EspiEvent], max_slots: int = 10) -> str:
- """
- Generuje linie gotowe do wklejenia w inputs Pine Script.
- Wskaznik ma 10 slotow -> bierzemy najnowsze (najwyzsze unix).
- """
- top = sorted(events, key=lambda e: e.unix_utc, reverse=True)[:max_slots]
- lines = ["// === Wklej do inputs wskaznika ESPI Front-Runner ==="]
- for i, e in enumerate(top, start=1):
- lines.append(f"// #{i}: {e.date} {e.time} CET/CEST | {e.number} | {e.title[:80]}")
- lines.append(f"// unix = {e.unix_utc}")
- lines.append("")
- lines.append("// Skrocone (same liczby do szybkiego wklejenia):")
- for i, e in enumerate(top, start=1):
- lines.append(f"ev{i} = {e.unix_utc} // {e.date} {e.time}")
- return "\n".join(lines)
- PINE_TEMPLATE = '''// @version=6
- // ESPI Front-Runner - "Przeciek" | {TICKER_UPPER}
- // Timestampy wygenerowane automatycznie przez espi_scraper.py
- // Wygenerowano: {GEN_DATE}
- // Eventow: {N_EVENTS}
- indicator("ESPI Front-Runner [{TICKER_UPPER}]", shorttitle="ESPI-{TICKER_UPPER}", overlay=true, max_lines_count=500, max_labels_count=500, max_boxes_count=500)
- // ============== INPUTS ==============
- grpMode = "Tryb"
- modeSel = input.string("Auto", "Tryb analizy", options=["Auto", "Intraday (minuty)", "HTF bars (bary)"], group=grpMode)
- grpWindow = "Okno analizy - Intraday"
- leadMin = input.int(5, "Okno pre-ESPI (minuty)", minval=1, maxval=60, group=grpWindow)
- postMin = input.int(5, "Okno post-ESPI (minuty)", minval=1, maxval=60, group=grpWindow)
- baseMin = input.int(60, "Baseline lookback (minuty)",minval=15,maxval=600, group=grpWindow)
- grpHtf = "Okno analizy - HTF"
- preBarsIn = input.int(1, "Pre bars", minval=1, maxval=20, group=grpHtf)
- postBarsIn = input.int(1, "Post bars", minval=1, maxval=20, group=grpHtf)
- baseBarsIn = input.int(20, "Baseline bars",minval=5, maxval=200,group=grpHtf)
- grpThresh = "Progi anomalii"
- volMult = input.float(3.0, "Volume x baseline do alertu", minval=1.5, step=0.5, group=grpThresh)
- retPct = input.float(0.5, "Return % do alertu (|pre ret|)", minval=0.1, step=0.1, group=grpThresh)
- grpView = "Wyglad"
- showBox = input.bool(true, "Rysuj box okna pre", group=grpView)
- showPost = input.bool(true, "Rysuj box okna post", group=grpView)
- showLabel = input.bool(true, "Etykieta z metrykami",group=grpView)
- colPre = input.color(color.new(color.red, 75), "Kolor box pre", group=grpView)
- colPost = input.color(color.new(color.green, 80), "Kolor box post", group=grpView)
- colLine = input.color(color.yellow, "Kolor linii ESPI", group=grpView)
- // ============== ESPI EVENTS ({N_EVENTS}) ==============
- var int[] events = array.from(
- {EVENTS_BLOCK}
- )
- // ============== TRYB ==============
- tfSec = timeframe.in_seconds(timeframe.period)
- htfAuto = tfSec >= 15 * 60
- useHtf = modeSel == "HTF bars (bary)" or (modeSel == "Auto" and htfAuto)
- preBars = useHtf ? preBarsIn : math.max(1, math.round(leadMin * 60 / tfSec))
- postBars = useHtf ? postBarsIn : math.max(1, math.round(postMin * 60 / tfSec))
- baseBars = useHtf ? baseBarsIn : math.max(1, math.round(baseMin * 60 / tfSec))
- msPerMin = 60 * 1000
- barSizeMs = tfSec * 1000
- // ============== LOGIKA ==============
- var line[] lns = array.new_line()
- var box[] bxPre = array.new_box()
- var box[] bxPost= array.new_box()
- var label[] lbls = array.new_label()
- f_drawnAlready(int ts) =>
- found = false
- if array.size(lns) > 0
- for i = 0 to array.size(lns) - 1
- ln = array.get(lns, i)
- if line.get_x1(ln) == ts * 1000
- found := true
- break
- found
- if array.size(events) > 0
- for i = 0 to array.size(events) - 1
- ts = array.get(events, i)
- tsMs = ts * 1000
- eventBarStart = tsMs - (tsMs % barSizeMs)
- postEnd = eventBarStart + postBars * barSizeMs
- preStart = eventBarStart - preBars * barSizeMs
- if time >= postEnd and time < postEnd + barSizeMs and not f_drawnAlready(ts)
- postFirst = 1
- postLast = postBars
- preFirst = postBars + 1
- preLast = postBars + preBars
- baseFirst = postBars + preBars + 1
- baseLast = postBars + preBars + baseBars
- baseVolSum = 0.0
- for k = baseFirst to baseLast
- baseVolSum += nz(volume[k])
- baseVolAvg = baseVolSum / baseBars
- preVolSum = 0.0
- for k = preFirst to preLast
- preVolSum += nz(volume[k])
- preVolAvg = preVolSum / preBars
- postVolSum = 0.0
- for k = postFirst to postLast
- postVolSum += nz(volume[k])
- postVolAvg = postVolSum / postBars
- pPreAnchor = close[preLast + 1]
- pPreEnd = close[preFirst]
- retPre = (pPreEnd / pPreAnchor - 1) * 100
- pPostAnchor= close[postLast + 1]
- pPostEnd = close[postFirst]
- retPost = (pPostEnd / pPostAnchor - 1) * 100
- volRatio = baseVolAvg > 0 ? preVolAvg / baseVolAvg : 0.0
- volRatioPost = baseVolAvg > 0 ? postVolAvg / baseVolAvg : 0.0
- ln = line.new(tsMs, low - (high-low), tsMs, high + (high-low), xloc=xloc.bar_time, extend=extend.both, color=colLine, width=2, style=line.style_dashed)
- array.push(lns, ln)
- if showBox
- priceHi = ta.highest(high, preBars)[preFirst]
- priceLo = ta.lowest (low, preBars)[preFirst]
- b = box.new(preStart, priceHi, eventBarStart, priceLo, xloc=xloc.bar_time, bgcolor=colPre, border_color=color.new(color.red, 30))
- array.push(bxPre, b)
- if showPost
- priceHi2 = ta.highest(high, postBars)[postFirst]
- priceLo2 = ta.lowest (low, postBars)[postFirst]
- b2 = box.new(eventBarStart, priceHi2, postEnd, priceLo2, xloc=xloc.bar_time, bgcolor=colPost, border_color=color.new(color.green, 30))
- array.push(bxPost, b2)
- if showLabel
- suspicious = (math.abs(retPre) >= retPct) or (volRatio >= volMult)
- modeStr = useHtf ? "HTF" : "INTRA"
- unit = useHtf ? "bars" : "min"
- txt = (suspicious ? "⚠ PRZECIEK?\\n" : "ESPI\\n") +
- "[" + modeStr + " " + str.tostring(preBars) + "/" + str.tostring(postBars) + " " + unit + "]\\n" +
- "pre ret: " + str.tostring(retPre, "#.##") + "%\\n" +
- "post ret: " + str.tostring(retPost, "#.##") + "%\\n" +
- "pre vol x base: " + str.tostring(volRatio, "#.##") + "x\\n" +
- "post vol x base: " + str.tostring(volRatioPost, "#.##") + "x"
- lb = label.new(tsMs, high, txt, xloc=xloc.bar_time, style=label.style_label_down, color=suspicious ? color.new(color.red, 20) : color.new(color.gray, 30), textcolor=color.white, size=size.small)
- array.push(lbls, lb)
- '''
- def write_pine_full(events: list[EspiEvent], path: Path, ticker: str) -> None:
- """Generuje gotowy Pine Script ze wszystkimi eventami w array.from()."""
- # Pine ma limit ~4096 tokenow w jednej linii, wiec dzielimy na wiele linii
- sorted_events = sorted(events, key=lambda e: e.unix_utc, reverse=True)
- lines = []
- for e in sorted_events:
- comment = f"{e.date} {e.time} | {e.number} | {e.title[:60]}"
- lines.append(f" {e.unix_utc}, // {comment}")
- # remove trailing comma on last item (Pine doesn't like it)
- if lines:
- last = lines[-1]
- lines[-1] = last.replace(", //", " //", 1)
- events_block = "\n".join(lines)
- out = PINE_TEMPLATE.format(
- TICKER_UPPER=ticker.upper(),
- GEN_DATE=datetime.now(WARSAW).strftime("%Y-%m-%d"),
- N_EVENTS=len(sorted_events),
- EVENTS_BLOCK=events_block,
- )
- path.write_text(out, encoding="utf-8")
- def format_paste(events: list[EspiEvent], compact: bool = False) -> str:
- """
- Format do wklejenia w input.text_area:
- <unix> <sentiment> # <data> <godzina> | <numer> | <tytul>
- Pine parser: pierwsza liczba = timestamp, druga (ze znakiem) = sentiment.
- compact=True: pomija komentarz (#...), samo <unix> <sentiment>.
- TradingView input.text_area limit ~4096 znakow.
- Pelny format: ~110 znakow/event -> max ~37 eventow.
- Compact: ~12 znakow/event -> max ~340 eventow.
- """
- sorted_events = sorted(events, key=lambda e: e.unix_utc, reverse=True)
- lines = []
- for e in sorted_events:
- sent_str = f"{e.sentiment:+d}"
- if compact:
- lines.append(f"{e.unix_utc} {sent_str}")
- else:
- lines.append(f"{e.unix_utc} {sent_str} # {e.date} {e.time} | {e.number} | {e.title[:70]}")
- return "\n".join(lines)
- def write_csv(events: list[EspiEvent], path: Path) -> None:
- with path.open("w", newline="", encoding="utf-8") as f:
- w = csv.writer(f)
- w.writerow(["date", "time", "unix_utc", "sentiment", "number", "company", "title", "url"])
- for e in sorted(events, key=lambda e: e.unix_utc):
- w.writerow([e.date, e.time, e.unix_utc, e.sentiment, e.number, e.company, e.title, e.url])
- CACHE_DIR = Path.home() / ".espi_cache"
- def cache_path(company_id: int) -> Path:
- return CACHE_DIR / f"{company_id}.txt"
- def save_cache(company_id: int, paste: str) -> Path:
- CACHE_DIR.mkdir(exist_ok=True)
- p = cache_path(company_id)
- p.write_text(paste, encoding="utf-8")
- return p
- def load_from_file(path: Path) -> str:
- if not path.exists():
- raise FileNotFoundError(f"Plik nie istnieje: {path}")
- return path.read_text(encoding="utf-8")
- def main() -> int:
- ap = argparse.ArgumentParser(description="Scrape ESPI events from biznes.pap.pl")
- ap.add_argument("company_id", nargs="?", type=int, help="ID spolki (np. 521 dla Polimex)")
- ap.add_argument("years", nargs="*", type=int, help="Lata do pobrania (np. 2026 2025)")
- ap.add_argument("--csv", type=Path, help="Zapisz wszystkie eventy do CSV")
- ap.add_argument("--slots", type=int, default=10, help="Ile topowych eventow do Pine inputs (domyslnie 10)")
- ap.add_argument("--pine-full", type=Path, help="Wygeneruj gotowy plik Pine ze WSZYSTKIMI eventami jako array.from()")
- ap.add_argument("--ticker", default="TICKER", help="Ticker do nazwy wskaznika (np. PXM)")
- ap.add_argument("--paste", action="store_true", help="Wypluj format do wklejenia w input.text_area Pine (jeden timestamp per linia + komentarz)")
- ap.add_argument("--compact", action="store_true", help="Jak --paste ale bez komentarzy: samo <unix> <sentiment>. Miesci sie w limicie 4096 znakow TradingView dla setek eventow.")
- ap.add_argument("--list-companies", metavar="QUERY", help="Znajdz ID spolki po nazwie")
- ap.add_argument("--link", metavar="URL", help="Pojedynczy artykul z biznes.pap.pl -> jedna linia paste (unix +sent # data godz | nr | tytul)")
- ap.add_argument("--load", metavar="COMPANY_ID", type=int, help="Wczytaj paste z cache (~/.espi_cache/<ID>.txt) zamiast scrapowac")
- ap.add_argument("--from-file", metavar="PATH", type=Path, dest="from_file", help="Wczytaj paste z podanego pliku zamiast scrapowac")
- ap.add_argument("--save", metavar="PATH", type=Path, help="Zapisz paste do pliku (dodatkowo do stdout)")
- ap.add_argument("--no-cache", action="store_true", help="Nie zapisuj automatycznie do cache po scrapowaniu")
- args = ap.parse_args()
- if args.list_companies:
- results = search_companies(args.list_companies)
- if not results:
- print(f"Brak dopasowan dla '{args.list_companies}'", file=sys.stderr)
- return 1
- for cid, name in results:
- print(f"{cid:>6} {name}")
- return 0
- if args.link:
- try:
- ev = parse_article(args.link)
- except Exception as e:
- print(f"BLAD: {e}", file=sys.stderr)
- return 1
- print(format_paste([ev]))
- return 0
- # --- tryb: wczytaj z pliku zamiast scrapowac ---
- if args.load is not None:
- p = cache_path(args.load)
- try:
- paste = load_from_file(p)
- except FileNotFoundError:
- print(f"Brak cache dla ID {args.load}. Uzyj najpierw: espi {args.load} <lata>", file=sys.stderr)
- return 1
- print(paste)
- return 0
- if args.from_file is not None:
- try:
- paste = load_from_file(args.from_file)
- except FileNotFoundError as e:
- print(f"BLAD: {e}", file=sys.stderr)
- return 1
- print(paste)
- return 0
- if not args.company_id or not args.years:
- ap.print_help()
- return 1
- all_events: list[EspiEvent] = []
- for year in args.years:
- print(f"[*] Scraping company={args.company_id} year={year}...", file=sys.stderr)
- evs = scrape(args.company_id, year)
- print(f" -> {len(evs)} events", file=sys.stderr)
- all_events.extend(evs)
- if not all_events:
- print("Brak eventow.", file=sys.stderr)
- return 1
- if args.csv:
- write_csv(all_events, args.csv)
- print(f"[*] CSV: {args.csv}", file=sys.stderr)
- if args.pine_full:
- write_pine_full(all_events, args.pine_full, args.ticker)
- print(f"[*] Pine (full): {args.pine_full} ({len(all_events)} events)", file=sys.stderr)
- return 0
- compact = args.compact
- paste = format_paste(all_events, compact=compact)
- # auto-cache po scrapowaniu (mozna wylaczyc --no-cache)
- if not args.no_cache and args.company_id:
- saved = save_cache(args.company_id, paste)
- print(f"[*] Cache: {saved}", file=sys.stderr)
- # jawny zapis do pliku (--save)
- if args.save:
- args.save.parent.mkdir(parents=True, exist_ok=True)
- args.save.write_text(paste, encoding="utf-8")
- print(f"[*] Zapisano: {args.save}", file=sys.stderr)
- print(paste)
- return 0
- if __name__ == "__main__":
- sys.exit(main())
Add Comment
Please, Sign In to add comment