From d962f8df382e8b518ef9cd94e0ede55a76624aab Mon Sep 17 00:00:00 2001
From: xob0t <5348245@gmail.com>
Date: Thu, 5 Dec 2024 09:06:46 +0300
Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?=
=?UTF-8?q?=D0=BE=D1=80=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?=
=?UTF-8?q?=D0=BD=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* парсинг рейтинга продавца
* проверка версии при запуске
* парсинг редиректов в каталог из поиска
fixes #9
---
README.md | 137 ++--
core/{db.py => db_utils.py} | 79 +--
core/exceptions.py | 10 +
core/interactive_config.py | 304 +++++----
core/main.py | 110 ++--
core/parser_base.py | 155 -----
core/parser_url.py | 814 ++++++++++++------------
core/{telegram_utils.py => telegram.py} | 32 +-
core/utils.py | 108 +++-
setup.py | 4 +-
10 files changed, 823 insertions(+), 930 deletions(-)
rename core/{db.py => db_utils.py} (64%)
create mode 100644 core/exceptions.py
delete mode 100644 core/parser_base.py
rename core/{telegram_utils.py => telegram.py} (71%)
diff --git a/README.md b/README.md
index 6ea246f..82e4812 100644
--- a/README.md
+++ b/README.md
@@ -1,80 +1,95 @@
-#
+#
+
```
____ ___ ____ ___ ____ ____ _____________ _____
/ __ `__ \/ __ `__ \/ __ \/ __ `/ ___/ ___/ _ \/ ___/
- / / / / / / / / / / / /_/ / /_/ / / (__ ) __/ /
- /_/ /_/ /_/_/ /_/ /_/ .___/\__,_/_/ /____/\___/_/
+ / / / / / / / / / / / /_/ / /_/ / / (__ ) __/ /
+ /_/ /_/ /_/_/ /_/ /_/ .___/\__,_/_/ /____/\___/_/
/_/
```
+
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/xob0t/mmparser/total)
+
## Сказать спасибо автору - [Yoomoney](https://yoomoney.ru/to/410018051351692)
-Связь со мной [tg](https://t.me/mobate)
+
+Связь со мной [tg](https://t.me/mobate) - Индивидуальной поддержкой бесплатно не занимаюсь
### Демо ускорено в 10 раз
[![asciicast](https://asciinema.org/a/fYFj0HVO16r16vaK1reEa4617.svg)](https://asciinema.org/a/fYFj0HVO16r16vaK1reEa4617)
+
Пример уведомления Telegram
## Особенности
-* Работа через api
-* Парсинг карточек товаров при парсинге каталога/поиска
-* Сохранение результатов в sqlite БД
-* Запуск с конфигом и/или аргументами
-* Интерактивное создание конфигов
-* Поддержка прокси строкой или списком из файла
-* Поддержка ссылок каталога, поиска, и карточек товара
-* Парсинг одной ссылки в многопотоке, по потоку на прокси/соединение
-* Импорт cookies экспортированних в формате Json с помошью [Cookie-Editor](https://chrome.google.com/webstore/detail/hlkenndednhfkekhgcdicdfddnkalmdm)
-* Блеклист продавцов
-* Regex фильтр по именам товаров
-* Уведомления в телеграм по заданным параметрам
-* Позволяет выставить время, через которое подходящий по параметрам уведомлений товар будет повторно отправлен в TG
-* Использование блеклиста продавцов с ограничением на списание бонусов
-* Сссылки на каталог супермаркетов не поддерживаются :(
+
+- Работа через api
+- Парсинг карточек товаров при парсинге каталога/поиска
+- Сохранение результатов в sqlite БД
+- Запуск с конфигом и/или аргументами
+- Интерактивное создание конфигов
+- Поддержка прокси строкой или списком из файла
+- Поддержка ссылок каталога, поиска, и карточек товара
+- Парсинг одной ссылки в многопотоке, по потоку на прокси/соединение
+- Импорт cookies экспортированних в формате Json с помошью [Cookie-Editor](https://chrome.google.com/webstore/detail/hlkenndednhfkekhgcdicdfddnkalmdm)
+- Блеклист продавцов
+- Regex фильтр по именам товаров
+- Уведомления в телеграм по заданным параметрам
+- Позволяет выставить время, через которое подходящий по параметрам уведомлений товар будет повторно отправлен в TG
+- Использование блеклиста продавцов с ограничением на списание бонусов
+- Сссылки на каталог супермаркетов не поддерживаются :(
## Установка:
- 1. Установить [Python](https://www.python.org/downloads/), в установщике поставить галочку "Добавить в PATH"
- 2. [Скачать парсер](https://github.com/xob0t/mmparser/releases/latest/download/mmparser.zip)
- 3. Установить парсер: `pip install mmparser.zip -U`
+
+1. Установить [Python](https://www.python.org/downloads/), в установщике поставить галочку "Добавить в PATH"
+2. [Скачать парсер](https://github.com/xob0t/mmparser/releases/latest/download/mmparser.zip)
+3. Установить парсер: `pip install mmparser.zip -U`
## Пример использования
+
### Кавычки обязательны!
+
### Просто парсинг url
+
```
mmparser "https://megamarket.ru/catalog/?q=%D0%BD%D0%BE%D1%83%D1%82%D0%B1%D1%83%D0%BA&suggestionType=frequent_query#?filters=%7B%2288C83F68482F447C9F4E401955196697%22%3A%7B%22min%22%3A229028%2C%22max%22%3A307480%7D%2C%22A03364050801C25CD0A856C734F74FE9%22%3A%5B%221%22%5D%7D&sort=1"
```
+
### Парсинг url с cookie файлом
+
```
mmparser -cookies "cookies.json" "https://megamarket.ru/catalog/details/processor-amd-ryzen-5-5600-am4-oem-600008773764/"
```
+
### Без аргументов, создание конфига
+
```
mmparser
```
+
### Запуск с конфигом
+
```
-mmparser -cfg "config.json"
+mmparser -config "config.json"
```
## Чтение результатов
+
При запуске парсер создаст в рабочей директории файл storage.sqlite
Это sqlite база данных, очень удобно читается в [DB Browser for SQLite](https://sqlitebrowser.org/)
## Запуск по расписанию на windows:
+
[Планировщик заданий Windows для начинающих](https://remontka.pro/windows-task-scheduler/)
#
```
-usage: mmparser [-h] [-job JOB_NAME] [-cfg CONFIG] [-i INCLUDE] [-e EXCLUDE] [-b BLACKLIST] [-ac] [-nc] [-c COOKIES]
- [-aa ACCOUNT_ALERT] [-a ADDRESS] [-p PROXY] [-pl PROXY_LIST] [-ad] [-tc TG_CONFIG]
- [-pva PRICE_VALUE_ALERT] [-pbva PRICE_BONUS_VALUE_ALERT] [-bva BONUS_VALUE_ALERT]
- [-bpa BONUS_PERCENT_ALERT] [-mb] [-art ALERT_REPEAT_TIMEOUT] [-t THREADS] [-d DELAY] [-ed ERROR_DELAY]
- [-log {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
+mmparser [-h] [-job-name JOB_NAME] [-config CONFIG] [-include INCLUDE] [-exclude EXCLUDE] [-blacklist BLACKLIST] [-all-cards] [-no-cards] [-cookies COOKIES] [-account-alert ACCOUNT_ALERT] [-address ADDRESS] [-proxy PROXY] [-proxy-list PROXY_LIST] [-allow-direct] [-tg-config TG_CONFIG] [-price-value-alert PRICE_VALUE_ALERT]
+ [-price-bonus-value-alert PRICE_BONUS_VALUE_ALERT] [-bonus-value-alert BONUS_VALUE_ALERT] [-bonus-percent-alert BONUS_PERCENT_ALERT] [-use-merchant-blacklist] [-alert-repeat-timeout ALERT_REPEAT_TIMEOUT] [-threads THREADS] [-delay DELAY] [-error-delay ERROR_DELAY] [-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
[url]
positional arguments:
@@ -82,52 +97,38 @@ positional arguments:
options:
-h, --help show this help message and exit
- -job JOB_NAME, --job-name JOB_NAME
- Название задачи, без этого параметра будет автоопределено
- -cfg CONFIG, --config CONFIG
- Путь к конфигу парсера
- -i INCLUDE, --include INCLUDE
- Парсить только товары, название которых совпадает с выражением
- -e EXCLUDE, --exclude EXCLUDE
- Пропускать товары, название которых совпадает с выражением
- -b BLACKLIST, --blacklist BLACKLIST
- Путь к файлу со списком игнорируемых продавцов
- -ac, --all-cards Всегда парсить карточки товаров
- -nc, --no-cards Не парсить карточки товаров
- -c COOKIES, --cookies COOKIES
- Путь к файлу с cookies в формате JSON (Cookie-Editor - Export Json)
- -aa ACCOUNT_ALERT, --account-alert ACCOUNT_ALERT
+ -job-name JOB_NAME Название задачи, без этого параметра будет автоопределено
+ -config CONFIG Путь к конфигу парсера
+ -include INCLUDE Парсить только товары, название которых совпадает с выражением
+ -exclude EXCLUDE Пропускать товары, название которых совпадает с выражением
+ -blacklist BLACKLIST Путь к файлу со списком игнорируемых продавцов
+ -all-cards Всегда парсить карточки товаров
+ -no-cards Не парсить карточки товаров
+ -cookies COOKIES Путь к файлу с cookies в формате JSON (Cookie-Editor - Export Json)
+ -account-alert ACCOUNT_ALERT
Если вы используйте cookie, и вход в аккаунт не выполнен, присылать уведомление в TG
- -a ADDRESS, --address ADDRESS
- Адрес, будет использовано первое сопадение
- -p PROXY, --proxy PROXY
- Строка прокси в формате protocol://username:password@ip:port
- -pl PROXY_LIST, --proxy-list PROXY_LIST
+ -address ADDRESS Адрес, будет использовано первое сопадение
+ -proxy PROXY Строка прокси в формате protocol://username:password@ip:port
+ -proxy-list PROXY_LIST
Путь к файлу с прокси в формате protocol://username:password@ip:port
- -ad, --allow-direct Использовать прямое соединение параллельно с прокси для ускорения работы в многопотоке
- -tc TG_CONFIG, --tg-config TG_CONFIG
- Telegram Bot Token и Telegram Chat Id в формате token$id
- -pva PRICE_VALUE_ALERT, --price-value-alert PRICE_VALUE_ALERT
+ -allow-direct Использовать прямое соединение параллельно с прокси для ускорения работы в многопотоке
+ -tg-config TG_CONFIG Telegram Bot Token и Telegram Chat Id в формате token$id
+ -price-value-alert PRICE_VALUE_ALERT
Если цена товара равна или ниже данного значения, уведомлять в TG
- -pbva PRICE_BONUS_VALUE_ALERT, --price-bonus-value-alert PRICE_BONUS_VALUE_ALERT
+ -price-bonus-value-alert PRICE_BONUS_VALUE_ALERT
Если цена-бонусы товара равна или ниже данного значения, уведомлять в TG
- -bva BONUS_VALUE_ALERT, --bonus-value-alert BONUS_VALUE_ALERT
+ -bonus-value-alert BONUS_VALUE_ALERT
Если количество бонусов товара равно или выше данного значения, уведомлять в TG
- -bpa BONUS_PERCENT_ALERT, --bonus-percent-alert BONUS_PERCENT_ALERT
+ -bonus-percent-alert BONUS_PERCENT_ALERT
Если процент бонусов товара равно или выше данного значения, уведомлять в TG
- -mb, --use-merchant-blacklist
- Использовать черный список продавцов с ограничением на списание бонусов.
- Для более эффективной работы рекомендуется установить парсер с поддержкой lxml:
- pip install mmparser.zip[lxml] -U
- -art ALERT_REPEAT_TIMEOUT, --alert-repeat-timeout ALERT_REPEAT_TIMEOUT
- Если походящий по параметрам товар уже был отправлен в TG, повторно уведомлять по истечении
- заданного времени, в часах
- -t THREADS, --threads THREADS
- Количество потоков. По умолчанию: 1 на каждое соединиение
- -d DELAY, --delay DELAY
- Задержка между запросами в секундах при работе в одном потоке. По умолчанию: 1.8
- -ed ERROR_DELAY, --error-delay ERROR_DELAY
+ -use-merchant-blacklist
+ Использовать черный список продавцов с ограничением на списание бонусов
+ -alert-repeat-timeout ALERT_REPEAT_TIMEOUT
+ Если походящий по параметрам товар уже был отправлен в TG, повторно уведомлять по истечении заданного времени, в часах
+ -threads THREADS Количество потоков. По умолчанию: 1 на каждое соединиение
+ -delay DELAY Задержка между запросами в секундах при работе в одном потоке. По умолчанию: 1.8
+ -error-delay ERROR_DELAY
Задержка между запосами в секундах в случае ошибки при работе в одном потоке. По умолчанию: 5
- -log {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
+ -log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
Уровень лога. По умолчанию: INFO
```
diff --git a/core/db.py b/core/db_utils.py
similarity index 64%
rename from core/db.py
rename to core/db_utils.py
index 3fc1775..c21606b 100644
--- a/core/db.py
+++ b/core/db_utils.py
@@ -30,24 +30,24 @@ def new_job(job_name):
"""INSERT INTO
jobs
(name,started) VALUES (?,?)""",
- (job_name,
- now)
+ (job_name, now),
)
job_id = cursor.lastrowid
cursor.execute(f"""
CREATE TABLE "{job_name}_{job_id}" (
- "goodsId" TEXT,
- "merchantId" TEXT,
+ "goods_id" TEXT,
+ "merchant_id" TEXT,
"url" TEXT,
"title" TEXT,
- "finalPrice" INTEGER,
- "finalPriceBonus" INTEGER,
- "bonusAmount" INTEGER,
- "bonusPercent" INTEGER,
- "availableQuantity" INTEGER,
- "deliveryPossibilities" TEXT,
- "merchantName" TEXT,
- "scraped" DATETIME,
+ "price" INTEGER,
+ "price_bonus" INTEGER,
+ "bonus_amount" INTEGER,
+ "bonus_percent" INTEGER,
+ "available_quantity" INTEGER,
+ "delivery_date" TEXT,
+ "merchant_name" TEXT,
+ "merchant_rating" FLOAT,
+ "scraped_at" DATETIME,
"notified" BOOL
);
""")
@@ -58,17 +58,18 @@ def new_job(job_name):
def add_to_db(
job_id,
job_name,
- goodsId,
- merchantId,
+ goods_id,
+ merchant_id,
url,
title,
- finalPrice,
- finalPriceBonus,
- bonusAmount,
- bonusPercent,
- availableQuantity,
- deliveryPossibilities,
- merchantName,
+ price,
+ price_bonus,
+ bonus_amount,
+ bonus_percent,
+ available_quantity,
+ delivery_date,
+ merchant_name,
+ merchant_rating,
notified,
):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -77,22 +78,23 @@ def add_to_db(
cursor.execute(
f"""INSERT INTO
"{job_name}_{job_id}"
- (goodsId,merchantId,url,title,finalPrice,finalPriceBonus,bonusAmount,
- bonusPercent,availableQuantity,deliveryPossibilities,
- merchantName,scraped,notified)
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
+ (goods_id,merchant_id,url,title,price,price_bonus,bonus_amount,
+ bonus_percent,available_quantity,delivery_date,
+ merchant_name,merchant_rating,scraped_at,notified)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
- goodsId,
- merchantId,
+ goods_id,
+ merchant_id,
url,
title,
- finalPrice,
- finalPriceBonus,
- bonusAmount,
- bonusPercent,
- availableQuantity,
- deliveryPossibilities,
- merchantName,
+ price,
+ price_bonus,
+ bonus_amount,
+ bonus_percent,
+ available_quantity,
+ delivery_date,
+ merchant_name,
+ merchant_rating,
now,
notified,
),
@@ -100,7 +102,7 @@ def add_to_db(
sqlite_connection.commit()
-def get_last_notified(goodsId, merchantId, finalPrice, bonusAmount):
+def get_last_notified(goods_id, merchant_id, price, bonus_amount):
sqlite_connection = sqlite3.connect(FILENAME)
cursor = sqlite_connection.cursor()
last_notified_row = None
@@ -113,12 +115,12 @@ def get_last_notified(goodsId, merchantId, finalPrice, bonusAmount):
job_tables = [table[0] for table in cursor.fetchall()]
# Construct a union query to select from all job tables at once
- union_query = " UNION ".join([f"SELECT scraped FROM '{table}' WHERE notified = 1 AND goodsId = ? AND merchantId = ? AND finalPrice = ? AND bonusAmount = ?" for table in job_tables])
+ union_query = " UNION ".join([f"SELECT scraped FROM '{table}' WHERE notified = 1 AND goods_id = ? AND merchant_id = ? AND price = ? AND bonus_amount = ?" for table in job_tables])
- union_query+="ORDER BY scraped DESC LIMIT 1"
+ union_query += "ORDER BY scraped DESC LIMIT 1"
# Concatenate all parameters to be passed into the execute function
- parameters = tuple([goodsId, merchantId, finalPrice, bonusAmount] * len(job_tables))
+ parameters = tuple([goods_id, merchant_id, price, bonus_amount] * len(job_tables))
cursor.execute(union_query, parameters)
row = cursor.fetchone()
@@ -128,6 +130,7 @@ def get_last_notified(goodsId, merchantId, finalPrice, bonusAmount):
cursor.close()
return last_notified_row
+
def finish_job(job_id):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sqlite_connection = sqlite3.connect(FILENAME)
diff --git a/core/exceptions.py b/core/exceptions.py
new file mode 100644
index 0000000..2bc031f
--- /dev/null
+++ b/core/exceptions.py
@@ -0,0 +1,10 @@
+class BaseException(Exception):
+ pass
+
+
+class ConfigError(BaseException):
+ pass
+
+
+class ApiError(BaseException):
+ pass
diff --git a/core/interactive_config.py b/core/interactive_config.py
index 957e63f..32a23ae 100644
--- a/core/interactive_config.py
+++ b/core/interactive_config.py
@@ -8,9 +8,8 @@
from curl_cffi import requests
from InquirerPy import inquirer
-from core.parser_url import Parser_url
-import core.telegram_utils as telegram_utils
-import core.utils as utils
+from .parser_url import Parser_url
+from . import telegram, utils, exceptions
console = Console()
@@ -42,15 +41,16 @@
}
-def accent(string):
+def accent(string: str) -> str:
return f"[bold cyan]{string}[/bold cyan]"
-def validate_url(url: str):
+def validate_url(url: str) -> bool:
+ """Проверка URL"""
try:
if not requests.get(url).ok:
return False
- except:
+ except Exception:
return False
parser = Parser_url(url)
@@ -65,26 +65,21 @@ def validate_url(url: str):
return False
-def save_config_dict(config_dict: dict, name: str):
+def save_config_dict(config_dict: dict, name: str) -> None:
+ """Сохранить конфиг в json"""
try:
file_name = f"{name}.json"
with open(file_name, "w", encoding="utf-8") as config_file:
json.dump(config_dict, config_file, indent=4, ensure_ascii=False)
return file_name
- except:
- raise Exception(f"Ошибка сохранения конфига {file_name}!")
+ except Exception as exc:
+ raise exceptions.ConfigError(f"Ошибка сохранения конфига {file_name}!") from exc
-def create_config():
- console.print("[green]Добро пожаловать в интерактивный конфигуратор mmparser!")
- console.print("[cyan]В квадратных скобках указаны значения по умолчанию")
-
- user_input = None
- config_dict["job_name"] = ""
- while not config_dict["job_name"]:
- user_input = console.input(
- "URL, который будем парсить. Парсинг всегда начнется с 1 страницы. (Рекомендуемые: товар или поиск):\n"
- )
+def get_job_name(config_dict: dict) -> None:
+ """Определить название задачи"""
+ while True:
+ user_input = console.input("URL, который будем парсить. Парсинг всегда начнется с 1 страницы. (Рекомендуемые: товар или поиск):\n")
if not user_input:
continue
console.print("[yellow]Идет проверка url...")
@@ -98,47 +93,28 @@ def create_config():
merchant = (parsed_input_url.get("merchant", {}) or {}).get("slug")
unknown = "Не определено"
config_dict["url"] = user_input
- config_dict["job_name"] = utils.remove_chars(
- search_text or collection_title or merchant or unknown
- )
+ config_dict["job_name"] = utils.remove_chars(search_text or collection_title or merchant or unknown)
+ break
- use_telegram = inquirer.confirm(
- message="Присылать уведомления в Telegram?", default=True
- ).execute()
+
+def get_telegram_config(config_dict: dict) -> None:
+ """Получить конфигурацию Telegram из введенных пользователем данных"""
+ use_telegram = inquirer.confirm(message="Присылать уведомления в Telegram?", default=True).execute()
if use_telegram:
while True:
- console.print(
- "Telegram Bot Token и Telegram Chat Id в формате [bold cyan]token$id[/bold cyan]"
- )
- console.print(
- "Пример: [bold white]633434921:AAErf79oop8XbNNCHAssWKtUGM6QnJnXwWE[cyan]$[/cyan]34278514[/bold white]"
- )
+ console.print("Telegram Bot Token и Telegram Chat Id в формате [bold cyan]token$id[/bold cyan]")
+ console.print("Пример: [bold white]633434921:AAErf79oop8XbNNCHAssWKtUGM6QnJnXwWE[cyan]$[/cyan]34278514[/bold white]")
user_input = console.input()
console.print("[yellow]Проверяем конфиг...")
- if telegram_utils.validate_credentials(user_input):
+ if telegram.validate_tg_credentials(user_input):
console.print(f"[green]Введенный конфиг: {user_input}")
config_dict["tg_config"] = user_input
break
console.print("[red]Некорректное значение!")
- while True:
- user_input = console.input(f"Название задачи \[{accent(config_dict['job_name'])}]:")or config_dict["job_name"]
- safe_user_input = utils.remove_chars(user_input)
- if not safe_user_input:
- continue
- if safe_user_input == user_input:
- config_dict["job_name"] = safe_user_input
- console.print(f"[green]{config_dict['job_name']}")
- break
- if safe_user_input != user_input:
- console.print(
- "[yellow]Из названия были убраны запрещенные символы, будет использовано:"
- )
- config_dict["job_name"] = safe_user_input
- console.print(f"[green]{config_dict['job_name']}")
- break
- console.print("[red]Не корркетное название")
+def get_parsing_config(config_dict: dict) -> None:
+ """Получить конфигурацию парсинга из введенных пользователем данных"""
if "/details/" not in config_dict["url"]:
while True:
warning = """[yellow]Данные о наличии других предложений у товара доступны только в поиске.
@@ -167,10 +143,7 @@ def create_config():
],
).execute()
- if (
- choice
- == "Когда в api есть информация, что есть другие предложения, или если ее нет совсем"
- ):
+ if choice == "Когда в api есть информация, что есть другие предложения, или если ее нет совсем":
break
if choice == "Всегда":
config_dict["all_cards"] = True
@@ -180,9 +153,7 @@ def create_config():
break
console.print("[red]Неверный ввод. Попробуйте еще раз.[/red]")
while True:
- user_input = console.input(
- f"Обрабатывать только товары, название которых совпадает с regex \[{accent('Пропуск')}]:"
- )
+ user_input = console.input(f"Обрабатывать только товары, название которых совпадает с regex \[{accent('Пропуск')}]:")
if user_input == "":
config_dict["include"] = None
break
@@ -192,9 +163,7 @@ def create_config():
break
console.print(f'[red]Неверное выражение "{user_input}" ![/red]')
while True:
- user_input = console.input(
- f"Игнорировать товары, название которых совпадает с regex \[{accent('Пропуск')}]:"
- )
+ user_input = console.input(f"Игнорировать товары, название которых совпадает с regex \[{accent('Пропуск')}]:")
if user_input == "":
config_dict["exclude"] = None
break
@@ -204,10 +173,11 @@ def create_config():
break
console.print(f'[red]Неверное выражение "{user_input}" ![/red]')
+
+def get_blacklist_config(config_dict: dict) -> None:
+ """Получить конфигурацию черного списка из введенных пользователем данных"""
while True:
- user_input = console.input(
- f"Использовать файл с black-листом продавцов? \[{accent('Пропуск')}]:"
- )
+ user_input = console.input(f"Использовать файл с black-листом продавцов? \[{accent('Пропуск')}]:")
if user_input == "":
config_dict["blacklist"] = None
break
@@ -217,10 +187,11 @@ def create_config():
break
console.print(f'[red]Файл "{user_input}" не найден![/red]')
+
+def get_cookie_config(config_dict: dict) -> None:
+ """Получить конфигурацию cookie из пользовательского ввода"""
while True:
- user_input = console.input(
- f"Путь к файлу с cookies в формате JSON (Cookie-Editor - Export Json) \[{accent('Пропуск')}]:"
- )
+ user_input = console.input(f"Путь к файлу с cookies в формате JSON (Cookie-Editor - Export Json) \[{accent('Пропуск')}]:")
if user_input == "":
config_dict["cookie_file_path"] = None
break
@@ -232,81 +203,73 @@ def create_config():
console.print(f"[red]Ошибка чтения cookies из файла {user_input}!")
console.print(f"[red]Файл {user_input} не найден")
+
+def get_address_config(config_dict: dict) -> None:
+ """Получить конфигурацию адреса из введенных пользователем данных"""
if config_dict["cookie_file_path"]:
- config_dict["address"] = console.input(
- f"Адрес, будет использовано первое сопадение. \[{accent('Адрес из cookies')}]:"
- )
+ config_dict["address"] = console.input(f"Адрес, будет использовано первое сопадение. \[{accent('Адрес из cookies')}]:")
else:
- config_dict["address"] = console.input(
- f"Адрес, будет использовано первое сопадение. \[{accent('Москва')}]:"
- )
+ config_dict["address"] = console.input(f"Адрес, будет использовано первое сопадение. \[{accent('Москва')}]:")
+
+def get_alert_config(config_dict: dict) -> None:
+ """Получить конфигурацию оповещения из пользовательского ввода"""
if config_dict["tg_config"]:
while True:
- user_input = console.input(
- f"Если цена товара равна или ниже данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:"
- )
+ user_input = console.input(f"Если цена товара равна или ниже данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:")
if user_input == "":
config_dict["price_value_alert"] = None
break
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["price_value_alert"] = num
- break
+ try:
+ config_dict["price_value_alert"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
+
while True:
- user_input = console.input(
- f"Если цена-бонусы товара равна или ниже данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:"
- )
+ user_input = console.input(f"Если цена-бонусы товара равна или ниже данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:")
if user_input == "":
config_dict["price_bonus_value_alert"] = None
break
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["price_bonus_value_alert"] = num
- break
+ try:
+ config_dict["price_bonus_value_alert"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
while True:
- user_input = console.input(
- f"Если количество бонусов товара равно или выше данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:"
- )
+ user_input = console.input(f"Если количество бонусов товара равно или выше данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:")
if user_input == "":
config_dict["bonus_value_alert"] = None
break
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["bonus_value_alert"] = num
- break
+ try:
+ config_dict["bonus_value_alert"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
while True:
- user_input = console.input(
- f"Если процент бонусов товара равен или выше данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:"
- )
+ user_input = console.input(f"Если процент бонусов товара равен или выше данного значения, уведомлять в TG \[{accent('Не уведомлять')}]:")
if user_input == "":
config_dict["bonus_percent_alert"] = None
break
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["bonus_percent_alert"] = num
- break
+ try:
+ config_dict["bonus_percent_alert"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
while True:
- user_input = console.input(
- f"Время в часах, через которое подходящий по параметрам уведомлений товар будет повторно отправлен в TG \[{accent(config_dict['alert_repeat_timeout'])}]:"
- )
+ user_input = console.input(f"Время в часах, через которое подходящий по параметрам уведомлений товар будет повторно отправлен в TG \[{accent(config_dict['alert_repeat_timeout'])}]:")
if user_input == "":
+ config_dict["alert_repeat_timeout"] = None
break
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["alert_repeat_timeout"] = num
- break
+ try:
+ config_dict["alert_repeat_timeout"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
+
+def get_proxy_config(config_dict: dict) -> None:
+ """Получить конфигурацию прокси из введенных пользователем данных"""
choice = inquirer.select(
message="Использовать прокси?",
choices=["Нет", "Из строки (один)", "Из файла (список)"],
@@ -314,9 +277,7 @@ def create_config():
if choice == "Из строки (один)":
while True:
- user_input = console.input(
- "Введите строку прокси в формате protocol://username:password@ip:port:"
- )
+ user_input = console.input("Введите строку прокси в формате protocol://username:password@ip:port:")
if user_input:
if utils.proxy_format_check(user_input):
config_dict["proxy"] = user_input
@@ -325,79 +286,106 @@ def create_config():
if choice == "Из файла (список)":
while True:
- user_input = console.input(
- "Путь к файлу со списком прокси в формате protocol://username:password@ip:port:"
- )
- path_is_valid = Path(user_input).exists()
- if path_is_valid:
+ user_input = console.input("Путь к файлу со списком прокси в формате protocol://username:password@ip:port:")
+ if Path(user_input).exists():
console.print(f"[green]Файл найден: {user_input}")
- config_dict["proxy_list"] = user_input
+ config_dict["proxy_file_path"] = user_input
break
console.print("[red]Файл не найден![/red]")
+
+def get_account_alert_config(config_dict: dict) -> None:
+ """Получить конфигурацию оповещения по ошибкам учетной записи из введенных пользователем данных"""
if config_dict["cookie_file_path"] and config_dict["tg_config"]:
config_dict["account_alert"] = inquirer.confirm(
message="Если вход в аккаунт не выполнен, присылать уведомление в Telegram?",
default=True,
).execute()
- if config_dict["proxy"]:
- config_dict["allow_direct"] = inquirer.confirm(
- message="Использовать прямое соединение параллельно с прокси для ускорения работы в многопотоке?",
- default=True,
- ).execute()
+def get_performance_config(config_dict: dict) -> None:
+ """Получить конфигурацию производительности из введенных пользователем данных"""
while True:
- user_input = console.input(
- f"Количество потоков. По умолчанию: 1 на каждое соединиение \[{accent('По умолчанию')}]:"
- )
+ user_input = console.input(f"Количество потоков. По умолчанию: 1 на каждое соединиение \[{accent('По умолчанию')}]:")
if user_input == "":
break
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["threads"] = int(num)
- break
+ try:
+ config_dict["threads"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
while True:
- user_input = (
- console.input(
- f"Задержка между запросами в секундах для одного соединения \[{accent(config_dict['delay'])}]:"
- )
- or config_dict["delay"]
- )
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["delay"] = num
- break
+ user_input = console.input(f"Задержка между запросами в секундах для одного соединения \[{accent(config_dict['delay'])}]:") or config_dict["delay"]
+ try:
+ config_dict["delay"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
while True:
- user_input = (
- console.input(
- f"Пауза в секундах в случае ошибки для одного соединения \[{accent(config_dict['error_delay'])}]:"
- )
- or config_dict["error_delay"]
- )
- if user_input:
- num = utils.convert_float(user_input)
- if num:
- config_dict["error_delay"] = num
- break
+ user_input = console.input(f"Пауза в секундах в случае ошибки для одного соединения \[{accent(config_dict['error_delay'])}]:") or config_dict["error_delay"]
+ try:
+ config_dict["error_delay"] = float(user_input)
+ break
+ except ValueError:
console.print("[red]Неверный ввод![/red]")
+
+def get_merchant_blacklist_config(config_dict: dict) -> None:
+ """Получить конфигурацию черного списка продавцов из введенных пользователем данных"""
config_dict["use_merchant_blacklist"] = inquirer.confirm(
message="Использовать черный список продавцов с ограничением на списание бонусов?",
default=False,
).execute()
+
+def create_config():
+ console.print("[green]Добро пожаловать в интерактивный конфигуратор mmparser!")
+ console.print("[cyan]В квадратных скобках указаны значения по умолчанию")
+
+ config_dict = {
+ "url": "",
+ "job_name": "Не определено",
+ "include": "",
+ "exclude": "",
+ "blacklist": "",
+ "all_cards": False,
+ "no_cards": False,
+ "cookie_file_path": "",
+ "address": "",
+ "proxy": "",
+ "allow_direct": True,
+ "proxy_file_path": "",
+ "tg_config": "",
+ "price_value_alert": "",
+ "price_bonus_value_alert": "",
+ "bonus_value_alert": "",
+ "bonus_percent_alert": "",
+ "alert_repeat_timeout": 0,
+ "use_merchant_blacklist": False,
+ "threads": 0,
+ "delay": 1.8,
+ "error_delay": 5,
+ "account_alert": False,
+ "log_level": "INFO",
+ }
+
+ get_job_name(config_dict)
+ get_telegram_config(config_dict)
+ get_parsing_config(config_dict)
+ get_blacklist_config(config_dict)
+ get_cookie_config(config_dict)
+ get_address_config(config_dict)
+ get_alert_config(config_dict)
+ get_proxy_config(config_dict)
+ get_account_alert_config(config_dict)
+ get_performance_config(config_dict)
+ get_merchant_blacklist_config(config_dict)
+
file_name = save_config_dict(config_dict, config_dict["job_name"])
console.print("[green]Настройка завершена!")
- console.print(
- f'Вы можете запустить парсер командой [bold white]mmparser -cfg "{file_name}"[/bold white]'
- )
+ console.print(f'Вы можете запустить парсер командой [bold white]mmparser -config "{file_name}"[/bold white]')
run = inquirer.confirm(message="Запустить сейчас?", default=True).execute()
if run:
console.print("[cyan]Запускаем парсер...")
diff --git a/core/main.py b/core/main.py
index 40a0ac6..97812bf 100644
--- a/core/main.py
+++ b/core/main.py
@@ -1,65 +1,70 @@
"""cli"""
+
import argparse
from pathlib import Path
+from rich_argparse import RichHelpFormatter
from core.parser_url import Parser_url
from core.interactive_config import create_config
from core.utils import read_json_file, print_logo
+from .exceptions import ConfigError
+
-def run_url_parser(args, config = {}):
+def run_url_parser(args: argparse.Namespace, config: dict = {}) -> None:
parser_instance = Parser_url(
- url = config.get('url') or args.url,
- job_name = config.get('job_name') or args.job_name,
- include = config.get('include') or args.include,
- exclude = config.get('exclude') or args.exclude,
- blacklist = config.get('blacklist') or args.blacklist,
- all_cards = config.get('all_cards') or args.all_cards,
- no_cards = config.get('no_cards') or args.no_cards,
- cookie_file_path = config.get('cookie_file_path') or args.cookies,
- address = config.get('address') or args.address,
- proxy = config.get('proxy') or args.proxy,
- allow_direct = config.get('allow_direct') or args.allow_direct,
- proxy_file_path = config.get('proxy_file_path') or args.proxy_list,
- tg_config = config.get('tg_config') or args.tg_config,
- price_value_alert = config.get('price_value_alert') or args.price_value_alert,
- price_bonus_value_alert = config.get('price_bonus_value_alert') or args.price_bonus_value_alert,
- bonus_value_alert = config.get('bonus_value_alert') or args.bonus_value_alert,
- bonus_percent_alert = config.get('bonus_percent_alert') or args.bonus_percent_alert,
- alert_repeat_timeout = config.get('alert_repeat_timeout') or args.alert_repeat_timeout,
- use_merchant_blacklist = config.get('use_merchant_blacklist') or args.use_merchant_blacklist,
- threads = config.get('threads') or args.threads,
- delay = config.get('delay') or args.delay,
- error_delay = config.get('error_delay') or args.error_delay,
- log_level = config.get('log_level') or args.log_level
+ url=config.get("url") or args.url,
+ job_name=config.get("job_name") or args.job_name,
+ include=config.get("include") or args.include,
+ exclude=config.get("exclude") or args.exclude,
+ blacklist=config.get("blacklist") or args.blacklist,
+ all_cards=config.get("all_cards") or args.all_cards,
+ no_cards=config.get("no_cards") or args.no_cards,
+ cookie_file_path=config.get("cookie_file_path") or args.cookies,
+ address=config.get("address") or args.address,
+ proxy=config.get("proxy") or args.proxy,
+ allow_direct=config.get("allow_direct") or args.allow_direct,
+ proxy_file_path=config.get("proxy_file_path") or args.proxy_list,
+ tg_config=config.get("tg_config") or args.tg_config,
+ price_value_alert=config.get("price_value_alert") or args.price_value_alert,
+ price_bonus_value_alert=config.get("price_bonus_value_alert") or args.price_bonus_value_alert,
+ bonus_value_alert=config.get("bonus_value_alert") or args.bonus_value_alert,
+ bonus_percent_alert=config.get("bonus_percent_alert") or args.bonus_percent_alert,
+ alert_repeat_timeout=config.get("alert_repeat_timeout") or args.alert_repeat_timeout,
+ use_merchant_blacklist=config.get("use_merchant_blacklist") or args.use_merchant_blacklist,
+ threads=config.get("threads") or args.threads,
+ delay=config.get("delay") or args.delay,
+ error_delay=config.get("error_delay") or args.error_delay,
+ log_level=config.get("log_level") or args.log_level,
)
parser_instance.parse()
+
def main():
- parser = argparse.ArgumentParser()
+ parser = argparse.ArgumentParser(prog="mmparser", description="Парсер/скрапер megamarket.ru", formatter_class=RichHelpFormatter)
parser.add_argument("url", nargs="?", type=str, help="URL для парсинга")
- parser.add_argument("-job", "--job-name", type=str, help="Название задачи, без этого параметра будет автоопределено")
- parser.add_argument("-cfg", "--config", type=str, help="Путь к конфигу парсера")
- parser.add_argument("-i", "--include", type=str, help="Парсить только товары, название которых совпадает с выражением")
- parser.add_argument("-e", "--exclude", type=str, help="Пропускать товары, название которых совпадает с выражением")
- parser.add_argument("-b", "--blacklist", type=str, help="Путь к файлу со списком игнорируемых продавцов")
- parser.add_argument("-ac", "--all-cards", action='store_true', help="Всегда парсить карточки товаров")
- parser.add_argument("-nc", "--no-cards", action='store_true', help="Не парсить карточки товаров")
- parser.add_argument("-c", "--cookies", type=str, help="Путь к файлу с cookies в формате JSON (Cookie-Editor - Export Json)")
- parser.add_argument("-aa", "--account-alert", type=str, help="Если вы используйте cookie, и вход в аккаунт не выполнен, присылать уведомление в TG")
- parser.add_argument("-a", "--address", type=str, help="Адрес, будет использовано первое сопадение")
- parser.add_argument("-p", "--proxy", type=str, help="Строка прокси в формате protocol://username:password@ip:port")
- parser.add_argument("-pl", "--proxy-list", type=str, help="Путь к файлу с прокси в формате protocol://username:password@ip:port")
- parser.add_argument("-ad", "--allow-direct", action="store_true", help="Использовать прямое соединение параллельно с прокси для ускорения работы в многопотоке")
- parser.add_argument("-tc", "--tg-config",type=str, help="Telegram Bot Token и Telegram Chat Id в формате token$id")
- parser.add_argument("-pva", "--price-value-alert", type=float, help="Если цена товара равна или ниже данного значения, уведомлять в TG")
- parser.add_argument("-pbva", "--price-bonus-value-alert", type=float, help="Если цена-бонусы товара равна или ниже данного значения, уведомлять в TG")
- parser.add_argument("-bva", "--bonus-value-alert", type=float, help="Если количество бонусов товара равно или выше данного значения, уведомлять в TG")
- parser.add_argument("-bpa", "--bonus-percent-alert", type=float, help="Если процент бонусов товара равно или выше данного значения, уведомлять в TG")
- parser.add_argument("-mb", "--use-merchant-blacklist", action="store_true", help="Использовать черный список продавцов с ограничением на списание бонусов")
- parser.add_argument("-art", "--alert-repeat-timeout", type=float, help="Если походящий по параметрам товар уже был отправлен в TG, повторно уведомлять по истечении заданного времени, в часах")
- parser.add_argument("-t", "--threads", type=int, help="Количество потоков. По умолчанию: 1 на каждое соединиение")
- parser.add_argument("-d", "--delay", type=float, help="Задержка между запросами в секундах при работе в одном потоке. По умолчанию: 1.8")
- parser.add_argument("-ed", "--error-delay", type=float, help="Задержка между запосами в секундах в случае ошибки при работе в одном потоке. По умолчанию: 5")
- parser.add_argument('-log', '--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], default='INFO', help='Уровень лога. По умолчанию: INFO')
+ parser.add_argument("-job-name", type=str, help="Название задачи, без этого параметра будет автоопределено")
+ parser.add_argument("-config", type=str, help="Путь к конфигу парсера")
+ parser.add_argument("-include", type=str, help="Парсить только товары, название которых совпадает с выражением")
+ parser.add_argument("-exclude", type=str, help="Пропускать товары, название которых совпадает с выражением")
+ parser.add_argument("-blacklist", type=str, help="Путь к файлу со списком игнорируемых продавцов")
+ parser.add_argument("-all-cards", action="store_true", help="Всегда парсить карточки товаров")
+ parser.add_argument("-no-cards", action="store_true", help="Не парсить карточки товаров")
+ parser.add_argument("-cookies", type=str, help="Путь к файлу с cookies в формате JSON (Cookie-Editor - Export Json)")
+ parser.add_argument("-account-alert", type=str, help="Если вы используйте cookie, и вход в аккаунт не выполнен, присылать уведомление в TG")
+ parser.add_argument("-address", type=str, help="Адрес, будет использовано первое сопадение")
+ parser.add_argument("-proxy", type=str, help="Строка прокси в формате protocol://username:password@ip:port")
+ parser.add_argument("-proxy-list", type=str, help="Путь к файлу с прокси в формате protocol://username:password@ip:port")
+ parser.add_argument("-allow-direct", action="store_true", help="Использовать прямое соединение параллельно с прокси для ускорения работы в многопотоке")
+ parser.add_argument("-tg-config", type=str, help="Telegram Bot Token и Telegram Chat Id в формате token$id")
+ parser.add_argument("-price-value-alert", type=float, help="Если цена товара равна или ниже данного значения, уведомлять в TG")
+ parser.add_argument("-price-bonus-value-alert", type=float, help="Если цена-бонусы товара равна или ниже данного значения, уведомлять в TG")
+ parser.add_argument("-bonus-value-alert", type=float, help="Если количество бонусов товара равно или выше данного значения, уведомлять в TG")
+ parser.add_argument("-bonus-percent-alert", type=float, help="Если процент бонусов товара равно или выше данного значения, уведомлять в TG")
+ parser.add_argument("-use-merchant-blacklist", action="store_true", help="Использовать черный список продавцов с ограничением на списание бонусов")
+ parser.add_argument("-alert-repeat-timeout", type=float, help="Если походящий по параметрам товар уже был отправлен в TG, повторно уведомлять по истечении заданного времени, в часах")
+ parser.add_argument("-threads", type=int, help="Количество потоков. По умолчанию: 1 на каждое соединиение")
+ parser.add_argument("-delay", type=float, help="Задержка между запросами в секундах при работе в одном потоке. По умолчанию: 1.8")
+ parser.add_argument("-error-delay", type=float, help="Задержка между запосами в секундах в случае ошибки при работе в одном потоке. По умолчанию: 5")
+ parser.add_argument("-log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO", help="Уровень лога. По умолчанию: INFO")
args = parser.parse_args()
print_logo()
@@ -74,11 +79,12 @@ def main():
if Path(args.config).exists():
try:
config = read_json_file(args.config)
- except:
- raise Exception('Ошибка чтения конфига!')
+ except Exception as exc:
+ raise ConfigError("Ошибка чтения конфига!") from exc
run_url_parser(args, config)
else:
- raise Exception('Файл конфига не найден!')
+ raise ConfigError("Файл конфига не найден!")
+
if __name__ == "__main__":
main()
diff --git a/core/parser_base.py b/core/parser_base.py
deleted file mode 100644
index 1a849a3..0000000
--- a/core/parser_base.py
+++ /dev/null
@@ -1,155 +0,0 @@
-"""base parser class"""
-
-import logging
-from datetime import datetime
-from pathlib import Path
-import json
-from time import sleep, time
-
-from curl_cffi import requests
-from rich.logging import RichHandler
-import core.telegram_utils as telegram_utils
-from core.utils import proxy_format_check
-
-
-class Parser_base:
- def __init__(
- self,
- cookie_file_path: str = "",
- proxy: str = "",
- allow_direct: bool = False,
- proxy_file_path: str = "",
- tg_config: str = "",
- delay: float = None,
- error_delay: float = None,
- log_level: str = "INFO",
- ):
- self.cookie_file_path = cookie_file_path
- self.proxy = proxy
- self.allow_direct = allow_direct
- self.proxy_file_path = proxy_file_path
- self.tg_config = tg_config
- self.connection_success_delay = delay or 1.8
- self.connection_error_delay = error_delay or 10.0
- self.log_level = log_level
-
- self.start_time = datetime.now()
-
- self.region_id = "50"
- self.session = None
- self.proxies: list = []
- self.parsed_proxies: list = []
- self.cookie_dict: dict = None
- self.profile: dict = {}
- self.rich_progress = None
- self.job_name: str = ""
-
- self.logger = None
-
- self.tg_client = None
-
- def _set_up(self):
- self._logger_setup(self.log_level)
- if self.tg_config:
- if not telegram_utils.validate_credentials(self.tg_config):
- raise Exception(f"Конфиг {self.tg_config} не прошел проверку!")
- self.tg_client = telegram_utils.Telegram(self.tg_config, self.logger)
- self._read_proxy_file() if self.proxy_file_path else None
- self._proxies_set_up()
- self._read_cookie_file() if self.cookie_file_path else None
-
- class Proxy:
- def __init__(self, proxy: str | None):
- self.proxy_string: str | None = proxy
- self.usable_at: int = 0
- self.busy = False
-
- def _logger_setup(self, log_level):
- logging.basicConfig(
- level=log_level,
- format="%(message)s",
- datefmt="%H:%M:%S",
- handlers=[RichHandler(rich_tracebacks=True)],
- )
- self.logger = logging.getLogger("rich")
-
- def _read_proxy_file(self):
- if Path(self.proxy_file_path).exists():
- proxy_file_contents: str = open(self.proxy_file_path, "r", encoding="utf-8").read()
- self.parsed_proxies = [line for line in proxy_file_contents.split("\n") if line]
- else:
- raise Exception(f"Путь {self.cookie_file_path} не найден!")
-
- def _read_cookie_file(self):
- if Path(self.cookie_file_path).exists():
- cookies = json.loads(open(self.cookie_file_path, "r", encoding="utf-8").read())
- self.cookie_dict = {cookie["name"]: cookie["value"] for cookie in cookies}
- else:
- raise Exception(f"Путь {self.cookie_file_path} не найден!")
-
- def _proxies_set_up(self):
- if self.proxy:
- is_valid_proxy = proxy_format_check(self.proxy)
- if not is_valid_proxy:
- raise Exception(f"Прокси {self.proxy} не верного формата!")
- self.proxies = [self.Proxy(self.proxy)]
- elif self.parsed_proxies:
- for proxy in self.parsed_proxies:
- is_valid_proxy = proxy_format_check(proxy)
- if not is_valid_proxy:
- raise Exception(f"Прокси {proxy} не верного формата!")
- self.proxies = [self.Proxy(proxy) for proxy in self.parsed_proxies]
- if self.proxies and self.allow_direct:
- self.proxies.append(self.Proxy(None))
- elif not self.proxies:
- self.proxies = [self.Proxy(None)]
-
- def _get_proxy(self) -> str:
- while True:
- free_proxies = [proxy for proxy in self.proxies if not proxy.busy]
- if free_proxies:
- oldest_proxy = min(free_proxies, key=lambda obj: obj.usable_at)
- current_time = time()
- if oldest_proxy.usable_at <= current_time:
- return oldest_proxy
- else:
- sleep(oldest_proxy.usable_at - time())
- return self._get_proxy()
- else:
- # No free proxies, wait and retry
- sleep(1)
-
- def _api_request(self, api_url: str, json_data: dict, tries: int = 10, delay: float = 0) -> dict:
- json_data["auth"] = {
- "locationId": self.region_id,
- "appPlatform": "WEB",
- "appVersion": 1710405202,
- "experiments": {},
- "os": "UNKNOWN_OS",
- }
- for i in range(0, tries):
- proxy: self.Proxy = self._get_proxy()
- proxy.busy = True
- self.logger.debug("Прокси : %s", proxy.proxy_string)
- try:
- response = requests.post(api_url, json=json_data, proxy=proxy.proxy_string, verify=False, impersonate="chrome")
- response_data: dict = response.json()
- except:
- response = None
- if response and response.status_code == 200 and not response_data.get("error"):
- proxy.usable_at = time() + delay
- proxy.busy = False
- return response_data
- if response and response.status_code == 200 and response_data.get("code") == 7:
- self.logger.debug("Соединение %s: слишком частые запросы", proxy.proxy_string)
- proxy.usable_at = time() + self.connection_error_delay
- else:
- sleep(1 * i)
- proxy.busy = False
-
- raise Exception("Ошибка получения данных api")
-
- def _get_profile(self):
- json_data = {}
- response_json = self._api_request("https://megamarket.ru/api/mobile/v1/securityService/profile/get", json_data)
- self.profile = response_json["profile"]
diff --git a/core/parser_url.py b/core/parser_url.py
index 21d75ba..76b58b8 100644
--- a/core/parser_url.py
+++ b/core/parser_url.py
@@ -1,33 +1,67 @@
-"""url"""
+"""mmparser"""
+import logging
+from datetime import datetime
+from time import sleep, time
+from dataclasses import dataclass, field
+from typing import Optional
import threading
import concurrent.futures
-import datetime
import sys
import json
import signal
from pathlib import Path
-from urllib.parse import urlparse, parse_qsl, parse_qs, unquote
-from rich.progress import (
- BarColumn,
- Progress,
- SpinnerColumn,
- TextColumn,
- TimeRemainingColumn,
-)
-from core.utils import validate_regex, slugify, load_blacklist
-import core.db as db
-from core.parser_base import Parser_base
-
-
-class Parser_url(Parser_base):
+from urllib.parse import urlparse, parse_qsl, parse_qs, unquote, urljoin
+
+from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeRemainingColumn
+from rich.logging import RichHandler
+from curl_cffi import requests
+
+from .exceptions import ConfigError, ApiError
+from . import db_utils, utils
+from .telegram import TelegramClient, validate_tg_credentials
+
+
+@dataclass
+class ParsedOffer:
+ title: str
+ url: str
+ image_url: str
+ price: float
+ price_bonus: int
+ bonus_amount: int
+ available_quantity: int
+ goods_id: str
+ delivery_date: datetime
+ merchant_id: str
+ merchant_name: str
+ merchant_rating: Optional[bool] = field(default=None)
+ notified: Optional[bool] = field(default=None)
+ scraped: Optional[datetime] = field(default=None)
+
+ @property
+ def bonus_percent(self) -> int:
+ """Рассичать процент бонусов от цены"""
+ if self.price == 0:
+ return 0
+ return int((self.bonus_amount / self.price) * 100)
+
+
+class Connection:
+ def __init__(self, proxy: str | None):
+ self.proxy_string: str | None = proxy
+ self.usable_at: int = 0
+ self.busy = False
+
+
+class Parser_url:
def __init__(
self,
url: str,
job_name: str = "",
include: str = "",
exclude: str = "",
- blacklist:str = "",
+ blacklist: str = "",
all_cards: bool = False,
no_cards: bool = False,
cookie_file_path: str = "",
@@ -48,66 +82,164 @@ def __init__(
error_delay: float = None,
log_level: str = "INFO",
):
- super().__init__(
- cookie_file_path,
- proxy,
- allow_direct,
- proxy_file_path,
- tg_config,
- delay,
- error_delay,
- log_level,
- )
-
- self.url = url
- self.job_name = job_name
- self.include = include
- self.exclude = exclude
- self.blacklist_path = blacklist
- self.all_cards = all_cards
- self.no_cards = no_cards
- self.address = address
+ self.cookie_file_path = cookie_file_path
self.proxy = proxy
- self.account_alert = account_alert
- self.use_merchant_blacklist = use_merchant_blacklist
- self.merchant_blacklist = load_blacklist() if use_merchant_blacklist else []
- self.price_value_alert = price_value_alert or float("-inf")
- self.price_bonus_value_alert = price_bonus_value_alert or float("-inf")
- self.bonus_value_alert = bonus_value_alert or float("inf")
- self.bonus_percent_alert = bonus_percent_alert or float("inf")
- self.alert_repeat_timeout = alert_repeat_timeout or 0
- self.threads = threads
-
- self.blacklist:list = []
+ self.allow_direct = allow_direct
+ self.proxy_file_path = proxy_file_path
+ self.tg_config = tg_config
+ self.connection_success_delay = delay or 1.8
+ self.connection_error_delay = error_delay or 10.0
+ self.log_level = log_level
+
+ self.start_time: datetime = None
+
+ self.region_id = "50"
+ self.session = None
+ self.connections: list[Connection] = []
+ self.parsed_proxies: set | None = None
+ self.cookie_dict: dict = None
+ self.profile: dict = {}
+ self.rich_progress = None
+ self.job_name: str = ""
+
+ self.logger: logging.Logger = self._create_logger(self.log_level)
+ self.tg_client: TelegramClient = None
+
+ self.url: str = url
+ self.job_name: str = job_name
+ self.include: str = include
+ self.exclude: str = exclude
+ self.blacklist_path: str = blacklist
+ self.all_cards: bool = all_cards
+ self.no_cards: bool = no_cards
+ self.address: str = address
+ self.proxy: str = proxy
+ self.account_alert: bool = account_alert
+ self.use_merchant_blacklist: bool = use_merchant_blacklist
+ self.merchant_blacklist: list = utils.load_blacklist() if use_merchant_blacklist else []
+ self.price_value_alert: float = price_value_alert or float("-inf")
+ self.price_bonus_value_alert: float = price_bonus_value_alert or float("-inf")
+ self.bonus_value_alert: float = bonus_value_alert or float("inf")
+ self.bonus_percent_alert: float = bonus_percent_alert or float("inf")
+ self.alert_repeat_timeout: float = alert_repeat_timeout or 0
+ self.threads: int = threads
+
+ self.blacklist: list = []
self.parsed_url: dict = None
self.scraped_tems_counter: int = 0
self.rich_progress = None
self.job_id: int = None
- self.address_id = None
-
- self.items_per_page: int = 44
+ self.address_id: str = None
self.lock = threading.Lock()
self._set_up()
- def _set_up(self):
+ def _create_logger(self, log_level: str) -> logging.Logger:
+ logging.basicConfig(
+ level=log_level,
+ format="%(message)s",
+ datefmt="%H:%M:%S",
+ handlers=[RichHandler(rich_tracebacks=True)],
+ )
+ return logging.getLogger("rich")
+
+ def _proxies_set_up(self) -> None:
+ """Настройка и валидация прокси"""
+ if self.proxy:
+ is_valid_proxy = utils.proxy_format_check(self.proxy)
+ if not is_valid_proxy:
+ raise ConfigError(f"Прокси {self.proxy} не верного формата!")
+ self.connections = [Connection(self.proxy)]
+ elif self.parsed_proxies:
+ for proxy in self.parsed_proxies:
+ is_valid_proxy = utils.proxy_format_check(proxy)
+ if not is_valid_proxy:
+ raise ConfigError(f"Прокси {proxy} не верного формата!")
+ self.connections = [Connection(proxy) for proxy in self.parsed_proxies]
+ if self.connections and self.allow_direct:
+ self.connections.append(Connection(None))
+ elif not self.connections:
+ self.connections = [Connection(None)]
+
+ def _get_connection(self) -> str:
+ """Получить самое позднее использованное `Соединение`"""
+ while True:
+ free_proxies = [proxy for proxy in self.connections if not proxy.busy]
+ if free_proxies:
+ oldest_proxy = min(free_proxies, key=lambda obj: obj.usable_at)
+ current_time = time()
+ if oldest_proxy.usable_at <= current_time:
+ return oldest_proxy
+ sleep(oldest_proxy.usable_at - time())
+ return self._get_connection()
+ # No free proxies, wait and retry
+ sleep(1)
+
+ def _api_request(self, api_url: str, json_data: dict, tries: int = 10, delay: float = 0) -> dict:
+ json_data["auth"] = {
+ "locationId": self.region_id,
+ "appPlatform": "WEB",
+ "appVersion": 1710405202,
+ "experiments": {},
+ "os": "UNKNOWN_OS",
+ }
+ for i in range(0, tries):
+ proxy: Connection = self._get_connection()
+ proxy.busy = True
+ self.logger.debug("Прокси : %s", proxy.proxy_string)
+ try:
+ response = requests.post(api_url, json=json_data, proxy=proxy.proxy_string, verify=False, impersonate="chrome")
+ response_data: dict = response.json()
+ except Exception:
+ response = None
+ if response and response.status_code == 200 and not response_data.get("error"):
+ proxy.usable_at = time() + delay
+ proxy.busy = False
+ return response_data
+ if response and response.status_code == 200 and response_data.get("code") == 7:
+ self.logger.debug("Соединение %s: слишком частые запросы", proxy.proxy_string)
+ proxy.usable_at = time() + self.connection_error_delay
+ else:
+ sleep(1 * i)
+ proxy.busy = False
+
+ raise ApiError("Ошибка получения данных api")
+
+ def _get_profile(self) -> None:
+ """Получить и сохранить информацию профиля ММ"""
+ response_json = self._api_request("https://megamarket.ru/api/mobile/v1/securityService/profile/get", json_data={})
+ self.profile = response_json["profile"]
+
+ def _set_up(self) -> None:
+ """Парсинг в валидация конфигурации"""
+ if self.tg_config:
+ if not validate_tg_credentials(self.tg_config):
+ raise ConfigError(f"Конфиг {self.tg_config} не прошел проверку!")
+ self.tg_client = TelegramClient(self.tg_config, self.logger)
+ self.parsed_proxies = self.proxy_file_path and utils.parse_proxy_file(self.proxy_file_path)
+ self._proxies_set_up()
+ self.cookie_dict = self.cookie_file_path and utils.parse_cookie_file(self.cookie_file_path)
+
# Make Ctrl-C work when deamon threads are running
signal.signal(signal.SIGINT, signal.SIG_DFL)
- super()._set_up()
- regex_check = validate_regex(self.include) if self.include else None
+
+ regex_check = self.include and utils.validate_regex(self.include)
if regex_check is False:
- raise Exception(f'Неверное выражение "{self.include}"!')
- regex_check = validate_regex(self.exclude) if self.exclude else None
+ raise ConfigError(f'Неверное выражение "{self.include}"!')
+ regex_check = self.exclude and utils.validate_regex(self.exclude)
if regex_check is False:
- raise Exception(f'Неверное выражение "{self.exclude}"!')
+ raise ConfigError(f'Неверное выражение "{self.exclude}"!')
if self.blacklist_path:
self._read_blacklist_file()
- self.threads = self.threads or len(self.proxies)
- if not Path(db.FILENAME).exists():
- db.create_db()
-
- def parse(self):
+ self.threads = self.threads or len(self.connections)
+ if not Path(db_utils.FILENAME).exists():
+ db_utils.create_db()
+
+ def parse(self) -> None:
+ """Метод запуска парсинга"""
+ utils.check_for_new_version()
+ self.start_time = datetime.now()
self.logger.info("Целевой URL: %s", self.url)
self.logger.info("Потоков: %s", self.threads)
if self.cookie_file_path:
@@ -116,81 +248,60 @@ def parse(self):
self.logger.info("Аккаунт: %s", self.profile["phone"])
else:
self.logger.warning("Вход в аккаунт не выполнен")
- if self.account_alert:
- self.tg_client.notify(
- f"Вход в аккаунт {self.cookie_file_path} не выполнен!"
- )
- raise Exception(
- f"Вход в аккаунт {self.cookie_file_path} не выполнен!"
- )
+ if self.account_alert and self.tg_client:
+ self.tg_client.notify(f"Вход в аккаунт {self.cookie_file_path} не выполнен!")
+ raise Exception(f"Вход в аккаунт {self.cookie_file_path} не выполнен!")
if self.address:
- self._get_address_from_string()
+ self._get_address_from_string(self.address)
elif self.profile.get("isAuthenticated"):
self._get_profile_default_address()
if (not self.address_id or not self.region_id) and self.cookie_dict:
- self._get_cookie_address()
+ self._get_address_info(self.cookie_dict)
self.parse_input_url()
if self.parsed_url and not self.job_name:
search_text = self.parsed_url.get("searchText", {})
- collection_title = (self.parsed_url.get("collection", {}) or {}).get(
- "title"
- )
+ collection_title = (self.parsed_url.get("collection", {}) or {}).get("title")
merchant = (self.parsed_url.get("merchant", {}) or {}).get("slug")
unknown = "Не_определено"
self.job_name = search_text or collection_title or merchant or unknown
- self.job_name = slugify(self.job_name)
+ self.job_name = utils.slugify(self.job_name)
if self.parsed_url["type"] == "TYPE_PRODUCT_CARD":
self._parse_card()
- self.logger.info(
- "%s %s", self.job_name, self.start_time.strftime("%d-%m-%Y %H:%M:%S")
- )
+ self.logger.info("%s %s", self.job_name, self.start_time.strftime("%d-%m-%Y %H:%M:%S"))
else:
- self.job_id = db.new_job(self.job_name)
- self.logger.info(
- "%s %s", self.job_name, self.start_time.strftime("%d-%m-%Y %H:%M:%S")
- )
- self._parse_all()
+ self.job_id = db_utils.new_job(self.job_name)
+ self.logger.info("%s %s", self.job_name, self.start_time.strftime("%d-%m-%Y %H:%M:%S"))
+ self._parse_multi_page()
self.logger.info("Спаршено %s товаров", self.scraped_tems_counter)
- db.finish_job(self.job_id)
+ db_utils.finish_job(self.job_id)
def _read_blacklist_file(self):
blacklist_file_contents: str = open(self.blacklist_path, "r", encoding="utf-8").read()
self.blacklist = [line for line in blacklist_file_contents.split("\n") if line]
- def _export_to_db(
- self,
- goodsId,
- merchantId,
- url,
- title,
- finalPrice,
- finalPriceBonus,
- bonusAmount,
- bonusPercent,
- availableQuantity,
- deliveryPossibilities,
- merchantName,
- notified,
- ):
+ def _export_to_db(self, parsed_offer: ParsedOffer) -> None:
+ """Экспорт одного предложения в базу данных"""
with self.lock:
- db.add_to_db(
+ db_utils.add_to_db(
self.job_id,
self.job_name,
- goodsId,
- merchantId,
- url,
- title,
- finalPrice,
- finalPriceBonus,
- bonusAmount,
- bonusPercent,
- availableQuantity,
- deliveryPossibilities,
- merchantName,
- notified,
+ parsed_offer.goods_id,
+ parsed_offer.merchant_id,
+ parsed_offer.url,
+ parsed_offer.title,
+ parsed_offer.price,
+ parsed_offer.price_bonus,
+ parsed_offer.bonus_amount,
+ parsed_offer.bonus_percent,
+ parsed_offer.available_quantity,
+ parsed_offer.delivery_date,
+ parsed_offer.merchant_name,
+ parsed_offer.merchant_rating,
+ parsed_offer.notified,
)
- def parse_input_url(self, tries=10):
+ def parse_input_url(self, tries: int = 10) -> dict:
+ """Парсинг url мм с использованием api самого мм"""
json_data = {"url": self.url}
response_json = self._api_request(
"https://megamarket.ru/api/mobile/v1/urlService/url/parse",
@@ -200,39 +311,29 @@ def parse_input_url(self, tries=10):
parsed_url = response_json["params"]
parsed_url = self._filters_convert(parsed_url)
parsed_url["type"] = response_json["type"]
- sorting = int(
- dict(parse_qsl(unquote(urlparse(self.url).fragment.lstrip("?")))).get(
- "sort", 0
- )
- )
+ sorting = int(dict(parse_qsl(unquote(urlparse(self.url).fragment.lstrip("?")))).get("sort", 0))
search_query_from_url = parse_qs(urlparse(self.url).query).get("q", "") or ""
- search_query_from_url = (
- search_query_from_url[0] if search_query_from_url else None
- )
+ search_query_from_url = search_query_from_url[0] if search_query_from_url else None
parsed_url["searchText"] = parsed_url["searchText"] or search_query_from_url
parsed_url["sorting"] = sorting
self.parsed_url = parsed_url
return parsed_url
- def _get_profile_default_address(self):
+ def _get_profile_default_address(self) -> None:
+ """Получить данные адреса аккаунта мм"""
json_data = {}
- response_json = self._api_request(
- "https://megamarket.ru/api/mobile/v1/profileService/address/list", json_data
- )
- address = [
- address
- for address in response_json["profileAddresses"]
- if address["isDefault"] is True
- ]
+ response_json = self._api_request("https://megamarket.ru/api/mobile/v1/profileService/address/list", json_data)
+ address = [address for address in response_json["profileAddresses"] if address["isDefault"] is True]
if address:
self.address_id = address[0]["addressId"]
self.region_id = address[0]["regionId"]
self.logger.info(f"Регион: {address[0]['region']}")
self.logger.info(f"Адрес: {address[0]['full']}")
- def _get_cookie_address(self):
- address = self.cookie_dict.get("address_info")
- region = self.cookie_dict.get("region_info")
+ def _get_address_info(self, cookie_dict: dict) -> None:
+ """Получить данные адреса принадлежащие аккаунту из cookies"""
+ address = cookie_dict.get("address_info")
+ region = cookie_dict.get("region_info")
if region:
region = json.loads(unquote(region))
self.region_id = self.region_id or region["id"]
@@ -242,30 +343,30 @@ def _get_cookie_address(self):
self.address_id = self.address_id or address["addressId"]
self.logger.info("Адрес: %s", address["full"])
- def _get_address_from_string(self):
- json_data = {"count": 10, "isSkipRegionFilter": True, "query": self.address}
+ def _get_address_from_string(self, address: str) -> None:
+ """Установить адрес доставки по строке адреса"""
+ json_data = {"count": 10, "isSkipRegionFilter": True, "query": address}
response_json = self._api_request(
"https://megamarket.ru/api/mobile/v1/addressSuggestService/address/suggest",
json_data,
)
- address = response_json["items"]
+ address = response_json.get("items")
if address:
self.address_id = address[0]["addressId"]
self.region_id = address[0]["regionId"]
self.logger.info("Регион: %s", address[0]["region"])
self.logger.info("Адрес: %s", address[0]["full"])
else:
- sys.exit(f"По запросу {self.address} адрес не найден!")
+ sys.exit(f"По запросу {address} адрес не найден!")
- def _get_merchant_inn(self, merchant_id):
+ def _get_merchant_inn(self, merchant_id: str) -> str:
+ """Получить ИНН по ID продавца"""
json_data = {"merchantId": merchant_id}
- response_json = self._api_request(
- "https://megamarket.ru/api/mobile/v1/partnerService/merchant/legalInfo/get",
- json_data,
- )
+ response_json = self._api_request("https://megamarket.ru/api/mobile/v1/partnerService/merchant/legalInfo/get", json_data)
return response_json["merchant"]["legalInfo"]["inn"]
- def _parse_item(self, item):
+ def _parse_item(self, item: dict):
+ """Парсинг дефолтного предложения товара"""
if item["favoriteOffer"]["merchantName"] in self.blacklist:
self.logger.debug("Пропуск %s", item["favoriteOffer"]["merchantName"])
return
@@ -276,48 +377,30 @@ def _parse_item(self, item):
self.logger.debug("Пропуск %s", item["favoriteOffer"]["merchantName"])
return
- delivery_possibilities = set()
- for delivery in item["favoriteOffer"]["deliveryPossibilities"]:
- delivery_info = f"{delivery['displayName']}, {delivery.get('displayDeliveryDate', '')} - {delivery.get('amount', 0)}р"
- delivery_possibilities.add(delivery_info)
- delivery_possibilities = (" \n").join(delivery_possibilities)
- price_bonus = (
- item["favoriteOffer"]["finalPrice"] - item["favoriteOffer"]["bonusAmount"]
- )
self.scraped_tems_counter += 1
- goodsId = item["goods"]["goodsId"].split("_")[0]
-
- notified = self._notify_if_notify_check(
- item["goods"]["title"],
- item["favoriteOffer"]["finalPrice"],
- price_bonus,
- item["favoriteOffer"]["bonusAmount"],
- item["favoriteOffer"]["bonusPercent"],
- item["goods"]["webUrl"],
- item["favoriteOffer"]["merchantName"],
- delivery_possibilities,
- item["favoriteOffer"]["availableQuantity"],
- item["goods"]["titleImage"],
- goodsId,
- item["favoriteOffer"]["merchantId"],
- )
- self._export_to_db(
- goodsId,
- item["favoriteOffer"]["merchantId"],
- item["goods"]["webUrl"],
- item["goods"]["title"],
- item["favoriteOffer"]["finalPrice"],
- price_bonus,
- item["favoriteOffer"]["bonusAmount"],
- item["favoriteOffer"]["bonusPercent"],
- item["favoriteOffer"]["availableQuantity"],
- delivery_possibilities,
- item["favoriteOffer"]["merchantName"],
- notified,
+ delivery_date_iso: str = item["favoriteOffer"]["deliveryPossibilities"][0].get("displayDeliveryDate", "")
+ delivery_date = delivery_date_iso.split("T")[0]
+
+ parsed_offer = ParsedOffer(
+ title=item["goods"]["title"],
+ price=item["favoriteOffer"]["finalPrice"],
+ delivery_date=delivery_date,
+ price_bonus=item["favoriteOffer"]["finalPrice"] - item["favoriteOffer"]["bonusAmount"],
+ goods_id=item["goods"]["goodsId"].split("_")[0],
+ bonus_amount=item["favoriteOffer"]["bonusAmount"],
+ url=item["goods"]["webUrl"],
+ available_quantity=item["favoriteOffer"]["availableQuantity"],
+ merchant_id=item["favoriteOffer"]["merchantId"],
+ merchant_name=item["favoriteOffer"]["merchantName"],
+ image_url=item["goods"]["titleImage"],
)
- def _filters_convert(self, parsed_url):
+ parsed_offer.notified = self._notify_if_notify_check(parsed_offer)
+ self._export_to_db(parsed_offer)
+
+ def _filters_convert(self, parsed_url: dict) -> dict:
+ """Конвертация фильтров каталога или поиска"""
for url_filter in parsed_url["selectedListingFilters"]:
if url_filter["type"] == "EXACT_VALUE":
url_filter["type"] = 0
@@ -327,131 +410,83 @@ def _filters_convert(self, parsed_url):
url_filter["type"] = 2
return parsed_url
- def _parse_offers(self, item, offers):
- for offer in offers:
- if offer["merchantName"] in self.blacklist:
+ def _parse_offer(self, item: dict, offer: dict) -> None:
+ """Парсинг предложения товара"""
+ if offer["merchantName"] in self.blacklist:
+ self.logger.debug("Пропуск %s", offer["merchantName"])
+ return
+
+ if self.use_merchant_blacklist:
+ merchant_inn = self._get_merchant_inn(offer["merchantId"])
+ if merchant_inn in self.merchant_blacklist:
self.logger.debug("Пропуск %s", offer["merchantName"])
- continue
+ return
- if self.use_merchant_blacklist:
- merchant_inn = self._get_merchant_inn(offer["merchantId"])
- if merchant_inn in self.merchant_blacklist:
- self.logger.debug("Пропуск %s", offer["merchantName"])
- continue
-
- delivery_possibilities = set()
- for delivery in offer["deliveryPossibilities"]:
- delivery_info = f"{delivery['displayName']}, {delivery.get('displayDeliveryDate', '')} - {delivery.get('amount', 0)}р"
- delivery_possibilities.add(delivery_info)
- delivery_possibilities = (" \n").join(delivery_possibilities)
- price_bonus = offer["finalPrice"] - offer["bonusAmountFinalPrice"]
- self.scraped_tems_counter += 1
- goodsId = item["goodsId"].split("_")[0]
- notified = self._notify_if_notify_check(
- item["title"],
- offer["finalPrice"],
- price_bonus,
- offer["bonusAmountFinalPrice"],
- offer["bonusPercent"],
- item["webUrl"],
- offer["merchantName"],
- delivery_possibilities,
- offer["availableQuantity"],
- item["titleImage"],
- goodsId,
- offer["merchantId"],
- )
- self._export_to_db(
- goodsId,
- offer["merchantId"],
- item["webUrl"],
- item["title"],
- offer["finalPrice"],
- price_bonus,
- offer["bonusAmountFinalPrice"],
- offer["bonusPercent"],
- offer["availableQuantity"],
- delivery_possibilities,
- offer["merchantName"],
- notified,
- )
+ delivery_date_iso: str = offer["deliveryPossibilities"][0]["date"]
+ delivery_date = delivery_date_iso.split("T")[0]
+
+ # добавление merchantId в конец url
+ offer_url: str = item["webUrl"]
+ if offer_url.endswith("/"):
+ offer_url = offer_url[:-1]
+ offer_url = f'{offer_url}_{offer["merchantId"]}'
+
+ parsed_offer = ParsedOffer(
+ delivery_date=delivery_date,
+ price_bonus=offer["finalPrice"] - offer["bonusAmountFinalPrice"],
+ goods_id=item["goodsId"].split("_")[0],
+ title=item["title"],
+ price=offer["finalPrice"],
+ bonus_amount=offer["bonusAmountFinalPrice"],
+ url=offer_url,
+ available_quantity=offer["availableQuantity"],
+ merchant_id=offer["merchantId"],
+ merchant_name=offer["merchantName"],
+ merchant_rating=offer["merchantSummaryRating"],
+ image_url=item["titleImage"],
+ )
- def _notify_if_notify_check(
- self,
- title,
- finalPrice,
- price_bonus,
- bonusAmount,
- bonusPercent,
- webUrl,
- merchantName,
- delivery_possibilities,
- availableQuantity,
- titleImage,
- goodsId,
- merchantId,
- ):
+ self.scraped_tems_counter += 1
+ parsed_offer.notified = self._notify_if_notify_check(parsed_offer)
+ self._export_to_db(parsed_offer)
+
+ def _notify_if_notify_check(self, parsed_offer: ParsedOffer):
+ """Отправить уведомление в tg если предложение подходит по параметрам"""
time_diff = 0
last_notified = None
if self.alert_repeat_timeout:
- last_notified = db.get_last_notified(
- goodsId, merchantId, finalPrice, bonusAmount
- )
- last_notified = datetime.datetime.strptime(last_notified, "%Y-%m-%d %H:%M:%S") if last_notified else None
+ last_notified = db_utils.get_last_notified(parsed_offer.goods_id, parsed_offer.merchant_id, parsed_offer.price, parsed_offer.bonus_amount)
+ last_notified = datetime.strptime(last_notified, "%Y-%m-%d %H:%M:%S") if last_notified else None
if last_notified:
- now = datetime.datetime.now()
+ now = datetime.now()
time_diff = now - last_notified
if (
- (
- bonusPercent >= self.bonus_percent_alert
- or bonusAmount >= self.bonus_value_alert
- or finalPrice <= self.price_value_alert
- or price_bonus <= self.price_bonus_value_alert
- )
+ (parsed_offer.bonus_percent >= self.bonus_percent_alert or parsed_offer.bonus_amount >= self.bonus_value_alert or parsed_offer.price <= self.price_value_alert or parsed_offer.price_bonus <= self.price_bonus_value_alert)
and (not last_notified or (last_notified and time_diff.total_seconds() > self.alert_repeat_timeout * 3600))
and self.tg_client
):
with concurrent.futures.ThreadPoolExecutor() as executor:
- message = self._tg_message_format(
- title,
- finalPrice,
- price_bonus,
- bonusAmount,
- bonusPercent,
- webUrl,
- merchantName,
- delivery_possibilities,
- availableQuantity,
- )
- executor.submit(self.tg_client.notify, message, titleImage)
+ message = self._format_tg_message(parsed_offer)
+ executor.submit(self.tg_client.notify, message, parsed_offer.image_url)
return True
return False
- def _tg_message_format(
- self,
- title,
- finalPrice,
- price_bonus,
- bonusAmount,
- bonusPercent,
- webUrl,
- merchantName,
- delivery_possibilities,
- availableQuantity,
- ):
+ def _format_tg_message(self, parsed_offer: ParsedOffer) -> str:
+ """Форматировать данные для отправки в telegram"""
return (
- f"🛍 Товар: {title}\n"
- f"💰 Цена: {finalPrice}р\n"
- f"💸 Цена-Бонусы: {price_bonus}\n"
- f"🟢 Бонусы: {bonusAmount}\n"
- f"🔢 Процент Бонусов: {bonusPercent}\n"
- f"✅ Доступно: {availableQuantity or '?'}\n"
- f"📦 Доставка: {delivery_possibilities}\n"
- f"🛒 Продавец: {merchantName}\n"
+ f"🛍 Товар: {parsed_offer.title}\n"
+ f"💰 Цена: {parsed_offer.price}₽\n"
+ f"💸 Цена-Бонусы: {parsed_offer.price_bonus}\n"
+ f"🟢 Бонусы: {parsed_offer.bonus_amount}\n"
+ f"🔢 Процент Бонусов: {parsed_offer.bonus_percent}\n"
+ f"✅ Доступно: {parsed_offer.available_quantity or '?'}\n"
+ f"📦 Доставка: {parsed_offer.delivery_date}\n"
+ f"🛒 Продавец: {parsed_offer.merchant_name} {parsed_offer.merchant_rating}{'⭐' if parsed_offer.merchant_rating else ''}\n"
f"☎️ Аккаунт: {self.profile.get('phone')}"
)
- def _get_offers(self, goods_id, delay=0):
+ def _get_offers(self, goods_id: str, delay: int = 0) -> list[dict]:
+ """Получить список предложений товара"""
json_data = {
"addressId": self.address_id,
"collectionId": None,
@@ -464,21 +499,16 @@ def _get_offers(self, goods_id, delay=0):
"requestVersion": 11,
"shopInfo": {},
}
- response_json = self._api_request(
- "https://megamarket.ru/api/mobile/v1/catalogService/productOffers/get",
- json_data,
- delay=delay,
- )
+ response_json = self._api_request("https://megamarket.ru/api/mobile/v1/catalogService/productOffers/get", json_data, delay=delay)
return response_json["offers"]
- def _get_page(self, offset: int):
+ def _get_page(self, offset: int) -> dict:
+ """Получить страницу каталога или поиска"""
json_data = {
"requestVersion": 10,
- "limit": self.items_per_page,
+ "limit": 44,
"offset": offset,
- "isMultiCategorySearch": self.parsed_url.get(
- "isMultiCategorySearch", False
- ),
+ "isMultiCategorySearch": self.parsed_url.get("isMultiCategorySearch", False),
"searchByOriginalQuery": False,
"selectedSuggestParams": [],
"expandedFiltersIds": [],
@@ -489,28 +519,11 @@ def _get_page(self, offset: int):
"selectedFilters": self.parsed_url.get("selectedListingFilters", []),
}
if self.parsed_url.get("type", "") == "TYPE_MENU_NODE":
- self.parsed_url["collection"] = (
- self.parsed_url["collection"]
- or self.parsed_url["menuNode"]["collection"]
- )
- json_data["collectionId"] = (
- self.parsed_url["collection"]["collectionId"]
- if self.parsed_url["collection"]
- else None
- )
- json_data["searchText"] = (
- self.parsed_url["searchText"] if self.parsed_url["searchText"] else None
- )
- json_data["selectedAssumedCollectionId"] = (
- self.parsed_url["collection"]["collectionId"]
- if self.parsed_url["collection"]
- else None
- )
- json_data["merchant"] = (
- {"id": self.parsed_url["merchant"]["id"]}
- if self.parsed_url["merchant"]
- else None
- )
+ self.parsed_url["collection"] = self.parsed_url["collection"] or self.parsed_url["menuNode"]["collection"]
+ json_data["collectionId"] = self.parsed_url["collection"]["collectionId"] if self.parsed_url["collection"] else None
+ json_data["searchText"] = self.parsed_url["searchText"] if self.parsed_url["searchText"] else None
+ json_data["selectedAssumedCollectionId"] = self.parsed_url["collection"]["collectionId"] if self.parsed_url["collection"] else None
+ json_data["merchant"] = {"id": self.parsed_url["merchant"]["id"]} if self.parsed_url["merchant"] else None
response_json = self._api_request(
"https://megamarket.ru/api/mobile/v1/catalogService/catalog/search",
@@ -518,62 +531,48 @@ def _get_page(self, offset: int):
delay=self.connection_success_delay,
)
- return response_json
-
- def _parse_page(self, offset: int) -> tuple[int, int, bool, int]:
- response_json = self._get_page(offset)
- if response_json.get("error") or not response_json.get("items"):
- raise Exception()
+ if response_json.get("error"):
+ raise ApiError()
if response_json.get("success") is True:
- self.items_per_page = int(response_json.get("limit"))
- page_progress = self.rich_progress.add_task(
- f"[orange]Страница {int(offset/self.items_per_page)+1}"
- )
- self.rich_progress.update(page_progress, total=len(response_json["items"]))
- self.logger.debug("%s успех", offset)
- for item in response_json["items"]:
- item_title = item["goods"]["title"]
- if self._exclude_check(item_title):
- self.rich_progress.update(page_progress, advance=1)
- continue
- if item["isAvailable"] is True and self._include_check(item_title):
- is_listing = self.parsed_url["type"] == "TYPE_LISTING"
- if self.all_cards or (
- not self.no_cards
- and (
- item["hasOtherOffers"]
- or item["offerCount"] > 1
- or is_listing
- )
- ):
- self.logger.info("Парсим предложения %s", item_title)
- offers = self._get_offers(
- item["goods"]["goodsId"],
- delay=self.connection_success_delay,
- )
- self._parse_offers(item["goods"], offers)
- else:
- self._parse_item(item)
- self.rich_progress.update(page_progress, advance=1)
+ return response_json
+
+ def _parse_page(self, response_json: dict) -> tuple[int, int, bool]:
+ """Парсинг страницы каталога или поиска"""
+ items_per_page = int(response_json.get("limit"))
+ page_progress = self.rich_progress.add_task(f"[orange]Страница {int(int(response_json.get('offset'))/items_per_page)+1}")
+ self.rich_progress.update(page_progress, total=len(response_json["items"]))
+ for item in response_json["items"]:
+ item_title = item["goods"]["title"]
+ if self._exclude_check(item_title) or (item["isAvailable"] is not True) or (not self._include_check(item_title)):
+ # пропускаем, если товар не доступен или исключен
+ self.rich_progress.update(page_progress, advance=1)
+ continue
+ is_listing = self.parsed_url["type"] == "TYPE_LISTING"
+ if self.all_cards or (not self.no_cards and (item["hasOtherOffers"] or item["offerCount"] > 1 or is_listing)):
+ self.logger.info("Парсим предложения %s", item_title)
+ offers = self._get_offers(item["goods"]["goodsId"], delay=self.connection_success_delay)
+ for offer in offers:
+ self._parse_offer(item["goods"], offer)
+ else:
+ self._parse_item(item)
+ self.rich_progress.update(page_progress, advance=1)
self.rich_progress.remove_task(page_progress)
- parse_next_page = (
- response_json["items"] and response_json["items"][-1]["isAvailable"]
- )
- parsed_page = offset if response_json.get("success") else False
- return parsed_page, int(response_json["total"]), parse_next_page
+ parse_next_page = response_json["items"] and response_json["items"][-1]["isAvailable"]
+ return parse_next_page
- def _exclude_check(self, title):
+ def _exclude_check(self, title: str) -> bool:
if self.exclude:
return self.exclude.match(title)
return False
- def _include_check(self, title):
+ def _include_check(self, title: str) -> bool:
if self.include:
return self.include.match(title)
return True
- def _create_progress_bar(self):
+ def _create_progress_bar(self) -> None:
+ """Создание и запуск полосы прогресса"""
self.rich_progress = Progress(
"{task.description}",
SpinnerColumn(),
@@ -583,70 +582,61 @@ def _create_progress_bar(self):
)
self.rich_progress.start()
- def _get_card_info(self, goods_id):
+ def _get_card_info(self, goods_id: str) -> dict:
+ """Получить карточку товара"""
json_data = {"goodsId": goods_id, "merchantId": "0"}
- response_json = self._api_request(
- "https://megamarket.ru/api/mobile/v1/catalogService/productCardMainInfo/get",
- json_data,
- )
+ response_json = self._api_request("https://megamarket.ru/api/mobile/v1/catalogService/productCardMainInfo/get", json_data)
return response_json["goods"]
- def _parse_card(self):
- offers = self._get_offers(self.parsed_url["goods"]["goodsId"])
+ def _parse_card(self) -> None:
+ """Парсинг карточки товара"""
item = self._get_card_info(self.parsed_url["goods"]["goodsId"])
- self.job_name = slugify(item["title"])
- self.job_id = db.new_job(self.job_name)
- self._parse_offers(item, offers)
+ offers = self._get_offers(self.parsed_url["goods"]["goodsId"])
+ self.job_name = utils.slugify(item["title"])
+ self.job_id = db_utils.new_job(self.job_name)
+ for offer in offers:
+ self._parse_offer(item, offer)
- def _parse_first_page(self, start_offset):
- parsed_page, total, parse_next_page = self._parse_page(start_offset)
- if not isinstance(parsed_page, int):
- self.logger.info("Ошибка получения первой страницы!")
- return total, parse_next_page
+ def _process_page(self, offset: int, main_job) -> bool:
+ """Получение и парсинг страницы каталога или поиска"""
+ response_json = self._get_page(offset)
+ parse_next_page = self._parse_page(response_json)
+ self.rich_progress.update(main_job, advance=1)
+ return parse_next_page
- def _parse_all(self):
+ def _parse_multi_page(self) -> None:
+ """Запуск и менеджмент парсинга каталога или поиска"""
start_offset = 0
+ response_json = self._get_page(start_offset)
+ if len(response_json["items"]) == 0 and response_json["processor"]["type"] in ("MENU_NODE", "COLLECTION"):
+ self.logger.debug("Редирект в каталог")
+ self.url = urljoin("https://megamarket.ru", response_json["processor"]["url"])
+ return self.parse()
+ items_per_page = int(response_json.get("limit"))
+ item_count_total = int(response_json["total"])
+
+ pages_to_parse = list(range(start_offset, item_count_total, items_per_page))
self._create_progress_bar()
- total, parse_next_page = self._parse_first_page(start_offset)
- if not parse_next_page:
- return
- pages = [
- i
- for i in range(
- start_offset + self.items_per_page, total, self.items_per_page
- )
- if i < total - 1
- ]
-
- main_job = self.rich_progress.add_task("[green]Общий прогресс")
- self.rich_progress.update(main_job, total=len(pages) + 1, advance=1)
- max_threads = self.threads or (
- len(pages) if len(pages) < self.threads else self.threads
- )
- while pages:
- with concurrent.futures.ThreadPoolExecutor(
- max_workers=max_threads
- ) as executor:
- futures = {
- executor.submit(self._parse_page, page): page for page in pages
- }
+ main_job = self.rich_progress.add_task("[green]Общий прогресс", total=len(pages_to_parse))
+ max_threads = min(len(pages_to_parse), self.threads)
+ while pages_to_parse:
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
+ futures = {executor.submit(self._process_page, page, main_job): page for page in pages_to_parse}
for future in concurrent.futures.as_completed(futures):
try:
- parsed_page, _, parse_next_page = future.result()
+ parse_next_page = future.result()
except Exception:
- parsed_page = False
- if parsed_page:
- self.rich_progress.update(main_job, advance=1)
- pages.remove(parsed_page) if parsed_page in pages else None
- if parse_next_page:
- continue
+ continue
+ page = futures[future]
+ if page in pages_to_parse:
+ pages_to_parse.remove(page)
+ if not parse_next_page:
self.logger.info("Дальше товары не в наличии, их не парсим")
for fut in futures:
future_page = futures[fut]
- if future_page > parsed_page:
- pages.remove(
- future_page
- ) if future_page in pages else None
- self.rich_progress.update(main_job, total=len(pages))
+ if future_page > page:
+ if future_page in pages_to_parse:
+ pages_to_parse.remove(future_page)
+ self.rich_progress.update(main_job, total=len(pages_to_parse))
fut.cancel()
self.rich_progress.stop()
diff --git a/core/telegram_utils.py b/core/telegram.py
similarity index 71%
rename from core/telegram_utils.py
rename to core/telegram.py
index a495820..58dcd7c 100644
--- a/core/telegram_utils.py
+++ b/core/telegram.py
@@ -1,6 +1,7 @@
from curl_cffi import requests
-def validate_credentials(tg_config):
+
+def validate_tg_credentials(tg_config: str):
def is_valid_token(bot_token):
url = f"https://api.telegram.org/bot{bot_token}/getMe"
return requests.get(url).ok
@@ -8,9 +9,9 @@ def is_valid_token(bot_token):
def is_valid_chat_id(bot_token, chat_id):
url = f"https://api.telegram.org/bot{bot_token}/getChat?chat_id={chat_id}"
return requests.get(url).ok
+
try:
- bot_token = tg_config.split('$')[0]
- chat_id = tg_config.split('$')[1]
+ bot_token, chat_id = tg_config.split("$")
except IndexError:
return False
if not bot_token or not chat_id:
@@ -19,22 +20,19 @@ def is_valid_chat_id(bot_token, chat_id):
return False
return True
-class Telegram:
+
+class TelegramClient:
def __init__(self, tg_config, logger):
- self.bot_token = tg_config.split('$')[0]
- self.chat_id = tg_config.split('$')[1]
+ self.bot_token = tg_config.split("$")[0]
+ self.chat_id = tg_config.split("$")[1]
self.logger = logger
if not self.bot_token or not self.chat_id:
raise Exception("Не валидный конфиг Telegram!")
- def notify(self, message, image_url = None):
+
+ def notify(self, message, image_url=None):
if image_url:
base_url = f"https://api.telegram.org/bot{self.bot_token}/sendPhoto"
- params = {
- 'chat_id': self.chat_id,
- 'caption': message,
- 'photo': image_url,
- 'parse_mode': 'HTML'
- }
+ params = {"chat_id": self.chat_id, "caption": message, "photo": image_url, "parse_mode": "HTML"}
try:
response = requests.get(base_url, params=params)
response.raise_for_status()
@@ -43,14 +41,10 @@ def notify(self, message, image_url = None):
self.logger.info(f"Ошибка отправки сообщения с картинкой: {e}")
else:
base_url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
- params = {
- 'chat_id': self.chat_id,
- 'text': message,
- 'parse_mode': 'HTML'
- }
+ params = {"chat_id": self.chat_id, "text": message, "parse_mode": "HTML"}
try:
response = requests.get(base_url, params=params)
response.raise_for_status()
self.logger.info("Сообщение успешно отправлено!")
except Exception as e:
- self.logger.info(f"Ошибка отправки сообщения: {e}")
\ No newline at end of file
+ self.logger.info(f"Ошибка отправки сообщения: {e}")
diff --git a/core/utils.py b/core/utils.py
index d0c4b79..f63af1a 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -1,9 +1,14 @@
-import pathlib
+from pathlib import Path
import re
import json
import time
+import pkg_resources
from curl_cffi import requests
from rich.console import Console
+from packaging import version
+
+from . import exceptions
+
try:
from lxml import html
except ImportError:
@@ -13,7 +18,7 @@
BLACKLIST_URL = "https://megamarket.ru/promo/prodavtsy-s-oghranichieniiem-na-spisaniie-bonusov/"
BLACKLIST_FILE = "merchant_blacklist.txt"
-UPDATE_INTERVAL = 86400 # 24 hours
+UPDATE_INTERVAL = 86400 # 24 часа
def print_logo():
@@ -28,38 +33,36 @@ def print_logo():
console.print("[green bold] ❤️ Сказать спасибо автору: https://yoomoney.ru/to/410018051351692")
console.print("")
-def slugify(text):
+
+def slugify(text: str) -> str:
# Convert to lowercase
lowercase_text = text.lower()
# Remove non-word characters and replace spaces with hyphens
- slug = re.sub(r'[^\w\s-]', '_', lowercase_text).strip().replace(' ', '_')
+ slug = re.sub(r"[^\w\s-]", "_", lowercase_text).strip().replace(" ", "_")
return slug
-def proxy_format_check(string):
- protocols = ['http', 'socks']
+
+def proxy_format_check(string: str) -> bool:
+ protocols = ("http", "socks")
for protocol in protocols:
if string.startswith(protocol):
return True
return False
-def validate_regex(pattern):
+
+def validate_regex(pattern: str) -> bool:
try:
re.compile(pattern)
except re.error:
return False
return True
-def convert_float(string):
- try:
- return float(string)
- except ValueError:
- return False
-def read_json_file(file_path):
+def read_json_file(file_path: str) -> dict | list | bool:
try:
- with open(file_path, 'r', encoding='utf-8') as file:
+ with open(file_path, "r", encoding="utf-8") as file:
json_data = file.read()
json_content = json.loads(json_data)
return json_content
@@ -67,13 +70,15 @@ def read_json_file(file_path):
print(f"Error: {e}")
return False
-def remove_chars(text):
- for ch in ['#', '?', '!', ':', '<', '>', '"', '/', '\\', '|', '*']:
+
+def remove_chars(text: str) -> str:
+ for ch in ["#", "?", "!", ":", "<", ">", '"', "/", "\\", "|", "*"]:
if ch in text:
- text = text.replace(ch, '')
+ text = text.replace(ch, "")
return text
-def parse_blacklist_page():
+
+def parse_blacklist_page() -> set[str]:
response = requests.get(BLACKLIST_URL)
html_content = response.text
@@ -81,33 +86,82 @@ def parse_blacklist_page():
if _has_lxml:
tree = html.fromstring(html_content)
- paragraphs = tree.xpath('//p/text()')
+ paragraphs = tree.xpath("//p/text()")
for p in paragraphs:
- inn_matches = re.findall(r'ИНН\s*(\d+)', p)
+ inn_matches = re.findall(r"ИНН\s*(\d+)", p)
inns.extend(inn_matches)
else:
- content_match = re.search(r'
.*?', html_content, re.DOTALL)
+ content_match = re.search(r".*?", html_content, re.DOTALL)
if not content_match:
raise ValueError("Не удалось найти нужную секцию на странице")
relevant_content = content_match.group(0)
- inn_matches = re.findall(r'ИНН\s*(\d+)', relevant_content)
+ inn_matches = re.findall(r"ИНН\s*(\d+)", relevant_content)
inns.extend(inn_matches)
inns = set(inns)
return inns
-def load_blacklist():
- blacklist_path = pathlib.Path(BLACKLIST_FILE)
+def load_blacklist() -> set[str]:
+ blacklist_path = Path(BLACKLIST_FILE)
if blacklist_path.exists():
file_age = time.time() - blacklist_path.stat().st_mtime
if file_age < UPDATE_INTERVAL:
- with open(BLACKLIST_FILE, 'r') as f:
+ with open(BLACKLIST_FILE, "r", encoding="UTF-8") as f:
return f.read().splitlines()
inns = parse_blacklist_page()
- with open(BLACKLIST_FILE, 'w') as f:
- f.write('\n'.join(inns))
+ with open(BLACKLIST_FILE, "w", encoding="UTF-8") as f:
+ f.write("\n".join(inns))
return inns
+
+
+def get_current_version(package_name: str) -> str | None:
+ try:
+ current_version = pkg_resources.get_distribution(package_name).version
+ return current_version
+ except pkg_resources.DistributionNotFound:
+ return None
+
+
+def get_latest_version(package_name: str) -> str | None:
+ url = f"https://api.github.com/repos/xob0t/{package_name}/releases/latest"
+
+ try:
+ response = requests.get(url, impersonate="chrome", timeout=2)
+ response.raise_for_status()
+ data = response.json()
+ return data.get("tag_name", "")
+ except Exception:
+ return None
+
+
+def check_for_new_version() -> None:
+ console = Console()
+ package_name = "mmparser"
+ current_version = get_current_version(package_name)
+ latest_version = get_latest_version(package_name)
+
+ try:
+ if version.parse(current_version) < version.parse(latest_version):
+ console.print(f"[orange]Доступна новая версия [bold]{package_name}[/bold]. Текущая: v{current_version}, Последняя: {latest_version}")
+ else:
+ console.print(f"[green]Вы используете последнюю версию [bold]{package_name}[/bold] (v{current_version}).")
+ except Exception:
+ console.print("[red]Ошибка проверки новой версии.")
+
+
+def parse_proxy_file(path: str) -> set[str]:
+ if Path(path).exists():
+ proxy_file_contents: str = open(path, "r", encoding="utf-8").read()
+ return set(line for line in proxy_file_contents.split("\n") if line)
+ raise exceptions.ConfigError(f"Путь {path} не найден!")
+
+
+def parse_cookie_file(path: str) -> dict:
+ if Path(path).exists():
+ cookies: list = json.loads(open(path, "r", encoding="utf-8").read())
+ return {cookie["name"]: cookie["value"] for cookie in cookies}
+ raise exceptions.ConfigError(f"Путь {path} не найден!")
diff --git a/setup.py b/setup.py
index 3300611..ecdc102 100644
--- a/setup.py
+++ b/setup.py
@@ -2,15 +2,17 @@
setup(
name="mmparser",
- version="0.6.6",
+ version="0.9.5",
description="Парсер megamarket.ru",
author="xob0t",
url="https://github.com/xob0t/mmparser",
packages=find_packages(),
install_requires=[
"rich",
+ "rich-argparse",
"curl-cffi",
"InquirerPy",
+ "packaging",
],
extras_require={
"lxml": ["lxml"],