Welcome to the D-age
对于网络上的公开数据,理论上只要由服务端发送到前端都可以由爬虫获取到。但是 Data-age 时代的到来,数据是新的黄金,毫不夸张的说,数据是未来的一切。基于统计学数学模型的各种人工智能的出现,离不开数据驱动。数据采集、清洗是最末端的技术成本,网络爬虫也是基础采集脚本。但是有几个值得关注的是:
- 对于实时变化的网络环境,爬虫的持续有效性如何保证
- 数据采集、清洗规则的适用范围
- 数据采集的时间与质量–效率
- 爬与反爬的恩怨
- 爬虫的法律界限
对于上面几个关注点,我最先关注的便是爬虫的法律界限 ,我曾经咨询过一个律师:
Q: 老师,我如果用爬虫爬取今日头条这种类型网站的千万级公开数据,算不算违法呢?
A: 爬取的公开数据不得进行非法使用或者商业利用
-
爬取的数据量
-
爬取数据的类型(数据具有巨大的商业价值,未经对方许可,任何人不得非法获取其数据并用于经营行为)
-
爬取的数据用途 (同行竞争?出售?经营?分析?实验?…)
-
是否遵循网站的 robots.txt 即 机器人协议
-
爬取行为是否会对对方网站造成不能承受的损失(大量的爬取请求会把一个小型网站拖垮)
其实爬虫构成犯罪的案例是开始增多的,相关新闻:
-
当爬虫遇上法律会有什么风险?
(https://www.sohu.com/a/256579233_161795)
-
程序员爬虫竟构成犯罪?
(https://baijiahao.baidu.com/s?id=1609682215455337498&wfr=spider&for=pc)
-
爬虫相关法律知识
(https://www.cnblogs.com/nick477931661/p/9139137.html)
如果你的上级或公司要求你爬取某些网站的大量公开数据,你会怎么办呢?可以参考第 2 条新闻。法律矛盾点关键在于前面考虑的前三点,如果是个人隐私数据,是不能爬取的,如果是非公开数据,是不能爬取的,而对于其他大量的公开数据爬取,看人家查不查的到你,要不要起诉你。技术在你的手上,非法与否在于你怎么去用。最好的爬取道德原则是:
-
减少并发请求
-
延长请求间隔
-
不进行公开出售数据
-
遵循网站 robots 协议
当然,反爬最有效的便(目的均在于拦截爬虫进入网站数据范围)是:
-
要求用户密码+验证码
-
加密数据
-
Javascript 混淆
-
CSS 混淆
-
针对 IP 请求频率封锁
-
针对 Cookie、Session 单个账户请求频率封锁单日请求次数
-
对关键数据进行拆分合并
-
对爬虫投毒(返回假数据)
-
完善 robots.txt
-
识别点击九宫图中没有包含 xxx 的图片等(终极验证码)
-
设置黑白名单、IP 用户组等
工欲善其事
针对网站的公开数据进行爬取,我们一般都要先对网站数据进行分析,定位,以确定其采集规则,如果网站设置了访问权限,那么便不属于我们的爬虫采集范围了:)
分析好采集规则,写好了采集数据持久化(存入数据库、导出为 Word、Excel、csv、下载等)的相关代码,整个爬虫运行正常。那么怎样才能提高采集速度呢?
-
多进程采集
-
多线程采集
-
异步协程采集
-
多进程 + 多线程采集
-
多进程 + 异步协程采集
-
分布式采集
异步爬虫是同步爬虫的升级版,在同步爬虫中,无论你怎么优化代码,同步 IO 的阻塞是最大的致命伤。同步阻塞会让采集任务一个个排着长队领票等待执行。而异步采集不会造成 IO 阻塞,充分利用了 IO 阻塞任务的等待时间去执行其他任务。
在 IO 模型中,只有 IO 多路复用(I/O multiplexing){在内核处理 IO 请求结果为可读或可写时调用回调函数} 不阻塞 “内核拷贝 IO 请求数据到用户空间”这个过程,实现异步 IO 操作。
同步爬虫
一般的同步爬虫,我们可以写一个,【以爬取图片网站
(http://www.quanjing.com/creative/SearchCreative.aspx?id=7)图片为例】,我们来看看其下载该网址所有图片所花费的时间:
以下代码为后面多个例程的共同代码:
#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_urls
def allot(pic_urls:list,n:int)->list:
#根据给定的组数,分配url给每一组
_len = len(pic_urls)
base = int(_len / n)
remainder = _len % n
groups = [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
所以为了提高采集速度,我们可以写一个多进程爬虫【以爬取图片网站
(http://www.quanjing.com/creative/SearchCreative.aspx?id=7)图片为例】:
为了对应多进程的进程数 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 Thread
def run_multithread_crawler(pic_urls:list,threads:int):
begin = 0
start = time.time()
while 1:
_threads = []
urls = pic_urls[begin:begin+threads]
if not urls:
break
for 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 += threads
end = 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
(https://www.python.org/dev/peps/pep-0492/)word-break: break-all
由于 asyncio 提供了基于 socket 的异步 I/O,支持 TCP 和 UDP 协议,但是不支持应用层协议 HTTP,所以需要安装异步 http 请求的 aiohttp 模块
单进程下的异步协程爬虫:
import asyncio
from asyncio import Semaphore
from aiohttp import ClientSession,TCPConnector
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:
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):
# 异步协程爬虫,最大并发请求数concurrency
tasks = []
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
(https://pypi.org/project/aiofiles/0.2.1/)。
针对比较大的多媒体数据下载,异步磁盘 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:
break
await 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上,地址:
https://github.com/01ly/article-codes/blob/master/articel_code_20181113.py
-END-
作者:linkin
来源:https://segmentfault.com/a/1190000016998351
免责声明:部分文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。本声明未涉及的问题参见国家有关法律法规,当本声明与国家法律法规冲突时,以国家法律法规为准。