爬虫之增量式爬虫
一:什么是增量式爬虫
爬虫策略:
-
广度优先
比如我们平时通过分页爬取的方式
-
深度优先
对于深度优先来说,必须使用增量爬虫
增量的含义就是不断的增加,它通过我们提供的一个入口,不断的去爬取数据,从而达到使数据不断增加的目的。
在我们平时的爬取过程中,会遇到一些问题:
-
页面内容发生变更
- 有些数据需要我们持久的慢慢的进行爬取
如果我们的爬虫页面发生了变化,也能够持续稳定的将变化的数据更新到数据库中,同时又能保证爬虫再执行的过程中,数据量也在不停的增加,这样的爬虫就叫增量爬虫。
增量爬虫的核心:去重,如果不进行去重会进入死链状态,详细可参考下面的案例
增量爬虫如何去重:
- 在请求发起之前,判断此url是否爬取过
- 在解析内容之后,判断该内容是否被爬取过
- 在保存数据时,判断将要写入的内容是否存在于存储介质中
二:案例演示
案例目标:猫眼电影-影人抓取,爬取尽可能多的演员信息
案例介绍:此案例的爬虫策略和之前博客中发布的爬虫案例的都不同,这个案例并没有分类页(列表页),没有一个完整的爬虫入口,只能够请求一个或多个演员,然后找到相关影人,从而尽可能多的获取。
- 编写调度器即定义一个队列进行存取
from lxml import etree
def main():
# 取出队列中的url,进行请求
url = scheduler.get()
# 定义downloader下载器处理取出的请求
response = downloader(url)
html = etree.HTML(response)
if __name__ == "__main__":
# 初始化任务池,尽可能多的放进一些url,这样搜索面才会大
start_urls = [
"https://maoyan.com/films/celebrity/789",
"https://maoyan.com/films/celebrity/29480",
"https://maoyan.com/films/celebrity/29481",
"https://maoyan.com/films/celebrity/28625",
"https://maoyan.com/films/celebrity/5392",
]
# 定义一个调度器队列
scheduler = Queue()
# 将任务池中的url放入到调度器的队列中
for url in start_urls:
scheduler.put(url)
# 定义一个main函数用来执行其它组件的功能
main()
- 编写下载器即downloader函数
作用:将从调度器拿到的url进行请求,并将请求后的响应返回给爬虫组件spider
def downloader(url):
response = requests.get(url)
return response.text
- 编写爬虫组件spider函数
作用:处理下载器发送过来的响应,获取数据
def spider(html):
# 创建一个item字典
item = {}
# 获取影人中文名字
zh_name = html.xpath('//p[@class="china-name cele-name"]/text()')[0]
# 获取影人英文名字
en_name = html.xpath('//p[@class="eng-name cele-name"]/text()')[0]
# 获取影人职业
profession = html.xpath('//span[@class="profession"]/text()')[0]
# 获取影人生日
birthday = html.xpath('//span[@class="birthday"]/text()')[0]
# 获取影人身高
height = html.xpath('//span[@class="height"]/text()')[0]
# 获取影人作品
display = html.xpath('//p[@class="movie-name"]/text()')
# 保存数据至item
item["zh_name"] = zh_name
item["en_name"] = en_name
item["profession"] = profession
item["birthday"] = birthday
item["height"] = height
item["display"] = display
这里出现了一个问题:如果影人没有英文名或是缺少了其它一些信息,那么我们就会得到一个空列表,之后用索引取值时就会报错,为了解决这个问题,我们再编写一个提取数据的函数,使得如果得到一个空列表则返回一个None。
编写extract_first提取函数
def extract_first(text):
if text:
return text[0]
return None
修改spider函数
def spider(html):
# 创建一个item字典
item = {}
# -------------------------------------------------------------------------------------
# 获取影人中文名字
zh_name = extract_first(html.xpath('//p[@class="china-name cele-name"]/text()'))
# 获取影人英文名字
en_name = extract_first(html.xpath('//p[@class="eng-name cele-name"]/text()'))
# 获取影人职业
profession = extract_first(html.xpath('//span[@class="profession"]/text()'))
# 获取影人生日
birthday = extract_first(html.xpath('//span[@class="birthday"]/text()'))
# 获取影人身高
height = extract_first(html.xpath('//span[@class="height"]/text()'))
# 获取影人作品
display = html.xpath('//p[@class="movie-name"]/text()')
# -------------------------------------------------------------------------------------
# 保存数据至item
item["zh_name"] = zh_name
item["en_name"] = en_name
item["profession"] = profession
item["birthday"] = birthday
item["height"] = height
item["display"] = display
return item
- 编写管道组件pipeline函数
作用:将获取到的数据保存到mongodb数据库中
def pipeline(item,url):
# 链接mongodb
client = pymongo.Client(host="127.0.0.1",port=27017)
# 进入数据库
db = client["first_text"]
# 进入集合
col = db["actor"]
# 插入数据,需进行判重
# 判重思路:一般都是通过url进行判重,在这里我们可以将主键id替换为我们
加密过的请求url,然后进行判重,如果此url已请求过则不再添加它的返回值
item["_id"] = encryption(url)
col.update({"_id":url},{"$set":item},True)
编写加密函数
def encryption(url):
# 初始化md5对象
md5 = hashlib.md5()
# 加密
md5.update(url.encode("utf-8"))
# 返回加密后的数据
return md5.hexdigest()
-
获取相关影人的url,并将获取到的url放入到队列中,不断进行请求,直到队列为空为止
对main函数进行修改
def main():
# ---------------------------------------------------------------------------------
# 使用死循环一直从队列中取出URL,在队列为空时退出循环
while True:
if scheduler.empty():
break
# ----------------------------------------------------------------------------------
# 取出队列中的url,进行请求
url = scheduler.get()
# 定义downloader下载器处理取出的请求
response = downloader(url)
html = etree.HTML(response)
# 定义spider爬虫组件接受下载器的请求并处理数据
item = spider(html)
# 定义pipeline管道对数据进行保存
pipeline(item,url)
# 获取相关影人的url
related_url_list = html.xpath('//div[@class="rel-item"]/a/@href')
for related_url in related_url_list:
full_related_url = "https://maoyan.com" + related_url
scheduler.put(full_related_url)
这里又出现了一个问题,假设某演员的相关影人中有他的朋友,当我们进入到他朋友的页面中后,如果他朋友的相关影人也有他那么就会不断的进行请求,就会形成一个死循环,所以我们要进行队列的去重操作。
- 定义去重函数url_seen,相当于Scrapy-Redis的去重器
去重器原理:将每一次取出的url放入到redis数据库中集合中,以放入时返回的值为判断条件,如果返回值为0则说明有重复的url,我们就不再进行请求,如果返回值为1,说明这个url没有被请求过,我们就可以对这个url发起请求
def url_seen(url):
"""去重函数"""
# 链接Redis数据库
r = redis.Redis(host="127.0.0.1",port=6379)
# 判重
res = r.sadd("maoyan:actor_url",encryption(url))
return res == 0
- 修改main函数,对队列中的url进行去重处理
def main():
# 使用死循环一直从队列中取出URL,在队列为空时退出循环
while True:
if scheduler.empty():
break
# 取出队列中的url,进行请求
url = scheduler.get()
# 在此处判断,即每一次从队列中取出url时,先去判断此url是否已经存在
# -----------------------------------------------------------
if not url_seen(url):
# -----------------------------------------------------------
# 定义downloader下载器处理取出的请求
response = downloader(url)
html = etree.HTML(response)
# 定义spider爬虫组件接受下载器的请求并处理数据
item = spider(html)
# 定义pipeline管道对数据进行保存
pipeline(item,url)
# 获取相关影人的url
related_url_list = html.xpath('//div[@class="rel-item"]/a/@href')
for related_url in related_url_list:
full_related_url = "https://maoyan.com" + related_url
# 将获取到的url放入到队列中
scheduler.put(full_related_url)
至此增量式爬虫编写完毕,在此提醒:运行文件的时候可别忘了打开mongodb和redis数据库的服务器,不然会报请求被拒绝的错误哦!
下面附完整无注释代码:
import redis
import hashlib
import pymongo
import requests
from lxml import etree
from queue import Queue
def extract_first(text):
if text:
return text[0]
return None
def encryption(url):
md5 = hashlib.md5()
md5.update(url.encode("utf-8"))
return md5.hexdigest()
def pipeline(item,url):
client = pymongo.MongoClient(host="127.0.0.1",port=27017)
db = client["first_text"]
col = db["actor"]
item["_id"] = encryption(url)
col.update_many({"_id":item["_id"]},{"$set":item},True)
def spider(html):
item = {}
zh_name = extract_first(html.xpath('//p[@class="china-name cele-name"]/text()'))
en_name = extract_first(html.xpath('//p[@class="eng-name cele-name"]/text()'))
profession = extract_first(html.xpath('//span[@class="profession"]/text()'))
birthday = extract_first(html.xpath('//span[@class="birthday"]/text()'))
height = extract_first(html.xpath('//span[@class="height"]/text()'))
display = html.xpath('//p[@class="movie-name"]/text()')
item["zh_name"] = zh_name
item["en_name"] = en_name
item["profession"] = profession
item["birthday"] = birthday
item["height"] = height
item["display"] = display
return item
def downloader(url):
response = requests.get(url)
return response.text
def url_seen(url):
r = redis.Redis(host="127.0.0.1",port=6379)
res = r.sadd("maoyan:actor_url",encryption(url))
return res == 0
def main():
while True:
if scheduler.empty():
break
url = scheduler.get()
if not url_seen(url):
response = downloader(url)
html = etree.HTML(response)
item = spider(html)
pipeline(item,url)
related_url_list = html.xpath('//div[@class="rel-item"]/a/@href')
for related_url in related_url_list:
full_related_url = "https://maoyan.com" + related_url
scheduler.put(full_related_url)
if __name__ == "__main__":
start_urls = [
"https://maoyan.com/films/celebrity/789",
"https://maoyan.com/films/celebrity/29480",
"https://maoyan.com/films/celebrity/29481",
"https://maoyan.com/films/celebrity/28625",
"https://maoyan.com/films/celebrity/5392",
]
scheduler = Queue()
for url in start_urls:
scheduler.put(url)
main()