多线程爬虫
有些时候,比如下载图片,因为下载图片是一个耗时的操作。如果采用之前那种同步的方式下载。那效率肯会特别慢。这时候我们就可以考虑使用多线程的方式来下载图片。
Queue(队列对象) Queue是python中的标准库,可以直接import Queue引用;
队列是线程间最常用的交换数据的形式
对于资源,加锁是个重要的环节。因为python原生的list,dict等,都是not thread safe的。而Queue,是线程安全的,因此在满足使用条件下,建议使用队列
包中常用的方法
- Queue.qsize() 返回队列的大小
- Queue.empty() 如果队列为空,返回True,反之False
- Queue.full() 如果队列满了,返回True,反之False
- Queue.full 与 maxsize 大小对应
- Queue.get([block[, timeout]])获取队列,timeout等待时间
迭代器
迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
可迭代对象 我们已经知道可以对list、tuple、str等类型的数据使用for...in...的循环语法从其中依次拿到数据进行使用,我们把这样的过程称为遍历,也叫迭代
可迭代对象的本质 可迭代对象进行迭代使用的过程,每迭代一次(即在for...in...中每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。
可迭代对象通过iter方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.
- 一个具备了 iter 方法的对象,就是一个 可迭代对象
iter()函数与next()函数
list、tuple等都是可迭代对象,我们可以通过iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用next()函数来获取下一条数据。iter()函数实际上就是调用了可迭代对象的iter方法。
-
迭代器Iterator
迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。实际上,在使用next()函数的时候,调用的就是迭代器对象的next方法。所以,我们要想构造一个迭代器,就要实现它的next方法。并且python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现iter方法,迭代器的iter方法返回自身即可。一个实现了iter方法和next方法的对象,就是迭代器。
for...in...循环的本质
for item in Iterable 循环的本质就是先通过 iter()函数 获取 可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用 next() 方法来获取下一个值并将其赋值给item,当遇到 StopIteration 的异常后循环结束。
生成器
- 生成器 利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器。
- 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
- yield关键字有两点作用:
- 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
- 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
- 可以使用next()函数让生成器从断点处继续执行.
协程
协程,英文叫做 Coroutine,又称微线程,纤程,协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。
协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。
我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。
- 协程和线程差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住
greenlet
为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单
gevent
greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
进程是资源分配的单位
线程是操作系统调度的单位
进程切换需要的资源很最大,效率很低
线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
协程切换任务资源很小,效率高
多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中所以是并发的
JavaScript
JavaScript 是网络上最常用也是支持者最多的客户端脚本语言。它可以收集 用户的跟踪数据,不需要重载页面直接提交表单,在页面嵌入多媒体文件,甚至运行网页游戏。
我们可以在网页源代码的<scripy>标签里看到,比如:
<script type="text/javascript" src="https://statics.huxiu.com/w/mini/static_2015/js/sea.js?v=201601150944"></script>
jQuery
jQuery 是一个十分常见的库,70% 最流行的网站(约 200 万)和约 30% 的其他网站(约 2 亿)都在使用。一个网站使用 jQuery 的特征,就是源代码里包含了 jQuery 入口,比如:
src="https://statics.huxiu.com/w/mini/static_2015/js/jquery-1.11.1.min.js?v=201512181512"></script>
Ajax
我们与网站服务器通信的唯一方式,就是发出 HTTP 请求获取新页面。如果提交表单之后,或从服务器获取信息之后,网站的页面不需要重新刷新,那么你访问的网站就在用Ajax 技术。
Ajax 其实并不是一门语言,而是用来完成网络任务(可以认为 它与网络数据采集差不多)的一系列技术。Ajax 全称是 Asynchronous JavaScript and XML(异步 JavaScript 和 XML),网站不需要使用单独的页面请求就可以和网络服务器进行交互 (收发信息)。
DHTML
Ajax 一样,动态 HTML(Dynamic HTML, DHTML)也是一系列用于解决网络问题的 技术集合。DHTML 是用客户端语言改变页面的 HTML 元素(HTML、CSS,或者二者皆 被改变)。比如页面上的按钮只有当用户移动鼠标之后才出现,背景色可能每次点击都会改变,或者用一个 Ajax 请求触发页面加载一段新内容,网页是否属于DHTML,关键要看有没有用 JavaScript 控制 HTML 和 CSS 元素。
Selenium
Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,类型像我们玩游戏用的按键精灵,可以按指定的命令自动操作,不同是Selenium 可以直接运行在浏览器上,它支持所有主流的浏览器(包括PhantomJS这些无界面的浏览器)。 Selenium 可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。
Selenium 自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。但是我们有时候需要让它内嵌在代码中运行,所以我们可以用一个叫 PhantomJS 的工具代替真实的浏览器。
Selenium 库里有个叫 WebDriver 的 API。WebDriver 有点儿像可以加载网站的浏览器,但是它也可以像 BeautifulSoup 或者其他 Selector 对象一样用来查找页面元素,与页面上的元素进行交互 (发送文本、点击等),以及执行其他动作来运行网络爬虫。
注意: 我们使用的有界面浏览器,它虽然方便我们观察,但是在实际运用中是非常消耗性能的 我们也可以使用Chrome的无界面浏览器,除了没有浏览器界面以外,其它的相关操作都与有界面浏览器相同
- 获取id标签值
element = driver.find_element_by_id("passwd-id")
- 获取name标签值
element = driver.find_element_by_name("user-name")
- 获取标签名值
element = driver.find_elements_by_tag_name("input")
- 也可以通过XPath来匹配
element = driver.find_element_by_xpath("//input[@id='passwd-id']")
Cookies
获取页面每个Cookies值,用法如下:
cookies = driver.get_cookies()
for cookie in cookies:
print("%s -> %s" % (cookie['name'], cookie['value']))
cookie_dict = {i['name']:i['value'] for i in cookies}
print(cookie_dict)
添加cookies
driver.add_cookie(cookie_dict)
删除Cookies,用法如下
- 删除一个特定的cookie
driver.delete_cookie("CookieName") - 删除所有cookie
driver.delete_all_cookies()
设置无头浏览器
opt = webdriver.ChromeOptions()
opt.set_headless()
设置代理
opt = webdriver.ChromeOptions()
opt.add_argument("--proxy-server=http://118.20.16.82:9999")
数据库基本命令
- 查看当前数据库名称:db
- 列出所有在物理上存在的数据库:show dbs
- 切换数据库 如果数据库不存在,则指向数据库,但不创建,直到插入数据或创建集合时数据库才被创建:use 数据库名称
- 查看当前数据库信息:db.stats()
- 数据库删除:删除当前指向的数据库,如果数据库不存在,则什么也不做:db.dropDatabase()
创建集合
:db.createCollection(name, options)
- 不限制集合大小:db.createCollection("stu")
查看当前数据库所有集合
- show collections:当前数据库的集合数
删除集合
- db.集合名称.drop() 如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false
数据的增、删、改、基本查询
插入文档
语法
db.集合名称.insert(document)
插入文档时,如果不指定_id参数,MongoDB会为文档分配一个唯一的ObjectId
db.stu.insert({name:'张三',gender:1})
多条插入
db.stu.insert([{name:'小明',gender:1},{name:'小红',gender:0}])
查询全部文档
语法: db.集合名称.find()
db.stu.find()
更新文档
全文档更新
db.stu.update({name:'xxxxx'},{name:'张xxx'})
指定属性更新,通过操作符$set
db.stu.insert({name:'李四',gender:1}) db.stu.update({name:'李四'},{$set:{name:'李四'}})
save() 方法
save() 方法通过传入的文档来替换已有文档,如果文档的_id已经存在则修改,如果文档的_id不存在则添加 db.集合名称.save(document)
db.stu.save({_id:'20180820101010','name':'保存',gender:1})
删除文档
- db.集合名称.remove(document)
- 只删除1条,1表示是否只删除一条为true的意思 db.集合名称.remove(document,1) db.集合名称.remove(document,{justOne:true})
- 表示删除全部 db.集合名称.remove({})
查询:
db.集合名称.find({条件文档})
方法findOne():查询,只返回第一个
db.集合名称.findOne({条件文档})
方法pretty():将结果格式化
db.集合名称.find({条件文档}).pretty()
查询出姓名等于李某某的学生
db.stu.find({name:'李某某'})
Limit与Skip方法
limit() 方法 读取指定数量的数据记录
基本语法如下所示:
db.COLLECTION_NAME.find().limit(num)
Skip() 方法 使用skip()方法来跳过指定数量的数据,skip方法同样接受一个数字参数作为跳过的记录条数。
基本语法格式如下:
db.集合名称.find().skip(num)
limit() 方法、Skip() 方法 同时使用,不分先后顺序 表示跳过多少条,返回多少条
查询第5至8条数据
db.stu.find().limit(4).skip(5)
sort() 方法排序
sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。
基本语法如下所示:
升序
db.集合名称.find().sort({要排序的字段:1})
降序
db.集合名称.find().sort({要排序的字段:-1})
根据多个字段排序: 例:先根据年龄做降序,再根据性别做升序
db.集合名称.find().sort({age:-1,gender:1})
Scrapy 框架
- Scrapy是用纯Python实现一个为了爬取网站数据、提取结构性数据而编写的应用框架,用途非常广泛。
- 框架的力量,用户只需要定制开发几个模块就可以轻松的实现一个爬虫,用来抓取网页内容以及各种图片,非常之方便。
- Scrapy 使用了 Twisted['twɪstɪd] 异步网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求。
异步:调用在发出之后,这个调用就直接返回,不管有无结果 非阻塞:关注的是程序在等待调用结果(消息,返回值)时的状态,指在不能立刻得到结果之前,该调用不会阻塞当前线程
- Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。
- Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。
- Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,
- Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),
- Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方.
- Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。
- Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)
制作 Scrapy 爬虫 一共需要4步:
- 新建项目
scrapy startproject 爬虫项目名称
新建一个新的爬虫
明确目标
(编写items.py):明确你想要抓取的目标
- 制作爬虫
scrapy genspider 爬虫文件名称 域名:制作爬虫开始爬取网页
- 存储内容
(pipelines.py):设计管道存储爬取内容
关于爬虫部分一些建议:
- 尽量减少请求次数,能抓列表页就不抓详情页,减轻服务器压力,程序员都是混口饭吃不容易。
- 当web网站反爬虫手段比较严格时,不要只看 Web 网站,还有手机 App 和 H5,这样的反爬虫措施一般比较少。
- 实际应用时候,之前一般防守方做到根据 IP 限制频次就结束了,除非很核心的数据,不会再进行更多的验证,毕竟成本的问题会考虑到还要考虑到用户体验。
- 如果真的对性能要求很高,可以考虑多线程、多进程(一些成熟的框架如 Scrapy都已支持),甚至分布式...