Files
DeveloperandClaude Opus 4.6 0a50c09dba Initial commit - Zhangmenu Django project
A menu management application built with Django, featuring:
- Dish management with cover images
- Order system with party dates and participants
- REST API with Django REST Framework
- Bootstrap-styled frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:27:23 +08:00

218 lines
7.5 KiB
Python

from urllib.parse import urlparse
import requests
from bs4 import BeautifulSoup
from django.conf import settings
import re
import json
class ParserService:
"""URL解析业务逻辑"""
@staticmethod
def parse_url(url):
"""解析外部URL获取菜品信息"""
if not url:
raise ValueError('请提供URL')
parsed_url = urlparse(url)
hostname = parsed_url.hostname
allowed_hosts = getattr(settings, 'ALLOWED_RECIPE_HOSTS', set())
if parsed_url.scheme not in {'http', 'https'}:
raise ValueError('URL协议不受支持')
if not hostname:
raise ValueError('URL缺少主机名')
if hostname not in allowed_hosts:
raise ValueError('该站点不在允许的解析范围内')
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
result = {
'name': '',
'cover_image': '',
'description': '',
'ingredients': []
}
# 根据不同网站选择解析方法
if hostname in {'xiachufang.com', 'www.xiachufang.com'}:
result = ParserService._parse_xiachufang(soup, url)
elif hostname in {'meishij.net', 'www.meishij.net'}:
result = ParserService._parse_meishij(soup)
elif hostname in {'caipuw.com', 'www.caipuw.com'}:
result = ParserService._parse_caipuw(soup)
else:
result = ParserService._parse_generic(soup)
return result
@staticmethod
def _parse_xiachufang(soup, url):
"""解析下厨房网站"""
result = {
'name': '',
'cover_image': '',
'description': '',
'ingredients': []
}
# 尝试从JSON-LD获取数据
json_ld = soup.find_all('script', type='application/ld+json')
for script in json_ld:
try:
data = json.loads(script.string)
if data.get('@type') == 'Recipe':
result['name'] = data.get('name', '')
result['cover_image'] = data.get('image', '')
if isinstance(result['cover_image'], list):
result['cover_image'] = result['cover_image'][0]
# 解析材料
for ing in data.get('recipeIngredient', []):
match = re.match(r'^([\d.]+\s*[克g个勺片块大中小]+)?\s*(.+)$', ing)
if match and match.group(2):
amount, name = match.groups()
result['ingredients'].append({
'name': name.strip(),
'amount': (amount.strip() + '克') if amount and not ('克' in amount or '个' in amount or '勺' in amount or '片' in amount or '块' in amount) else (amount.strip() if amount else '')
})
else:
result['ingredients'].append({'name': ing, 'amount': ''})
# 解析做法
steps = []
recipe_steps = data.get('recipeInstructions', [])
if isinstance(recipe_steps, list):
for step in recipe_steps:
text = step.get('text', '') if isinstance(step, dict) else str(step)
if text:
steps.append(text)
elif isinstance(recipe_steps, str):
parts = re.split(r'(?=\d+\.)', recipe_steps)
for part in parts:
part = re.sub(r'^\d+\.\s*', '', part.strip())
if part:
steps.append(part)
if steps:
result['description'] = '\n\n'.join([f'{i+1}. {s}' for i, s in enumerate(steps)])
break
except (TypeError, ValueError, json.JSONDecodeError):
continue
# 回退到HTML解析
if not result['name']:
title = soup.select_one('h1.title')
if title:
result['name'] = title.get_text(strip=True)
if not result['cover_image']:
cover = soup.select_one('div.cover img') or soup.select_one('div.image img')
if cover:
result['cover_image'] = cover.get('src', '')
return result
@staticmethod
def _parse_meishij(soup):
"""解析美食杰网站"""
result = {
'name': '',
'cover_image': '',
'description': '',
'ingredients': []
}
title = soup.select_one('h1.recipe_title')
if title:
result['name'] = title.get_text(strip=True)
cover = soup.select_one('div.recipe_img img')
if cover:
result['cover_image'] = cover.get('src', '')
# 材料
ing_list = soup.select('div.yl li')
for ing in ing_list:
text = ing.get_text(strip=True)
if text:
parts = text.split()
if len(parts) >= 2:
result['ingredients'].append({'name': parts[0], 'amount': ' '.join(parts[1:])})
elif len(parts) == 1:
result['ingredients'].append({'name': parts[0], 'amount': ''})
# 做法
steps = []
step_list = soup.select('div.step_content')
for step in step_list:
text = step.get_text(strip=True)
if text:
steps.append(text)
result['description'] = '\n\n'.join([f'{i+1}. {s}' for i, s in enumerate(steps)])
return result
@staticmethod
def _parse_caipuw(soup):
"""解析菜谱大全网站"""
result = {
'name': '',
'cover_image': '',
'description': '',
'ingredients': []
}
title = soup.select_one('h1.title')
if title:
result['name'] = title.get_text(strip=True)
cover = soup.select_one('div.image img')
if cover:
result['cover_image'] = cover.get('src', '')
# 材料
ing_list = soup.select('div.ingredients li') or soup.select('ul.ingredients li')
for ing in ing_list:
text = ing.get_text(strip=True)
if text:
result['ingredients'].append({'name': text, 'amount': ''})
# 做法
steps = []
step_list = soup.select('div.steps li') or soup.select('div.step')
for step in step_list:
text = step.get_text(strip=True)
if text:
steps.append(text)
result['description'] = '\n\n'.join([f'{i+1}. {s}' for i, s in enumerate(steps)])
return result
@staticmethod
def _parse_generic(soup):
"""通用解析"""
result = {
'name': '',
'cover_image': '',
'description': '',
'ingredients': []
}
title = soup.select_one('h1') or soup.select_one('title')
if title:
result['name'] = title.get_text(strip=True)[:100]
img = soup.select_one('article img') or soup.select_one('div.content img') or soup.select_one('img')
if img:
result['cover_image'] = img.get('src', '')
return result