关于Python爬虫种类、法律、轮子的一二三
Welcome to the D-age
对于网络上的公开数据,理论上只要由服务端发送到前端都可以由爬虫获取到。但是Data-age时代的到来,数据是新的黄金,毫不夸张的说,数据是未来的一切。基于统计学数学模型的各种人工智能的出现,离不开数据驱动。数据采集、清洗是最末端的技术成本,网络爬虫也是基础采集脚本。但是有几个值得关注的是:
- 对于实时变化的网络环境,爬虫的持续有效性如何保证
- 数据采集、清洗规则的适用范围
- 数据采集的时间与质量--效率
- 爬与反爬的恩怨
- 爬虫的法律界限
法律的边界,技术无罪
对于上面几个关注点,我最先关注的便是爬虫的法律界限 ,我曾经咨询过一个律师:
Q: 老师,我如果用爬虫爬取今日头条这种类型网站的千万级公开数据,算不算违法呢?
A: 爬取的公开数据不得进行非法使用或者商业利用
简单的概括便是爬虫爬取的数据如果进行商业出售或者有获利的使用,便构成了“非法使用”。而一般的爬虫程序并不违法,其实这是从法律专业的一方来解读,如果加上技术层面的维度,那么应该从这几方面考虑:
- 爬取的数据量
- 爬取数据的类型(数据具有巨大的商业价值,未经对方许可,任何人不得非法获取其数据并用于经营行为)
- 爬取的数据用途 (同行竞争?出售?经营?分析?实验?...)
- 是否遵循网站的robots.txt 即 机器人协议
- 爬取行为是否会对对方网站造成不能承受的损失(大量的爬取请求会把一个小型网站拖垮)
其实爬虫构成犯罪的案例是开始增多的,相关新闻:
- 当爬虫遇上法律会有什么风险?
- 程序员爬虫竟构成犯罪?
- 爬虫相关法律知识
如果你的上级或公司要求你爬取某些网站的大量公开数据,你会怎么办呢?可以参考第2条新闻。法律矛盾点关键在于前面考虑的前三点,如果是个人隐私数据,是不能爬取的,如果是非公开数据,是不能爬取的,而对于其他大量的公开数据爬取,看人家查不查的到你,要不要起诉你。技术在你的手上,非法与否在于你怎么去用。最好的爬取道德原则是:
- 减少并发请求
- 延长请求间隔
- 不进行公开出售数据
- 遵循网站 robots协议
当然,反爬最有效的便(目的均在于拦截爬虫进入网站数据范围)是:
- 要求用户密码+验证码
- 加密数据
- js混淆
- css混淆
- 针对IP请求频率封锁
- 针对cookie、session单个账户请求频率封锁单日请求次数
- 对关键数据进行拆分合并
- 对爬虫投毒(返回假数据)
- 完善robots.txt
- 识别点击九宫图中没有包含xxx的图片等(终极验证码)
- 设置黑白名单、IP用户组等
工欲善其事
针对网站的公开数据进行爬取,我们一般都要先对网站数据进行分析,定位,以确定其采集规则,如果网站设置了访问权限,那么便不属于我们的爬虫采集范围了:)
分析好采集规则,写好了采集数据持久化(存入数据库、导出为word、excel、csv、下载等)的相关代码,整个爬虫运行正常。那么怎样才能提高采集速度呢?
- 多进程采集
- 多线程采集
- 异步协程采集
- 多进程 + 多线程采集
- 多进程 + 异步协程采集
- 分布式采集
异步爬虫是同步爬虫的升级版,在同步爬虫中,无论你怎么优化代码,同步IO的阻塞是最大的致命伤。同步阻塞会让采集任务一个个排着长队领票等待执行。而异步采集不会造成IO阻塞,充分利用了IO阻塞任务的等待时间去执行其他任务。
在IO 模型中,只有IO多路复用(I/O multiplexing){在内核处理IO请求结果为可读或可写时调用回调函数} 不阻塞 “内核拷贝IO请求数据到用户空间”这个过程,实现异步IO操作。
同步爬虫
一般的同步爬虫,我们可以写一个,(以爬取图片网站图片为例),我们来看看其下载该网址所有图片所花费的时间:
以下代码为后面多个例程的共同代码:
#coding:utf-8
import time
from lxml import etree
import urllib.request as request#目标网址
url = 'http://www.quanjing.com/creative/SearchCreative.aspx?id=7'def download_one_pic(url:str,name:str,suffix:str='jpg'):#下载单张图片path = '.'.join([name,suffix])response = request.urlopen(url)wb_data = response.read()with open(path,'wb') as f:f.write(wb_data)def download_many_pic(urls:list):#下载多张图片start = time.time()for i in urls:ts = str(int(time.time() * 1000))download_one_pic(i, ts)end = time.time()print(u'下载完成,%d张图片,耗时:%.2fs' % (len(urls), (end - start)))def get_pic_urls(url:str)->list:#获取页面所有图片链接response = request.urlopen(url)wb_data = response.read()html = etree.HTML(wb_data)pic_urls = html.xpath('//a[@class="item lazy"]/img/@src')return pic_urlsdef allot(pic_urls:list,n:int)->list:#根据给定的组数,分配url给每一组_len = len(pic_urls)base = int(_len / n)remainder = _len % ngroups = [pic_urls[i * base:(i + 1) * base] for i in range(n)]remaind_group = pic_urls[n * base:]for i in range(remainder):groups[i].append(remaind_group[i])return [i for i in groups if i]
同步爬虫:
def crawler():#同步下载pic_urls = get_pic_urls(url)download_many_pic(pic_urls)
执行同步爬虫,
crawler()
输出(时间可能不一样,取决于你的网速):
下载完成,196张图片,耗时:49.04s
在同一个网络环境下,排除网速时好时坏,可以下载多几次取平均下载时间,在我的网络环境下,我下载了5次,平均耗时约55.26s
多进程爬虫
所以为了提高采集速度,我们可以写一个多进程爬虫(以爬取图片网站图片为例):
为了对应多进程的进程数n,我们可以将图片链接列表分成n组,多进程爬虫:
from multiprocessing.pool import Pool
def multiprocess_crawler(processors:int):#多进程爬虫pool = Pool(processors)pic_urls = get_pic_src(url)#对应多进程的进程数processors,我们可以将图片链接列表分成processors组url_groups = allot(pic_urls,processors)for i in url_groups:pool.apply_async(func=download_many_pic,args=(i,))pool.close()pool.join()
执行爬虫,进程数设为4,一般是cpu数量:
multiprocess_crawler(4)
输出:
下载完成,49张图片,耗时:18.22s
下载完成,49张图片,耗时:18.99s
下载完成,49张图片,耗时:18.97s
下载完成,49张图片,耗时:19.51s
可以看出,多进程比原先的同步爬虫快许多,整个程序耗时19.51s,为什么不是同步爬虫的55s/4 ≈ 14s呢?因为进程间的切换需要耗时。
如果把进程数增大,那么:
进程数:10 , 耗时:12.3s
进程数:30 , 耗时:2.81s
进程数:40 , 耗时:11.34s
对于多进程爬虫来说,虽然实现异步爬取,但也不是越多进程越好,进程间切换的开销不仅会让你崩溃,有时还会让你的程序崩溃。一般用进程池Pool维护,Pool的processors设为CPU数量。进程的数量设置超过100个便让我的程序崩溃退出。使用进程池可以保证当前在跑的进程数量控制为设置的数量,只有池子没满才能加新的进程进去。
多线程爬虫
多线程版本可以在单进程下进行异步采集,但线程间的切换开销也会随着线程数的增大而增大。当线程间需要共享变量内存时,此时会有许多不可预知的变量读写操作发生,python为了使线程同步,给每个线程共享变量加了全局解释器锁GIL。而我们的爬虫不需要共享变量,因此是线程安全的,不用加锁。多线程版本:
import random
from threading import Threaddef run_multithread_crawler(pic_urls:list,threads:int):begin = 0start = time.time()while 1:_threads = []urls = pic_urls[begin:begin+threads]if not urls:breakfor i in urls:ts = str(int(time.time()*10000))+str(random.randint(1,100000))t = Thread(target=download_one_pic,args=(i,ts))_threads.append(t)for t in _threads:t.setDaemon(True)t.start()for t in _threads:t.join()begin += threadsend = time.time()print(u'下载完成,%d张图片,耗时:%.2fs' % (len(pic_urls), (end - start)))def multithread_crawler(threads:int):pic_urls = get_pic_src(url)run_multithread_crawler(pic_urls,threads)
并发线程数太多会让我们的系统开销越大,使程序花费时间越长,同时也会增大目标网站识别爬虫机器行为的几率。因此设置好一个适当的线程数以及爬取间隔是良好的爬虫习惯。
执行多线程爬虫,设置线程数为50
multithreads_crawler(50)
输出:
下载完成,196张图片,耗时:3.10s
增大线程数,输出:
线程数:50,耗时:3.10s
线程数:60,耗时:3.07s
线程数:70,耗时:2.50s
线程数:80,耗时:2.31s
线程数:120,耗时:3.67s
可以看到,线程可以有效的提高爬取效率,缩短爬取时间,但必须是一个合理的线程数,越多有时并不是越好的,一般是几十到几百个之间,数值比多进程进程数大许多。
异步协程爬虫
Python3.5引入了async/await 异步协程语法。详见PEP492
由于asyncio提供了基于socket的异步I/O,支持TCP和UDP协议,但是不支持应用层协议HTTP,所以需要安装异步http请求的aiohttp模块
单进程下的异步协程爬虫:
import asyncio
from asyncio import Semaphore
from aiohttp import ClientSession,TCPConnectorasync def download(session:ClientSession,url:str,name:str,sem:Semaphore,suffix:str='jpg'):path = '.'.join([name,suffix])async with sem:async with session.get(url) as response:wb_data = await response.read()with open(path,'wb') as f:f.write(wb_data)async def run_coroutine_crawler(pic_urls:list,concurrency:int):# 异步协程爬虫,最大并发请求数concurrencytasks = []sem = Semaphore(concurrency)conn =TCPConnector(limit=concurrency)async with ClientSession(connector=conn) as session:for i in pic_urls:ts = str(int(time.time() * 10000)) + str(random.randint(1, 100000))tasks.append(asyncio.create_task(download(session,i,ts,sem)))start = time.time()await asyncio.gather(*tasks)end = time.time()print(u'下载完成,%d张图片,耗时:%.2fs' % (len(pic_urls), (end - start)))def coroutine_crawler(concurrency:int):pic_urls = get_pic_src(url)loop = asyncio.get_event_loop()loop.run_until_complete(run_coroutine_crawler(pic_urls,concurrency))loop.close()
执行异步协程爬虫,设置最大并发请求数为100:
coroutine_crawler(100)
输出:
下载完成,196张图片,耗时:2.27s
可以看出,异步多协程的下载请求效率并不比多线程差,由于磁盘IO读写阻塞,所以还可以进一步优化,使用aiofiles。
针对比较大的多媒体数据下载,异步磁盘IO可以使用aiofiles,以上述例子download可以改为:
import aiofiles
async def download(session:ClientSession,url:str,name:str,sem:Semaphore,suffix:str='jpg'):path = '.'.join([name,suffix])async with sem:async with session.get(url) as response:async with aiofiles.open(path,'wb') as fd:while 1:wb_data_chunk = await response.content.read(1024)if not wb_data_chunk:breakawait fd.write(wb_data_chunk)
多进程 + 多线程 爬虫
实际采集大量数据的过程中,往往是多种手段来实现爬虫,这样可以充分利用机器CPU,节省采集时间。
下面使用多进程(进程数为CPU数,4)+ 多线程 (线程数设为50)来对例子进行更改(上面各个例子导入的模块默认使用):
def mixed_process_thread_crawler(processors:int,threads:int):pool = Pool(processors)pic_urls = get_pic_src(url)url_groups = allot(pic_urls,processors)for group in url_groups:pool.apply_async(run_multithread_crawler,args=(group,threads))pool.close()pool.join()
执行爬虫:
mixed_process_thread_crawler(4,50)
输出:
下载完成,49张图片,耗时:2.73s
下载完成,49张图片,耗时:2.76s
下载完成,49张图片,耗时:2.76s
下载完成,49张图片,耗时:2.76s
采集时间与异步协程和多线程并无多大的差异,可以使用更大数据量做实验区分。因为多进程+多线程,CPU切换上下文也会造成一定的开销,所以进程数与线程数不能太大,并发请求的时间间隔也要考虑进去。
多进程 + 异步协程 爬虫
使用多进程(进程数为CPU数,4)+ 异步协程(最大并发请求数设为50)来对例子进行更改(上面各个例子导入的模块默认使用):
def _coroutine_crawler(pic_urls:list,concurrency:int):loop = asyncio.get_event_loop()loop.run_until_complete(run_coroutine_crawler(pic_urls, concurrency))loop.close()def mixed_process_coroutine_crawler(processors:int,concurrency:int):pool = Pool(processors)pic_urls = get_pic_src(url)url_groups = allot(pic_urls, processors)for group in url_groups:pool.apply_async(_coroutine_crawler, args=(group, concurrency))pool.close()pool.join()
执行爬虫 :
mixed_process_coroutine_crawler(4,50)
输出:
下载完成,49张图片,耗时:2.56s
下载完成,49张图片,耗时:2.54s
下载完成,49张图片,耗时:2.56s
下载完成,49张图片,耗时:2.62s
效果与多进程 + 多线程 爬虫差不多,但是CPU减少了切换线程上下文的开销,而是对每一个协程任务进行监视回调唤醒。使用IO多路复用的底层原理实现。
分布式采集
关于分布式采集将会单独写一章,使用Map-Reduce+redis来实现分布式爬虫。
轮子们,你们辛苦了
现实生活中的爬虫不止上面那些,但是基本的骨架是一样的,对于特定的网站需要制定特定的采集规则,所以通用的数据采集爬虫很难实现。所以针对某个网站的数据采集爬虫是需要定制的,但是在不同之中包含着许多的相同、重复性的过程,比如说采集流程,或者对请求头部的伪造,数据持久化的处理等,采集框架应运而生。Scrapy就是目前比较成熟的一个爬虫框架。它可以帮助我们大大减少重复性的代码编写,可以更好的组织采集流程。而我们只需要喝一杯咖啡,编写自己的采集规则,让Scrapy去给我们管理各种各样的爬虫,做些累活。如果你是一个爬虫爱好者,那么scrapy是你的不错选择。由于好奇scrapy的实现流程,所以我才开始打开他的源码学习。
有些人觉得scrapy太重,他的爬虫只需要简单的采集,自己写一下就可以搞定了。但如果是大量的爬虫采集呢?怎么去管理这些爬虫呢?怎样才能提高采集效率呀?
Scrapy helps~!!
另外还有另一个Python采集框架:pyspider。国人编写的,cool~
感谢轮子们的父母,还有那些辛苦工作的轮子们,你们辛苦了~
本文所用代码 均在GitHub上,地址:这里
关于Python爬虫种类、法律、轮子的一二三相关推荐
- Python爬虫种类、法律、轮子,轮子们,你们辛苦了
对于网络上的公开数据,理论上只要由服务端发送到前端都可以由爬虫获取到.但是Data-age时代的到来,数据是新的黄金,毫不夸张的说,数据是未来的一切.基于统计学数学模型的各种人工智能的出现,离不开数据 ...
- Python爬虫可以用来做什么呢?(涉及法律的灰色地带,还希望大家不要尝试。)
前言 自 TIOBE 榜单创建至今的 20 多年来,本月排行榜的榜首位置首次出现了除 Java 和 C 以外的第三个编程语言--Python.这也就意味着,Java 和 C 的长期霸权已经结束. 大数 ...
- 经典的Python爬虫和网络编程面试题
1.动态加载又对及时性要求很高怎么处理? Selenium+Phantomjs 尽量不使用 sleep 而使用 WebDriverWait 2.分布式爬虫主要解决什么问题? (1)ip (2)带宽 ( ...
- 最全python爬虫面试笔试题及答案汇总,三万多字,持续更新,适合新手,应届生
目录 一些经典的Python爬虫和网络编程面试题... 1 1.动态加载又对及时性要求很高怎么处理?... 1 2.分布式爬虫主要解决什么问题?... 1 3.什么是 URL?... 1 4.pyth ...
- Python培训分享:python爬虫可以用来做什么?
爬虫又被称为网络蜘蛛,它可以抓取我们页面的一些相关数据,近几年Python技术的到来,让我们对爬虫有了一个新的认知,那就是Python爬虫,下面我们就来看看python爬虫可以用来做什么? Pytho ...
- 利用python爬虫与数据分析,打造最强玩法,轻松成为大神级玩家!
前言: 最近迷上了一款游戏,但是作为一名程序员的我是不可能只玩游戏的,我必须把它的官网数据采集下来! 环境: windows python3.6.5 模块: requests jsonpath pyg ...
- Python 爬虫修养-处理动态网页
Python 爬虫修养-处理动态网页 本文转自:i春秋社区 0x01 前言 在进行爬虫开发的过程中,我们会遇到很多的棘手的问题,当然对于普通的问题比如 UA 等修改的问题,我们并不在讨论范围,既然要将 ...
- 学会python爬虫怎么赚钱-自学python爬虫赚钱经历
广告关闭 2017年12月,云+社区对外发布,从最开始的技术博客到现在拥有多个社区产品.未来,我们一起乘风破浪,创造无限可能. 最后面试的时候,掌握的技能,sql,python,r. 会一点简单的爬虫 ...
- python爬虫能干啥-Python爬虫还能干什么?
1.收集数据 python爬虫程序可用于收集数据.这也是最直接和最常用的方法.由于爬虫程序是一个程序,程序运行得非常快,不会因为重复的事情而感到疲倦,因此使用爬虫程序获取大量数据变得非常简单和快速. ...
最新文章
- linux ubuntu基础,linux基础入门详细分析(基于ubuntu)
- java双等号比较字符串,Java与两个字符串==比较是错误的?
- hbuilder/hbuilderx 无法检测到模拟器
- mfc cedit 默认显示内容_拼多多评价不显示的几大原因,看完之后才知道是触碰了这几个环节...
- 【HTTP】之HTTP 错误 401.3 - 访问被资源 ACL 拒绝
- 【练习】2021下半年数据结构刷题笔记和总结 (三)栈 队列 链表 枚举算法
- CF1016F:Road Projects(树形dp)
- Docker保存修改后的镜像
- 为防 Android 碎片化?Google 强迫开发者使用自有开发工具!
- 总结windows下堆溢出的三种利用方式
- python程序设计常见运算符号_python常用运算符
- python基础案例教程课后答案_Python基础案例教程
- 测试理论学习(分类、流程、方法)
- 大数据-浅谈hive优化
- RestTemplate的No instances available for xxx
- zbox mysql_我服务器原来有apache和mysql,然后我把zbox的apache端口改成8000,mysql改成3307,无法打开网页...
- 面试圣经——京东之行
- 程序人生 - 变脸的原理
- 大数据之保险行业的领导驾驶舱到底怎么做!附上模板
- 软件工程 wc.exe 代码统计作业