# Техническая справка: Парсер ЕИС Закупок (zakupki.gov.ru)
**Версия:** 1.0
**Дата:** 26 марта 2026
**Статус:** Production-ready
---
## Содержание
1. [Обзор системы](#1-обзор-системы)
2. [Парсируемый ресурс](#2-парсируемый-ресурс)
3. [Архитектура](#3-архитектура)
4. [Процесс загрузки данных](#4-процесс-загрузки-данных)
5. [Структура данных](#5-структура-данных)
6. [Хранение в БД](#6-хранение-в-бд)
7. [API](#7-api)
8. [Фоновые задачи](#8-фоновые-задачи)
9. [Конфигурация](#9-конфигурация)
10. [Примеры](#10-примеры)
---
## 1. Обзор системы
### Назначение
Сервис парсит данные о государственных закупках из **Единой информационной системы в сфере закупок (ЕИС)** — zakupki.gov.ru.
### Поддерживаемые законы
- **44-ФЗ** — Федеральный закон "О контрактной системе в сфере закупок"
- **223-ФЗ** — Федеральный закон "О закупках товаров, работ, услуг отдельными видами юридических лиц"
### Возможности
- SOAP API (int44.zakupki.gov.ru)
- Парсинг XML-архивов
- Поддержка 80+ регионов РФ
- Инкрементальная синхронизация
- Точечный запрос по номеру закупки
- Связывание с организациями
- Отслеживание прогресса (BackgroundJob)
- Логирование (ParserLoadLog)
---
## 2. Парсируемый ресурс
### Источник данных
| Параметр | Значение |
|----------|----------|
| **Название** | Единая информационная система в сфере закупок (ЕИС) |
| **Домен** | zakupki.gov.ru |
| **SOAP API** | https://int44.zakupki.gov.ru/eis-integration/services/getDocsIP |
| **Протокол** | SOAP 1.2 over HTTPS |
| **Формат** | XML в ZIP-архивах |
| **Авторизация** | Токен через Госуслуги |
### Получение токена
```
URL: https://zakupki.gov.ru/pmd/auth/welcome
Требуется: Учётная запись Госуслуги (ЕСИА)
Токен: individualPerson_token (в SOAP-заголовке)
```
### Методы SOAP API
#### getDocsByOrgRegionRequest
Запрос по региону и периоду.
**Параметры:**
- `orgRegion` — код региона ("77" = Москва)
- `subsystemType` — "PRIZ" (44-ФЗ), "OOS223" (223-ФЗ)
- `documentType44` — тип документа:
- `epNotificationEF2020` — электронный аукцион
- `epNotificationOK2020` — открытый конкурс
- `epNotificationZK2020` — запрос котировок
- `periodInfo/exactDate` — дата (YYYY-MM-DD)
**Ответ:**
```xml
https://zakupki.gov.ru/opendata/download/...
```
#### getDocsByReestrNumberRequest
Точечный запрос по номеру закупки.
**Параметры:**
- `reestrNumber` — номер (например, "0888200000224000038")
---
## 3. Архитектура
### Компоненты
```
┌─────────────────────────────────────────────────────┐
│ Celery Task Layer │
│ parse_procurements │ sync_procurements │
│ └──────┬────────────┴────────────┬────────────────┤
│ ▼ ▼ │
│ BackgroundJob (progress) │ │
│ ParserLoadLog (audit) │ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Service Layer │
│ ProcurementService │
│ - save_procurements() (bulk upsert) │
│ - get_last_loaded_period() │
│ - find_by_inn(), find_by_purchase_number() │
│ │
│ RegistryOrganizationResolver │
│ - build_lookup() (INN/OGRN → Org ID) │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Client Layer │
│ ZakupkiClient │
│ - fetch_procurements() │
│ - _fetch_via_soap() │
│ - _build_soap_request_*() │
│ - _parse_soap_response() │
│ - _parse_archive_content() │
│ - _parse_xml_record() │
│ │
│ BaseHTTPClient │
│ - get(), post(), download_file() │
│ - Proxy support │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Data Layer │
│ ProcurementRecord (Django Model) │
│ - 30+ fields │
│ - Indexes & constraints │
│ │
│ Procurement (dataclass DTO) │
└─────────────────────────────────────────────────────┘
```
### Файловая структура
```
src/apps/parsers/
├── clients/
│ ├── base.py # Базовый HTTP-клиент
│ └── zakupki/
│ ├── __init__.py # ZakupkiClient (888 строк)
│ └── schemas.py # Dataclass схемы
├── models.py # Django модели
├── services.py # Business logic
├── tasks.py # Celery задачи
├── views.py # DRF ViewSet
├── serializers.py # DRF Serializers
├── admin.py # Django Admin
└── migrations/
├── 0006_add_procurement_model.py
├── 0010_link_registry_organizations.py
└── 0011_add_normalized_date_and_amount_fields.py
```
---
## 4. Процесс загрузки данных
### 4.1. Полный цикл (parse_procurements)
```python
# 1. Создание лога
load_log, batch_id = ParserLoadLogService.create_load_log_with_next_batch_id(
source=ParserLoadLog.Source.PROCUREMENTS,
status="in_progress"
)
# 2. BackgroundJob для отслеживания
job = BackgroundJob.objects.create(
task_id=task_id,
task_name="apps.parsers.tasks.parse_procurements",
status="in_progress"
)
# 3. Клиент
client = ZakupkiClient(
token=settings.ZAKUPKI_TOKEN,
proxies=proxies
)
# 4. SOAP запрос
soap_request = client._build_soap_request_by_region(
region_code="77",
law_type="44",
year=2025,
month=3
)
response = requests.post(
"https://int44.zakupki.gov.ru/eis-integration/services/getDocsIP",
data=soap_request,
headers={
"Content-Type": "text/xml; charset=utf-8",
"individualPerson_token": settings.ZAKUPKI_TOKEN
}
)
# 5. Парсинг ответа → archive_url
archive_url = client._parse_soap_response(response)
# 6. Скачивание ZIP
archive_content = client.http_client.download_file(
archive_url,
headers={"individualPerson_token": settings.ZAKUPKI_TOKEN}
)
# 7. Распаковка и парсинг XML
procurements = client._parse_archive_content(archive_content, archive_url)
# 8. Сохранение (bulk upsert)
saved_count = ProcurementService.save_procurements(
procurements,
batch_id=batch_id,
region_code="77",
data_year=2025,
data_month=3,
chunk_size=500
)
# 9. Обновление статуса
ParserLoadLogService.update(load_log, status="success", records_count=saved_count)
job.complete(result={"batch_id": batch_id, "saved": saved_count})
```
### 4.2. Инкрементальная синхронизация (sync_procurements)
```python
# 1. Последняя загруженная дата
last_year, last_month = ProcurementService.get_last_loaded_period(
region_code="77",
law_type="44-FZ"
)
# 2. Начальная точка
if last_year and last_month:
start_year, start_month = last_year, last_month + 1
else:
start_year, start_month = 2025, 1 # по умолчанию
# 3. Загрузка месяц за месяцем
empty_months_count = 0
year, month = start_year, start_month
while year < current_year or (year == current_year and month <= current_month):
procurements = client.fetch_procurements(
region_code=region_code,
year=year,
month=month
)
if procurements:
ProcurementService.save_procurements(...)
empty_months_count = 0
else:
empty_months_count += 1
if empty_months_count >= 2: # остановка
break
# следующий месяц
month += 1
if month > 12:
year += 1
month = 1
```
### 4.3. Парсинг XML
```python
def _parse_xml_record(element: ET.Element) -> Procurement:
# Поиск с учётом namespace
def find_child(tag): ...
# Извлечение текста
def get_text(tags):
for tag in tags:
if tag in element.attrib:
return element.attrib[tag]
child = find_child(tag)
if child is not None and child.text:
return child.text.strip()
return ""
# Вложенные структуры
def get_nested_text(parent_tags, child_tags): ...
# Маппинг полей
purchase_number = get_text(["purchaseNumber", "regNum"])
purchase_name = get_text(["purchaseObjectInfo", "name"])
customer_inn = get_nested_text(
["customer", "organizationInfo"],
["INN", "inn"]
)
max_price = get_nested_text(
["lot", "lotData"],
["maxPrice", "initialSum"]
)
publish_date = get_text(["publishDate", "createDate"])
end_date = get_text(["endDate", "submissionCloseDate"])
status = get_text(["state", "status"])
# Определение закона
law_type = ""
if "44" in element.tag or "fcs" in element.tag.lower():
law_type = "44-FZ"
elif "223" in element.tag:
law_type = "223-FZ"
return Procurement(...)
```
### 4.4. Нормализация
```python
def normalize_to_date(value: str | None) -> date | None:
"""Строка → date (YYYY-MM-DD, DD.MM.YYYY, ISO 8601)"""
if not value:
return None
candidate = str(value).strip().replace("T", " ").replace("Z", "")
for fmt in ["%Y-%m-%d", "%d.%m.%Y", "%Y-%m-%d %H:%M:%S"]:
try:
return datetime.strptime(candidate, fmt).date()
except ValueError:
continue
# Fallback: regex
match = re.search(r"\b\d{4}-\d{2}-\d{2}\b", candidate)
if match:
return datetime.strptime(match.group(0), "%Y-%m-%d").date()
return None
def normalize_to_decimal(value: str | None) -> Decimal | None:
"""Строка → Decimal (удаление ₽, пробелов, замена запятой)"""
if not value:
return None
normalized = (
str(value)
.replace("\u00a0", "")
.replace(" ", "")
.replace("₽", "")
.replace("руб.", "")
.replace("руб", "")
)
normalized = re.sub(r"[^0-9,.\-]", "", normalized)
# Обработка разделителя
if "," in normalized and "." in normalized:
if normalized.rfind(",") > normalized.rfind("."):
normalized = normalized.replace(".", "").replace(",", ".")
else:
normalized = normalized.replace(",", "")
elif "," in normalized:
normalized = normalized.replace(",", ".")
try:
return Decimal(normalized)
except (InvalidOperation, ValueError):
return None
```
---
## 5. Структура данных
### DTO (Procurement dataclass)
**Файл:** `src/apps/parsers/clients/zakupki/schemas.py`
```python
@dataclass(frozen=True)
class Procurement:
purchase_number: str # Реестровый номер
purchase_name: str # Наименование
customer_inn: str # ИНН заказчика
customer_kpp: str # КПП заказчика
customer_ogrn: str # ОГРН заказчика
customer_name: str # Наименование заказчика
max_price: str # НМЦ (строка)
currency_code: str # Код валюты (RUB)
placement_method: str # Способ определения
publish_date: str # Дата публикации
end_date: str # Дата окончания
status: str # Статус
law_type: str # 44-ФЗ / 223-ФЗ
purchase_object_info: str = "" # Объект закупки
href: str = "" # Ссылка
```
### Пример данных
```json
{
"purchase_number": "0888200000224000038",
"purchase_name": "Поставка офисной бумаги",
"customer_inn": "7707083893",
"customer_kpp": "770701001",
"customer_ogrn": "1027700034460",
"customer_name": "ПАО СБЕРБАНК",
"max_price": "1500000.00",
"currency_code": "RUB",
"placement_method": "Электронный аукцион",
"publish_date": "2025-03-15",
"end_date": "2025-03-25T18:00:00",
"status": "Подача заявок",
"law_type": "44-FZ"
}
```
---
## 6. Хранение в БД
### 6.1. Таблица: parsers_procurement
**Модель:** `ProcurementRecord`
**Файл:** `src/apps/parsers/models.py`
#### Поля
| Поле | Тип БД | Django | Null | Index | Описание |
|------|--------|--------|------|-------|----------|
| id | BIGSERIAL | BigAutoField | NO | PK | Первичный ключ |
| created_at | TIMESTAMP | DateTimeField | NO | ✓ | Дата создания |
| updated_at | TIMESTAMP | DateTimeField | NO | - | Дата обновления |
| load_batch | INTEGER | PositiveIntegerField | NO | ✓ | ID пакета |
| purchase_number | VARCHAR(100) | CharField | NO | ✓ | Реестровый номер |
| purchase_name | TEXT | TextField | NO | - | Наименование |
| customer_inn | VARCHAR(20) | CharField | NO | ✓ | ИНН |
| customer_kpp | VARCHAR(20) | CharField | YES | - | КПП |
| customer_ogrn | VARCHAR(20) | CharField | YES | ✓ | ОГРН |
| customer_name | TEXT | TextField | NO | - | Наименование заказчика |
| max_price | VARCHAR(50) | CharField | YES | - | НМЦ (строка) |
| max_price_amount | DECIMAL(20,2) | DecimalField | YES | ✓ | НМЦ (число) |
| currency_code | VARCHAR(10) | CharField | NO | - | Код валюты |
| placement_method | VARCHAR(255) | CharField | YES | - | Способ определения |
| publish_date | VARCHAR(30) | CharField | YES | - | Дата (строка) |
| publish_date_normalized | DATE | DateField | YES | ✓ | Дата (date) |
| end_date | VARCHAR(30) | CharField | YES | - | Дата окончания (строка) |
| end_date_normalized | DATE | DateField | YES | ✓ | Дата окончания (date) |
| status | VARCHAR(100) | CharField | YES | - | Статус |
| law_type | VARCHAR(20) | CharField | YES | ✓ | Тип закона |
| purchase_object_info | TEXT | TextField | YES | - | Объект |
| href | VARCHAR(500) | URLField | YES | - | Ссылка |
| region_code | VARCHAR(10) | CharField | YES | ✓ | Код региона |
| data_year | SMALLINT | PositiveSmallIntegerField | YES | ✓ | Год данных |
| data_month | SMALLINT | PositiveSmallIntegerField | YES | ✓ | Месяц данных |
| registry_organization_id | BIGINT | ForeignKey | YES | - | FK к организациям |
#### Индексы
```sql
-- Одиночные (db_index=True)
CREATE INDEX ON parsers_procurement(created_at);
CREATE INDEX ON parsers_procurement(load_batch);
CREATE INDEX ON parsers_procurement(purchase_number);
CREATE INDEX ON parsers_procurement(customer_inn);
CREATE INDEX ON parsers_procurement(customer_ogrn);
CREATE INDEX ON parsers_procurement(max_price_amount);
CREATE INDEX ON parsers_procurement(publish_date_normalized);
CREATE INDEX ON parsers_procurement(end_date_normalized);
CREATE INDEX ON parsers_procurement(law_type);
CREATE INDEX ON parsers_procurement(region_code);
CREATE INDEX ON parsers_procurement(data_year);
CREATE INDEX ON parsers_procurement(data_month);
-- Составные (Meta.indexes)
CREATE INDEX ON parsers_procurement(customer_inn, purchase_number);
CREATE INDEX ON parsers_procurement(load_batch, customer_inn);
CREATE INDEX ON parsers_procurement(law_type, data_year, data_month);
```
#### Ограничения
```sql
-- Уникальность номера
ALTER TABLE parsers_procurement
ADD CONSTRAINT unique_procurement_purchase_number
UNIQUE (purchase_number);
-- FK на организации
ALTER TABLE parsers_procurement
ADD CONSTRAINT fk_registry_organization
FOREIGN KEY (registry_organization_id)
REFERENCES registers_organization(id)
ON DELETE SET NULL;
```
#### DDL (CREATE TABLE)
```sql
CREATE TABLE "parsers_procurement" (
"id" bigserial NOT NULL PRIMARY KEY,
"created_at" timestamp with time zone NOT NULL,
"updated_at" timestamp with time zone NOT NULL,
"load_batch" integer NOT NULL,
"purchase_number" varchar(100) NOT NULL,
"purchase_name" text NOT NULL,
"customer_inn" varchar(20) NOT NULL,
"customer_kpp" varchar(20),
"customer_ogrn" varchar(20),
"customer_name" text NOT NULL,
"max_price" varchar(50),
"max_price_amount" decimal(20, 2),
"currency_code" varchar(10) NOT NULL DEFAULT 'RUB',
"placement_method" varchar(255),
"publish_date" varchar(30),
"publish_date_normalized" date,
"end_date" varchar(30),
"end_date_normalized" date,
"status" varchar(100),
"law_type" varchar(20),
"purchase_object_info" text,
"href" varchar(500),
"region_code" varchar(10),
"data_year" smallint,
"data_month" smallint,
"registry_organization_id" bigint,
CONSTRAINT "unique_procurement_purchase_number" UNIQUE ("purchase_number"),
CONSTRAINT "fk_registry_organization"
FOREIGN KEY ("registry_organization_id")
REFERENCES "registers_organization" ("id")
ON DELETE SET NULL
);
```
### 6.2. Таблица: parsers_load_log
**Модель:** `ParserLoadLog`
| Поле | Тип | Null | Index | Описание |
|------|-----|------|-------|----------|
| id | BIGSERIAL | NO | PK | Первичный ключ |
| created_at | TIMESTAMP | NO | - | Дата создания |
| updated_at | TIMESTAMP | NO | - | Дата обновления |
| batch_id | INTEGER | NO | ✓ | ID пакета |
| source | VARCHAR(50) | NO | ✓ | Источник |
| records_count | INTEGER | NO | - | Количество |
| status | VARCHAR(20) | NO | - | Статус |
| error_message | TEXT | YES | - | Ошибка |
**Ограничение:** UNIQUE (source, batch_id)
```sql
CREATE TABLE "parsers_load_log" (
"id" bigserial NOT NULL PRIMARY KEY,
"created_at" timestamp with time zone NOT NULL,
"updated_at" timestamp with time zone NOT NULL,
"batch_id" integer NOT NULL,
"source" varchar(50) NOT NULL,
"records_count" integer NOT NULL DEFAULT 0,
"status" varchar(20) NOT NULL DEFAULT 'success',
"error_message" text,
CONSTRAINT "unique_load_batch_per_source" UNIQUE ("source", "batch_id")
);
```
**Значения source:**
- `procurements` — Госзакупки (ЕИС)
- `industrial` — Промышленное производство
- `manufactures` — Реестр производителей
- `inspections` — Единый реестр проверок
- `fns_reports` — Бухгалтерская отчётность ФНС
### 6.3. Связь с организациями
```python
registry_organization = models.ForeignKey(
"registers.Organization",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="procurement_records"
)
```
**Алгоритм связывания:**
```python
class RegistryOrganizationResolver:
@classmethod
def build_lookup(cls, identifiers):
"""
identifiers: [(inn, ogrn), ...]
Returns:
by_pair: {(inn, ogrn): org_id}
by_inn: {inn: org_id}
by_ogrn: {ogrn: org_id}
"""
# 1. Уникальные значения
inn_values = {int(inn) for inn, _ in identifiers if inn}
ogrn_values = {int(ogrn) for _, ogrn in identifiers if ogrn}
# 2. Запрос организаций
organizations = Organization.objects.filter(
Q(inn__in=inn_values) | Q(ogrn__in=ogrn_values)
).values("id", "inn", "ogrn")
# 3. Построение индексов
by_pair, by_inn, by_ogrn = {}, {}, {}
for org in organizations:
inn = normalize(org["inn"])
ogrn = normalize(org["ogrn"])
org_id = org["id"]
if inn and ogrn:
by_pair[(inn, ogrn)] = org_id
if inn:
by_inn[inn] = org_id
if ogrn:
by_ogrn[ogrn] = org_id
return Lookup(by_pair, by_inn, by_ogrn)
@classmethod
def resolve_organization_id(cls, lookup, inn, ogrn):
"""Приоритет: пара → INN → OGRN"""
inn_norm = normalize(inn)
ogrn_norm = normalize(ogrn)
if inn_norm and ogrn_norm:
if (inn_norm, ogrn_norm) in lookup.by_pair:
return lookup.by_pair[(inn_norm, ogrn_norm)]
if inn_norm and inn_norm in lookup.by_inn:
return lookup.by_inn[inn_norm]
if ogrn_norm and ogrn_norm in lookup.by_ogrn:
return lookup.by_ogrn[ogrn_norm]
return None
```
---
## 7. API
### Endpoints
**Base:** `/api/v1/zakupki/`
#### GET /api/v1/zakupki/
Список закупок.
**Параметры:**
| Параметр | Тип | Описание |
|----------|-----|----------|
| customer_inn | string | Фильтр по ИНН |
| customer_ogrn | string | Фильтр по ОГРН |
| purchase_number | string | Фильтр по номеру |
| law_type | string | 44-FZ / 223-FZ |
| status | string | Статус |
| region_code | string | Код региона |
| data_year | integer | Год |
| data_month | integer | Месяц |
| load_batch | integer | Пакет |
| search | string | Поиск по названию/номеру/заказчику |
| ordering | string | Сортировка |
| page, page_size | integer | Пагинация |
**Пример:**
```http
GET /api/v1/zakupki/?customer_inn=7707083893&law_type=44-FZ&data_year=2025
Authorization: Bearer
```
**Ответ:**
```json
{
"count": 156,
"next": ".../api/v1/zakupki/?page=2",
"results": [
{
"id": 12345,
"purchase_number": "0888200000224000038",
"purchase_name": "Поставка офисной бумаги",
"customer_inn": "7707083893",
"max_price_amount": "1500000.00",
"publish_date_normalized": "2025-03-15",
"law_type": "44-FZ",
"status": "Подача заявок"
}
]
}
```
#### GET /api/v1/zakupki/{id}/
Детали закупки.
### Serializer
**Файл:** `src/apps/parsers/serializers.py`
```python
class ProcurementSerializer(serializers.ModelSerializer):
class Meta:
model = ProcurementRecord
fields = [
"id", "load_batch", "purchase_number", "purchase_name",
"customer_inn", "customer_kpp", "customer_ogrn", "customer_name",
"max_price", "max_price_amount", "currency_code",
"placement_method", "publish_date", "publish_date_normalized",
"end_date", "end_date_normalized", "status", "law_type",
"purchase_object_info", "href", "region_code",
"data_year", "data_month", "registry_organization",
"created_at", "updated_at"
]
read_only_fields = fields
```
---
## 8. Фоновые задачи
### Celery Tasks
**Файл:** `src/apps/parsers/tasks.py`
#### parse_procurements
Одноразовая загрузка.
```python
@shared_task(bind=True)
def parse_procurements(
self,
region_code: str | None = None,
year: int | None = None,
month: int | None = None,
law_type: str = "44",
proxies: list[str] | None = None,
requested_by_id: int | None = None,
) -> dict:
"""
Returns:
{"batch_id": int, "saved": int, "status": "success"}
"""
```
**Вызов:**
```python
parse_procurements.delay(
region_code="77",
year=2025,
month=3,
law_type="44"
)
```
#### sync_procurements
Инкрементальная синхронизация.
```python
@shared_task(bind=True)
def sync_procurements(
self,
region_code: str,
law_type: str = "44",
proxies: list[str] | None = None,
) -> dict:
"""
Логика:
1. Проверить последнюю дату в БД
2. Если нет данных — начать с 01.01.2025
3. Загружать месяц за месяцем
4. Остановиться после 2 месяцев без данных
Returns:
{
"batch_id": int,
"total_saved": int,
"results": [{"year": 2025, "month": 3, "fetched": 150, "saved": 145}],
"status": "success"
}
"""
```
**Вызов:**
```python
sync_procurements.delay(
region_code="77",
law_type="44"
)
```
### Periodic Tasks (Celery Beat)
**Файл:** `src/core/celery.py`
```python
CELERY_BEAT_SCHEDULE = {
"sync-procurements-daily": {
"task": "apps.parsers.tasks.sync_procurements",
"schedule": crontab(hour=2, minute=0), # Ежедневно в 02:00
"kwargs": {"region_code": "77", "law_type": "44"},
},
}
```
### Progress Tracking
```python
# Создание
job = BackgroundJob.objects.create(
task_id=task_id,
task_name="apps.parsers.tasks.parse_procurements",
status="in_progress"
)
# Прогресс
job.update_progress(50, "Загрузка за 03/2025...")
# Завершение
job.complete(result={"batch_id": 123, "saved": 150})
# Ошибка
job.fail(error="SOAP API timeout")
```
---
## 9. Конфигурация
### Переменные окружения
**Файл:** `.env.prod.example` / `.env.dev`
```bash
# Токен ЕИС (Госуслуги)
ZAKUPKI_TOKEN=
# Прокси (опционально)
PARSER_PROXIES=http://user:pass@proxy1:8080,http://user:pass@proxy2:8080
# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=mostovik
POSTGRES_USER=postgres
POSTGRES_PASSWORD=
# Redis (Celery)
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0
# Django
DJANGO_SETTINGS_MODULE=config.settings.production
SECRET_KEY=
ALLOWED_HOSTS=example.com
```
### Django Settings
**Файл:** `src/settings/base.py`
```python
INSTALLED_APPS = [
# ...
"apps.parsers",
"apps.registers",
]
ZAKUPKI_TOKEN = os.getenv("ZAKUPKI_TOKEN", "")
PARSER_PROXIES = []
if parser_proxies := os.getenv("PARSER_PROXIES"):
PARSER_PROXIES = [p.strip() for p in parser_proxies.split(",")]
```
### Docker Compose
**Файл:** `docker-compose.prod.yml`
```yaml
services:
celery:
image: registry.example.com/mostovik/celery:latest
command: celery -A config worker --loglevel=info --concurrency=4
env_file: .env.prod
depends_on: [postgres, redis]
celery-beat:
image: registry.example.com/mostovik/celery:latest
command: celery -A config beat --loglevel=info
env_file: .env.prod
depends_on: [postgres, redis]
```
---
## 10. Примеры
### 10.1. Прямой вызов клиента
```python
from apps.parsers.clients.zakupki import ZakupkiClient
from django.conf import settings
client = ZakupkiClient(
token=settings.ZAKUPKI_TOKEN,
proxies=["http://proxy.example.com:8080"]
)
# По региону
procurements = client.fetch_procurements(
region_code="77",
year=2025,
month=3,
law_type="44"
)
# По номеру
procurements = client.fetch_by_reestr_number(
reestr_number="0888200000224000038",
law_type="44"
)
# Context manager
with ZakupkiClient(token=settings.ZAKUPKI_TOKEN) as client:
procurements = client.fetch_procurements(region_code="77", year=2025)
```
### 10.2. Сервис
```python
from apps.parsers.services import ProcurementService
# Поиск по ИНН
procurements = ProcurementService.find_by_inn("7707083893")
# Поиск по номеру
procurement = ProcurementService.find_by_purchase_number(
"0888200000224000038"
).first()
# Последний период
last_year, last_month = ProcurementService.get_last_loaded_period(
region_code="77",
law_type="44-FZ"
)
print(f"Last loaded: {last_year}/{last_month}")
```
### 10.3. API
```bash
# Все закупки заказчика
curl -X GET "http://localhost:8000/api/v1/zakupki/?customer_inn=7707083893" \
-H "Authorization: Bearer "
# Фильтрация
curl -X GET "http://localhost:8000/api/v1/zakupki/?data_year=2025&law_type=44-FZ" \
-H "Authorization: Bearer "
# Поиск
curl -X GET "http://localhost:8000/api/v1/zakupki/?search=бумага" \
-H "Authorization: Bearer "
# Детали
curl -X GET "http://localhost:8000/api/v1/zakupki/12345/" \
-H "Authorization: Bearer "
```
### 10.4. SQL
```sql
-- Закупки заказчика за 2025
SELECT
purchase_number,
purchase_name,
max_price_amount,
publish_date_normalized,
status
FROM parsers_procurement
WHERE customer_inn = '7707083893'
AND data_year = 2025
ORDER BY publish_date_normalized DESC;
-- Сумма по регионам
SELECT
region_code,
COUNT(*) as count,
SUM(max_price_amount) as total
FROM parsers_procurement
WHERE data_year = 2025
GROUP BY region_code
ORDER BY total DESC;
-- Последние загрузки
SELECT
batch_id,
created_at,
records_count,
status,
error_message
FROM parsers_load_log
WHERE source = 'procurements'
ORDER BY created_at DESC
LIMIT 10;
-- Статистика по законам
SELECT
law_type,
COUNT(*) as count,
SUM(max_price_amount) as total,
AVG(max_price_amount) as avg
FROM parsers_procurement
GROUP BY law_type;
```
### 10.5. Мониторинг
```python
from apps.parsers.models import ParserLoadLog, ProcurementRecord
from django.db.models import Count, Sum
# Логи
logs = ParserLoadLog.objects.filter(
source=ParserLoadLog.Source.PROCUREMENTS
).order_by("-created_at")[:10]
# Статистика
stats = ProcurementRecord.objects.values("region_code").annotate(
count=Count("*"),
total=Sum("max_price_amount")
).order_by("-count")
# По законам
by_law = ProcurementRecord.objects.values("law_type").annotate(
count=Count("*")
)
```
---
## Приложения
### A. Коды регионов
| Код | Регион |
|-----|--------|
| 01 | Адыгея |
| 77 | Москва |
| 78 | Санкт-Петербург |
| 99 | Все регионы |
### B. Типы документов 44-ФЗ
| document_type | Значение |
|---------------|----------|
| notification | Электронный аукцион |
| notification_ok | Открытый конкурс |
| notification_zk | Запрос котировок |
### C. Статусы
- Планирование
- Публикация извещения
- Подача заявок
- Рассмотрение заявок
- Заключение контракта
- Исполнение
- Завершено
- Отменено
---
**Файл:** `docs/Техническая справка ЕИС Закупки.md`
**Код:** `src/apps/parsers/`
**Тесты:** `tests/apps/parsers/`