解密:爬虫与反爬虫技术简介
优采云 发布时间: 2022-09-21 15:14解密:爬虫与反爬虫技术简介
vivo 网络安全团队 - 谢鹏
随着互联网大数据时代的到来,网络爬虫也成为了互联网的重要产业。它是一个自动获取网页数据和信息的爬虫程序,是网站搜索引擎的重要组成部分。通过爬虫,您可以获得您想要的相关数据信息,并让爬虫辅助您的工作,从而降低成本,提高业务成功率,提高业务效率。
一方面,本文从爬虫和反反爬虫的角度解释了如何高效地爬取网络上的开放数据。另一方面,也介绍了反爬虫的技术手段。就数据处理如何使服务器过载提供一些建议。
爬虫是指按照一定的规则自动从万维网上抓取信息的程序。本次就简单介绍一下爬虫、反爬虫、反爬虫的技术原理和实现。介绍的案例仅用于安全研究。和学习,而不是做很多爬行或申请业务。
一、爬虫的技术原理与实现1.1 爬虫的定义
爬虫分为两类:通用爬虫和专注爬虫。前者的目标是爬取尽可能多的网站,同时保持一定的内容质量。比如百度等搜索引擎就是这类爬虫。图 1 是一个通用爬虫。搜索引擎基础设施:
首先,选择互联网中的一部分网页,将这些网页的链接地址作为*敏*感*词*URL;将这些*敏*感*词*URL放入待爬取URL队列中,爬虫依次从待爬取URL队列中读取;通过DNS解析URL,将链接地址转换为网站服务器对应的IP地址;网页下载器通过网站服务器下载网页,下载的网页为网页文档的形式。过滤掉已经爬取的网址;继续对没有被爬取的URL进行爬取,直到待爬取的URL队列为空。
图1.通用搜索引擎的基础架构
爬虫通常从一个或多个URL开始,在爬取过程中不断将符合要求的新URL放入待爬队列中,直到满足程序的停止条件。
我们日常看到的爬虫基本都是后者,目标是在爬取少量网站的同时尽可能保持准确的内容质量。一个典型的例子如图2所示,抢票软件,利用爬虫登录票务网络,爬取信息辅助业务。
图2. 抢票软件
在了解了爬虫的定义之后,我们应该如何编写爬虫程序来爬取我们想要的数据。我们可以先了解一下目前常用的爬虫框架,因为它可以写一些常用爬虫功能的实现代码,然后留下一些接口。在做不同爬虫项目时,我们只需要根据实际情况编写少量改动即可。,并根据需要调用这些接口,即可以实现一个爬虫项目。
1.2 爬虫框架介绍
常用的搜索引擎爬虫框架如图3所示。首先,Nutch是专门为搜索引擎设计的爬虫,不适合精准爬取。Pyspider和Scrapy都是用python语言编写的爬虫框架,都支持分布式爬虫。此外,由于其可视化的操作界面,Pyspider 比 Scrapy 的全命令行操作更加人性化,但功能不如 Scrapy 强大。
图3.爬虫框架对比
1.3 简单的爬虫示例
除了使用爬虫框架进行爬取外,还可以从零开始编写爬虫程序。步骤如图 4 所示:
图4.爬虫基本原理
接下来,我们将通过一个简单的示例来演示上述步骤。我们要爬取的是某个应用市场的列表。我们以此为例,因为这个网站没有任何反爬的手段。这些步骤可以很容易地爬取到内容。
图5.网页及其对应的源码
网页及其对应的源码如图5所示。对于网页上的数据,假设我们要爬取排行榜上每个应用的名称及其分类。
我们先分析网页源码发现可以直接在网页源码中搜索“抖音”等app名称,然后看到app名称,app category等都在一个tag中,所以我们只需要请求网页地址,获取返回的网页源码,然后对网页源码进行正则匹配,提取出想要的数据,保存即可,如图图 6。
#获取网页源码
def get_one_page(url):
try:
response = requests.get(url)
if response.status_code == 200:
return response.text
return None
except RequestException:
return None
#正则匹配提取目标信息并形成字典
def parse_one_page(html):
pattern = re.compile('.*?data-src="(.*?)".*?.*?det.*?>(.*?)</a>.*?p.*?(.*?)</a>.*?',re.S)
items = re.findall(pattern, html)
j = 1
for item in items[:-1]:
yield {'index': str(j),
'name': item[1],
'class':item[2]
}
j = j+1
#结果写入txt
def write_to_file(content):
with open(r'test.txt', 'a', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False)+'\n')
图6.爬虫代码及结果
二、反爬虫相关技术
在了解具体的反爬措施之前,我们先介绍一下反爬的定义和含义。限制爬虫程序访问服务器资源和获取数据的行为称为反爬虫。爬虫的访问速度和目的与普通用户不同。大多数爬虫都会毫无节制地爬取目标应用程序,给目标应用程序的服务器带来巨大压力。机器人发出的网络请求被运营商称为“垃圾流量”。为了保证服务器的正常运行或者降低服务器的压力和运行成本,开发者不得不借助各种技术手段来限制爬虫对服务器资源的访问。
那么为什么要做反爬虫呢?答案很明显。爬虫流量会增加服务器的负载。过多的爬虫流量会影响服务的正常运行,导致收入损失。另一方面,部分核心数据的泄露会导致数据拥有者缺乏竞争力。
常见的反爬虫方法如图7所示,主要包括文本混淆、动态页面渲染、验证码校验、请求签名校验、大数据风控、js混淆和蜜罐等。文本混淆包括css偏移、图片伪装文本、自定义字体等。控制策略的制定往往基于参数验证、行为频率和模式异常。
图7.常用反爬虫方法
2.1 CSS 偏移反爬虫
在构建网页时,需要使用 CSS 来控制各种字符的位置。情况也是如此。您可以使用 CSS 将浏览器中显示的文本以无序的方式存储在 HTML 中,从而限制爬虫。CSS 偏移反爬是一种反爬的方法,它使用 CSS 样式将乱序的文本排版成正常的人类阅读顺序。这个概念不是很好理解。我们可以通过对比两段文字来加深对这个概念的理解:
在以上两段中,浏览器应该显示正确的信息。如果我们按照上面提到的爬虫步骤,定期分析网页并提取信息,就会发现学号有误。
看图8所示的例子,如果我们要爬取这个网页上的机票信息,我们首先需要对网页进行分析。红框显示的467价格对应的是中国民航石家庄到上海的机票,但是分析网页源码发现代码中有3对b标签,第一对b tags 收录 3 对 i 标签,其中 i 标签中的数字都是 7,也就是说第一对 b 标签的显示结果应该是 777。第二对 b 标签中的数字是 6,而第三对b标签中的数字是4,所以我们将无法通过正则匹配直接得到正确的票价。
图8.CSS偏移反爬虫示例
2.2 图像伪装反爬虫
图片伪装反爬虫,其本质是用图片替换原创内容,使爬虫程序无法正常获取,如图9所示。这个反爬虫的原理很简单,就是将应该是将前端页面中的普通文字内容替换为图片。这种情况下,可以直接用ocr来识别图片中的文字,绕过。并且因为是用图片而不是文字来显示,所以图片本身会比较清晰,没有很多噪声干扰,ocr识别的结果会非常准确。
图 9. 图像伪装反爬虫示例
2.3 自定义字体反爬虫
在 CSS3 时代,开发者可以使用 @font-face 来指定网页的字体。开发人员可以将自己喜欢的字体文件放在 Web 服务器上,并在 CSS 样式中使用它。当用户使用浏览器访问网页应用时,浏览器会下载对应的字体到用户的电脑,但是当我们使用爬虫程序时,由于没有对应的字体映射关系,无法直接获取到有效数据爬行。
如图10所示,网页中各个店铺的评价数、人均、品味、环境等信息都是乱码,爬虫无法直接读取内容。
图10.自定义字体反爬虫示例
2.4页动态渲染反爬虫
根据渲染方式的不同,网页大致可以分为客户端渲染和服务器端渲染。
客户端渲染和服务端渲染最重要的区别就是谁来完成html文件的完整拼接。如果是在服务端完成,然后返回给客户端,就是服务端渲染。大量的工作完成了html的拼接,也就是客户端渲染。
图11.客户端渲染示例
2.5 验证码反爬虫
几乎所有的应用程序在涉及到用户信息的安全性时都会弹出验证码供用户识别,以确保操作是人的行为,而不是大型机器。那为什么会出现验证码呢?在大多数情况下,这是因为 网站 被访问太频繁或行为不端,或者是直接限制某些自动化操作。分类如下:
在很多情况下,比如登录和注册,这些验证码几乎都是可用的,它们的目的是为了限制恶意注册、恶意爆破等,这也是一种反爬的手段。当一些网站遇到访问频率高的行为时,可能会直接弹出登录窗口,需要我们登录才能继续访问。此时验证码直接绑定在登录表单上。检测到异常后,采用强制登录的方式进行反爬。一些比较常规的网站如果遇到访问频率稍高的情况,会主动弹出验证码供用户识别并提交,验证当前访问者是否网站是一个真实的人。
常见的验证码包括图形验证码、行为验证码、短信、扫描验证码等,如图12所示。对于是否成功通过验证码,除了能够准确完成相应的点击、选择、输入等根据验证码的要求,通过验证码风控也很关键;比如对于滑块验证码,验证码风控可能是检测滑动轨迹,如果检测到的轨迹是非人为的,则判断为高风险,导致无法通过成功。
图12.验证码反爬虫方法
2.6 请求签名验证反爬虫
签名验证是防止服务器被恶意链接和篡改数据的有效方法之一,也是后端API最常用的保护方法之一。签名是根据数据源进行计算或加密的过程。用户签名后,会生成一个一致且唯一的字符串,这是您访问服务器的身份标志。由于其一致性和唯一性两大特点,可以有效防止服务器将伪造数据或篡改数据当作正常数据处理。
上面2.4小节中提到的网站是通过客户端渲染网页,通过ajax请求获取数据,一定程度上增加了爬虫的难度。接下来分析ajax请求,如图13所示,会发现ajax请求是用请求签名的,分析的是加密参数,如果要破解请求接口,需要破解的加密方法参数,这无疑进一步增加了难度。
图1 3. Ajax 请求排名数据
2.7 蜜罐反爬虫
蜜罐反爬虫是一种隐藏链接的手段,用于检测网页中的爬虫程序。隐藏的链接不会显示在页面上,普通用户无法访问,但爬虫程序可能会放置要爬取的链接。排队,并向链接发起请求,开发者可以利用这个特性来区分普通用户和爬虫。如图14,查看网页源码,页面上只有6个产品,col-md-3的产品
有 8 对标签。这个 CSS 样式的作用是隐藏标签,所以我们在页面上只看到 6 个项目,爬虫会提取 8 个项目的 URL。
图14.蜜罐反爬虫示例
三、反反爬升相关技术
对于上节提到的反爬相关技术,反爬技术手段有以下几种:CSS偏移反爬、自定义字体反爬、动态页面渲染反爬、验证码破解等。这些方法都有详细描述。
3.1 CSS 偏移反爬3.1.1 CSS 偏移逻辑介绍
那么对于上面的2.1css偏移反爬虫的例子,我们如何才能得到正确的机票价格。仔细看CSS样式,可以发现每个带数字的标签都有一个样式集,第一对b标签中的i标签对的样式是一样的,都是width: 16px;另外,还要注意最外层span标签对的样式是width:48px。
如果按照CSS样式的线索分析,第一对b标签中的三对i标签刚好占据span标签对的位置,它们的位置如图15所示。此时价格显示在网页应该是777,但是由于第2对和第3对b标签里面有值,所以我们还需要计算它们的位置。由于第二对b标签的位置样式是left:-32px,所以第二对b标签中的值6会覆盖原来第一对b标签中的第二个数字7,页面应该显示的数字是767。
按照这个规则,第三对b标签的位置样式是left:-48px,这个标签的值会覆盖第一对b标签中的第一个数字7,最终显示的票价是467。
图15.偏移逻辑
3.1.2 CSS偏移反爬代码实现
因此,我们会根据上述CSS样式的规则编写代码来爬取网页以获得正确的机票价格。代码和结果如图 16 所示。
if __name__ == '__main__':
url = 'http://www.porters.vip/confusion/flight.html'
resp = requests.get(url)
sel = Selector(resp.text)
em = sel.css('em.rel').extract()
for element in range(0,1):
element = Selector(em[element])
element_b = element.css('b').extract()
b1 = Selector(element_b.pop(0))
base_price = b1.css('i::text').extract()
print('css偏移前的价格:',base_price)
alternate_price = []
for eb in element_b:
eb = Selector(eb)
style = eb.css('b::attr("style")').get()
position = ''.join(re.findall('left:(.*)px', style))
value = eb.css('b::text').get()
alternate_price.append({'position': position, 'value': value})
print('css偏移值:',alternate_price)
for al in alternate_price:
position = int(al.get('position'))
value = al.get('value')
plus = True if position >= 0 else False
index = int(position / 16)
base_price[index] = value
print('css偏移后的价格:',base_price)
图16. CSS 偏移反爬代码及结果
3.2 自定义字体反爬
针对上述2.3自定义字体反爬虫情况,解决方法是在网页中提取自定义字体文件(通常是WOFF文件),并将映射关系收录到爬虫代码中,可以得到有效数据。解决步骤如下:
查找问题:查看网页源码,发现关键字符被编码替换,如
分析:查看网页,发现应用了css自定义字符集隐藏
查找:查找css文件的url,得到字符集对应的url,如PingFangSC-Regular-num
查找:查找和下载字符集 url
对比:将字符集中的字符与网页源代码中的代码进行比较,发现代码的后四位对应的字符,即网页源代码对应的味道是< @8.9 分
3.3 页面动态渲染,防爬虫
客户端渲染的反爬虫,在浏览器源码中看不到页面代码,需要进行渲染,进一步获取渲染结果。对于这种反爬虫,有几种破解方法:
在浏览器中,可以通过开发者工具直接查看ajax的具体请求方法、参数等;模拟真人通过selenium操作浏览器,得到渲染结果。后续操作步骤与服务端渲染流程相同;如果渲染的数据隐藏在html结果的js变量中,可以直接定期提取;如果有JS生成的加密参数,可以找到加密部分的代码,然后用pyexecJS模拟JS的执行,返回执行结果。3.4 验证码破解
以下是识别滑块验证码的示例。如图 17 所示,是使用目标检测模型识别滑块验证码间隙位置的结果示例。这种破解滑块验证码的方法对应的是模拟真人。方法。不使用接口破解的原因是加密算法很难破解,而且加密算法可能每天都在变化,所以破解的时间成本比较大。
图17.通过目标检测模型识别滑块验证码的差距
3.4.1 爬取滑块验证码图片
因为yolov5使用的目标检测模型是监督学习,所以需要爬取滑块验证码的图片并标记,然后输入到模型中进行训练。通过模拟真人在场景中爬取一些验证码。
图1 8.抓取的滑块验证码图片
3.4.2 手动打标
这次使用 labelImg 手动标注图片。手动标记需要很长时间。100张照片通常需要大约40分钟。自动标记码写起来比较复杂,主要是需要把验证码的所有背景图片和gap图片分别提取出来,然后随机生成gap位置作为标签,把gap放到对应的gap中位置以生成图片作为输入。
图1 9.标注验证码图片及标注后生成的xml文件
3.4.3 目标检测模型yolov5
直接从github下载clone yolov5的官方代码,基于pytorch。
接下来的步骤如下:
数据格式转换:将手动标注的图片和标签文件转换为yolov5接收到的数据格式,得到yolov5格式的1100张图片和1100个标签文件;新建数据集:新建custom.yaml文件,创建自己的数据集,包括训练集和验证集的目录、类别个数、类别名称;训练调优:修改模型配置文件和训练文件后,进行训练,根据训练结果调优超参数。
将xml文件转换为yolov5格式的部分脚本:
for member in root.findall('object'):
class_id = class_text.index(member[0].text)
xmin = int(member[4][0].text)
ymin = int(member[4][1].text)
xmax = int(member[4][2].text)
ymax = int(member[4][3].text)
# round(x, 6) 这里我设置了6位有效数字,可根据实际情况更改
center_x = round(((xmin + xmax) / 2.0) * scale / float(image.shape[1]), 6)
center_y = round(((ymin + ymax) / 2.0) * scale / float(image.shape[0]), 6)
box_w = round(float(xmax - xmin) * scale / float(image.shape[1]), 6)
box_h = round(float(ymax - ymin) * scale / float(image.shape[0]), 6)
file_txt.write(str(class_id))
file_txt.write(' ')
file_txt.write(str(center_x))
file_txt.write(' ')
file_txt.write(str(center_y))
file_txt.write(' ')
file_txt.write(str(box_w))
file_txt.write(' ')
file_txt.write(str(box_h))
file_txt.write('\n')
file_txt.close()
训练参数设置:
parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default='yolov5s.pt', help='initial weights path')
parser.add_argument('--cfg', type=str, default='./models/yolov5s.yaml', help='model.yaml path')
parser.add_argument('--data', type=str, default='data/custom.yaml', help='data.yaml path')
parser.add_argument('--hyp', type=str, default='data/hyp.scratch.yaml', help='hyperparameters path')
# parser.add_argument('--epochs', type=int, default=300)
parser.add_argument('--epochs', type=int, default=50)
# parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs')
parser.add_argument('--batch-size', type=int, default=8, help='total batch size for all GPUs')
parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='[train, test] image sizes')
parser.add_argument('--rect', action='store_true', help='rectangular training')
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
parser.add_argument('--notest', action='store_true', help='only test final epoch')
parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
parser.add_argument('--evolve', action='store_true', help='evolve hyperparameters')
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
parser.add_argument('--cache-images', action='store_true', help='cache images for faster training')
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers')
parser.add_argument('--project', default='runs/train', help='save to project/name')
parser.add_argument('--entity', default=None, help='W&B entity')
parser.add_argument('--name', default='exp', help='save to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--quad', action='store_true', help='quad dataloader')
parser.add_argument('--linear-lr', action='store_true', help='linear LR')
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
parser.add_argument('--upload_dataset', action='store_true', help='Upload dataset as W&B artifact table')
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval for W&B')
parser.add_argument('--save_period', type=int, default=-1, help='Log model after every "save_period" epoch')
parser.add_argument('--artifact_alias', type=str, default="latest", help='version of dataset artifact to be used')
opt = parser.parse_args()
3.4.4 目标检测模型的训练结果
该模型在 50 次迭代时基本达到了精度、召回率和 mAP 的瓶颈。预测结果也存在以下问题:大部分gap都可以准确框起来,但也有少量的frame error,两个gap,没有gap。
图20.顶部:模型训练结果趋势图;下:模型在部分验证集上的预测结果
四、总结
这次简单介绍一下爬虫和反爬虫的技术手段。介绍的技术和案例仅用于安全研究和学习,不会用于大量爬虫或商业应用。
对于爬虫,出于爬取互联网公开数据进行数据分析等目的,应遵守网站机器人协议,在不影响网站正常运行的前提下进行数据爬取,遵守与法律。对于反爬虫来说,只要人类能够正常访问网页,爬虫就可以用相同的资源进行爬取。因此,反爬虫的目的是防止爬虫在海量采集网站信息的过程中使服务器过载,从而防止爬虫行为阻碍用户体验,提高用户使用率网站 对服务很满意。
最佳实践:《Python3 网络爬虫开发实战》:什么是AJAX?
《Python3网络爬虫开发》:什么是AJAX
Ajax,全称是Asynchronous JavaScript and XML,即异步JavaScript和XML。它不是一种编程语言,而是一种使用 JavaScript 与服务器交换数据并更新网页的部分内容而不刷新页面且页面链接不改变的技术。
对于一个传统的网页,如果要更新它的内容,就必须刷新整个页面,但是使用Ajax,你可以在不完全刷新的情况下更新页面的内容。在这个过程中,页面实际上是在后台与服务器交互的。获取到数据后,使用 JavaScript 改变网页,从而更新网页的内容。
向服务器发送请求
要向服务器发送请求,需要用到 open() 和 send() 方法
xhttp.open("GET", "ajax_info.txt", true);
xhttp.send();
获取还是发布?
GET 比 POST 更简单、更快,并且可以在大多数情况下使用。
但是,在以下情况下始终使用 POST:
获取请求
一个简单的 GET 请求:
1. 实例介绍
在浏览网页的时候,我们会发现很多网页已经向下滚动,可以查看更多选项。以微博为例:微博页面一直下滑。可以发现,往下滑几条微博后就消失了,取而代之的是一个加载*敏*感*词*,再过一会,下方还会继续出现新的微博。博客内容,这个过程其实就是**Ajax加载**的过程,如图。
2. 基本原理
在对Ajax有了初步的了解之后,我们再来详细了解一下它的基本原理。向网页更新发送 Ajax 请求的过程可以简单分为以下 3 个步骤:
下面我们将详细描述这些过程中的每一个。
这是 JavaScript 对 Ajax 的底层实现。实际上,它创建了一个新的 XMLHttpRequest 对象,然后调用 onreadystatechange 属性设置*敏*感*词*器,然后调用 open 和 send 方法向链接(即服务器)发送请求。用Python实现请求发送后,可以得到响应结果,但是这里的请求发送是通过JavaScript来完成的。由于设置了*敏*感*词*器,当服务器返回响应时,会触发onreadystatechange对应的方法,然后可以在该方法中解析响应内容。
解析内容
得到响应后会触发onreadystatechange属性对应的方法,可以通过xmlhttp的responseText属性获取响应内容。这类似于 Python 中使用 requests 向服务器发出请求,然后得到响应的过程。那么返回的内容可能是 HTML,也可能是 JSON,然后你只需要在方法中使用 JavaScript 进行进一步处理即可。例如,如果是 JSON,则可以对其进行解析和转换。
呈现网页
JavaScript 具有更改网页内容的能力。解析响应内容后,可以调用 JavaScript 对网页进行解析处理。例如,通过document.getElementById().innerHTML的操作,可以改变一个元素中的源代码,从而改变网页上显示的内容。该操作也称为DOM操作,即对网页文档进行更改、删除等操作。
在上面的例子中,document.getElementById(“myDiv”).innerHTML=xmlhttp.responseText会将ID为myDiv的节点内部的HTML代码改成服务器返回的内容,这样服务器返回的新数据就是显示在 myDiv 元素内。页面的某些部分似乎已更新。
我们观察到这 3 个步骤实际上是由 JavaScript 完成的,它完成了请求、解析和渲染的整个过程。
回想一下微博的pull-to-refresh,这其实就是JavaScript向服务器发送Ajax请求,然后获取新的微博数据,解析,在网页中渲染。
因此,我们知道,真正的数据实际上是一次又一次地从 Ajax 请求中获取的。如果你想捕获这些数据,你需要知道这些请求是如何发送的,发送到哪里,发送了什么参数。如果我们知道这一点,难道我们不能用 Python 来模拟这个发送操作并得到结果吗?
总结
本节我们简单了解Ajax请求的基本原理及其带来的页面加载效果。在下一节中,我们将介绍如何分析 Ajax 请求。