简易多线程爬虫框架

本文首发于知乎

本文使用多线程实现一个简易爬虫框架,让我们只需要关注网页的解析,不用自己设置多线程、队列等事情。调用形式类似scrapy,而诸多功能还不完善,因此称为简易爬虫框架。

这个框架实现了Spider类,让我们只需要写出下面代码,即可多线程运行爬虫

class DouBan(Spider):def __init__(self):super(DouBan, self).__init__()self.start_url = 'https://movie.douban.com/top250'self.filename = 'douban.json' # 覆盖默认值self.output_result = False self.thread_num = 10def start_requests(self): # 覆盖默认函数yield (self.start_url, self.parse_first)def parse_first(self, url): # 只需要yield待爬url和回调函数r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')movies = soup.find_all('div', class_ = 'info')[:5]for movie in movies:url = movie.find('div', class_ = 'hd').a['href']yield (url, self.parse_second)nextpage = soup.find('span', class_ = 'next').aif nextpage:nexturl = self.start_url + nextpage['href']yield (nexturl, self.parse_first)else:self.running = False # 表明运行到这里则不会继续添加待爬URL队列def parse_second(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')mydict = {}title = soup.find('span', property = 'v:itemreviewed')mydict['title'] = title.text if title else Noneduration = soup.find('span', property = 'v:runtime')mydict['duration'] = duration.text if duration else Nonetime = soup.find('span', property = 'v:initialReleaseDate')mydict['time'] = time.text if time else Noneyield mydictif __name__ == '__main__':douban = DouBan()douban.run()
复制代码

可以看到这个使用方式和scrapy非常相似

  • 继承类,只需要写解析函数(因为是简易框架,因此还需要写请求函数)
  • 用yield返回数据或者新的请求及回调函数
  • 自动多线程(scrapy是异步)
  • 运行都一样只要run
  • 可以设置是否存储到文件等,只是没有考虑可扩展性(数据库等)

下面我们来说一说它是怎么实现的

我们可以对比下面两个版本,一个是上一篇文章中的使用方法,另一个是进行了一些修改,将一些功能抽象出来,以便扩展功能。

上一篇文章版本代码请读者自行点击链接去看,下面是修改后的版本代码。

import requests
import time
import threading
from queue import Queue, Empty
import json
from bs4 import BeautifulSoupdef run_time(func):def wrapper(*args, **kw):start = time.time()func(*args, **kw)end = time.time()print('running', end-start, 's')return wrapperclass Spider():def __init__(self):self.start_url = 'https://movie.douban.com/top250'self.qtasks = Queue()self.data = list()self.thread_num = 5self.running = Truedef start_requests(self):yield (self.start_url, self.parse_first)def parse_first(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')movies = soup.find_all('div', class_ = 'info')[:5]for movie in movies:url = movie.find('div', class_ = 'hd').a['href']yield (url, self.parse_second)nextpage = soup.find('span', class_ = 'next').aif nextpage:nexturl = self.start_url + nextpage['href']yield (nexturl, self.parse_first)else:self.running = Falsedef parse_second(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')mydict = {}title = soup.find('span', property = 'v:itemreviewed')mydict['title'] = title.text if title else Noneduration = soup.find('span', property = 'v:runtime')mydict['duration'] = duration.text if duration else Nonetime = soup.find('span', property = 'v:initialReleaseDate')mydict['time'] = time.text if time else Noneyield mydictdef start_req(self):for task in self.start_requests():self.qtasks.put(task)def parses(self):while self.running or not self.qtasks.empty():try:url, func = self.qtasks.get(timeout=3)print('crawling', url)for task in func(url):if isinstance(task, tuple):self.qtasks.put(task)elif isinstance(task, dict):self.data.append(task)else:raise TypeError('parse functions have to yield url-function tuple or data dict')except Empty:print('{}: Timeout occurred'.format(threading.current_thread().name))print(threading.current_thread().name, 'finished')@run_timedef run(self, filename=False):ths = []th1 = threading.Thread(target=self.start_req)th1.start()ths.append(th1)for _ in range(self.thread_num):th = threading.Thread(target=self.parses)th.start()ths.append(th)for th in ths:th.join()if filename:s = json.dumps(self.data, ensure_ascii=False, indent=4)with open(filename, 'w', encoding='utf-8') as f:f.write(s)print('Data crawling is finished.')if __name__ == '__main__':Spider().run(filename='frame.json')
复制代码

这个改进主要思路如下

  • 我们希望写解析函数时,像scrapy一样,用yield返回待抓取的URL和它对应的解析函数,于是就做了一个包含(URL,解析函数)的元组队列,之后只要不断从队列中获取元素,用函数解析url即可,这个提取的过程使用多线程
  • yield可以返回两种类型数据,一种是元组(URL,解析函数),一种是字典(即我们要的数据),通过判断分别加入不同队列中。元组队列是不断消耗和增添的过程,而字典队列是一只增加,最后再一起输出到文件中
  • queue.get时,加入了timeout参数并做异常处理,保证每一个线程都能结束

这里其实没有特别的知识,也不需要解释很多,读者自己复制代码到文本文件里对比就知道了

然后框架的形式就是从第二种中,剥离一些通用的设定,让用户自定义每个爬虫独特的部分,完整代码如下(本文开头的代码就是下面这块代码的后半部分)

import requests
import time
import threading
from queue import Queue, Empty
import json
from bs4 import BeautifulSoupdef run_time(func):def wrapper(*args, **kw):start = time.time()func(*args, **kw)end = time.time()print('running', end-start, 's')return wrapperclass Spider():def __init__(self):self.qtasks = Queue()self.data = list()self.thread_num = 5self.running = Trueself.filename = Falseself.output_result = Truedef start_requests(self):yield (self.start_url, self.parse)def start_req(self):for task in self.start_requests():self.qtasks.put(task)def parses(self):while self.running or not self.qtasks.empty():try:url, func = self.qtasks.get(timeout=3)print('crawling', url)for task in func(url):if isinstance(task, tuple):self.qtasks.put(task)elif isinstance(task, dict):if self.output_result:print(task)self.data.append(task)else:raise TypeError('parse functions have to yield url-function tuple or data dict')except Empty:print('{}: Timeout occurred'.format(threading.current_thread().name))print(threading.current_thread().name, 'finished')@run_timedef run(self):ths = []th1 = threading.Thread(target=self.start_req)th1.start()ths.append(th1)for _ in range(self.thread_num):th = threading.Thread(target=self.parses)th.start()ths.append(th)for th in ths:th.join()if self.filename:s = json.dumps(self.data, ensure_ascii=False, indent=4)with open(self.filename, 'w', encoding='utf-8') as f:f.write(s)print('Data crawling is finished.')class DouBan(Spider):def __init__(self):super(DouBan, self).__init__()self.start_url = 'https://movie.douban.com/top250'self.filename = 'douban.json' # 覆盖默认值self.output_result = False self.thread_num = 10def start_requests(self): # 覆盖默认函数yield (self.start_url, self.parse_first)def parse_first(self, url): # 只需要yield待爬url和回调函数r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')movies = soup.find_all('div', class_ = 'info')[:5]for movie in movies:url = movie.find('div', class_ = 'hd').a['href']yield (url, self.parse_second)nextpage = soup.find('span', class_ = 'next').aif nextpage:nexturl = self.start_url + nextpage['href']yield (nexturl, self.parse_first)else:self.running = False # 表明运行到这里则不会继续添加待爬URL队列def parse_second(self, url):r = requests.get(url)soup = BeautifulSoup(r.content, 'lxml')mydict = {}title = soup.find('span', property = 'v:itemreviewed')mydict['title'] = title.text if title else Noneduration = soup.find('span', property = 'v:runtime')mydict['duration'] = duration.text if duration else Nonetime = soup.find('span', property = 'v:initialReleaseDate')mydict['time'] = time.text if time else Noneyield mydictif __name__ == '__main__':douban = DouBan()douban.run()
复制代码

我们这样剥离之后,就只需要写后半部分的代码,只关心网页的解析,不用考虑多线程的实现了。

欢迎关注我的知乎专栏

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明

转载于:https://juejin.im/post/5b129bd7e51d4506a74d22f4

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/452430.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【小松教你手游开发】【unity实用技能】给每个GameObject的打开关闭加上一个渐变...

在游戏开发中,经常会因为直接将GameObject,setActive的方式打开关闭,这种方式效果太过生硬而给它加上一个Tween 可能是AlphaTween或者ScaleTween。 再加上一个PlayTween来做控制。 这样子需要在每个GameObject上加上这几个Component不说&…

静态网页和动态网页

静态网页是指不应用程序而直接或间接制作成html的网页,这种网页的内容是固定的,修改和更新都必须要通过专用的网页制作工具,比如Dreamweaver。动态网页是指使用网页脚本语言,比如php、asp、asp.net等,通过脚本将网站内…

在微型计算机中 如果电源突然中断,微型计算机在工作中电源突然中断,则其中的信息全部丢失,再次通电后也不能恢复的..._考试资料网...

请根据下面的文字材料,完成一节课的教学设计。 丝绸之路 一座古朴典雅的“丝绸之路”巨型石雕,矗立在西安市玉祥门外。那驮着彩绸的一峰峰骆驼,高鼻凹眼的西域商人,精神饱满,栩栩如生。商人们在这个东方大都市开了眼界…

Vmware上安装RedHat Linux 7.3操作系统手册

文章目录1.点击“创建新的虚拟机”,勾选“自定义”选项,点击一步;2.默认选择最高版本的workstations,点击下一步;3.选择“稍后安装操作系统”,点击下一步;4&a…

软件开发的“三重门”

自从上次写了“程序员技术练级攻略” 以来,就觉得似乎还有很多东西没有谈到,但当时没有继续思考了。而春节前有人问我,是做底层技术,还是做业务。这问题让我思考了很多,不由自主地回顾了一 下我这十多年的软件开发经历…

软件工程15 个人阅读作业1

Task1:注册个人博客账号 个人博客地址 https://www.cnblogs.com/bmr666/ Task2:注册码云账号 码云账号 https://gitee.com/bmr666 Task3:完成博客-阅读与思考 阅读参考材料,并回答下面几个问题: (1&#xf…

Windows Server 2008操作系统安装手册

文章目录1.输入语言和其他首选项,然后单击“下一步”继续;2.点击“现在安装”,启动安装程序;3.选择要安装的操作系统,这里选择Windows Server 2008 R2 Enterprise(完全安…

云计算机有哪些特征,你知道云计算有哪些核心特征吗?

你知道云计算有哪些核心特征吗?下面跟小编一起来了解下吧!!!1、敏捷:使用户得以快速的,且以低价格的获得技术架构资源。2、应用程序界面API的可达性是指允许软件与云以类似“人机交互这种用户界面设施交互相所相一致的方式”来交互。云计算系统典型的运…

从玩扑克到软件开发

我以前不是做软件开发的。在加入ThoughtWorks两年之前,我主要靠玩扑克为生。当然,如果你曾跟我打听过我前臂上的纹身,那你肯定已然听过我的故事了。要是还没有,等下次我们一起喝一杯时,我可以讲给你听。 我从未因为花…

什么是IPsec协议

IPSec 协议不是一个单独的协议,它给出了应用于IP层上网络数据安全的一整套体系结构,包括网络认证协议 Authentication Header(AH)、封装安全载荷协议Encapsulating Security Payload(ESP)、密钥管理协议Int…

python 字符串、列表和元祖之间的切换

>>> s[http,://,www,baidu,.com] >>> url.join(s) >>> url http://wwwbaidu.com >>> 上面的代码片段是将列表转换成字符串>>> s(hello,world,!) >>> d .join(s) >>> d hello world ! >>> 以上代码片段…

你真的懂函数吗?

函数声明方式 匿名函数 function后面直接跟括号,中间没有函数名的就是匿名函数。 let fn function() {console.log(我是fn) } let fn2 fn console.log(fn.name) //fn console.log(fn2.name)//fn,fn和fn2指向的是同一个function。 复制代码具名函数 fun…

静态html的ajax如何发请求,静态页面ajax - 冥焱的个人空间 - OSCHINA - 中文开源技术交流社区...

1.静态页面$.ajax({type:"get",url:"http://localhost:8080/app/register/sendSMS",//请求地址必须带http协议data:{"phone":phone},async:false,//是否异步dataType: "jsonp",//固定格式jsonp: "callback",//固定格式jsonp…

Diango博客--12.开发 Django 博客文章阅读量统计功能

文章目录0.models中增加新字段1.models中增加方法2.迁移数据库3.修改视图函数4.在模板中显示阅读量0.models中增加新字段 为了记录文章的浏览量,需要在文章的数据库表中新增一个用于存储阅读量的字段。 文件位置:blog/models.py class Post(models.Mo…

c++ try_catch throw

使用throw抛出异常 本人节选自《21天学通C》一书 抛出异常(也称为抛弃异常)即检测是否产生异常,在C中,其采用throw语句来实现,如果检测到产生异常,则抛出异常。该语句的格式为: throw 表达式…

数字证书和数字签名

什么是数字证书?由于Internet网电子商务系统技术使在网上购物的顾客能够极其方便轻松地获得商家和企业的信息,但同时也增加了对某些敏感或有价值的数据被滥用的风险. 为了保证互联网上电子交易及支付的安全性,保密性等,防范交易及支付过程中的欺诈行为&a…

域名劫持

转载于:https://www.cnblogs.com/xinghen1216/p/8548323.html

cesium html源码,Cesium源码的本地运行及调试

CesiumJS源码运行有两种方式:基于node.js运行官方下载地址:https://cesium.com/cesiumjs/下载解压后,在根目录安装依赖后,就可直接运行npm initnpm start如果调试代码呢,官方的示例都是在Sandcastle里放着,…

Diango博客--13.将“视图函数”类转化为“类视图”

文章目录0.思路引导1.ListView2.将 index 视图函数改写为类视图3.将 category 视图函数改写为类视图4.将 archive 视图函数改写成类视图5.将 tag 视图函数改写成类视图6.DetailView7.将DetailView视图函数改写成类视图0.思路引导 1)在开发网站的过程中,…

es6之数据结构 set,WeakSet,mapWeakMap

{let list new Set();list.add(1);list.add(2);list.add(1);console.log(list); //Set(2) {1, 2} let arr[1,2,3,1,2] let list2new Set(arr); console.log(list2); //Set(3) {1, 2, 3} } Set ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的&a…