加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程开发 > Python > 正文

反爬虫精讲!通过学习这篇让你学会无视百分之85的反爬网站!

发布时间:2020-12-17 00:58:35 所属栏目:Python 来源:网络整理
导读:背景介绍: 为了平衡社区成员的贡献和索取,? 一起帮 ?引入了? 帮帮币 ?。当用户积分(帮帮点)达到一定数额之后,就会“掉落”一定数量的“帮帮币”。为了增加趣味性,帮帮币“掉落”之后所有用户都可以“捡

背景介绍:

为了平衡社区成员的贡献和索取,?一起帮?引入了?帮帮币?。当用户积分(帮帮点)达到一定数额之后,就会“掉落”一定数量的“帮帮币”。为了增加趣味性,帮帮币“掉落”之后所有用户都可以“捡取”,谁先捡到归谁。

进群:548377875 ??即可获取数十套PDF哦!记住是分开私信,不是一起私信!

但这样就产生了一个问题,因为这个“帮帮币”是可以买卖有价值的,所以难免会有恶意用户用爬虫不断的扫描,导致这样的情况出现:

注:经核实,?乔布斯的同学?其实没有用爬虫,就是手工点,点出来的!还能说什么呢?只能表示佩服啊佩服……

所以我们需要一种机制,阻止这种爬虫的行为。

大致思路:

这个问题我们有一个很便利的前提:只有注册用户才能够“捡起”帮帮币。所以,我们不需要通过“封IP”(需获取真实IP)这种方式来阻断爬虫爬行,而是直接封注册用户,非常方便。

那么如何判断一个请求是真实用户,还是爬虫呢?我们决定使用最简单的方法:记录访问频次。当某一个用户的访问频次高于设定值时(比如:5分钟10次),就判定该用户“有爬虫嫌疑”。

此外,为了防止误判(确实有用户手快),我们还应该给用户一个“解锁”的功能:通过输入验证码来确定不是爬虫。

细节设计:

一个最核心的问题是:用什么来?记录用户的访问频次??

数据库?感觉没必要,这个数据又不需要长期保留,访问一次就做一次I/O操作在性能上接受不了,所以我们决定使用内存。

但是,具体需要记录那些数据,又用什么样的数据结构呢?

最后我们选择使用缓存,记录最简单的“用户ID -> 访问次数”键值对,来解决这个问题,因为:

  • 利用缓存的自动清除(expire)特性,清除过期数据,保证记录的访问次数始终是在一定时间内的。
  • 缓存的读写速度很快,性能上没有压力

当然,这里其实还是有那么点问题的。比如,假设缓存时间是5分钟,最多访问次数是10次。0:10,开始缓存访问次数,一直累加,到0:14,共记录访问次数7次,没有问题;然而,一过0:15,缓存被清空,0:16的时候,缓存里只有0:15到0:16这一分钟的数据,没有过去5分钟(从0:11到0:16)的数据。所以用户可以控制一直爬虫,访问9次,然后就歇着,5分钟过后,再继续访问9次,然后再歇5分钟……

唉~~真这么拼,我还真没什么办法?但如果这么一个频次他能接受的话,我其实也无所谓,你就慢慢爬呗。或者,我们后台做更大的监控,把每个用户的每次访问都记录下来,进行统计,找出异常。那时候可能就真的需要数据库了(为了提高性能可以内存里放一个DataTable,定时同步到Database)。但暂时来说,没有这个必要。

此外,还有一个问题,是不是只需要记录用户访问频次?

如果按上述方案,在缓存里记录访问频次,通过缓存数据来判断是否允许继续访问,会有一个问题:缓存到期失效之后,这个用户就又可以自由访问目标页面了!相当于到期自动解锁。

我觉得这还是不科学,如果认定是爬虫,只能是人工解锁(识别码验证)。所以在数据库用户表里添加一个“已锁定”(Locked)字段,如果用户被锁定,Update其为当前时间;未锁定时(解锁后)为NULL。

具体实现:

为了重用,我们需要利用 Authorize Fitler,在它的OnAuthorization()方法里面进行检查和记录。

代码本身应该比较简单,if...else...的逻辑:

 ///1. 先根据数据库捡查当前用户是否被锁定
 ///2. 如果被锁定,直接拦截。否则:
 ///3. 在缓存中检查有无当前用户的访问次数记录
 /// 3.1 没有,新建一条他的缓存。否则:
 /// 3.2 检查该用户已访问次数
 /// 3.2.1 如果已到达访问次数限制,拦截并在数据库中锁定该用户。否则
 /// 3.2.2 累加用户的访问次数

精简注释代码如下:

 public class NeedLogOn : AuthorizeAttribute
 {
 public override void OnAuthorization(AuthorizationContext filterContext)
 {
 HttpContextBase context = filterContext.HttpContext;
 ///Autofac相关操作,获取正取的ISharedService实例
 ISharedService service = AutofacConfig.Container.Resolve();
 _NavigatorModel model = service.Get(); //从数据库获取当前User的信息
 ///截断式编程,减少if...else的{}嵌套
 if (model.Locked.HasValue)
 {
 ///model.Locked 来自数据库,用户已经被锁定,拦截
 visitTooMuch(filterContext);
 return;
 }
 string cacheKey = CacheKey.MAX_VISIT + model.Id;
 ///非常有意思,不能直接使用int值类型,必须使用引用类型的
 VisitCounter amount;
 if (context.Cache[cacheKey] == null)
 {
 amount = new VisitCounter { Value = 1 };
 ///新建立一条Cache
 context.Cache.Add(cacheKey,amount,null,DateTime.Now.AddSeconds(Config.Seconds),Cache.NoSlidingExpiration,CacheItemPriority.Normal,null);
 }
 else
 {
 amount = context.Cache[cacheKey] as VisitCounter;
 if (amount.Value >= Config.MaxVisit)
 {
 ///在数据库中锁定该用户
 service.LockCurrentUser();
 BaseService.Commit();
 ///立即清除Cache
 context.Cache.Remove(cacheKey);
 visitTooMuch(filterContext);
 return;}
 else
 {
 ///不能使用:currentVisitAmount++;
 ///context.Cache[cacheKey] = currentVisitAmount;
 ///见:https://stackoverflow.com/questions/2118067/cached-item-never-expiring
 amount.Value++;
 }
 }
 }
 }
 public class VisitCounter
 {
 public int Value { get; set; }
 }

仔细观察代码,你会发现两个问题。这就是飞哥我曾经掉的坑啊!o(╥﹏╥)o

1、为什么要引入?VisitCounter类?

缓存里就存放着这个类的实例,而这个类其实就包裹一个int Value;干嘛呢,这是?为什么不直接用int呢?直接把int存到Cache里不行吗?

不行啊!艹。

存进去,没问题;取出来,也没问题;但更新(累加)的时候有问题啊。你怎么更新?

 //取出缓存
 currentVisitAmount = Convert.ToInt32(context.Cache[cacheKey]);
 //累加
 currentVisitAmount++;
 //再存进去
 context.Cache[cacheKey] = currentVisitAmount;

这样不行的,具体的解释看这里:?Cached item never expiring?。

简单的说,context.Cache[cacheKey] = currentVisitAmount; 这一句,等于重新插入了一条永不过期的缓存。万万没想到啊!这个bug把飞哥都差点搞疯了,本来cache的调试都非常麻烦,还搞个这种幺蛾子。

所以解决的办法是什么呢?在Cache里存一个引用类型值,然后不改Cache,只改引用类实例里的值就OK了。代码就不重复了。

2、在锁定用户的同时,清除该用户的cache

这里啊,曾经走了点弯路。

我最开始是在解锁用户的时候清除该用户的Cache。

 [NeedLogOn]
 public ActionResult Unlock()
 {
 string userId = getCurrentUserId();
 string cacheKey = CacheKey.MAX_VISIT + userId;
 HttpContext.Cache.Remove(cacheKey);
 return View(new ImageCodeModel());
 }

结果不知道咋回事,时灵时不灵。我把本地代码,连接服务器数据库,开着Debug模式,一步一会的进去看,OK,没问题;但把本地代码发布到服务器,duang,不行了?!没法调试,只有写log啥的,坑得我不要不要的……

后来突然发现,这里有“坏代码的味道”:重复。你看这个cacheKey的构建,是不是在 NeedLogOn.OnAuthorization()里构建过一次?重复使用的代码是不是就应该封装?所以呢,开始呢,是想弄一个方法出来获得cacheKey,比如striing GetVisitLimitCacheKey()啥的,但这个方法要让Controller里的UnLock()和Filter里的OnAuthorization()都能调用,放在哪里呢?

突然灵光一闪:为什么 Cache.Remove 要写在UnLock()里面呢?

其实只要用户被锁定,他的缓存信息就没用了。因为我们已经在数据库中标明了他被Locked,所以NeedLogOn.OnAuthorization()拦截住他,不需要Cache呀!尽早的清除这个Cache,还能提高那么一点点的性能。

最关键的是,这样代码更紧凑了:cacheKe在同一个方法里被使用,cache操作在同一个方法类完成,避免了代码分散耦合,优雅多了!

最后的总结:

最近“老编程之家”的话题比较热,至少我是这么感觉的(我这都是开第三个“老编程之家”的QQ群了,群号拿去:834748431)。

我个人认为,老编程之家“没用了”“干不动了”“没公司要了”……,这些东西肯定是YY出来的。代码的质量在于它的逻辑它的内核,你怎么定义问题怎么解决问题,先有了思路然后才有实现。思路清晰了,实现才有可能优雅。至于什么“喜欢学习新技术”“没有女朋友能加班”,就有些外行了。年轻人真“喜欢”学习新技术吗?哈哈,虽然我大叔了,但也年轻过,你别骗我。问题是年轻人要想往上爬,只能学习新技术。一些需要经验积累的东西,他没法学,难道他来学管理学架构学带团队?至于加班,其实是一种陋习,我这么多QQ群,一到上班时间就热闹起来,一到周末就冷冷清清,你说他们的工作强度有多大,需要天天加班才能完成?而且稍有经验的开发人员都知道,项目的进度,一定程度之后,加人加班都是无效的。每天996,人家究竟是在写代码,还是在写bug,鬼大爷才知道!

当然,即使是老编程之家,好的代码也都是改出来的,“如切如磋,如琢如磨”,需要一个不断打磨的过程。

“一起帮”的代码,需要打磨的地方,其实还有很多很多。但是呢,这里面有一个成本的问题。群里经常有网友吐槽他们公司的代码烂,要是早些年呢,我也会和他们一起吐槽,吐吐更健康。但现在,还是有点兔死狐悲的感觉。我就在想:哪一天,可能我的代码也会被人这样吐槽吧,哈哈……

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读