craft_parser_init
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*.xlsx
|
||||||
|
*.csv
|
||||||
|
.mypy_cache
|
||||||
|
__pycache__
|
||||||
|
*.log
|
||||||
26
craft_parser/craft.py
Normal file
26
craft_parser/craft.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
CRAFT_TYPES = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CraftItem:
|
||||||
|
def __init__(self, name: str, img_url: str, img_shift: tuple[int, int], is_base=False, ):
|
||||||
|
self.name = name
|
||||||
|
self.is_base = is_base
|
||||||
|
self.img_url = img_url
|
||||||
|
self.img_shift = img_shift
|
||||||
|
|
||||||
|
|
||||||
|
class CraftRecipe:
|
||||||
|
def __init__(self, output_item_name: str, output_count: int, craft_type: str):
|
||||||
|
self.output_item_name = output_item_name
|
||||||
|
self.output_count = output_count
|
||||||
|
self.craft_type = craft_type
|
||||||
|
self.recipe_id = None # будет назначен при сохранении
|
||||||
|
|
||||||
|
|
||||||
|
class CraftComponent:
|
||||||
|
def __init__(self, recipe_id: int, input_item: str, count: int):
|
||||||
|
self.recipe_id = recipe_id
|
||||||
|
self.input_item = input_item
|
||||||
|
self.count = count
|
||||||
|
|
||||||
71
craft_parser/craft_facade.py
Normal file
71
craft_parser/craft_facade.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from craft_storage import CraftStorage
|
||||||
|
import device_extractor
|
||||||
|
import simple_extractor
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
class CraftFacade:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.visited: set[str] = set()
|
||||||
|
logger.add('file_{time}.log')
|
||||||
|
logger.add(sys.stdout, colorize=True, format="<green>{time}</green> <level>{message}</level>")
|
||||||
|
|
||||||
|
# ---------- low level ----------
|
||||||
|
|
||||||
|
def _get_page_content(self, url: str) -> str | None:
|
||||||
|
time.sleep(0.5)
|
||||||
|
try:
|
||||||
|
r = requests.get(url)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f'Ошибка загрузки {url}: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_soup(self, html: str) -> BeautifulSoup:
|
||||||
|
return BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- public API ----------
|
||||||
|
|
||||||
|
def extract(self, url: str, storage: CraftStorage, recursive: bool = True):
|
||||||
|
if not url:
|
||||||
|
logger.error('Битая ссылка')
|
||||||
|
return
|
||||||
|
if url in self.visited:
|
||||||
|
logger.debug(f'Уже посещали: {url}')
|
||||||
|
return
|
||||||
|
|
||||||
|
html = self._get_page_content(url)
|
||||||
|
if not html:
|
||||||
|
return
|
||||||
|
|
||||||
|
soup = self._get_soup(html)
|
||||||
|
logger.info(f'Анализирую {url}')
|
||||||
|
|
||||||
|
self.visited.add(url)
|
||||||
|
|
||||||
|
all_links: set[str] = set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
links = simple_extractor.extract_crafts(soup, storage)
|
||||||
|
all_links.update(links)
|
||||||
|
links = device_extractor.extract_crafts(soup, storage)
|
||||||
|
all_links.update(all_links)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Ошибка в парсере: {e}')
|
||||||
|
|
||||||
|
logger.info(f'Готово: {url}')
|
||||||
|
|
||||||
|
if not recursive:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('Начинаю обход внешних ссылок...')
|
||||||
|
for link in all_links:
|
||||||
|
self.extract(link, storage, recursive=True)
|
||||||
122
craft_parser/craft_storage.py
Normal file
122
craft_parser/craft_storage.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from craft import CraftComponent, CraftItem, CraftRecipe
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
class CraftStorage:
|
||||||
|
def __init__(self):
|
||||||
|
# items
|
||||||
|
self.items_df = pd.DataFrame(columns=[
|
||||||
|
"name", "img_url", "is_base", "img_shift"
|
||||||
|
])
|
||||||
|
|
||||||
|
##Чтобы на конкат не орал
|
||||||
|
self.items_df["is_base"] = self.items_df["is_base"].astype('bool')
|
||||||
|
|
||||||
|
# recipes
|
||||||
|
self.recipes_df = pd.DataFrame(columns=[
|
||||||
|
"recipe_id", "output_item_name", "output_count", "craft_type"
|
||||||
|
])
|
||||||
|
|
||||||
|
# components
|
||||||
|
self.components_df = pd.DataFrame(columns=[
|
||||||
|
"recipe_id", "input_item", "count"
|
||||||
|
])
|
||||||
|
self.recipe_signatures = set()
|
||||||
|
self._recipe_id_seq = 1
|
||||||
|
|
||||||
|
def try_add_recipe_signature(self, output_item_name: str, components: list[CraftComponent], craft_type: str) -> bool:
|
||||||
|
# сортируем, чтобы порядок не влиял
|
||||||
|
parts = sorted(
|
||||||
|
(c.input_item, c.count) for c in components
|
||||||
|
)
|
||||||
|
sig = (output_item_name, tuple(parts), craft_type)
|
||||||
|
if sig in self.recipe_signatures:
|
||||||
|
logger.warning(f'Дубликат рецепта {output_item_name}. Пропускаю ')
|
||||||
|
#print(f'CRAFT: {}')
|
||||||
|
# for el in components:
|
||||||
|
# print(f'{el.input_item} : {el.count}')
|
||||||
|
# print('Пропускаю')
|
||||||
|
# print('....')
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.recipe_signatures.add(sig)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def add_item(self, item: CraftItem):
|
||||||
|
# проверяем, есть ли уже такой item
|
||||||
|
if item.name in self.items_df["name"].values:
|
||||||
|
logger.warning(f'{item.name} уже есть в датасете, скипаю')
|
||||||
|
return # уже есть, не дублируем
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"name": item.name,
|
||||||
|
"img_url": item.img_url,
|
||||||
|
"is_base": bool(item.is_base),
|
||||||
|
"img_shift": item.img_shift
|
||||||
|
}
|
||||||
|
|
||||||
|
self.items_df = pd.concat(
|
||||||
|
[self.items_df, pd.DataFrame([row])],
|
||||||
|
ignore_index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_recipe(self, recipe: CraftRecipe) -> int:
|
||||||
|
recipe_id = self._recipe_id_seq
|
||||||
|
self._recipe_id_seq += 1
|
||||||
|
|
||||||
|
recipe.recipe_id = recipe_id
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"recipe_id": recipe_id,
|
||||||
|
"output_item_name": recipe.output_item_name,
|
||||||
|
"output_count": recipe.output_count,
|
||||||
|
"craft_type": recipe.craft_type
|
||||||
|
}
|
||||||
|
|
||||||
|
self.recipes_df = pd.concat(
|
||||||
|
[self.recipes_df, pd.DataFrame([row])],
|
||||||
|
ignore_index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return recipe_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def add_component(self, component: CraftComponent):
|
||||||
|
# защита от дубликатов
|
||||||
|
exists = (
|
||||||
|
(self.components_df["recipe_id"] == component.recipe_id) &
|
||||||
|
(self.components_df["input_item"] == component.input_item) &
|
||||||
|
(self.components_df['count'] == component.count)
|
||||||
|
).any()
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
logger.warning(f"""
|
||||||
|
Уже существует такая запись для рецепта: {component.recipe_id} <--- {component.input_item}*{component.count}
|
||||||
|
Скипаем
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"recipe_id": component.recipe_id,
|
||||||
|
"input_item": component.input_item,
|
||||||
|
"count": component.count
|
||||||
|
}
|
||||||
|
|
||||||
|
self.components_df = pd.concat(
|
||||||
|
[self.components_df, pd.DataFrame([row])],
|
||||||
|
ignore_index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
os.makedirs('data', exist_ok=True)
|
||||||
|
self.items_df.to_csv('data/items.csv', index=False)
|
||||||
|
self.recipes_df.to_csv('data/recipes.csv', index=False)
|
||||||
|
self.components_df.to_csv('data/recipe_components.csv', index=False)
|
||||||
|
self.items_df.to_excel('data/items.xlsx', index=False)
|
||||||
|
self.recipes_df.to_excel('data/recipes.xlsx', index=False)
|
||||||
|
self.components_df.to_excel('data/recipe_components.xlsx', index=False)
|
||||||
261
craft_parser/device_extractor.py
Normal file
261
craft_parser/device_extractor.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import re
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from craft import CraftComponent, CraftItem, CraftRecipe
|
||||||
|
from craft_storage import CraftStorage
|
||||||
|
from dto import ParsedItem, ParsedRecipeInput, SpriteData
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
def extract_crafts(soup: BeautifulSoup, storage: CraftStorage) -> set[str]:
|
||||||
|
|
||||||
|
craft_containers = soup.find_all("div", class_="craft-gui")
|
||||||
|
if(craft_containers.__len__() < 1):
|
||||||
|
logger.info('Машинных рецептов не найдено')
|
||||||
|
return set()
|
||||||
|
|
||||||
|
src_links : set[str] = set()
|
||||||
|
|
||||||
|
for container in craft_containers:
|
||||||
|
is_hidden = check_is_hidden(container)
|
||||||
|
if(is_hidden):
|
||||||
|
continue
|
||||||
|
|
||||||
|
output_item = parse_craft_item(container)
|
||||||
|
# Получаем список рецептов (может быть несколько для печи)
|
||||||
|
recipes_list = parse_craft_components_and_recipe(
|
||||||
|
container, output_item.item.name, output_item.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
for recipe_input in recipes_list: # Обрабатываем каждый рецепт отдельно
|
||||||
|
if not storage.try_add_recipe_signature(
|
||||||
|
output_item.item.name,
|
||||||
|
recipe_input.components,
|
||||||
|
recipe_input.recipe.craft_type
|
||||||
|
):
|
||||||
|
continue # Дубликат
|
||||||
|
|
||||||
|
storage.add_item(output_item.item)
|
||||||
|
for input_item in recipe_input.items:
|
||||||
|
storage.add_item(input_item)
|
||||||
|
|
||||||
|
recipe_id = storage.add_recipe(recipe_input.recipe)
|
||||||
|
for component in recipe_input.components:
|
||||||
|
component.recipe_id = recipe_id
|
||||||
|
storage.add_component(component)
|
||||||
|
|
||||||
|
logger.info(f'Добавлен рецепт машинного крафта для {output_item.item.name} '
|
||||||
|
f'(ингредиент: {recipe_input.components[0].input_item})')
|
||||||
|
|
||||||
|
return src_links
|
||||||
|
|
||||||
|
def check_is_hidden(container : BeautifulSoup) -> bool:
|
||||||
|
if(container.find_parent('table', class_='collapsed')):
|
||||||
|
logger.warning(f'{container} Не будет исследован. Причина: помечен как скрытый')
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_craft_item(container) -> ParsedItem|None:
|
||||||
|
output_span = container.find('span', class_='gt-output')
|
||||||
|
if(output_span is None):
|
||||||
|
logger.error(f'ошибка для \n{container}\n: не найдено ячейки с результатом!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
output_span_name_container = output_span.find('span', class_='invslot-item')
|
||||||
|
if(output_span_name_container is None):
|
||||||
|
logger.error(f'ошибка для \n{output_span}\n: не найден текстовый контейнер!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
data_from_span = extract_data_from_sprite_span(output_span_name_container)
|
||||||
|
if(data_from_span is not None):
|
||||||
|
output_item_title = data_from_span.title
|
||||||
|
output_item_img_shift = data_from_span.shift
|
||||||
|
output_item_img_url = data_from_span.img_url
|
||||||
|
output_amount = data_from_span.amount
|
||||||
|
else:
|
||||||
|
data_from_img = extract_data_from_sprite_img(output_span_name_container)
|
||||||
|
if(data_from_img is None):
|
||||||
|
logger.error(f'ошибка для \n{output_span_name_container}\n: не удалось извлечь картинку и заголовок ни одним из способов')
|
||||||
|
return None
|
||||||
|
output_item_title = data_from_img.title
|
||||||
|
output_item_img_shift = data_from_img.shift
|
||||||
|
output_item_img_url = data_from_img.img_url
|
||||||
|
output_amount = 1
|
||||||
|
|
||||||
|
item = CraftItem(output_item_title, output_item_img_url, output_item_img_shift)
|
||||||
|
return ParsedItem(item=item, amount=output_amount)
|
||||||
|
|
||||||
|
def parse_craft_components_and_recipe(container, output_item_name: str, output_item_count: int) -> list[ParsedRecipeInput]:
|
||||||
|
input_span = container.find('span', class_='gt-input')
|
||||||
|
if not input_span:
|
||||||
|
logger.error(f'Ошибка: нет блока gt-input в {container}')
|
||||||
|
return []
|
||||||
|
|
||||||
|
recipe_info = extract_recipe_type(container)
|
||||||
|
if not recipe_info:
|
||||||
|
return []
|
||||||
|
|
||||||
|
is_furnace = 'Печь' in recipe_info
|
||||||
|
recipes = [] # Список рецептов
|
||||||
|
|
||||||
|
if is_furnace:
|
||||||
|
# Для печи каждый предмет в левом слоте — отдельный рецепт
|
||||||
|
left_cell = input_span.find('span', class_='invslot')
|
||||||
|
if left_cell:
|
||||||
|
item_spans = left_cell.find_all('span', class_='invslot-item')
|
||||||
|
for item_span in item_spans:
|
||||||
|
data = extract_data_from_sprite_span(item_span) or extract_data_from_sprite_img(item_span)
|
||||||
|
if data:
|
||||||
|
item = CraftItem(data.title, data.img_url, data.shift)
|
||||||
|
# Создаём отдельный рецепт для каждого ингредиента
|
||||||
|
recipe = CraftRecipe(output_item_name, output_item_count, recipe_info)
|
||||||
|
component = CraftComponent(-1, item.name, data.amount)
|
||||||
|
recipes.append(ParsedRecipeInput(
|
||||||
|
items=[item],
|
||||||
|
recipe=recipe,
|
||||||
|
components=[component],
|
||||||
|
source_links=[]
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
# Для других машин — один рецепт со всеми ингредиентами
|
||||||
|
item_spans = input_span.find_all('span', class_='invslot-item')
|
||||||
|
items = []
|
||||||
|
components = []
|
||||||
|
for item_span in item_spans:
|
||||||
|
data = extract_data_from_sprite_span(item_span) or extract_data_from_sprite_img(item_span)
|
||||||
|
if data:
|
||||||
|
|
||||||
|
|
||||||
|
item = CraftItem(data.title, data.img_url, data.shift)
|
||||||
|
|
||||||
|
##skip ic2 stuff
|
||||||
|
if(item.name=='Энергия'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
components.append(CraftComponent(-1, item.name, data.amount))
|
||||||
|
|
||||||
|
recipe = CraftRecipe(output_item_name, output_item_count, recipe_info)
|
||||||
|
recipes.append(ParsedRecipeInput(
|
||||||
|
items=items,
|
||||||
|
recipe=recipe,
|
||||||
|
components=components,
|
||||||
|
source_links=[]
|
||||||
|
))
|
||||||
|
|
||||||
|
return recipes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def extract_recipe_type(container) -> str|None:
|
||||||
|
processor_container = container.find('span', class_='minetip')
|
||||||
|
if(processor_container is None):
|
||||||
|
logger.error(f'Ошибка для {container}: не найден тип машинного рецепта')
|
||||||
|
return None
|
||||||
|
return clean_recipe_type_str(processor_container.get('data-minetip-text'))
|
||||||
|
|
||||||
|
def clean_recipe_type_str(input: str):
|
||||||
|
pattern = r"\s*//&7Модификация:/.*$"
|
||||||
|
|
||||||
|
input = input.replace('&3','')
|
||||||
|
input = re.sub(pattern, "", input).strip()
|
||||||
|
return input
|
||||||
|
|
||||||
|
|
||||||
|
def extract_data_from_sprite_img(input_span_name_container) -> SpriteData|None:
|
||||||
|
input_item_img = input_span_name_container.find('img')
|
||||||
|
if(input_item_img is None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
title = input_item_img.get('alt')
|
||||||
|
if(title is None):
|
||||||
|
logger.error(f'ошибка для \n{input_span_name_container}\n: не найдено описание картинки!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
url = input_item_img.get('src')
|
||||||
|
if(url is None):
|
||||||
|
logger.error(f'ошибка для \n{input_span_name_container}\n: не найден url картинки!')
|
||||||
|
return None
|
||||||
|
return SpriteData(
|
||||||
|
title=title,
|
||||||
|
shift=(0, 0),
|
||||||
|
img_url=url,
|
||||||
|
source_link=None)
|
||||||
|
|
||||||
|
def extract_data_from_sprite_span(input_span_name_container) -> SpriteData|None:
|
||||||
|
input_item_sprite_span = input_span_name_container.find('span', class_='sprite')
|
||||||
|
|
||||||
|
|
||||||
|
#Контейнера со спрайтом может не быть!
|
||||||
|
if(input_item_sprite_span is None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
input_item_title = input_item_sprite_span.get('title')
|
||||||
|
if(input_item_title is None):
|
||||||
|
logger.error(f'ошибка для \n{input_span_name_container}\n: не найдено заголовка предмета')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
amount_container = input_span_name_container.find('span', class_='invslot-stacksize')
|
||||||
|
output_amount = 1
|
||||||
|
if(amount_container is not None):
|
||||||
|
logger.info(f'Для объекта {input_item_title} найдено количество : {amount_container.text}')
|
||||||
|
output_amount = int(amount_container.text)
|
||||||
|
|
||||||
|
|
||||||
|
item_source_link = get_item_source_link(input_span_name_container)
|
||||||
|
|
||||||
|
input_item_img_url = extract_img_classname(input_item_sprite_span)
|
||||||
|
if(input_item_img_url is None):
|
||||||
|
logger.error(f'Ошибка для \n{input_item_img_url}\n: не найден файл спрайта')
|
||||||
|
return None
|
||||||
|
|
||||||
|
input_item_img_shift = extract_img_shift(input_item_sprite_span)
|
||||||
|
if(input_item_img_shift is None):
|
||||||
|
logger.error(f'Ошибка для \n{input_item_sprite_span}\n: не найдено смещения для спрайта')
|
||||||
|
return None
|
||||||
|
|
||||||
|
return SpriteData(
|
||||||
|
title=input_item_title,
|
||||||
|
shift=input_item_img_shift,
|
||||||
|
img_url=input_item_img_url,
|
||||||
|
source_link=item_source_link,
|
||||||
|
amount=output_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
def extract_img_shift(output_item_sprite_span):
|
||||||
|
if(output_item_sprite_span is None):
|
||||||
|
return None
|
||||||
|
css_value = output_item_sprite_span.get('style')
|
||||||
|
if(css_value is None):
|
||||||
|
return None
|
||||||
|
# Шаблон: ищем два числа (с возможным минусом) перед 'px'
|
||||||
|
pattern = r'(-?\d+)px\s+(-?\d+)px'
|
||||||
|
match = re.search(pattern, css_value)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
x = int(match.group(1))
|
||||||
|
y = int(match.group(2))
|
||||||
|
return (x, y)
|
||||||
|
else:
|
||||||
|
return None # Если шаблон не найден
|
||||||
|
|
||||||
|
def extract_img_classname(sprite_span):
|
||||||
|
if(sprite_span is None):
|
||||||
|
return None
|
||||||
|
'sprite industrialcraft-2-inv-sprite'
|
||||||
|
classes = sprite_span.get('class', [])
|
||||||
|
for cls in classes:
|
||||||
|
if cls.endswith('-sprite'):
|
||||||
|
return cls
|
||||||
|
return None # Если не найдено
|
||||||
|
|
||||||
|
def get_item_source_link(input_span_name_container) -> str|None:
|
||||||
|
link_container = input_span_name_container.find('a')
|
||||||
|
if(link_container is None):
|
||||||
|
return None
|
||||||
|
link = link_container.get('href')
|
||||||
|
if(link is None):
|
||||||
|
return None
|
||||||
|
return f'https://ru.minecraft.wiki{link}'
|
||||||
23
craft_parser/dto.py
Normal file
23
craft_parser/dto.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
from craft import CraftItem, CraftComponent, CraftRecipe
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParsedItem:
|
||||||
|
item: CraftItem
|
||||||
|
amount: int
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParsedRecipeInput:
|
||||||
|
items: List[CraftItem]
|
||||||
|
recipe: CraftRecipe
|
||||||
|
components: List[CraftComponent]
|
||||||
|
source_links: List[str]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SpriteData:
|
||||||
|
amount: int
|
||||||
|
title: str
|
||||||
|
shift: Tuple[int, int]
|
||||||
|
img_url: str
|
||||||
|
source_link: Optional[str] = None
|
||||||
17
craft_parser/main.py
Normal file
17
craft_parser/main.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from craft_facade import CraftFacade
|
||||||
|
from craft_storage import CraftStorage
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
url = 'https://ru.minecraft.wiki/w/IndustrialCraft_2/Термальная_центрифуга'
|
||||||
|
storage = CraftStorage()
|
||||||
|
facade = CraftFacade()
|
||||||
|
facade.extract(url, storage, recursive=False)
|
||||||
|
storage.save()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
222
craft_parser/notebook.ipynb
Normal file
222
craft_parser/notebook.ipynb
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "d3b20ae9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import requests\n",
|
||||||
|
"from bs4 import BeautifulSoup\n",
|
||||||
|
"from collections import defaultdict, deque\n",
|
||||||
|
"from urllib.parse import urljoin, urlparse\n",
|
||||||
|
"\n",
|
||||||
|
"from craft import CraftComponent, CraftItem, CraftRecipe\n",
|
||||||
|
"from craft_storage import CraftStorage\n",
|
||||||
|
"from collections import Counter\n",
|
||||||
|
"import simple_extractor\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "6189c095",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def get_page_content(url):\n",
|
||||||
|
" \"\"\"Загружает HTML-контент страницы.\"\"\"\n",
|
||||||
|
" try:\n",
|
||||||
|
" response = requests.get(url)\n",
|
||||||
|
" response.raise_for_status()\n",
|
||||||
|
" return response.text\n",
|
||||||
|
" except requests.RequestException as e:\n",
|
||||||
|
" print(f\"Ошибка при загрузке страницы: {e}\")\n",
|
||||||
|
" return None\n",
|
||||||
|
" \n",
|
||||||
|
"def get_soup(content):\n",
|
||||||
|
" soup = BeautifulSoup(content, \"html.parser\")\n",
|
||||||
|
" return soup"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"id": "3ea684bc",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Анализирую https://ru.minecraft.wiki/w/IndustrialCraft_2/Квантовая_броня....\n",
|
||||||
|
"Укреплённое стекло уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Улучшенная электросхема уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовый шлем\n",
|
||||||
|
"Композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Лазуротроновый кристалл уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовый жилет\n",
|
||||||
|
"Лазуротроновый кристалл уже есть в датасете, скипаю\n",
|
||||||
|
"Основной корпус машины уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Светопыль уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовые поножи\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Лазуротроновый кристалл уже есть в датасете, скипаю\n",
|
||||||
|
"Резиновые ботинки уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовые ботинки\n",
|
||||||
|
"Квантовый шлем уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Лазуротроновый кристалл уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Улучшенная электросхема уже есть в датасете, скипаю\n",
|
||||||
|
"Укреплённое стекло уже есть в датасете, скипаю\n",
|
||||||
|
"Улучшенная электросхема уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовый шлем\n",
|
||||||
|
"Квантовый жилет уже есть в датасете, скипаю\n",
|
||||||
|
"Композит уже есть в датасете, скипаю\n",
|
||||||
|
"Композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Лазуротроновый кристалл уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовый жилет\n",
|
||||||
|
"Квантовые поножи уже есть в датасете, скипаю\n",
|
||||||
|
"Основной корпус машины уже есть в датасете, скипаю\n",
|
||||||
|
"Лазуротроновый кристалл уже есть в датасете, скипаю\n",
|
||||||
|
"Основной корпус машины уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Светопыль уже есть в датасете, скипаю\n",
|
||||||
|
"Светопыль уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовые поножи\n",
|
||||||
|
"Квантовые ботинки уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Иридиевый композит уже есть в датасете, скипаю\n",
|
||||||
|
"Резиновые ботинки уже есть в датасете, скипаю\n",
|
||||||
|
"Лазуротроновый кристалл уже есть в датасете, скипаю\n",
|
||||||
|
"Резиновые ботинки уже есть в датасете, скипаю\n",
|
||||||
|
"Добавлен рецепт крафта для предмета Квантовые ботинки\n",
|
||||||
|
"Закончен поиск по странице https://ru.minecraft.wiki/w/IndustrialCraft_2/Квантовая_броня\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stderr",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n",
|
||||||
|
"d:\\Development\\Python\\craft_calc\\craft_storage.py:56: FutureWarning: In a future version, object-dtype columns with all-bool values will not be included in reductions with bool_only=True. Explicitly cast to bool dtype instead.\n",
|
||||||
|
" self.items_df = pd.concat(\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"url = 'https://ru.minecraft.wiki/w/IndustrialCraft_2/Квантовая_броня'\n",
|
||||||
|
"content = get_page_content(url)\n",
|
||||||
|
"soup = get_soup(content)\n",
|
||||||
|
"storage = CraftStorage()\n",
|
||||||
|
"simple_extractor.extract_crafts(soup,url,storage)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"id": "df4b0ee0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
" recipe_id input_item count\n",
|
||||||
|
"0 1 Укреплённое стекло 2\n",
|
||||||
|
"1 1 Иридиевый композит 2\n",
|
||||||
|
"2 1 Улучшенная электросхема 2\n",
|
||||||
|
"3 1 Нановолоконный шлем 1\n",
|
||||||
|
"4 1 Лазуротроновый кристалл 1\n",
|
||||||
|
"5 1 Шлем-акваланг 1\n",
|
||||||
|
"6 2 Иридиевый композит 4\n",
|
||||||
|
"7 2 Композит 2\n",
|
||||||
|
"8 2 Нановолоконный жилет 1\n",
|
||||||
|
"9 2 Лазуротроновый кристалл 1\n",
|
||||||
|
"10 2 Электрический реактивный ранец 1\n",
|
||||||
|
"11 3 Основной корпус машины 2\n",
|
||||||
|
"12 3 Иридиевый композит 2\n",
|
||||||
|
"13 3 Светопыль 2\n",
|
||||||
|
"14 3 Лазуротроновый кристалл 1\n",
|
||||||
|
"15 3 Нановолоконные поножи 1\n",
|
||||||
|
"16 4 Иридиевый композит 2\n",
|
||||||
|
"17 4 Резиновые ботинки 2\n",
|
||||||
|
"18 4 Нановолоконные ботинки 1\n",
|
||||||
|
"19 4 Лазуротроновый кристалл 1\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"print(storage.components_df.head(20))"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.10.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
231
craft_parser/simple_extractor.py
Normal file
231
craft_parser/simple_extractor.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
from collections import Counter
|
||||||
|
import re
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from craft import CraftComponent, CraftItem, CraftRecipe
|
||||||
|
from craft_storage import CraftStorage
|
||||||
|
from dto import ParsedItem, ParsedRecipeInput, SpriteData
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
def extract_crafts(soup: BeautifulSoup, storage: CraftStorage) -> set[str]:
|
||||||
|
|
||||||
|
craft_containers = soup.find_all("div", class_="mcui-Crafting_Table-container")
|
||||||
|
if(craft_containers.__len__() < 1):
|
||||||
|
logger.info('Классических рецептов не найдено')
|
||||||
|
return set()
|
||||||
|
|
||||||
|
src_links : set[str] = set()
|
||||||
|
|
||||||
|
for container in craft_containers:
|
||||||
|
is_hidden = check_is_hidden(container)
|
||||||
|
if(is_hidden):
|
||||||
|
continue
|
||||||
|
output_item = parse_craft_item(container)
|
||||||
|
if(output_item is None):
|
||||||
|
continue
|
||||||
|
input_kit = parse_craft_components_and_recipe(container, output_item.item.name, output_item.amount)
|
||||||
|
if(input_kit is None):
|
||||||
|
continue
|
||||||
|
|
||||||
|
src_links.update(input_kit.source_links)
|
||||||
|
|
||||||
|
already_exists = not storage.try_add_recipe_signature(output_item.item.name, input_kit.components, 'classic')
|
||||||
|
if(already_exists):
|
||||||
|
continue
|
||||||
|
|
||||||
|
storage.add_item(output_item.item)
|
||||||
|
for input_item in input_kit.items:
|
||||||
|
storage.add_item(input_item)
|
||||||
|
|
||||||
|
recipe_id = storage.add_recipe(input_kit.recipe)
|
||||||
|
for component in input_kit.components:
|
||||||
|
component.recipe_id = recipe_id
|
||||||
|
storage.add_component(component)
|
||||||
|
logger.info(f'Добавлен рецепт крафта для предмета {output_item.item.name}')
|
||||||
|
|
||||||
|
return src_links
|
||||||
|
|
||||||
|
def check_is_hidden(container : BeautifulSoup) -> bool:
|
||||||
|
if(container.find_parent('table', class_='collapsed')):
|
||||||
|
logger.warning(f'{container} Не будет исследован. Причина: помечен как скрытый')
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_craft_item(container) -> ParsedItem|None:
|
||||||
|
output_amount = 1
|
||||||
|
output_span = container.find('span', class_='mcui-output')
|
||||||
|
if(output_span is None):
|
||||||
|
logger.error(f'ошибка для \n{container}\n: не найдено ячейки с результатом!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
output_span_name_container = output_span.find('span', class_='invslot-item')
|
||||||
|
if(output_span_name_container is None):
|
||||||
|
logger.error(f'ошибка для \n{output_span}\n: не найден текстовый контейнер!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
data_from_span = extract_data_from_sprite_span(output_span_name_container)
|
||||||
|
if(data_from_span is not None):
|
||||||
|
output_item_title = data_from_span.title
|
||||||
|
output_item_img_shift = data_from_span.shift
|
||||||
|
output_item_img_url = data_from_span.img_url
|
||||||
|
else:
|
||||||
|
data_from_img = extract_data_from_sprite_img(output_span_name_container)
|
||||||
|
if(data_from_img is None):
|
||||||
|
logger.error(f'ошибка для \n{output_span_name_container}\n: не удалось извлечь картинку и заголовок ни одним из способов')
|
||||||
|
return None
|
||||||
|
output_item_title = data_from_img.title
|
||||||
|
output_item_img_shift = data_from_img.shift
|
||||||
|
output_item_img_url = data_from_img.img_url
|
||||||
|
|
||||||
|
item = CraftItem(output_item_title, output_item_img_url, output_item_img_shift)
|
||||||
|
return ParsedItem(item=item, amount=output_amount)
|
||||||
|
|
||||||
|
def parse_craft_components_and_recipe(container, output_item_name: str, output_count = 1) -> ParsedRecipeInput|None:
|
||||||
|
input_span = container.find('span', class_='mcui-input')
|
||||||
|
if(input_span is None):
|
||||||
|
logger.error(f'ошибка для \n{container}\n: не найдено рецепта крафта')
|
||||||
|
return None
|
||||||
|
|
||||||
|
input_span_name_containers = input_span.find_all('span', class_='invslot-item')
|
||||||
|
if(input_span_name_containers is None or input_span_name_containers.__len__() == 0):
|
||||||
|
logger.error(f'Ошибка для \n{input_span}\n: не найдено айтемов в рецепте')
|
||||||
|
return None
|
||||||
|
|
||||||
|
craft_items = []
|
||||||
|
recipe = None
|
||||||
|
craft_components = []
|
||||||
|
src_links = []
|
||||||
|
#existing_items = set()
|
||||||
|
|
||||||
|
for input_span_name_container in input_span_name_containers:
|
||||||
|
|
||||||
|
if(input_span_name_container is None):
|
||||||
|
logger.error('ошибка для набора контейнеров: не найден текстовый контейнер!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
data_from_span = extract_data_from_sprite_span(input_span_name_container)
|
||||||
|
if(data_from_span is not None):
|
||||||
|
input_item_title = data_from_span.title
|
||||||
|
input_item_img_shift = data_from_span.shift
|
||||||
|
input_item_img_url = data_from_span.img_url
|
||||||
|
if(data_from_span.source_link is not None):
|
||||||
|
src_links.append(data_from_span.source_link)
|
||||||
|
else:
|
||||||
|
data_from_img = extract_data_from_sprite_img(input_span_name_container)
|
||||||
|
if(data_from_img is None):
|
||||||
|
logger.error(f'ошибка для \n{input_span_name_container}\n: не удалось извлечь картинку и заголовок ни одним из способов')
|
||||||
|
return None
|
||||||
|
input_item_title = data_from_img.title
|
||||||
|
input_item_img_shift = data_from_img.shift
|
||||||
|
input_item_img_url = data_from_img.img_url
|
||||||
|
|
||||||
|
|
||||||
|
craft_items.append(CraftItem(input_item_title, input_item_img_url, input_item_img_shift))
|
||||||
|
|
||||||
|
counter = Counter(c.name for c in craft_items)
|
||||||
|
recipe = CraftRecipe(output_item_name, output_count, 'Верстак')
|
||||||
|
for element in counter.most_common(50):
|
||||||
|
##WARN айдишник изменится при сохранении
|
||||||
|
craft_components.append(CraftComponent(-1, element[0], element[1]))
|
||||||
|
|
||||||
|
return ParsedRecipeInput(
|
||||||
|
items=craft_items,
|
||||||
|
recipe=recipe,
|
||||||
|
components=craft_components,
|
||||||
|
source_links=[x for x in src_links if x is not None]
|
||||||
|
)
|
||||||
|
|
||||||
|
def extract_data_from_sprite_img(input_span_name_container) -> SpriteData|None:
|
||||||
|
input_item_img = input_span_name_container.find('img')
|
||||||
|
if(input_item_img is None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
title = input_item_img.get('alt')
|
||||||
|
if(title is None):
|
||||||
|
logger.error(f'ошибка для \n{input_span_name_container}\n: не найдено описание картинки!')
|
||||||
|
return None
|
||||||
|
|
||||||
|
url = input_item_img.get('src')
|
||||||
|
if(url is None):
|
||||||
|
logger.error(f'ошибка для \n{input_span_name_container}\n: не найден url картинки!')
|
||||||
|
return None
|
||||||
|
return SpriteData(
|
||||||
|
amount=1,
|
||||||
|
title=title,
|
||||||
|
shift=(0, 0),
|
||||||
|
img_url=url,
|
||||||
|
source_link=None)
|
||||||
|
|
||||||
|
def extract_data_from_sprite_span(input_span_name_container) -> SpriteData|None:
|
||||||
|
input_item_sprite_span = input_span_name_container.find('span', class_='sprite')
|
||||||
|
|
||||||
|
#Контейнера со спрайтом может не быть!
|
||||||
|
if(input_item_sprite_span is None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
input_item_title = input_item_sprite_span.get('title')
|
||||||
|
if(input_item_title is None):
|
||||||
|
logger.error(f'ошибка для \n{input_span_name_container}\n: не найдено заголовка предмета')
|
||||||
|
return None
|
||||||
|
|
||||||
|
output_amount=1
|
||||||
|
amount_container = input_span_name_container.find('span', class_='invslot-stacksize')
|
||||||
|
if(amount_container is not None):
|
||||||
|
logger.info(f'Для объекта {input_item_title} найдено количество : {amount_container.text}')
|
||||||
|
output_amount = int(amount_container.text)
|
||||||
|
|
||||||
|
item_source_link = get_item_source_link(input_span_name_container)
|
||||||
|
|
||||||
|
input_item_img_url = extract_img_classname(input_item_sprite_span)
|
||||||
|
if(input_item_img_url is None):
|
||||||
|
logger.error(f'Ошибка для \n{input_item_img_url}\n: не найден файл спрайта')
|
||||||
|
return None
|
||||||
|
|
||||||
|
input_item_img_shift = extract_img_shift(input_item_sprite_span)
|
||||||
|
if(input_item_img_shift is None):
|
||||||
|
logger.error(f'Ошибка для \n{input_item_sprite_span}\n: не найдено смещения для спрайта')
|
||||||
|
return None
|
||||||
|
|
||||||
|
return SpriteData(
|
||||||
|
amount=output_amount,
|
||||||
|
title=input_item_title,
|
||||||
|
shift=input_item_img_shift,
|
||||||
|
img_url=input_item_img_url,
|
||||||
|
source_link=item_source_link
|
||||||
|
)
|
||||||
|
|
||||||
|
def extract_img_shift(output_item_sprite_span):
|
||||||
|
if(output_item_sprite_span is None):
|
||||||
|
return None
|
||||||
|
css_value = output_item_sprite_span.get('style')
|
||||||
|
if(css_value is None):
|
||||||
|
return None
|
||||||
|
# Шаблон: ищем два числа (с возможным минусом) перед 'px'
|
||||||
|
pattern = r'(-?\d+)px\s+(-?\d+)px'
|
||||||
|
match = re.search(pattern, css_value)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
x = int(match.group(1))
|
||||||
|
y = int(match.group(2))
|
||||||
|
return (x, y)
|
||||||
|
else:
|
||||||
|
return None # Если шаблон не найден
|
||||||
|
|
||||||
|
def extract_img_classname(sprite_span):
|
||||||
|
if(sprite_span is None):
|
||||||
|
return None
|
||||||
|
'sprite industrialcraft-2-inv-sprite'
|
||||||
|
classes = sprite_span.get('class', [])
|
||||||
|
for cls in classes:
|
||||||
|
if cls.endswith('-sprite'):
|
||||||
|
return cls
|
||||||
|
return None # Если не найдено
|
||||||
|
|
||||||
|
def get_item_source_link(input_span_name_container) -> str|None:
|
||||||
|
link_container = input_span_name_container.find('a')
|
||||||
|
if(link_container is None):
|
||||||
|
return None
|
||||||
|
link = link_container.get('href')
|
||||||
|
if(link is None):
|
||||||
|
return None
|
||||||
|
return f'https://ru.minecraft.wiki{link}'
|
||||||
Reference in New Issue
Block a user