使用Scrapy框架爬取美食杰的菜谱信息
1.前提环境
安装好Pycharm,Pycharm里安装好Scrapy框架
2.创建Scrapy工程
在Pycharm左下角栏目找到Terminal,如下图:
点击Terminal,进入命令行窗口,如下图:
在该窗口执行以下命令,创建Scrapy工程
scrapy startproject 项目名称
例:scrapy startproject menu
进入工程目录,找到spiders文件夹,在其目录下用下面命令创建爬虫器。
scrapy genspider 爬虫名 域名
例: scrapy genspider getmenu https://www.meishij.net/
这时候我们的工程目录是这样的
为了方便运行爬虫程序,我创建了一个start.py文件来启动爬虫。
start.py
from scrapy import cmdline
cmdline.execute('scrapy crawl getmenu'.split()) #getmenu应该换成自己创建的爬虫器名称,我上面创建的爬虫器名称是getmenu
3.修改基本配置
3.1配置模拟请求
在配置文件settings.py中找到含有USER_AGENT这一行,把它的值改为浏览器的user-agent。=
USER_AGENT ='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'
user-agent的获取方式:打开浏览器,随便进入一个网页,按F12或右键-检查元素,出现下图窗口,并点击“网络”,窗口如下:
然后刷新页面,一堆请求就展示出来了,如下图:
随便点击一个请求,就会显示详细的请求信息,这时候就可以在详细的请求信息找到user-agent
3.2配置爬虫间隔
在配置文件settings.py中找到DOWNLOAD_DELAY,并设置你想要的时间间隔,单位是秒(s)
DOWNLOAD_DELAY = 0.1 #每隔0.1s爬取一条数据
当然,settings.py中还有很多参数可以设置,目前用暂时用不到,就先不作配置。
4.编写爬虫器的代码
在爬取网页数据时,我们往往要对网页的结构进行简单分析,只有这样才能写出高效率的爬虫程序。
4.1确定爬虫的目标网址
我的目标网址是:https://www.meishij.net/fenlei/chaofan/(美食杰的炒饭分区,如下图)
4.2确定要爬取的数据项
我想爬取的数据项有:菜谱名称、菜谱图片、做法步骤、主材料、辅料等菜谱的详细信息,而在上面的目标的网址中,只展示了菜谱的图片和名称等内容,不够详细,并不能达到我的要求。所以,就可能需要编写二级或多级url请求来实现进入菜谱的详情页面。
确定了要爬取的数据项后,为了方便对爬取的数据进行存储、修改等操作,编写items.py文件(当然,如果你只是想要观察一下爬取到了什么数据,可以不编写,在爬虫器每爬取一条数据就输出即可)。items.py如下:
import scrapy
class MenuItem(scrapy.Item):
name = scrapy.Field() #菜谱名称
img = scrapy.Field() #菜谱图片
step = scrapy.Field() #做法步骤
material1 = scrapy.Field() #主材料
material2 = scrapy.Field() #辅料
energy = scrapy.Field() #热量
sugar = scrapy.Field() #含糖量
fat = scrapy.Field() #脂肪
needtime = scrapy.Field() #所需时间
uptime = scrapy.Field() # 上传时间(这条不需要通过爬虫获得,在生成每条爬虫记录时插入系统时间即可)
level = scrapy.Field() #难度等级
4.3编写爬虫器
上面我们已经规划好要从什么网站爬取什么数据了,接下来就是编写爬虫器了。
首先,我们要确保从目标网址可以爬取到整个页面,以下代码验证以下是否可以爬取到https://www.meishij.net/fenlei/chaofan/的页面。
from datetime import datetime
import scrapy
from ..items import MenuItem
class GetmenuSpider(scrapy.Spider):
name = 'getmenu'
allowed_domains = ['https://www.meishij.net']
start_urls = ['https://www.meishij.net/fenlei/chaofan/'] #爬取的目标网址
def parse(self, response):
print(response.text) #打印爬取的页面
然后运行start.py,执行爬虫程序,结果如下:
从图中打印结果看出,这个网址的内容是能够被爬取成功的。
接下来,根据items.py文件中你要爬取的数据项,看看这个网址中的数据能不能满足你的要求。
通过观察,在这个网址中(https://www.meishij.net/fenlei/chaofan/),只有菜谱名称、菜谱图片能够爬取到,而做法步骤、主材料、热量、含糖量等数据项根本不存于这个网页。于是我们随便点进去一个菜谱,查看以下它的详情页(https://www.meishij.net/zuofa/huangjindanchaofan_22.html为例子),如下:
我惊人地发现我要爬取的数据项都在这个网页中(手动狗头)[/doge],于是,我只要通过目标网址,也就是爬虫开始的网址(https://www.meishij.net/fenlei/chaofan/)中跳转到对应的菜谱详情网址,然后进行爬取即可。
那么,如何在目标网址中跳转到对应的菜谱详情网址呢?我们需要对目标网址源码进行适当的分析
。
在目标页面按F12或者右键-检查元素打开下图窗口:
将鼠标放在某个菜谱的位置右键-检查元素
点击之后如下:
由上图可知,在class=“imgw”的div下有一个class=“list_s2_item_img”的a标签,它的href属性就是菜谱的详情链接,所以,要想进入每个菜谱的详情也买你,就要通过这条链接进入。
这时,爬虫器代码如下:
import scrapy
class GetmenuSpider(scrapy.Spider):
name = 'getmenu'
allowed_domains = ['https://www.meishij.net']
start_urls = ['https://www.meishij.net/fenlei/chaofan/']
def parse(self, response):
urls = response.xpath('//div[@class="imgw"]/a[@class="list_s2_item_img"]/@href').extract() #菜谱详情的链接
#每爬取到一条链接,就提交给下一个函数爬取菜谱详情。
for url in urls:
yield scrapy.Request(url, callback=self.parse2, dont_filter=True) # 提交url到下一层
def parse2(self, response):
print(response.text)
在上面代码中,parse函数的作用是从它开始进行爬虫,它只爬取网页的每个菜谱对应的链接,每爬取一条链接就提交给parse2函数。parse2函数是对单个菜谱的详情信息进行爬取。
通过上面的方法,我们可以通过目标网址(https://www.meishij.net/fenlei/chaofan/)来进入前页的所有菜谱(21条)的详情页面,下面将编写parse2函数,对菜谱的详情页进行爬取。
4.3.1爬取菜谱名称、难度、所需时间、主料、辅料
随便点进去一个菜谱详情页,并将鼠标移到菜谱名称出,然后右键-检查元素,如下图
由上图可见,菜谱名称存放在class="recipe_title"的div下的h1标签中,且它class="recipe_title"的又被包含在class="recipe_header_info"的div里面,将鼠标移动到class="recipe_header_info"的div,发现这个div存放的是下图信息
由图可知,菜谱名称,难度、所需时间、主料、辅料都在这个div中,而这些都是我们需要爬取的数据,这时,爬虫器代码如下:
import scrapy
from ..items import MenuItem
class GetmenuSpider(scrapy.Spider):
name = 'getmenu'
allowed_domains = ['https://www.meishij.net']
start_urls = ['https://www.meishij.net/fenlei/chaofan/']
def parse(self, response):
urls = response.xpath('//div[@class="imgw"]/a[@class="list_s2_item_img"]/@href').extract()
for url in urls:
yield scrapy.Request(url, callback=self.parse2, dont_filter=True) # 提交url到下一层
def parse2(self, response):
menuitem = MenuItem()
#菜谱名
menuitem['name'] = response.xpath('//div[@class="recipe_header_c"]/div[2]/h1/text()').extract()
#主材料+整合
mat1 = ""
material1list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[1]/div[2]/strong/a/text()').extract()
for i in range(0, len(material1list)):
mat1 = mat1 + material1list[i]
menuitem['material1'] = mat1
#配料+整合
mat2 = ""
material2list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[2]/div[2]/strong/a/text()').extract()
for i in range(0, len(material2list)):
mat2 = mat2 + material2list[i]
menuitem['material2'] = mat2
#难度等级
menuitem['level'] = response.xpath('//div[@class="info2"]/div[4]/strong/text()').extract()
#所需时间
menuitem['needtime'] = response.xpath('//div[@class="info2"]/div[3]/strong/text()').extract()
print(menuitem)
运行start.py启动爬虫程序,结果如下图
由图可知,我们获取到了菜谱名称,难度、所需时间、主料、辅料,但是除了主料、辅料这两个数据项外,其他数据项的值是一个列表,这对我们操作数据是非常不方便的(例如要把爬虫数据存入数MySQL数据库,列表类型的数据是存不进去的),所以我们将列表类型的数据转换成字符串类型,这时,爬虫器代码如下:
import scrapy
from ..items import MenuItem
class GetmenuSpider(scrapy.Spider):
name = 'getmenu'
allowed_domains = ['https://www.meishij.net']
start_urls = ['https://www.meishij.net/fenlei/chaofan/']
def parse(self, response):
urls = response.xpath('//div[@class="imgw"]/a[@class="list_s2_item_img"]/@href').extract()
for url in urls:
yield scrapy.Request(url, callback=self.parse2, dont_filter=True) # 提交url到下一层
def parse2(self, response):
menuitem = MenuItem()
#菜谱名
menuitem['name'] = response.xpath('//div[@class="recipe_header_c"]/div[2]/h1/text()').extract()
#主材料+整合
mat1 = ""
material1list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[1]/div[2]/strong/a/text()').extract()
for i in range(0, len(material1list)):
mat1 = mat1 + material1list[i]
menuitem['material1'] = mat1
#配料+整合
mat2 = ""
material2list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[2]/div[2]/strong/a/text()').extract()
for i in range(0, len(material2list)):
mat2 = mat2 + material2list[i]
menuitem['material2'] = mat2
#难度等级
menuitem['level'] = response.xpath('//div[@class="info2"]/div[4]/strong/text()').extract()
#所需时间
menuitem['needtime'] = response.xpath('//div[@class="info2"]/div[3]/strong/text()').extract()
#格式化--列表转字符串
menuitem['name'] = ''.join(menuitem['name'])
menuitem['level'] = ''.join(menuitem['level'])
menuitem['needtime'] = ''.join(menuitem['needtime'])
print(menuitem)
运行start.py启动爬虫程序,结果如下图
目前为止,我们已经成功爬取了菜谱名称,难度、所需时间、主料、辅料,还需要爬取的数据有:
4.3.2爬取菜谱图片链接
与上面方法同理,不难发现,图片链接class="recipe_header_c"的div下的第一个div下的img标签的src属性。
故爬虫器代码的parse2里面应该加上下面的代码
#菜谱图片
menuitem['img'] = response.xpath('//div[@class="recipe_header_c"]/div[1]/img/@src').extract()
4.3.3爬取菜谱的做法步骤
与以上方法同理,不难发现,class="recipe_step"的div有许多个,经过验证,是存放步骤的div,且每个div存放一个步骤,这时我们需要将所以步骤拿到,即拿到所有存放步骤的div,然后取出其里面的class="step_content"的div的p标签的内容,就是每个步骤的内容啦
所以,为了爬取菜谱的做法步骤,爬虫器代码的parse2应增加以下代码:
#步骤+整合
steplist = response.xpath('//div[@class="recipe_step"]/div[@class="step_content"]/p/text()').extract() #这里存放了一道菜谱的多个步骤
step = ""
for i in range(0, len(steplist)): #遍历步骤列表,合成一个字符串作为菜谱步骤
step = step + steplist[i]
menuitem['step'] = step
4.3.4爬取热量、含糖量、脂肪含量
与上面方法同理,不难发现,热量、含糖量、脂肪含量都被包含在class="jztbox“的div,如下图
经过观察,热量在class="jztbox“的div下面的第3个div下面的第1个div下的内容,含糖量在class="jztbox“的div下面的第2个div下面的第1个div下的内容,脂肪含量在class="jztbox“的div下面的第4个div下面的第1个div下的内容,故爬虫器代码的parse2应增加以下代码:
menuitem['energy'] = response.xpath('//div[@class="jztbox"]/div[3]/div[1]/text()').extract()
menuitem['sugar'] = response.xpath('//div[@class="jztbox"]/div[2]/div[1]/text()').extract()
menuitem['fat'] = response.xpath('div[@class="jztbox"]/div[4]/div[1]/text()').extract()
这时,爬虫器的代码如下:
import scrapy
from ..items import MenuItem
class GetmenuSpider(scrapy.Spider):
name = 'getmenu'
allowed_domains = ['https://www.meishij.net']
start_urls = ['https://www.meishij.net/fenlei/chaofan/']
def parse(self, response):
urls = response.xpath('//div[@class="imgw"]/a[@class="list_s2_item_img"]/@href').extract()
for url in urls:
yield scrapy.Request(url, callback=self.parse2, dont_filter=True) # 提交url到下一层
def parse2(self, response):
menuitem = MenuItem()
#菜谱名
menuitem['name'] = response.xpath('//div[@class="recipe_header_c"]/div[2]/h1/text()').extract()
#主材料+整合
mat1 = ""
material1list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[1]/div[2]/strong/a/text()').extract()
for i in range(0, len(material1list)):
mat1 = mat1 + material1list[i]
menuitem['material1'] = mat1
#配料+整合
mat2 = ""
material2list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[2]/div[2]/strong/a/text()').extract()
for i in range(0, len(material2list)):
mat2 = mat2 + material2list[i]
menuitem['material2'] = mat2
#难度等级
menuitem['level'] = response.xpath('//div[@class="info2"]/div[4]/strong/text()').extract()
#所需时间
menuitem['needtime'] = response.xpath('//div[@class="info2"]/div[3]/strong/text()').extract()
# 菜谱图片
menuitem['img'] = response.xpath('//div[@class="recipe_header_c"]/div[1]/img/@src').extract()
# 步骤+整合
steplist = response.xpath('//div[@class="recipe_step"]/div[@class="step_content"]/p/text()').extract()
step = ""
for i in range(0, len(steplist)):
step = step + steplist[i]
menuitem['step'] = step
#热量、含糖量、脂肪含量
menuitem['energy'] = response.xpath('//div[@class="jztbox"]/div[3]/div[1]/text()').extract()
menuitem['sugar'] = response.xpath('//div[@class="jztbox"]/div[2]/div[1]/text()').extract()
menuitem['fat'] = response.xpath('div[@class="jztbox"]/div[4]/div[1]/text()').extract()
#格式化--列表转字符串
menuitem['name'] = ''.join(menuitem['name'])
menuitem['level'] = ''.join(menuitem['level'])
menuitem['needtime'] = ''.join(menuitem['needtime'])
menuitem['img'] = ''.join(menuitem['img'])
menuitem['energy'] = ''.join(menuitem['energy'])
menuitem['sugar'] = ''.join(menuitem['sugar'])
menuitem['fat'] = ''.join(menuitem['fat'])
print(menuitem)
5.将爬虫数据存储到数据库(MySQL)
5.1安装MySQLdb插件
MySQLdb用于在python中连接数据库,将这个文件(mysqlclient-2.0.1-cp37-cp37m-win_amd64.whl 提取码:6666)拷贝到Pycharm的工作空间下(我的是workplace,是自定义的),然后在Terminal命令窗口进入该文件所在的文件夹,执行 install mysqlclient-2.0.1-cp37-cp37m-win_amd64.whl,等待安装完成即可。
验证是否安装成功,只需要在.py文件
import MySQLdb
不报错即可
5.2数据库前期准备
新建一个名称位menu的数据库
创建一个用户名为user2、密码为123的用户
创建一个cookbook2的表
cookbook2结构如下(注意编码格式,设置能展示中文的,否则可能插入会出现乱码或者插入失败)
5.3数据库连接配置
在settings.py文件增加以下代码:
mysql_host='127.0.0.1' #地址
mysql_user='user2' #经过授权的用户名
mysql_passwd='123' #经过授权的访问密码
mysql_db='menu' #数据库名称
mysql_tb='cookbook2' #表名
mysql_port=3306 #端口,默认3306
在settings.py文件找到ITEM_PIPELINES这个参数,并增加以下代码:
'menu.pipelines.MenusqlPipeline': 2,
我的ITEM_PIPELINES如下:
ITEM_PIPELINES = {
#数字越小优先级越高
#menu.pipelines.Class,Class与pipelines.py里的类名对应,如:menu.pipelines.MenusqlPipeline
'menu.pipelines.MenuPipeline': 300,
'menu.pipelines.MenusqlPipeline': 2, #提交到数据库
}
在pipelines.py增加以下代码:
import MySQLdb
from .settings import mysql_host,mysql_db,mysql_user,mysql_passwd,mysql_port
class MenusqlPipeline(object):
def __init__(self):
host=mysql_host
user=mysql_user
passwd=mysql_passwd
port=mysql_port
db=mysql_db
self.connection=MySQLdb.connect(host,user,passwd,db,port,charset='utf8')
self.cursor=self.connection.cursor()
def process_item(self, item, spider):
sql = "insert into cookbook2(name,step,sugar,energy,fat,material1,needtime,img,level,material2) values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
#注意 params添加的顺序要和sql语句插入的顺序一致,否则会出现错位甚至插入失败的情况
params=list()
params.append(item['name'])
params.append(item['step'])
params.append(item['sugar'])
params.append(item['energy'])
params.append(item['fat'])
params.append(item['material1'])
params.append(item['needtime'])
params.append(item['material2'])
params.append(item['img'])
params.append(item['level'])
self.cursor.execute(sql,tuple(params))
self.connection.commit()
return item
def __del__(self):
self.cursor.close()
self.connection.close()
5.4启动爬虫
爬虫器最后一行增加 return menuitem,如下:
import scrapy
from ..items import MenuItem
class GetmenuSpider(scrapy.Spider):
name = 'getmenu'
allowed_domains = ['https://www.meishij.net']
start_urls = ['https://www.meishij.net/fenlei/chaofan/']
def parse(self, response):
urls = response.xpath('//div[@class="imgw"]/a[@class="list_s2_item_img"]/@href').extract()
for url in urls:
yield scrapy.Request(url, callback=self.parse2, dont_filter=True) # 提交url到下一层
def parse2(self, response):
menuitem = MenuItem()
#菜谱名
menuitem['name'] = response.xpath('//div[@class="recipe_header_c"]/div[2]/h1/text()').extract()
#主材料+整合
mat1 = ""
material1list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[1]/div[2]/strong/a/text()').extract()
for i in range(0, len(material1list)):
mat1 = mat1 + material1list[i]
menuitem['material1'] = mat1
#配料+整合
mat2 = ""
material2list = response.xpath(
'//div[@class="recipe_ingredientsw"]/div[2]/div[2]/strong/a/text()').extract()
for i in range(0, len(material2list)):
mat2 = mat2 + material2list[i]
menuitem['material2'] = mat2
#难度等级
menuitem['level'] = response.xpath('//div[@class="info2"]/div[4]/strong/text()').extract()
#所需时间
menuitem['needtime'] = response.xpath('//div[@class="info2"]/div[3]/strong/text()').extract()
# 菜谱图片
menuitem['img'] = response.xpath('//div[@class="recipe_header_c"]/div[1]/img/@src').extract()
# 步骤+整合
steplist = response.xpath('//div[@class="recipe_step"]/div[@class="step_content"]/p/text()').extract()
step = ""
for i in range(0, len(steplist)):
step = step + steplist[i]
menuitem['step'] = step
#热量、含糖量、脂肪含量
menuitem['energy'] = response.xpath('//div[@class="jztbox"]/div[3]/div[1]/text()').extract()
menuitem['sugar'] = response.xpath('//div[@class="jztbox"]/div[2]/div[1]/text()').extract()
menuitem['fat'] = response.xpath('div[@class="jztbox"]/div[4]/div[1]/text()').extract()
#格式化--列表转字符串
menuitem['name'] = ''.join(menuitem['name'])
menuitem['level'] = ''.join(menuitem['level'])
menuitem['needtime'] = ''.join(menuitem['needtime'])
menuitem['img'] = ''.join(menuitem['img'])
menuitem['energy'] = ''.join(menuitem['energy'])
menuitem['sugar'] = ''.join(menuitem['sugar'])
menuitem['fat'] = ''.join(menuitem['fat'])
# print(menuitem)
return menuitem
通过运行start.py启动爬虫,查看数据库是否成功插入:插入成功!