Compare commits

...

1 Commits

Author SHA1 Message Date
zein
12c4dcf33b Финальная сборка.
Собраны все ссылки, удалены дубликаты. Осталось 85к ссылок. Парсим рецепты
2025-11-24 03:24:08 +03:00
5 changed files with 85221 additions and 191 deletions

View File

@@ -1,6 +1,9 @@
from os import times
import requests import requests
from bs4 import BeautifulSoup as bs from bs4 import BeautifulSoup as bs
import re import re
import time
from dns.name import empty from dns.name import empty
@@ -15,8 +18,10 @@ def try_request(link, max_retries=5):
return response return response
else: else:
retries += 1 retries += 1
time.sleep(2)
except: except:
retries += 1 retries += 1
time.sleep(2)
def try_soup(response): def try_soup(response):
try: try:
@@ -42,30 +47,33 @@ def extract_nutrition(calories_info):
return dict(zip(['calories', 'proteins', 'fats', 'carbs'], numbers)) return dict(zip(['calories', 'proteins', 'fats', 'carbs'], numbers))
except: except:
return print('БЖУ не найдены') return None
def extract_tags_from_detailed_tags(main_container): def extract_tags_from_detailed_tags(main_container):
try:
detailed_tags = main_container.find(class_='detailed_tags') detailed_tags = main_container.find(class_='detailed_tags')
tags = {} tags = {}
for span_b in detailed_tags.find_all('span', class_='b'): for span_b in detailed_tags.find_all('span', class_='b'):
label = span_b.get_text(strip=True).rstrip(':') label = span_b.get_text(strip=True).rstrip(':')
next_span = span_b.find_next_sibling('span') next_span = span_b.find_next_sibling('span')
if next_span: if next_span:
tag_list = [a.get_text(strip=True) for a in next_span.find_all('a')] tag_list = [a.get_text(strip=True) for a in next_span.find_all('a')]
else: else:
tag_list = [] tag_list = []
if label == 'Назначение': label = 'occasion' if label == 'Назначение': label = 'occasion'
elif label == 'Основной ингредиент': continue elif label == 'Основной ингредиент': continue
elif label == 'Блюдо': label = 'type_dish' elif label == 'Блюдо': label = 'type_dish'
elif label == 'География кухни': label = 'cuisine' elif label == 'География кухни': label = 'cuisine'
tags[label] = tag_list tags[label] = tag_list
return tags return tags
except Exception as e:
print(e)
def try_extr_ingredient(span_b, class_, portions=1): def try_extr_ingredient(span_b, class_, portions=1):
@@ -80,52 +88,60 @@ def try_extr_ingredient(span_b, class_, portions=1):
def extr_ingredient(main_container): def extr_ingredient(main_container):
#Сбор ингредиентов try:
portions = int(main_container.find(class_='yield value').get_text(strip=True)) #Сбор ингредиентов
portions = int(main_container.find(class_='yield value').get_text(strip=True))
tags = {} tags = {}
for span_b in main_container.find_all(class_='ingredient flex-dot-line'): for span_b in main_container.find_all(class_='ingredient flex-dot-line'):
label = try_extr_ingredient(span_b, class_='name') label = try_extr_ingredient(span_b, class_='name')
value_ingredient = try_extr_ingredient(span_b, 'value', portions) value_ingredient = try_extr_ingredient(span_b, 'value', portions)
unit_name = try_extr_ingredient(span_b, 'u-unit-name') unit_name = try_extr_ingredient(span_b, 'u-unit-name')
#print(label, value_ingredient, unit_name) #print(label, value_ingredient, unit_name)
tags[label] = {'unit':unit_name, 'amount':value_ingredient} tags[label] = {'unit':unit_name, 'amount':value_ingredient}
return tags return tags
except Exception as e:
print(e)
def extr_steps(main_container): def extr_steps(main_container):
# На сайте есть страницы исключения по шагам готовки. Фото есть не везде, тогда ищем класс detailed_step_description_big noPhotoStep
# Класс detailed_step_description_big noPhotoStep ищет текст через get_text(), а не через тег title
steps = [] try:
count = 1 # На сайте есть страницы исключения по шагам готовки. Фото есть не везде, тогда ищем класс detailed_step_description_big noPhotoStep
# Класс detailed_step_description_big noPhotoStep ищет текст через get_text(), а не через тег title
recipeInstructions = main_container.find(class_='instructions') steps = []
count = 1
main_container = recipeInstructions.find_all(class_='stepphotos') recipeInstructions = main_container.find(class_='instructions')
# Проверяем страницу исключение main_container = recipeInstructions.find_all(class_='stepphotos')
if not main_container:
main_container = recipeInstructions.find_all(class_='detailed_step_description_big noPhotoStep') # Проверяем страницу исключение
if not main_container:
main_container = recipeInstructions.find_all(class_='detailed_step_description_big noPhotoStep')
for items in main_container: for items in main_container:
img = items.get('href') img = items.get('href')
title = items.get('title') title = items.get('title')
# Если класс detailed_step_description_big noPhotoStep, то ищем через get_text. Сейчас title пустой, тк его нет на странице # Если класс detailed_step_description_big noPhotoStep, то ищем через get_text. Сейчас title пустой, тк его нет на странице
if title is None: if title is None:
title = items.get_text() #Теперь тайтл заполнен title = items.get_text() #Теперь тайтл заполнен
steps.append({ steps.append({
'img': img, 'img': img,
'title': title 'title': title
}) })
return steps
return steps
except Exception as e:
return None

View File

@@ -1,4 +1,7 @@
from pymongo import MongoClient from pymongo import MongoClient
import log_err as lg
from pymongo import MongoClient, ReplaceOne
def connect_to_mongo(): def connect_to_mongo():
"""Подключение к MongoDB""" """Подключение к MongoDB"""
@@ -8,16 +11,34 @@ def connect_to_mongo():
def import_json_in_mongo(recipe_data): def import_json_in_mongo(recipe_data):
"""Сохраняет один рецепт (для обратной совместимости и ошибок)"""
collection = connect_to_mongo() collection = connect_to_mongo()
print(recipe_data)
try: try:
collection.replace_one({"_id": recipe_data["_id"]}, recipe_data, upsert=True) collection.replace_one({"_id": recipe_data["_id"]}, recipe_data, upsert=True)
print(f"Рецепт '{recipe_data.get('recipe_name', recipe_data['_id'])}' успешно сохранён.") print(f"Рецепт '{recipe_data.get('recipe_name', recipe_data['_id'])}' успешно сохранён.")
except Exception as e: except Exception as e:
print(f"Ошибка при сохранении рецепта {recipe_data.get('_id')}: {e}") print(f"Ошибка при сохранении рецепта {recipe_data.get('_id')}: {e}")
url = recipe_data.get('url', 'unknown')
lg.log_error(url, str(e), 'Buff_err.json')
def bulk_write_recipes(recipes_list):
"""Сохраняет список рецептов массово с помощью bulk_write"""
if not recipes_list:
return
collection = connect_to_mongo()
try:
requests = [
ReplaceOne({"_id": r["_id"]}, r, upsert=True)
for r in recipes_list
]
result = collection.bulk_write(requests, ordered=False)
print(f"✅ Bulk-запись: {len(recipes_list)} рецептов "
f"(upserted: {result.upserted_count}, modified: {result.modified_count})")
except Exception as e:
print(f"❌ Ошибка при bulk-записи: {e}")
# Опционально: можно сохранить recipes_list в файл для повторной обработки

39
log_err.py Normal file
View File

@@ -0,0 +1,39 @@
import json
import os
def log_error(url, error, filename='Err.json'):
"""
Добавляет ошибку в список ошибок в JSON-файле.
Args:
url (str): URL, на котором произошла ошибка.
error (str or Exception): Описание ошибки.
filename (str): Имя файла для логирования (по умолчанию 'Err.json').
"""
# Приводим error к строке, чтобы избежать проблем с сериализацией
error_str = str(error)
# Загружаем существующие ошибки (если файл существует)
if os.path.exists(filename):
try:
with open(filename, 'r', encoding='utf-8') as f:
errors = json.load(f)
except (json.JSONDecodeError, OSError):
errors = []
else:
errors = []
# Добавляем новую ошибку
errors.append({
"url": url.strip(),
"error": error_str
})
# Сохраняем обратно
with open(filename, 'w', encoding='utf-8') as f:
json.dump(errors, f, ensure_ascii=False, indent=4)

211
parser.py
View File

@@ -3,166 +3,99 @@ from bs4 import BeautifulSoup as bs
import function as f import function as f
import json import json
import os import os
import log_err as lg
import import_in_BD as ib import import_in_BD as ib
link = 'https://povar.ru/list/' link = 'https://povar.ru/list/'
buffer = []
def buffer_import():
global buffer
total_type_recip = {}
def save_to_json(new_data, filename='total_type_recip.json'):
# Загружаем существующие данные, если файл существует
if os.path.exists(filename):
with open(filename, 'r', encoding='utf-8') as f:
try:
existing_data = json.load(f)
except json.JSONDecodeError:
existing_data = {}
else:
existing_data = {}
# Сливаем new_data в existing_data
for category, groups in new_data.items():
if category not in existing_data:
existing_data[category] = {}
for group, recipes in groups.items():
# Перезаписываем только если ещё не было или чтобы не дублировать — можно использовать set позже
existing_data[category][group] = recipes
# Сохраняем обратно
with open(filename, 'w', encoding='utf-8') as f:
json.dump(existing_data, f, ensure_ascii=False, indent=4)
def pars_group(link):
#Сбор видов блюд
response = f.try_request(link)
soup = bs(response.text, 'html.parser')
main_container = soup.find_all(class_='ingredientItem')
for items in main_container:
item = items.find_all('a')
title = items.find_all('h2')
title = title[0].get_text()
#if title == 'Выпечка': break
# Инициализируем категорию, если ещё не создана
if title not in total_type_recip:
total_type_recip[title] = {}
print(title)
for i in item[1::]:
name_group = i.get_text()
link_group = 'https://povar.ru' + i.get('href')
print('-'*5, name_group, link_group)
total_type_recip[title][name_group] = []
pars_dishs(title, name_group, link_group)
print('-'*50)
def pars_dishs(title='', name_group='', link='https://povar.ru/list/spagetti/', page=0):
global total_type_recip
#Сбор списка рецептов
recipes = []
while True:
page += 1
new_link = link + str(page)
soup = f.try_soup(f.try_request(new_link))
if soup == False: break
main_container = soup.find_all(class_='listRecipieTitle')
for items in main_container:
recipe_name = items.get_text()
recipe_link = 'https://povar.ru' + items.get('href')
print('-'*10,recipe_name, recipe_link)
#pars_recipie(title, name_group, recipe_name, recipe_link)
recipes.append({'name': recipe_name, 'url': recipe_link})
print('-'*50)
# После сбора всех страниц — записываем в глобальную структуру
total_type_recip[title][name_group] = recipes
# И сразу сохраняем ВЕСЬ словарь в JSON
save_to_json(total_type_recip)
def pars_recipie(title=0, name_group=0, recipe_name=0 ,link='https://povar.ru/recipes/slivochnaya_karbonara-73186.html'):
response = f.try_request(link)
soup = bs(response.text, 'html.parser')
main_container = soup.find(class_='cont_area hrecipe')
name_id = link.split('/')[-1]
try: try:
name_id = name_id.replace('.html', '') ib.bulk_write_recipes(buffer)
except: pass buffer.clear()
except Exception as bulk_e:
print(name_id) print(f"⚠️ Bulk-ошибка: {bulk_e}. Переключаемся на поштучную запись...")
for recipe in buffer:
photo = main_container.find(class_='photo').get('src') try:
ib.import_json_in_mongo(recipe) # ← ОДИН рецепт
recipies = {'recipes': {}} except Exception as single_e:
lg.log_error(recipe.get('url', 'unknown'), f"MongoDB-фейл: {single_e}")
detailed_tags = f.extract_tags_from_detailed_tags(main_container) #Собираем теги buffer.clear()
ingredients = f.extr_ingredient(main_container) #Собираем ингредиенты
calories_info = f.extract_nutrition(main_container.find_all(class_='circle')) #БЖУ
steps = f.extr_steps(main_container) #Сборка шагов
recip = {'_id' : name_id,
def open_url():
total_type_recip = json.load(open('unique_urls.json', 'r', encoding='utf-8'))
for url in total_type_recip:
if len(buffer) >= 200:
print('❗️ Сейвим', len(buffer))
print(buffer)
buffer_import()
pars_recipie(url)
buffer_import()
def pars_recipie(url='https://povar.ru/recipes/slivochnaya_karbonara-73186.html'):
try:
response = f.try_request(url)
soup = bs(response.text, 'html.parser')
recipe_name = soup.find(class_='detailed fn').text
main_container = soup.find(class_='cont_area hrecipe')
steps = f.extr_steps(main_container) #Сборка шагов
if steps is None:
lg.log_error(url, 'Нет шагов')
return None
name_id = url.split('/')[-1].strip()
try:
name_id = name_id.replace('.html', '').strip()
except: pass
photo = main_container.find(class_='photo').get('src')
detailed_tags = f.extract_tags_from_detailed_tags(main_container) #Собираем теги
ingredients = f.extr_ingredient(main_container) #Собираем ингредиенты
calories_info = f.extract_nutrition(main_container.find_all(class_='circle')) #БЖУ
recip = {'_id' : name_id,
'recipe_name':recipe_name, 'recipe_name':recipe_name,
'url':link, 'url':url,
'preview_img':photo, 'preview_img':photo,
'tags':detailed_tags, 'tags':detailed_tags,
'ingredients':ingredients, 'ingredients':ingredients,
'nutritional_value':calories_info, 'nutritional_value':calories_info,
'steps':steps} 'steps':steps}
print('Шагов - ',len(steps)) print('',recipe_name)
print('🤍',recip)
#ib.import_json_in_mongo(recip) buffer.append(recip)
except Exception as e:
print(url, e)
lg.log_error(url, e)
pars_group(link)
#pars_dishs()
#pars_recipie() #pars_recipie()
open_url()

85021
unique_urls.json Normal file

File diff suppressed because it is too large Load Diff