fix: устранены все найденные аудитом баги и тихие падения
- SQL injection паттерн → параметризованные запросы во всех местах - except: pass/continue → logger.warning() везде, ничего не тонет молча - WAL mode + индекс domain_dedup_key в database.py - try/finally для conn в main.py, утечка соединения устранена - backoff 30с при 403/429 от Rusprofile/ЕГРЮЛ - ликвидированные компании → egrul_status="liquidated" - max_candidates в contacts_finder считает только реальных кандидатов - DB_PATH абсолютный (Path(__file__).parent), HH_PAUSE_BETWEEN_QUERIES в config - HH_SIGNAL_QUERIES дубль убран из launcher.py → импорт из config - path traversal защита в egrul_enricher debug_dump_html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -303,7 +303,8 @@ def find_company_website(
|
||||
}
|
||||
|
||||
seen: set[str] = set()
|
||||
max_candidates = 6 # проверим не больше 6 разных сайтов
|
||||
max_candidates = 6 # проверим не больше 6 реальных (не агрегаторных) кандидатов
|
||||
real_candidates = 0
|
||||
|
||||
for query in queries:
|
||||
try:
|
||||
@@ -324,7 +325,8 @@ def find_company_website(
|
||||
seen.add(host)
|
||||
if not _is_company_site(found_url):
|
||||
continue
|
||||
if len(seen) > max_candidates:
|
||||
real_candidates += 1
|
||||
if real_candidates > max_candidates:
|
||||
break
|
||||
|
||||
root = f"{urlparse(found_url).scheme}://{host}"
|
||||
|
||||
@@ -52,7 +52,7 @@ def _api_key() -> Optional[str]:
|
||||
load_dotenv()
|
||||
key = os.environ.get("DADATA_API_KEY")
|
||||
except ImportError:
|
||||
pass
|
||||
logger.warning("[dadata] python-dotenv не установлен, .env не загружен")
|
||||
return key
|
||||
|
||||
|
||||
@@ -282,8 +282,12 @@ def enrich_via_dadata(
|
||||
best = d
|
||||
break
|
||||
if not best:
|
||||
# Все ликвидированы — берём первого, не записываем в БД
|
||||
# Все ликвидированы — берём первого, логируем, выставляем статус
|
||||
logger.warning(f"[dadata] Все совпадения ликвидированы для '{name}'")
|
||||
best = suggestions[0].get("data") or {}
|
||||
parsed = _parse_party(best)
|
||||
parsed["egrul_status"] = "liquidated"
|
||||
return parsed
|
||||
|
||||
parsed = _parse_party(best)
|
||||
return parsed
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Стратегия запросов:
|
||||
- Случайный User-Agent
|
||||
- timeout 10 сек
|
||||
- При 403/429 — пропускаем (логируем как 'error')
|
||||
- При 403/429 — пауза 30 сек + пропускаем (логируем как 'error')
|
||||
- Пауза между лидами в run_egrul_enrichment, не здесь
|
||||
|
||||
Ограничения:
|
||||
@@ -18,6 +18,7 @@
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from urllib.parse import quote
|
||||
@@ -379,7 +380,8 @@ def enrich_egrul_by_inn(
|
||||
allow_redirects=True,
|
||||
)
|
||||
if resp.status_code in (403, 429):
|
||||
logger.warning(f" Rusprofile blocked us ({resp.status_code}) для ИНН {inn}")
|
||||
logger.warning(f"[egrul] Rusprofile вернул {resp.status_code}, пауза 30 сек (ИНН {inn})")
|
||||
time.sleep(30)
|
||||
return result
|
||||
if resp.status_code != 200:
|
||||
return result
|
||||
@@ -610,10 +612,11 @@ def enrich_egrul(
|
||||
)
|
||||
|
||||
if resp.status_code in (403, 429):
|
||||
logger.warning(f" Rusprofile blocked us ({resp.status_code}) для '{name}'")
|
||||
logger.warning(f"[egrul] Rusprofile вернул {resp.status_code}, пауза 30 сек ('{name}')")
|
||||
time.sleep(30)
|
||||
return result
|
||||
if resp.status_code != 200:
|
||||
logger.debug(f" Rusprofile вернул {resp.status_code} для '{name}'")
|
||||
logger.warning(f"[egrul] Rusprofile вернул {resp.status_code} для '{name}'")
|
||||
return result
|
||||
|
||||
# Если редирект на /id/N или /ip/N — Rusprofile уверен в матче.
|
||||
@@ -657,12 +660,20 @@ def enrich_egrul(
|
||||
|
||||
# Debug: сохранить HTML для отладки regex'ов
|
||||
if debug_dump_html:
|
||||
from pathlib import Path
|
||||
dump_path = Path(debug_dump_html).resolve()
|
||||
try:
|
||||
with open(debug_dump_html, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
logger.info(f" [debug] HTML сохранён в {debug_dump_html}")
|
||||
except Exception as e:
|
||||
logger.warning(f" [debug] Не удалось сохранить HTML: {e}")
|
||||
dump_path.relative_to(Path.cwd())
|
||||
except ValueError:
|
||||
logger.warning(f"[egrul] debug_dump_html путь вне CWD, пропускаю: {debug_dump_html}")
|
||||
debug_dump_html = None
|
||||
if debug_dump_html:
|
||||
try:
|
||||
with open(dump_path, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
logger.info(f" [debug] HTML сохранён в {dump_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f" [debug] Не удалось сохранить HTML: {e}")
|
||||
|
||||
# Извлекаем все поля (один общий парсер для by_name и by_inn).
|
||||
# Передаём company_name=name_for_search (без HR-суффиксов) чтобы
|
||||
|
||||
@@ -13,6 +13,7 @@ False positives возможны (например, упоминание "yclien
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -273,6 +274,7 @@ def analyze_website(url: str, timeout: float = 6.0) -> dict:
|
||||
timeout=(timeout, timeout), verify=False, stream=False,
|
||||
)
|
||||
if sub_resp.status_code != 200:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
sub_html = sub_resp.text[:MAX_HTML_BYTES]
|
||||
sub_inn, sub_ogrn, sub_kpp = _extract_inn_ogrn_kpp(sub_html)
|
||||
@@ -281,8 +283,11 @@ def analyze_website(url: str, timeout: float = 6.0) -> dict:
|
||||
ogrn_val = ogrn_val or sub_ogrn
|
||||
kpp_val = kpp_val or sub_kpp
|
||||
logger.debug(f" ИНН найден на {path}: {inn_val}")
|
||||
time.sleep(0.5)
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
except Exception as e:
|
||||
logger.warning(f"[website_analyzer] подстраница {path} сайта {url}: {e}")
|
||||
continue
|
||||
if inn_val:
|
||||
result["inn"] = inn_val
|
||||
|
||||
Reference in New Issue
Block a user