整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

这种反爬虫手段有点意思,看我破了它

种反爬虫手段被广泛应用在一线互联网企业的产品中,例如汽车资讯类网站、小说类网站等文字密度较大的站点。在开始学习之前,我们先来看看具体的现象。打开网址:


https://implicit-style-css_0.crawler-lab.com
复制代码

呈现在我们眼前的是这样一个界面:



这次的任务,就是拿到页面上所呈现的内容的文本。在编写爬虫代码之前,我们要做几件事:

  • 确定目标内容的来源,也就是找到响应目标内容的那次请求
  • 确定目标内容在网页中的位置

其实就是最基本的观察和分析。

网络请求方面,打开浏览器调试工具并切换到 Network 面板后,看到页面只加载了 2 个资源:



一个 html 文档和一个 js 文件,想必我们要的内容就在 html 文档中。点击该请求,浏览器开发者工具就会分成两栏,左侧依然是请求记录列表,右侧显示的是指定请求的详情。右侧面板切换到 Response,就可以看到服务器响应的内容:



看样子,我们要的东西就在这次响应正文中。咋一看,我们直接取 class 为 rdtext 的 div 标签下的 p 标签中的文本内容即可。然而事情并没有那么简单,细心的读者可能发现了,响应正文中显示的内容和页面中呈现的文字并不完全相同——响应正文中少了一些标点符号和文字,多了一些 span 标签。例如页面中显示的是:

夜幕团队 NightTeam 于 2019 年 9 月 9 日正式成立,团队由爬虫领域中实力强劲的多名开发者组成:崔庆才、周子淇、陈祥安、唐轶飞、冯威、蔡晋、戴煌金、张冶青和韦世东。
复制代码

而响应正文中看到的是:

<p>夜幕团队 NightTeam 于 2019 年 9 月 9 日正式成立<span class="context_kw0"></span>团队由爬虫领域中实力强劲<span class="context_kw1"></span>多<span class="context_kw21"></span>开发者组成:崔庆才、周子淇、陈祥安、唐轶飞、冯威、蔡晋、戴煌金、张冶青和韦世东<span class="context_kw2"></span>
</p>
复制代码

这句话中,被 span 标签替代的有逗号字、字。整体看一遍,发现这些 span 标签都带有 class 属性。

明眼人一看就知道,又是利用了浏览器渲染原理做的反爬虫措施。不明所以的读者请去翻阅《Python3 反爬虫原理与绕过实战》。

既然跟 span 和 class 有关,那我们来看一下 class 属性到底设置了什么。class 名为 context_kw0 的 span 标签样式如下:

.context_kw0::before {
    content: ",";
}
复制代码

再看看其他的,class 属性为 context_kw21 的 span 标签样式如下:

.context_kw21::before {
    content: "名";
}
复制代码

原来被替换掉的文字出现在这里!看到这里,想必聪明的你也知道是怎么回事了!

解决这个问题的办法很简单,只需要提取出 span 标签 class 属性名称对应的 content 值,然后将其恢复到文本中即可。

属性名有个规律:context_kw + 数字。也就是说 context_kw 有可能是固定的,数字是循环出来的,或者是数组中的下标?大胆猜想一下,假设有这么一个字典:

{0: ",", 1: "的",  21: "名"}
复制代码

那么将 context_kw 与字典的键组合,就得到了 class 的名称,对应的值就作为 content,这好像很接近了。中高级爬虫工程师心中都明白:在网页中,能干出如此之事唯有借助 JavaScript。不明白的读者请去翻阅《Python3 反爬虫原理与绕过实战》。

那就搜一下吧!

唤起浏览器调试工具的全局搜索功能,输入 context_kw 并会车。然后在搜索结果中寻找看上去有用的信息,例如:



发现 JavaScript 代码中出现了 context_kw,关键的信息是 .context_kw + i + _0xea12('0x2c')。代码还混淆了一下!看不出的读者可以找作者韦世东报名《JavaScript 逆向系列课》,学完就能够很快找到看上去有用的代码,并且看懂代码的逻辑。

这里手把手带读一下这些 JavaScript 代码。第一段,也就是 977 行代码原文如下:

var _0xa12e = ['appendChild', 'fromCharCode', 'ifLSL', 'undefined', 'mPDrG', 'DWwdv', 'styleSheets', 'addRule', '::before', '.context_kw', '::before{content:\x20\x22', 'cssRules', 'pad', 'clamp', 'sigBytes', 'YEawH', 'yUSXm', 'PwMPi', 'pLCFG', 'ErKUI', 'OtZki', 'prototype', 'endWith', 'test', '8RHz0u9wbbrXYJjUcstWoRU1SmEIvQZQJtdHeU9/KpK/nBtFWIzLveG63e81APFLLiBBbevCCbRPdingQfzOAFPNPBw4UJCsqrDmVXFe6+LK2CSp26aUL4S+AgWjtrByjZqnYm9H3XEWW+gLx763OGfifuNUB8AgXB7/pnNTwoLjeKDrLKzomC+pXHMGYgQJegLVezvshTGgyVrDXfw4eGSVDa3c/FpDtban34QpS3I=', 'enc', 'Latin1', 'parse', 'window', 'location', 'href', '146385F634C9CB00', 'decrypt', 'ZeroPadding', 'toString', 'split', 'length', 'style', 'type', 'setAttribute', 'async', 'getElementsByTagName', 'NOyra', 'fgQCW', 'nCjZv', 'parentNode', 'insertBefore', 'head'];
        (function (_0x4db306, _0x3b5c31) {
            var _0x24d797 = function (_0x1ebd20) {
                while (--_0x1ebd20) {
                    _0x4db306['push'](_0x4db306['shift']());
                }
            };
复制代码

往下延伸阅读,还能看到 CryptoJS 这个词,看到它就应该晓得代码中使用了一些加密解密的操作。

第二段,1133 行代码原文如下:

for (var i = 0x0; i < words[_0xea12('0x18')]; i++) {
            try {
                document[_0xea12('0x2a')][0x0][_0xea12('0x2b')]('.context_kw' + i + _0xea12('0x2c'), 'content:\x20\x22' + words[i] + '\x22');
            } catch (_0x527f83) {
                document['styleSheets'][0x0]['insertRule'](_0xea12('0x2d') + i + _0xea12('0x2e') + words[i] + '\x22}', document[_0xea12('0x2a')][0x0][_0xea12('0x2f')][_0xea12('0x18')]);
            }
        }
复制代码

这里循环的是 words,然后将 words 元素的下标和对应元素组合,这和我们猜想的是非常接近的,现在要找到 words

怎么找?

又不会吗?

搜索就可以了,顺着搜索结果看,找到了定义 words 的代码:

var secWords = decrypted[_0xea12('0x16')](CryptoJS['enc']['Utf8'])[_0xea12('0x17')](',');
var words = new Array(secWords[_0xea12('0x18')]);
复制代码

按照这个方法,我们最后发现 CSS 的 content 的内容都是数组 _0xa12e 中一个经过加密的元素先经过 AES 解密再经过一定处理后得到的值。

捋清楚逻辑之后,就可以开始抠出我们需要的 JS 代码了。

这个代码虽然经过混淆,但还是比较简单的,所以具体的抠代码步骤就不演示了,这里提示一下在抠出代码之后两个需要改写的点。

第一个是下图中的异常捕获,这里判断了当前的 URL 是否为原网站的,但调试时,在 Node 环境下执行是没有 window 对象、document 对象的,如果不做修改会出现异常,所以需要把带有这些对象的代码注释掉,例如下面 if 判断语句:

try {
	if (top[_0xea12('0x10')][_0xea12('0x11')][_0xea12('0x12')] != window[_0xea12('0x11')]['href']) {
	top['window'][_0xea12('0x11')]['href'] = window[_0xea12('0x11')][_0xea12('0x12')];
}
复制代码

其他的地方还需要自己踩坑。

修改完后就可以获取到所有被替换过的字符了,接下来只需要把它们替换进 HTML 里就可以还原出正常的页面,replace 就不演示了噢。

反爬虫原理

例子中用到的是 ::before,下方文字描述了它的作用:

在 CSS 中,::before 用于创建一个伪元素,其将成为匹配选中的元素的第一个子元素。常通过 content 属性来为一个元素添加修饰性的内容。

引用自:developer.mozilla.org/zh-CN/docs/…

举个例子,新建一个 HTML 文档,并在里面写上如下内容:

<q>大家好,我是咸鱼</q>,<q>我是 程序员中的一员</q>
复制代码

然后为 q 标签设置样式:

q::before { 
  content: "«";
  color: blue;
}
q::after { 
  content: "»";
  color: red;
}
复制代码

完整代码如下(写给没有 HTML 基础的朋友):

<style>

q::before { 
  content: "«";
  color: blue;
}
q::after { 
  content: "»";
  color: red;
}

</style>
<q>大家好,我是咸鱼</q>,<q>我是 程序员中的一员</q>
复制代码


我们在样式中,为 q 标签加上了 ::before 和 ::after 属性,并设置了 content 和对应的颜色。于是乎,在被q 标签包裹着的内容前会出现蓝色的 符号,而后面会出现红色的 符号。

简单易懂吧!

小结

本文简单介绍了隐式 Style–CSS 在反爬虫中的应用,并通过一个简单的实例学习了如何应对这种情况,相信尝试过的你已经清楚地知道下次碰到这种反爬的时候该如何破解了。

当然呢,这个例子还不够完善,没有完全覆盖到隐式 Style–CSS 在反爬虫中的所有应用方式,如果读者朋友对这类反爬虫有兴趣的话,不妨多找几个例子自己动手试试,也欢迎通过留言区与我交流讨论。


最后,小编想说:我是一名python开发工程师,

整理了一套最新的python系统学习教程,

想要这些资料的可以关注私信小编“01”即可(免费分享哦)希望能对你有所帮助

ava 内存模型中的 happen-before 是什么?

Happen-before 关系,是Java 内存模型中保证多线程可见性的机制,也是早期语言规范中含糊可见性概念的一个精确定义。

它的具体表现形式,包括但远不止 synchronized,volatile,lock 操作顺序等方面。

happen-before 原则

  • 程序顺序规则:一个线程内执行的每个操作,都保证 happen-before 后面的操作,这样就保证了程序顺序规则,
  • volatile 变量规则:对于 volatile 变量,对他的写操作,保证 happen-before 在随后对改变量的读取操作。
  • 监视器锁规则:对于一个锁的解锁操作,保证 happen-before 加锁操作。
  • 线程启动 happen-before 规则:Thread 对象的 start() 方法先行于此线程的每一个动作
  • 线程中断 happen-before 规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。
  • 对象终结规则,一个对象的初始化完成happen-before 于 finalizer() 方法的开始
  • 传递性:happen-before 存在传递性,a happen-before b ,b happen-before c ,那么 a happen-before c 。

happen-before 保障了顺序执行,也包括了内存读写的操作顺序。

happen-before 与 JMM 的关系

image

如何学习 Java 内存模型(JMM)

JMM 可以看作是深入理解Java并发编程、编译器和JM内部机制的必要条件,但这同时也是个容易让初学者无所适从的主题。

  • 明确目的,建议不要一头扎进各种CPU体系结构,纠结于不同的缓存、流水线、执行单元等。这些东西虽然很酷,但其复杂性是超乎想象的,很可能会无谓增加学习难度,也未必有实践价值。
  • 克制住对”秘籍"的诱惑。有些时候,某些编程方式看起來能起到特定效果,但分不清是实现差异导致的表现,还是"规范″要求的行为,就不要依赖于这种"表现"去编程,尽量遵循语言规范进行,这样我们的应用行为才能更加可靠、可预计。

JMM 可以解决什么问题?

简化多线程编程,保证程序可移植性

Java 是最早尝试提供内存模型的语言,可简化多线程编程,保障程序可移植。 早期的 C/C++ 不存在内存模型的概念,依赖处理器本身的内存一致性模型。 但是不同的处理器差异比较大,不能保证 C++ 程序在处理器A 可以运行,在处理器B 上也可以运行。

  • https://en.wikipedia.org/wiki/Memory_ordering

对指令重排序提供清晰的规范

过于范范的内存模型定义,有很多模棱两可之处,对 synchronized 或者 volatile 产生的指令重排序问题,如果没有清晰的规范,不能保证一些多线程程序的正确性。

所以,Java迫切需要一个完善的JMM,能够让普通Java开发者和编译器、JVM工程师,能够淸地达成共识。换句话说,可以相对简单并准确地判断岀,多线程程序什么样的执行序列是符合规范的。

JVM 开发者

对于编译器、JVM开发者,关注点可能是如何使用类似内存屏( Memory-Barrier)之类技术,保证执行结果符合JMM的推断。

Java 应用工程师

对于Java应用开发者,则可能更加关注 volatile、 synchronized等语义,如何利用类{ happen- before的规则,写出可靠的多线程应用。

image

Java 内存模型的抽象定义

包含本地内存和主内存的定义

JMM 是怎么解决可见性的问题

image

JMM 内部是怎样实现 happen-before 原则的?

  • 内存屏障
  • 禁止重排序

以 volatile 变量为例:

  • 对改变量的写操作之后,编译器插入一个写屏障
  • 对改变量的读操作之前,编译器会插入一个读屏障

内存屏障能够在类似变量读、写操作之后,保证其他线程对 volatile变量的修改对当前线程可见,或者本地修改对其他线程提倛可见性。换句话说,线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值

如果你对更多内存屏障的细节感兴趣,或者想了解不同体系结构的处理器模型,建议参考JSR-133相关文档,我个人认为这些都是和特定硬件相关的,内存屏障之类只是实现JMM规范的技术手段,并不是规范的要求。

  • http://gee.cs.oswego.edu/dl/jmm/cookbook.html
class VolatileExample {
int  a = 0;
volatile boolean flag= false;
public void writer(){
   a=1;        // 1
   flag = true; //2
} 
public void reader(){
  if(flag){  //3
    int i = a ;//4
    ...
  }
} 

假设线程A执行 writer方法之后,线程B执行 reader0方法。根据 happens-before规则,这个过程建立的 happens-before关系可以分为3类:

  1. 根据程序次序规则,1 happens-before2;3 happens-before4。
  2. 根据 volatile 规则,2 happens-before3
  3. 根据 happens-before的传递性规则,1 happens-before4。

上述 happens-before关系的图形化表现形式如下:

image

在上图中,每一个箭头链接的两个节点,代表了一个 happens-before关系。黑色箭头表示程序顺序规则;橙色箭头表示 volatile规则;蓝色箭头表示组合这些规则后提供的 happens-before保证。 最终读取到的i 就是 1 。

image

线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

当读一个 volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。如图所示,在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变成一致。

image

  • 线程A写一个 volatile变量,实质上是线程A向接下来将要读这个 volatile变量的某个线程发出了(其对共享变量所做修改的)消息
  • 线程B读一个 volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息
  • 线程A写一个 volatile变量,随后线程B读这个 volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

有序性,原子性,可见性是线程安全的基本保障。

volatile 使用了内存屏障实现可见性

  • 在每个 volatile写操作的前面插入一个Storestore屏障。
  • 在每个 volatile写操作的后面插入一个Storeload屏障。
  • 在每个 volatile读操作的后面插入一个Loadload屏障。
  • 在每个 volatile读操作的后面插入一个Loadstore屏障。

image

我们经常会说 volatile b比synchronized之类更加轻量,但轻量也仅仅是相对的, volatile的读、写仍然要比普通的读写要开销更大,所以如果你是在性能高度敏感的场景,除非你确定需要它的语义,不然慎用。

程序员开发者社区

近因为一些网页的需要,需要比较深入的使用了CSS 的「伪元素」( Pseudo Element ),发现原来不只是用用before或after 而已,可以玩的东西还真是不少,所以就来篇文章,把这些比较不常玩的用法归纳整理下,希望对你的日常工作有所帮助。

什么是「伪元素」?

「伪元素」之所以称作「伪」,除了英文从「Pseudo」翻译过来之外,就是因为它并不是真正网页里的元素,但行为与表现又和真正网页元素一样,也可以对其使用CSS 操控。

跟伪元素类似的还有「伪类」( Pseudo classes ),在W3C的定义里总共有五个伪元素(其他仍在测试阶段),分别是::before、::after、::first-line、::first-letter和::selection,为了和伪类区分,伪元素使用两个冒号「::」开头,而伪类使用一个冒号「:」开头(像是:hover、:target...等)。

虽然现在的浏览器就算写一个冒号也可以正常运作,不过为了方便区分,用两个冒号还是比较好的,而且不论浏览器是什么,::selection必须是两个冒号才能正常运作。

参考:MDN Pseudo-elements、伪类child和of-type

认识::before 与::after

::before、::after大概是最常使用的伪元素,两者都是以display:inline-block的属性存在,::before是在原本的元素「之前」加入内容,::after则是在原本的元素「之后」加入内容,同时伪元素也会「继承」原本元素的属性,如果原本文字是黑色,伪元素的文字也会是黑色。

举例来说,下面这段程式码,有一个div 内容是「大家好,我是div」,使用::before、::after 之后,会在原本div 的前后各添加一段文字,并且让这两段文字都呈现红色。

div::before{
 content:"我是 before";
 color:red;
}
div::after{
 content:"我是 after";
 color:red;
}

实用的content

上述的内容乍看之下很容易理解,比较需要注意的是一定要具备content的属性,就算是只有content:"";都可以,因为没有content的伪元素是不会出现在画面上的,然而content是个很特别的属性,它可以使用attr直接获取内容元素的属性值( attribute ),举例来说,在HTML里有一个超连结,点击后会弹出新视窗并连结至Google:

<a href="https://www.google.com" target="_blank">google</a>

使用下列的程式码用法,将会把超连结的href 内容与target 内容,透过伪元素一前一后的显示出来。

a::before{
 content: attr(href);
 color:red;
}
a::after{
 content: attr(target);
 color:green;
}

此外content内容是可以「相加」的,不过用法不像JavaScript使用+号来相连,而是直接用一个空白键就可以不断的累加下去,以下面的程式码来说,可以在刚刚撷取的超连结文字后方和target属性前方,加入标点符号。

a::before{
 content: "( " attr(href) " ) < ";
 color:red;
}
a::after{
 content: " > ( " attr(target) " ) ";
 color:green;
}

content 甚至可以使用url 放入图片的功能,下列的程式码会呈现出三张图片。

div::before{
 content:url(图片网址) url(图片网址) url(图片网址);
}

通过调整border的属性,我们可以实现上下左右的三角形,再结合伪元素before,after,content可以绘制多种多边形,笔者在这篇文章有过介绍,感兴趣的可以看看 :只用1个div,你能用CSS绘制:正3、4、5、6、7、8边形吗?

content搭配quotes使用

在CSS里有个不常用的属性就是quotes,这是做为定义「括号格式」的属性,也就是如果在一段文字被包住,这段文字的前后就会出现自定义的标签替换(可以是括号、特殊符合、文字等),而且quotes支持多层嵌套,也就是你可以一层层的写下去,以下面这段HTML文字举例:

最外层<q>第一层<q>第二层</q><q>第二层<q>第三层</q></q></q>

quotes 的属性如果只写一层,就会看到只出现一种括号,前后括号使用空白分隔,两组为一个单位,前后可以不同符号。

q{
 quotes: ' < ' ' > ';
}

如果写了三层,就会看到出现三种括号,也会把文字当作括号使用。

q{
 quotes: ' < ' ' > ' ' ya ' ' ya ' ' ( ' ' ) ' ;
}

(请注意开合标签的就近分配原则)

同样的道理,我们可以应用在content里面,而且通过伪元素::before和::after处于前后的预设位置,甚至不用就实现前后括号的效果,以下面这段HTML文字举例,把刚刚的q全部换成span:

最外层<span>第一层<span>第二层</span><span>第二层<span>第三层</span></span></span>

CSS的部分比较特别,在伪元素content里使用了open-quote (启始括号)和close-quote (结束括号)这两个有趣的值,换句话说open-quote对应到,close-quote对应到,此外也由于括号是在伪元素内,就可以指定不同的颜色或样式了。

span{
 quotes: ' < ' ' > ' ' ya ' ' ya ' ' ( ' ' ) ' ;
}
span::before{
 content:open-quote;
 color:red;
}
span::after{
 content:close-quote;
 color:#aaa;
}

文章来源:https://www.oxxostudio.tw/articles/201706/pseudo-element-1.html

原文作者:oxxostudio

由于网页为繁体内容,术语描述和标点话术的差异的问题,笔者在保证不改变原意的基础上做了调整,并且内容页进行了验证确认无误,欢迎大家指正。

小结

虽然说伪元素很好用,但伪元素的内容实际上不存在网页里( 如果打开浏览器的开发者工具,是看不到内容的),所以如果在里头塞了太多的重要的内容,反而会影响到SEO 的成效,因此对于使用伪元素的定位,还是当作「辅助」性质会比较恰当。