赞
踩
这是大二的暑假专业实训内容,让我们在10天里用一种新语言python完成一个小项目,自己的收获还是蛮多的,因此想把这个项目完整写下来。
建立一个“智能菜谱”网站,可以通过图片识别分析出图片中的菜品,可以查看其详细信息,还可给出该菜品的详细做法。菜品数据来源于爬虫所得数据。
(包括但不仅限于以下内容)
1.Python爬虫:requests基本库知识、xpath和lxml解析库知识、http请求头、防盗链知识
2.PythonWeb:flask框架、html、js、css
3.数据库:sql语句,mysql相关操作
4.人工智能:百度AI开放平台
1.IDE:Pycharm2019.1
2.Python:3.7.3
3.数据库:MySQL8.0、Navicat Premium 12
4.测试浏览器:Chrome、Edge
由于是做菜谱的,自然我们要爬取一个美食网站,此次我们选择的是家常菜谱大全_美食天下:https://home.meishichina.com/recipe-menu.html
目标页①如下:
我们要爬取的是上面的所有菜,但这只是一个菜名,详细信息还要点击菜名进入详情页。
我们试着点击‘红烧肉’菜名进入目标页②。
目标页②如下:
可以看出进入目标页②,所展示的仍然是菜名列表,为了获得详情信息,我们还需点击一次菜名。
我们再试着点击上图倒数第二个菜进入目标页③。
目标页③如下:
没错了,是我们最终要得到的东西!分析页面内容,我们可以爬取的东西有:
菜品图片、菜名、食材、做法、烹饪窍门
总结:
1)从上面分析可以看出,我们总共要跳转两次,解析三个页面。
2)跳转两次则要得到2个目标链接。
3)爬取的内容有:菜品图片、菜名、食材、做法、烹饪窍门
我们可以用浏览器的开发者工具查看网页结构,在chrome浏览器里右键“检查”,或者直接F12即可进入。
目标页①(得到第一个目标链接):点击图中箭头所指按钮,可快速定位标签。
接下来,我们定位“红烧肉”所在标签位置:
1)由此我们便可以清晰地看出“红烧肉”的目标链接所在位置。
2)用xpath路径语言表示即为:(以下写法可以更复杂或更简单)
(’//div[@class=“wrap”]//div[@class=“category_sub clear”]/ul/li/a/@href’)
3)当然这种写法不只获得“红烧肉”一个链接,它会得到图中所有的菜品链接。
4)这样我们就得到了第一个要跳转的目标链接。
目标页②(得到第二个目标链接):
1)这里我们选的是“做法大全”中的菜系,由于只是一个简单的示范网站,因此可以不必爬取太多内容,因此这里我们只选择爬取其中的第2、3个菜(随便选的)。
2)这里说下怎么实现只选第2、3个菜:
a.首先可以看出目标链接位于li标签中,所以我们首先定位到div标签:xpath表示::(’//div[@class=“msb”]/div[@class=“msb_list clear”]/ul/li’)
b.a中返回的是所有的li节点的列表,我们只需用列表切片:list[1:3]便可以只选择第2、3个li标签节点
3)在第2)步的基础上再xpath得到目标链接(xpath后还可再次xpath):xpath表示:(‘div/a/@href’)
4)这样我们便可以得到第二个要跳转的目标链接了。
目标页③(得到菜品图、菜名、食材、做法、小窍门)
具体图解在这里就不贴了,跟上面的分析方法一样,这里只写一下xpath表示:(仅供参考,不一定完全相同)
1)菜品图:(’//div[@class=“recipDetail”]/div[@class=“recipe_De_imgBox”]/a/img/@src’)
2)菜名:(’//div[@class=“userTop clear”]/h1/a/text()’)
3)食材:(’//div[@class=“recipDetail”]/fieldset[@class=“particulars”]/div[@class=“recipeCategory_sub_R clear”]/ul/li//text()’)
4)做法:(’//div[@class=“recipDetail”]/div[@class=“recipeStep”]/ul/li/div[@class=“recipeStep_word”]/text()’)
5)小窍门:(’//div[@class=“recipDetail”]/div[@class=“recipeTip”]/text()’)
tips:这里xpath解析获得的内容并没有经过数据清洗、里面还可能参杂着诸多干扰字符,
如:"\n、\t、''、' ' ”等等,这会让我们对数据的使用造成麻烦。
因此,我们需要对爬取出来的数据进行过滤清洗,具体做法会在代码部分进行讲解。
1.首先,我们需要一个函数来请求我们的目标链接,返回网页源码:
def get_page(url): try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ' (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36', 'Referer': 'https://www.meishichina.com/' } response = requests.get(url, headers=headers) ''' # 设置编码 if response.encoding == 'ISO-8859-1': encodings = requests.utils.get_encodings_from_content(response.text) if encodings: encoding = encodings[0] else: encoding = response.apparent_encoding else: encoding = response.encoding response.encoding = encoding ''' # 由于已经知道了网页的编码方式,故而不用上面的通用猜测编码方式,这样会加快速度 response.encoding = 'utf-8' # 获取内容 html = response.text if response.status_code == 200: return html return None except RequestException: return None
这里请求头需要加上“User-Agent”和“Referer“,不然有可能返回不到正确的内容。简要说明一下其作用:
User-Agent:可以使服务器识别客户使用的操作系统 及版本、 浏览器及版本等信息。 在做爬虫时加上此信息,可以伪装为浏览器;如果不加,很可能会被识别州为爬虫。
Referer:此内容用来标识这个请求是从哪个页面发过来的,服务器可以拿到这一信息并做相 应的处理,如做来源统计、防盗链处理等。
还有编码问题已在代码中注解。
我们试着运行此函数:
print(get_page('https://home.meishichina.com/recipe-menu.html'))
返回:
运行正常!
def parse_mean_page(html):
# 解析菜单页
# mean = []
html_tree = etree.HTML(html)
html_path = html_tree.xpath('//div[@class="category_box"]/div[@class="category_sub clear"]/ul/li')
for data in html_path:
# title = (data.xpath('a/text()'))[0]
href = (data.xpath('a/@href'))[0]
yield href # 用生成器
# mean.append(href)
# print(mean)
# return mean # 返回‘菜名1的链接’的列表:[href1, href2, ...] # 数据太多,用生成器
最开始注释掉的部分是用列表来存储所有的目标链接,但这样数据太多的话会占用很大空间,最后是看到了生成器的知识,便把这个改为生成器来实现了。用yield可以极大地节约空间。
试着运行下:
gen = parse_mean_page(get_page('https://home.meishichina.com/recipe-menu.html'))
print(type(gen))
for data in gen:
print(data)
返回:
运行正常!
2)解析目标页②:
def parse_method_page(html):
# 解析一个菜名的不同做法页
method = []
html_tree = etree.HTML(html)
html_path = html_tree.xpath('//div[@class="msb_list clear"][2]/ul/li')
html_path_short = html_path[1:3] # 同一道菜只选择两个做法
for data in html_path_short:
# method_title = (data.xpath('div/a/span/text()'))[0]
method_href = (data.xpath('div/a/@href'))[0]
# method_img = (data.xpath('div/a/img/@data-src'))[0]
method.append(method_href)
# print(method)
return method # 返回‘菜名2的链接’的列表:[href1, href2]
由于这里的列表中只有两个值,故而不需用生成器。
运行:
print(parse_method_page(get_page('https://www.meishichina.com/mofang/hongshaorou/')))
返回:
运行正常!
3)解析目标页③:
def parse_detail_page(html): # 解析一个菜的做法详情页 detail = [] html_tree = etree.HTML(html) # 图片: img_url = (html_tree.xpath('//div[@class="recipDetail"]/div[@class="recipe_De_imgBox"]/a/img/@src'))[0] # 菜名: detail_name = (html_tree.xpath('//div[@class="userTop clear"]/h1/a/text()'))[0] # 食材清单: material_list = html_tree.xpath('//div[@class="recipDetail"]/fieldset[@class="particulars"]') detail_material = [] # 存放主料、辅料、调料 for material in material_list: each_material = ''.join(material.xpath('div[@class="recipeCategory_sub_R clear"]/ul/li//text()'))\ .replace('\n\n\n', ' ').replace('\n', '') # 根据规律格式化 detail_material.append(each_material) new_material = '$$'.join(detail_material) # 得到含有分隔符的字符串 # 步骤: detail_step = html_tree.xpath('//div[@class="recipDetail"]/div[@class="recipeStep"]' '/ul/li/div[@class="recipeStep_word"]/text()') new_step = '$$'.join(detail_step) # 得到含有分隔符的字符串 # detail_step = detail_step.replace("\n\n\n\n\n", " ").replace("\n", "") # 小窍门: tips = (html_tree.xpath('//div[@class="recipDetail"]/div[@class="recipeTip"]/text()')) tips_list = [] for tip in tips: new_tip = tip.strip() # 删除空白符\n、\t、' '等,但会得到一个空字符串'' tips_list.append(new_tip) new_list = filter(None, tips_list) # 去掉列表中的空字符''和None # print(type(new_list)) # <class 'filter'> new_tips = '$$'.join(new_list) # 得到含有分隔符的字符串 # 整合: detail.append(img_url) detail.append(detail_name) detail.append(new_material) detail.append(new_step) detail.append(new_tips) return detail # 返回[图片地址, 菜名2, 配料, 步骤, 小窍门]的列表
a.相关的数据清洗,代码中都有注释,这里说明一下注释中的"得到含有分隔符的字符串":
b.由于数据要存到数据库,数据库中只给列表中的每一项分配了一个字段,而有些项中却包含几段内容(比如“步骤”项就包括了好几段内容,存入数据库只能在一个字段中,这样以后取的时候也会整个就取出来了,无法达到分隔效果,这里为什么不给“步骤”中的每一项分配一个字段呢?因为其中的项目数目是不固定的,因此无法实现),靠一个字段无法分隔,因此便给分段的内容加了分隔符’$$’,这样后面取出来的时候可以利用split函数进行分隔,得到原本的分段内容,虽然很麻烦但我也只能想到这种方法了。
运行:
print(parse_detail_page(get_page('https://home.meishichina.com/recipe-30999.html')))
返回:
运行正常!
3.以上便是实现了对爬虫相关函数的设计。
# import time import pymysql import spider # 引入自己写的爬取菜品的相关函数模块 # 连接database def connect(): # 建立连接 conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='******', database='new_mean', charset='utf8') if conn: print("数据库连接成功!") return conn else: print("数据库连接失败!") return None def insert_db(conn, detail_list): # 得到一个可以执行SQL语句的光标对象 cursor = conn.cursor() sql = "INSERT INTO mean_table(img_url, food_name, material, step, tips) VALUES (%s, %s, %s, %s, %s);" try: # 执行SQL语句 cursor.execute(sql, detail_list) # 提交事务 conn.commit() except Exception as e: # 有异常,回滚事务 conn.rollback() finally: cursor.close() def close_db(conn): conn.close() print("数据库已关闭!") def load(): url = "https://home.meishichina.com/recipe-menu.html" mean_list_url = spider.parse_mean_page(spider.get_page(url)) conn = connect() i = 0 print("载入成功:") for data1 in mean_list_url: method_list = spider.parse_method_page(spider.get_page(data1)) # 获得一个菜的做法url列表 for data2 in method_list: html = spider.get_page(data2) # 有时会遇到返回网页源码错误的问题,这里遇到了我们就忽略,继续下一次操作 if html is None: continue detail_list = spider.parse_detail_page(html) # 获得一个菜的详情界面 # print(detail_list) insert_db(conn, detail_list) i += 1 print("\r已插入:{0}个...".format(i), end='') # time.sleep(0.2) print() close_db(conn)
这样所有结果便存到数据库中了!
数据库中内容如下:
具体代码已经上传到GitHub:智能菜谱
tips:这一块实际没用到自己学的什么,就是用了百度的一个开放接口,但我还是会把详细过程记录下来
1.下载相应SDK:pip install baidu-aip
2.新建AipImageClassify:AipImageClassify是图像识别的Python SDK客户端,为使用图像识别的开发人员提供了一系列的交互方法。
from aip import AipImageClassify
""" 你的 APPID AK SK """
APP_ID = '你的 App ID'
API_KEY = '你的 Api Key'
SECRET_KEY = '你的 Secret Key'
client = AipImageClassify(APP_ID, API_KEY, SECRET_KEY)
3.设置可选参数:
""" 调用菜品识别 """
client.dishDetect(image);
""" 如果有可选参数 """
options = {}
options["top_num"] = 3
options["filter_threshold"] = "0.7"
options["baike_num"] = 5
""" 带参数调用菜品识别 """
client.dishDetect(image, options)
详细信息请参考:百度AI图像识别Python SDK文档
知道了大致页面布局,接着便用flask框架来设计路由和视图:
<div class="footer" style='background: rgba(221,221,221,0.7)'> <div> <form action="/" method="post" enctype="multipart/form-data"> <input type="url" name="img_url" /> <input class="button bt1" type="submit" value="上传URL" /> </form> <form action="/" method="post" enctype="multipart/form-data"> <input name="food" type="text"/> <input type="submit" value="搜索菜品"/> </form> <form action="/" method="post" enctype="multipart/form-data"> <input type="file" id="file" name="img" accept="image/png, image/jpeg, image/jpg" /> <input type="submit" onclick="changepic(this)" value="上传图片"/> </form> </div> {% if msg %} {{ msg }} {% endif %} </div>
b: 后端路由 :
@app.route('/', methods=['GET', 'POST']) def index(): food_name = [] result_url = [] calorie = [] descriptions = [] image_url = [] if request.method == 'GET': return render_template('index.html') else: img = request.files.get('img') img_url = request.form.get('img_url') if not img: # 没有上传图片则取url if not img_url: food = request.form.get('food') print("name", food) if not food: return render_template('index.html', msg='请选择一种搜索方式!') return redirect(url_for("show", food_name=food)) else: try: img = ur.urlopen(img_url) except Exception: return render_template('index.html', msg="请上传图片或URL地址!") # 图片信息上传到百度接口 client = AipImageClassify(APP_ID, API_KEY, SECRET_KEY) options = {'top_num': 5, 'baike_num': 5} food_info = client.dishDetect(img.read(), options) print(food_info) for info in food_info['result']: if info['name'] == '非菜': return render_template('index.html', msg="未识别到菜品类!请重新上传!") else: food_name.append(info['name']) if 'description' in info['baike_info']: descriptions.append(info['baike_info']['description']) else: descriptions.append('暂无此信息') if info['has_calorie'] is True: calorie.append(info['calorie']) else: calorie.append('暂无此信息') if 'image_url' in info['baike_info']: image_url.append(info['baike_info']['image_url']) else: image_url.append('../static/image/暂无图片.jpg') name_length = len(food_name) for num in food_name: url = url_for("show", food_name=num) result_url.append(url) return render_template('search.html', result_url=result_url, calorie=calorie, food_name=food_name, descriptions=descriptions, name_length=name_length, image_url=image_url, img_url=img_url)
最后一句是跳转到search.html(识别结果页)页面:
search.html主要代码:
<div class="footer" style='background: rgba(221,221,221,0.7);text-align: left'> {% if img_url %} <p><b>你上传的url图片为:</b></p> <ul> <li style="list-style: none"><br /> <img class="fakeimg" style="height: 300px;" src="{{ img_url }}" alt="上传的url图片" /> </li> </ul> {% endif %} {% if name_length %} <p><b>你可能想找:</b></p> <ol> {% for i in range(name_length) %} <li> <br /><img class="fakeimg" style="height: 280px;" src="{{ image_url[i] }}" alt="百科图片" /><br /> 名字:<b>{{ food_name[i] }}</b> <span><a href="{{ result_url[i] }}">查看做法</a></span><br /> 卡路里:{{ calorie[i] }}<br /> 描述:<br /><span>{{ descriptions[i] }}</span><br /> </li> {% endfor %} {% else %} {{ msg }} </ol> {% endif %} </div>
2.详情页:
a: 前端主要代码:
result.html
<div class="leftcolumn"> {% if results %} {% for data in results %} <div class="card" style='background: rgba(221,221,221,0.7)'> <h2>名字:{{ data[1] }}</h2> <img class="fakeimg" style="height:300px;" src="{{ data[0]}}" alt="图片示例" /> <div class="fakeimg_two" style="background: rgba(255,250,240,0.7)"> <b>食材明细:</b><br /> <ol> {% if data[2][0] %} <li>主料:{{ data[2][0] }}</li> {% endif %} {% if data[2][1] %} <li>辅料:{{ data[2][1] }}</li> {% endif %} {% if data[2][2] %} <li>配料:{{ data[2][2] }}</li> {% endif %} </ol> </div> <div class="fakeimg_two" style="background: rgba(255,255,240,0.7)"> <b>做法:</b><br /> <ol> {% for step in data[3] %} <li>{{ step}}</li> {% endfor %} </ol> </div> {% if data[4][0]!='' %} <div class="fakeimg_two" style="background: rgba(255,250,240,0.7)"> <b>小贴士:</b><br /><br /> {% for tip in data[4] %} <span>{{ tip }}</span><br /> {% endfor %} </div> {% endif %} </div> {% endfor %} {% else %} <p>本站暂时没有此菜品,试试其他关键字吧!</p> {% endif %} </div>
b: 后端路由:
@app.route('/result/<food_name>')
def show(food_name):
food_name = food_name
conn = dboperation.connect()
results = dboperation.search_db(conn, food_name)
return render_template('result.html', food_name=food_name, results=results)
数据存到了数据库,自然要取出来,上面代码已经用到了数据库读入模板,相关操作都很简单,这里只贴一下主要代码:
dboperation.py
import pymysql def connect(): conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='fff252852', database='new_mean', charset='utf8') # 得到一个可以执行SQL语句的光标对象 if conn: print("数据库连接成功!") return conn else: print("数据库连接失败!") return None def search_db(conn, name): cur = conn.cursor() sql = "SELECT * FROM mean_table WHERE food_name LIKE '%%%s%%'" % name try: cur.execute(sql) # 执行sql语句 results = cur.fetchall() # 获取查询的所有记录 result_list = [] for data in results: # 数据读入格式化 materials = data[3].split("$$") steps = data[4].split("$$") tips = data[5].split("$$") mean_tuple = (data[1], data[2], materials, steps, tips) result_list.append(mean_tuple) return result_list # 遍历结果 except Exception as e: print("数据库执行失败!") raise e finally: cur.close() conn.close() print("数据库关闭成功!")
这里涉及到读取数据的格式化,也就是利用存入时的分隔符"$$"来分隔数据,让我们能在flask中更好的调用这些数据。
具体代码后面会上传到GitHub。
所学到的知识:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。