安装好Pycharm,Pycharm里安装好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
在配置文件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
在配置文件settings.py中找到DOWNLOAD_DELAY,并设置你想要的时间间隔,单位是秒(s)
DOWNLOAD_DELAY = 0.1 #每隔0.1s爬取一条数据
当然,settings.py中还有很多参数可以设置,目前用暂时用不到,就先不作配置。
在爬取网页数据时,我们往往要对网页的结构进行简单分析,只有这样才能写出高效率的爬虫程序。
我的目标网址是:https://www.meishij.net/fenlei/chaofan/(美食杰的炒饭分区,如下图)
我想爬取的数据项有:菜谱名称、菜谱图片、做法步骤、主材料、辅料等菜谱的详细信息,而在上面的目标的网址中,只展示了菜谱的图片和名称等内容,不够详细,并不能达到我的要求。所以,就可能需要编写二级或多级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() #难度等级
上面我们已经规划好要从什么网站爬取什么数据了,接下来就是编写爬虫器了。
首先,我们要确保从目标网址可以爬取到整个页面,以下代码验证以下是否可以爬取到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函数,对菜谱的详情页进行爬取。
随便点进去一个菜谱详情页,并将鼠标移到菜谱名称出,然后右键-检查元素,如下图
由上图可见,菜谱名称存放在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启动爬虫程序,结果如下图
目前为止,我们已经成功爬取了菜谱名称,难度、所需时间、主料、辅料,还需要爬取的数据有:
与上面方法同理,不难发现,图片链接class="recipe_header_c"的div下的第一个div下的img标签的src属性。
故爬虫器代码的parse2里面应该加上下面的代码
#菜谱图片 menuitem['img'] = response.xpath('//div[@class="recipe_header_c"]/div[1]/img/@src').extract()
与以上方法同理,不难发现,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
与上面方法同理,不难发现,热量、含糖量、脂肪含量都被包含在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)
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
不报错即可
新建一个名称位menu的数据库
创建一个用户名为user2、密码为123的用户
创建一个cookbook2的表
cookbook2结构如下(注意编码格式,设置能展示中文的,否则可能插入会出现乱码或者插入失败)
在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()
爬虫器最后一行增加 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启动爬虫,查看数据库是否成功插入:插入成功!