经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » MongoDB » 查看文章
使用scrapy实现增量式爬取方式
来源:jb51  时间:2022/6/21 10:20:44  对本文有异议

实现爬虫的增量式爬取有两种方法,一是在获得页面解析的内容后判断该内容是否已经被爬取过,二是在发送请求之前判断要被请求的url是否已经被爬取过,前一种方法可以感知每个页面的内容是否发生变化,能获取页面新增或者变化的内容,但是由于要对每个url发送请求,所以速度比较慢,而对网站服务器的压力也比较大,后一种无法获得页面变化的内容,但是因为不用对已经爬取过的url发送请求,所以对服务器压力比较小,速度比较快,适用于爬取新增网页

下面用一个小说网站爬虫的例子来介绍在scrapy中这两种方式的实现

1.要爬取的信息

在scrapy中,信息通过item来封装,这里我定义两个item,一个用于封装每本小说的信息,一个用于封装每个章节的信息

1.BookItem

  1. class BookItem(scrapy.Item):
  2. _id = scrapy.Field() #小说id,用于定位章节信息,章节唯一
  3. novel_Name = scrapy.Field() #小说名称
  4. novel_Writer = scrapy.Field()#小说作者
  5. novel_Type = scrapy.Field()#小说类型
  6. novel_Status = scrapy.Field()#小说状态,连载或者完结
  7. novel_UpdateTime = scrapy.Field()#最后更新时间
  8. novel_Words = scrapy.Field() #总字数
  9. novel_ImageUrl = scrapy.Field()#封面图片
  10. novel_AllClick = scrapy.Field()#总点击
  11. novel_MonthClick = scrapy.Field()#月点击
  12. novel_WeekClick = scrapy.Field()#周点击
  13. novel_AllComm = scrapy.Field()#总推荐
  14. novel_MonthComm = scrapy.Field()#月推荐
  15. novel_WeekComm = scrapy.Field()#周推荐
  16. novel_Url = scrapy.Field()#小说url
  17. novel_Introduction = scrapy.Field()#小说简介

2.ChapterItem

  1. class ChapterItem(scrapy.Item):
  2. chapter_Url = scrapy.Field()#章节url
  3. _id = scrapy.Field()#章节id
  4. novel_Name = scrapy.Field()#小说名称
  5. chapter_Name = scrapy.Field()#章节名称
  6. chapter_Content = scrapy.Field()#内容
  7. novel_ID = scrapy.Field()#小说id
  8. is_Error = scrapy.Field()#是否异常

2.解析信息

这里我是用的是scrapy自带的通用爬虫模块,只需要指定信息解析方式,需要跟进的url就够了

1.指定需要跟进的url和回调函数

  1. allowed_domains = ["23us.so"] #允许爬取的域名
  2. start_urls = ["http://www.23us.so/xiaoshuo/414.html"]#种子url
  3. #跟进的url
  4. rules=(
  5. Rule(LinkExtractor(allow=("xiaoshuo/\d*\.html")),callback="parse_book_message",follow=True),
  6. Rule(LinkExtractor(allow=("files/article/html/\d*?/\d*?.index.html")),callback="parse_book_chapter",follow=True),
  7. Rule(LinkExtractor(allow=("files/article/html/\d*?/\d*?/\d*?.html")),callback="parse_chapter_content",follow=True),
  8. Rule(LinkExtractor(allow=(".*")),follow=True),
  9. )

2.解析方法

1.解析书籍信息方法

  1. #解析小说信息页面
  2. def parse_book_message(self,response):
  3. if not response.body:
  4. print(response.url+"已经被爬取过了,跳过")
  5. return;
  6. ht = response.body.decode("utf-8")
  7. text = html.fromstring(ht)
  8. novel_Url = response.url
  9. novel_Name = text.xpath(".//dl[@id='content']/dd[1]/h1/text()")[0].split(" ")[0] if response.xpath(".//dl[@id='content']/dd[1]/h1/text()") else "None"
  10. novel_ImageUrl = text.xpath(".//a[@class='hst']/img/@src")[0] if response.xpath(".//a[@class='hst']/img/@src") else "None"
  11. novel_ID = int(response.url.split("/")[-1].split(".")[0]) if response.url.split("/")[-1].split(".") else "None"
  12. novel_Type = text.xpath(".//table[@id='at']/tr[1]/td[1]/a/text()") if response.xpath(".//table[@id='at']/tr[1]/td[1]/a/text()") else "None"
  13. novel_Writer = "".join(text.xpath(".//table[@id='at']/tr[1]/td[2]/text()")) if response.xpath(".//table[@id='at']/tr[1]/td[2]/text()") else "None"
  14. novel_Status = "".join(text.xpath(".//table[@id='at']/tr[1]/td[3]/text()")) if response.xpath(".//table[@id='at']/tr[1]/td[3]/text()") else "None"
  15. novel_Words = self.getNumber("".join(text.xpath(".//table[@id='at']/tr[2]/td[2]/text()"))) if response.xpath(".//table[@id='at']/tr[2]/td[2]/text()") else "None"
  16. novel_UpdateTime = "".join(text.xpath(".//table[@id='at']/tr[2]/td[3]/text()")) if response.xpath(".//table[@id='at']/tr[2]/td[3]/text()") else "None"
  17. novel_AllClick = int("".join(text.xpath(".//table[@id='at']/tr[3]/td[1]/text()"))) if response.xpath(".//table[@id='at']/tr[3]/td[1]/text()") else "None"
  18. novel_MonthClick = int("".join(text.xpath(".//table[@id='at']/tr[3]/td[2]/text()"))) if response.xpath(".//table[@id='at']/tr[3]/td[2]/text()") else "None"
  19. novel_WeekClick = int("".join(text.xpath(".//table[@id='at']/tr[3]/td[3]/text()"))) if response.xpath(".//table[@id='at']/tr[3]/td[3]/text()") else "None"
  20. novel_AllComm = int("".join(text.xpath(".//table[@id='at']/tr[4]/td[1]/text()"))) if response.xpath(".//table[@id='at']/tr[4]/td[1]/text()") else "None"
  21. novel_MonthComm = int("".join(text.xpath(".//table[@id='at']/tr[4]/td[3]/text()"))) if response.xpath(".//table[@id='at']/tr[4]/td[2]/text()") else "None"
  22. novel_WeekComm = int("".join(text.xpath(".//table[@id='at']/tr[4]/td[3]/text()"))) if response.xpath(".//table[@id='at']/tr[4]/td[3]/text()") else "None"
  23. pattern = re.compile('<p>(.*)<br')
  24. match = pattern.search(ht)
  25. novel_Introduction = "".join(match.group(1).replace("&nbsp;","")) if match else "None"
  26. #封装小说信息类
  27. bookitem = BookItem(
  28. novel_Type = novel_Type[0],
  29. novel_Name = novel_Name,
  30. novel_ImageUrl = novel_ImageUrl,
  31. _id = novel_ID, #小说id作为唯一标识符
  32. novel_Writer = novel_Writer,
  33. novel_Status = novel_Status,
  34. novel_Words = novel_Words,
  35. novel_UpdateTime = novel_UpdateTime,
  36. novel_AllClick = novel_AllClick,
  37. novel_MonthClick = novel_MonthClick,
  38. novel_WeekClick = novel_WeekClick,
  39. novel_AllComm = novel_AllComm,
  40. novel_MonthComm = novel_MonthComm,
  41. novel_WeekComm = novel_WeekComm,
  42. novel_Url = novel_Url,
  43. novel_Introduction = novel_Introduction,
  44. )
  45. return bookitem

2.解析章节信息

  1. def parse_chapter_content(self,response):
  2. if not response.body:
  3. print(response.url+"已经被爬取过了,跳过")
  4. return;
  5. ht = response.body.decode('utf-8')
  6. text = html.fromstring(ht)
  7. soup = BeautifulSoup(ht)
  8. novel_ID = response.url.split("/")[-2]
  9. novel_Name = text.xpath(".//p[@class='fr']/following-sibling::a[3]/text()")[0]
  10. chapter_Name = text.xpath(".//h1[1]/text()")[0]
  11. '''
  12. chapter_Content = "".join("".join(text.xpath(".//dd[@id='contents']/text()")).split())
  13. if len(chapter_Content) < 25:
  14. chapter_Content = "".join("".join(text.xpath(".//dd[@id='contents']//*/text()")))
  15. pattern = re.compile('dd id="contents".*?>(.*?)</dd>')
  16. match = pattern.search(ht)
  17. chapter_Content = "".join(match.group(1).replace("&nbsp;","").split()) if match else "爬取错误"
  18. '''
  19. result,number = re.subn("<.*?>","",str(soup.find("dd",id='contents')))
  20. chapter_Content = "".join(result.split())
  21. print(len(chapter_Content))
  22. novel_ID = response.url.split("/")[-2]
  23. return ChapterItem(
  24. chapter_Url = response.url,
  25. _id=int(response.url.split("/")[-1].split(".")[0]),
  26. novel_Name=novel_Name,
  27. chapter_Name=chapter_Name,
  28. chapter_Content= chapter_Content,
  29. novel_ID = novel_ID,
  30. is_Error = len(chapter_Content) < 3000
  31. )

3.scrapy中实现增量式爬取的几种方式

1.缓存

通过开启缓存,将每个请求缓存至本地,下次爬取时,scrapy会优先从本地缓存中获得response,这种模式下,再次请求已爬取的网页不用从网络中获得响应,所以不受带宽影响,对服务器也不会造成额外的压力,但是无法获取网页变化的内容,速度也没有第二种方式快,而且缓存的文件会占用比较大的内存,在setting.py的以下注释用于设置缓存

  1. #HTTPCACHE_ENABLED = True
  2. #HTTPCACHE_EXPIRATION_SECS = 0
  3. #HTTPCACHE_DIR = 'httpcache'
  4. #HTTPCACHE_IGNORE_HTTP_CODES = []
  5. #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

这种方式比较适合内存比较大的主机使用,我的阿里云是最低配的,在爬取半个晚上接近27W个章节信息后,内存就用完了

2.对item实现去重

本文开头的第一种方式,实现方法是在pipelines.py中进行设置,即在持久化数据之前判断数据是否已经存在,这里我用的是mongodb持久化数据,逻辑如下

  1. #处理书信息
  2. def process_BookItem(self,item):
  3. bookItemDick = dict(item)
  4. try:
  5. self.bookColl.insert(bookItemDick)
  6. print("插入小说《%s》的所有信息"%item["novel_Name"])
  7. except Exception:
  8. print("小说《%s》已经存在"%item["novel_Name"])
  9. #处理每个章节
  10. def process_ChapterItem(self,item):
  11. try:
  12. self.contentColl.insert(dict(item))
  13. print('插入小说《%s》的章节"%s"'%(item['novel_Name'],item['chapter_Name']))
  14. except Exception:
  15. print("%s存在了,跳过"%item["chapter_Name"])
  16. def process_item(self, item, spider):
  17. '''
  18. if isinstance(item,ChaptersItem):
  19. self.process_ChaptersItem(item)
  20. '''
  21. if isinstance(item,BookItem):
  22. self.process_BookItem(item)
  23. if isinstance(item,ChapterItem):
  24. self.process_ChapterItem(item)
  25. return item
  26.  

两种方法判断mongodb中是否存在已有的数据,一是先查询后插入,二是先设置唯一索引或者主键再直接插入,由于mongodb的特点是插入块,查询慢,所以这里直接插入,需要将唯一信息设置为”_id”列,或者设置为唯一索引,在mongodb中设置方法如下

  1. db.集合名.ensureIndex({"要设置索引的列名":1},{"unique":1})

需要用什么信息实现去重,就将什么信息设置为唯一索引即可(小说章节信息由于数据量比较大,用于查询的列最好设置索引,要不然会非常慢),这种方法对于服务器的压力太大,而且速度比较慢,我用的是第二种方法,即对已爬取的url进行去重

3.对url实现去重

对我而言,这种方法是最好的方法,因为速度快,对网站服务器的压力也比较小,不过网上的资料比较少,后来在文档中发现scrapy可以自定义下载中间件,才解决了这个问题

文档原文如下

class scrapy.downloadermiddlewares.DownloaderMiddleware

process_request(request, spider) 当每个request通过下载中间件时,该方法被调用。

process_request() 必须返回其中之一: 返回 None 、返回一个 Response 对象、返回一个 Request对象或raise IgnoreRequest 。

如果其返回 None ,Scrapy将继续处理该request,执行其他的中间件的相应方法,直到合适的下载器处理函数(downloadhandler)被调用, 该request被执行(其response被下载)。

如果其返回 Response 对象,Scrapy将不会调用 任何 其他的 process_request() 或process_exception() 方法,或相应地下载函数; 其将返回该response。 已安装的中间件的process_response() 方法则会在每个response返回时被调用。

如果其返回 Request 对象,Scrapy则停止调用process_request方法并重新调度返回的request。当新返回的request被执行后,相应地中间件链将会根据下载的response被调用。

如果其raise一个 IgnoreRequest 异常,则安装的下载中间件的 process_exception()方法会被调用。如果没有任何一个方法处理该异常,则request的errback(Request.errback)方法会被调用。如果没有代码处理抛出的异常,则该异常被忽略且不记录(不同于其他异常那样)。

所以只需要在process_request中实现去重的逻辑就可以了,代码如下

  1. class UrlFilter(object):
  2. #初始化过滤器(使用mongodb过滤)
  3. def __init__(self):
  4. self.settings = get_project_settings()
  5. self.client = pymongo.MongoClient(
  6. host = self.settings['MONGO_HOST'],
  7. port = self.settings['MONGO_PORT'])
  8. self.db = self.client[self.settings['MONGO_DB']]
  9. self.bookColl = self.db[self.settings['MONGO_BOOK_COLL']]
  10. #self.chapterColl = self.db[self.settings['MONGO_CHAPTER_COLL']]
  11. self.contentColl = self.db[self.settings['MONGO_CONTENT_COLL']]
  12. def process_request(self,request,spider):
  13. if (self.bookColl.count({"novel_Url":request.url}) > 0) or (self.contentColl.count({"chapter_Url":request.url}) > 0):
  14. return http.Response(url=request.url,body=None)

但是又会有一个问题,就是有可能下次开启时,种子url已经被爬取过了,爬虫会直接关闭,后来想到一个笨方法解决了这个问题,即在pipeline.py里的open_spider方法中再爬虫开启时删除对种子url的缓存

  1. def open_spider(self,spider):
  2. self.bookColl.remove({"novel_Url":"http://www.23us.so/xiaoshuo/414.html"})

4.结果

目前一个晚上爬取了大约1000部小说35W个章节的信息,还在继续爬取中

以上为个人经验,希望能给大家一个参考,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号