反爬蟲之有個操作也許可以檢測aiohttp、httpx,requests也尷尬?

前言


最近行業市場不太景氣啊,趁着有時間多學學吧,武裝自己,等機會
剛好,發現一個很6的東西。這個問題是在差不多半個月前,羣友 @十一 發現的,然後在羣裏跟大家討論。

 

 

 


這個網站,請求的時候,requests正常:

 

 

 

 

 

原始的curl也可以:

 

 

 

aiohttp,直接報錯

 

 

 

 

httpx,也直接報錯:

 

 

 

不過httpx的報錯要明顯點,這就進入了有意思的環節

分析



看到這裏,估計就有朋友開始冷笑了:搞爬蟲大部分不都用requests的嗎,誰會用httpx和aiohttp這些啊,感覺這個是不是沒啥用啊。
別急,朋友,在文章的下面會說這個,當然如果你趕時間或者覺得沒啥意思,那就沒必要浪費時間繼續看了

1.httpx


拿着上面的報錯,去網上搜,找到如下issues:
https://github.com/encode/httpx/issues/1561
https://github.com/encode/httpx/issues/1363


看完裏面各位大佬的一頓分析,說是hyper庫的問題:

 

 

 

 

 

 

然後hyper庫的開發者,如下鏈接回覆:
https://github.com/python-hyper/h11/issues/113
大概意思是這個不是一個問題,而是http請求的嚴格性判斷問題,請求頭的協議,按國際標準,是不能出現 “[Cache-Control]” 這種帶有特殊符號作爲響應頭的鍵名的,所以報錯
而requests卻可以,或許是因爲requests的校驗不嚴格,直接就放過了:

 

 

 

而,瀏覽器訪問也是可以的:

 

 

 


那麼我個人就有理由認爲

這是一個bug,httpx和aiohttp都存在的bug

 

httpx的作者,對這個問題那段時間確實在嘗試解決,github機器人都想關閉了,httpx作者還不想放棄:

 

 

 

 

且至今沒解決,遇到的人還不少,至少,上個月都還有人在說這個問題

  

 

臥槽,這時間,上週都還有人在問啊:

 

 

 

 

 

其中也有說解決辦法,看到有個老哥說改h11的源碼,改成這樣:

 

 

 

 

 

但是報錯依然在

 

 

 

 

另外有個老哥說了這個方法:

 

 

 

h11.readers.header_field_re = re.compile(b"(?P<field_name>[-!#$%&'*+.^`/|~0-9a-zA-Z]+):[ \t](?P<field_value>([^\\x00\\s]+(?:[ \t]+[^\\x00\\s]+))?)[ \t]*")

  

 

我把這個代碼直接放我代碼裏,報錯了,根本沒有這個屬性

 

 

經過一頓查閱,他換了個屬性名,是這個:

 

 

import h11from h11 import _readersimport re
h11._readers.header_field_re = re.compile(b"(?P<field_name>[-!#$%&'*+.^`/|~0-9a-zA-Z]+):[ \t](?P<field_value>([^\\x00\\s]+(?:[ \t]+[^\\x00\\s]+))?)[ \t]*")

  

 

但是我試了,換成了新的報錯:

 

 

也許是正則表達式寫的不完美:

 

 

正則表達式通用匹配一下,然後就可以了:

 

 

import httpximport h11
from h11 import _readers
import re # h11._readers.header_field_re = re.compile(b"(?P<field_name>[-!#$%&'*+.^`/|~0-9a-zA-Z]+):[ \t](?P<field_value>([^\\x00\\s]+(?:[ \t]+[^\\x00\\s]+))?)[ \t]*")
h11._readers.header_field_re = re.compile(b"(?P<field_name>.*?):[ \t](?P<field_value>.*?)") headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'}
url = 'https://scanstatus.sxxxxx'
# url = 'https://club.xxxxxx/thread-26829078-1-1.html'
resp = httpx.get(url=url, headers=headers)
print(resp.text)

  


但是,我換了一個網站,也就是那個issue裏某華某爲的社區地址,不出正常的結果:

 

 

所以,修改正則表達式並不是一個通用的方案。

 

插一句,在請求的時候,就不要開代理或者抓包軟件了,不然他報錯更奇怪:

 

 

 

綜上所述,也就是,只要服務器的返回頭埋的坑夠多,這個方法根本無法完美解決

那麼這個問題,httpx作者就給了個這個解釋就沒下文了

https://github.com/encode/httpx/issues/767#issuecomment-1367498458

  


就這麼不了了之了。。。

 

因爲根據http協議的響應頭原理,確實不會出現這種不標準的字段

更官方的解釋可以去查查http協議原理,或者看看以下資料:

https://zh.wikipedia.org/zh/HTTP%E5%A4%B4%E5%AD%97%E6%AE%B5https://developer.mozilla.org/zh-CN/docs/Web/API/Headerhttps://developer.mozilla.org/zh-CN/docs/Glossary/Response_header

  


我的理解如下:

客戶端需要先解析響應頭,通過響應頭的一些規範來解析返回的響應體的,既然你響應頭都報錯了,那後續的響應體解析自然也不會往下走了,直接報錯退出。


2.aiohttp

 

對於aiohttp來說,像上面那麼加正則表達式是無效的:

 

 

 

 

改完發現並沒有用

 

 

這有說,改成1,也試了,不行

 

 

 

 

更多的就不演示了

反正就是一頓查找,發現aiohttp上並沒有合理方案解決

 

針對的解決方案


1.用requests


上面有了,這裏就不展示了

但是requests我們知道,它是不支持http2.0協議的

2.用正則表達式替換


上面也有了,這裏不貼了但是不能完美覆蓋後續的響應頭特殊參數

3.用urllib3處理


這個庫用的倒不多,可以直接請求:

 

 

但是他也是個可以直接拿來請求的庫,相比requets,那很多功能還是沒有。其實requests本質就是藉助了urllib3庫的
所以,這個庫,不出意外應該也是不支持http2.0的

用非常規的請求庫嘗試 


既然目前的方法,除了requests/urllib3,其他都不行,那我在這個基礎之上,強制http2,是不是就可以幹掉requets庫了,python的常規請求庫直接無可用的,直接乾死這些爬蟲?

想的挺美的

1.用tls-client試試

雖然他是用來對抗tls的,試試呢:

 

 

可行。爲啥,因爲他原理就是完全模擬瀏覽器,大概看了它的源碼,用curl_impersonate庫,打包成了一個dll(具體怎麼打包的不可知,這部分沒開源,插一句,據羣友反饋,也是因爲這個dll,會導致內存泄漏),然後可以直接用,上面說過了,瀏覽器可以訪問,那這個庫肯定也可以訪問了

那麼測試那個某爲的網址:

 

 

不行,被識別了,牛吧,還是得是某爲啊

2.用curl_cffi試試

 

 


 

測試那個某爲的網址:

 

 

這麼對比,看來curl_cffi纔是獲勝者啊,只能說,牛逼啊!!!


實現一個anti aiohttp、httpx的服務


簡單的用fastapi 實現一個簡單的服務,設置了下響應頭:

 

 

1.用httpx請求

httpx代碼不變,只是把url換了,果然報錯

 

 

2.用aiohttp請求

再來看aiohttp,果然報錯,哈哈哈哈

 

 

2.用http.client請求

 

 

 

 

3.用urllib3、requests測試


 

 

 

 

 

 

還得是這哥倆啊,直接能跑

4.用node的request請求看看

 

 

 

刺激,也給防住了。

5.用golang 看看


刺激啊,也沒有正常返回

 

 

 

 

6.用postman看看

postman也直接無返回

 

 


7.用安卓看看

package com.geek.spiderclient;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;

public class MainActivity extends AppCompatActivity {
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.hello);

        new Thread(new Runnable() {
            String result = "";

            @Override
            public void run() {
                try {
                    String url_string = "http://192.168.30.251:8000/";// 由於高版本有https的限制,需要修改targetSdk爲27及一下。
                    URL url = new URL(url_string);
                    //得到connection對象。
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    //設置請求方式
                    connection.setRequestMethod("GET");
                    //連接
                    connection.connect();
                    //得到響應碼
                    int responseCode = connection.getResponseCode();
                    Log.d("headers", "" + connection.getHeaderFields());
                    if (responseCode == HttpURLConnection.HTTP_OK) {
                        //得到響應流
                        InputStream inputStream = connection.getInputStream();
                        //將響應流轉換成字符串
                        result = is2String(inputStream);//將流轉換爲字符串。
                        Log.d("result", "result=============" + result);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Log.e("result", "runOnUiThread");
                        textView.setText(result);
                    }
                });
            }
        }).start();
    }

    public String is2String(InputStream is) {
        //連接後,創建一個輸入流來讀取response
        BufferedReader bufferedReader = null;
        try {
            bufferedReader = new BufferedReader(new InputStreamReader(is, "utf-8"));
            String line = "";
            StringBuilder stringBuilder = new StringBuilder();
            String response = "";
            //每次讀取一行,若非空則添加至 stringBuilder
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line);
            }
            //讀取所有的數據後,賦值給 response
            response = stringBuilder.toString().trim();
            return response;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

  



打包好之後:

 

 

安卓是可以的


再來看看服務端這邊,其實是有正常請求的,但是客戶端那邊無法正常解而已。

 

 


那麼有朋友估計有想法,那既然報錯,我捕獲異常強制打印結果不行嗎?
這裏還是用回到python httpx庫試試

 

 

不行的哈哈,原因前面說過了

配置將服務強制http2.0協議

 

1.配置服務端

說幹就幹,準備嘗試把代碼移植刀服務器上,直接啓動這邊的服務,然後nginx搭建好,配置好http2,我買的服務器它默認沒給開80和443等常規的庫,問了客服說要申請備案了纔行,臥槽,很迷,無所謂,我把這個服務搭建到6363端口上:

 

 


nginx如下配置:
server {
      # listen 6363;
      listen [::]:6363 ssl  http2; # managed by Certbot
      listen 6363  ssl http2; # managed by Certbot
      ssl_certificate /root/cert.pem; # managed by Certbot
      ssl_certificate_key /root/key.pem; # managed by Certbot
      if ($server_protocol !~* "HTTP/2.0") {
        return 444;
      }
      root /data/www/fast-tortoise;
      server_name 0.0.0.0;
      location / {
            proxy_set_header x-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_pass http://127.0.0.1:8000/; # gunicorn綁定的fastapi的端口號
        }
      # 配置static的靜態文件:
      location ~ ^\/static\/.*$ {
            root /data/www/fast-tortoise/static;
         }
}

  

 

證書文件用以下命令生成即可:前提得安裝openssl庫
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

  



2.請求測試


配置好了之後,首先requests肯定是不行了,因爲不支持http2.0。這裏就不演示了。

用httpx測試下,唉?直接就給放行了

 

 


臥槽?反爬蟲路途創業未半中道崩卒....
.....

看來http2.0把這個問題修復了呀......

......
那就只能在http1.0,http1.1下防住一部分的爬蟲了

那有朋友估計要問了,你這個操作,好像只是有點奇怪,但實際並沒有文章標題說的那麼邪乎啊,說直接點,基本沒啥用啊

解惑


如果堅持看到這裏的朋友,還沒有忘記前面說的有關requests問的話。ok,這裏開始說說。
其實我一開始發現這個所謂的bug的時候,感覺也不是太有用,requests都能跑,沒把requests防住,那肯定沒啥意思啊。
那麼這個所謂的bug,真的沒用嗎?
只是這一個的話肯定防不住的,可以用來跟其他反爬手段組合啊,比如tls指紋,或者其他的風控檢測手段等等的。

其實,我在發這篇文章之前,也開發了兩個爬蟲練習題

 

 


第一題就是這個bug
第二題是另外一個檢測手段,可以檢測到requests和httpx,tls-client,curl_cffi,aiohttp,還有常規的請求軟件,而且沒有用到tls指紋
本來說是先讓羣友拿來玩,然後過幾天發文章公開檢測手段以及如何bypass的。
我沒想到,我還沒發多久,就有 intellectual disability 搞我服務器.....
頓時就覺得沒意思了,服務關了,第二題的檢測手段和bypass的方法我也不打算公開了。由於這篇文章之前就答應過羣友 @十一,且早就寫好了,那就繼續發吧。
想知道第一種檢測手段,還有哪些特殊字符不能被正常解析嗎?有想知道第二種檢測手段的朋友?
可以時不時的關注我的動向,也許我會在某一天用新的形式發出來

結語


爬蟲的核心,還是指紋等靜態特徵完全的模擬瀏覽器環境,行爲等動態特徵完全的模擬人爲操作


工作避坑&內推(僅成都)、技術交流、商務合作、技術交流羣

 

掃碼或者搜ID:geekbyte

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章