Socket爬蟲:Python版

紅後 發表於 2023-01-09
Python 爬蟲

簡述:較為底層的爬蟲實現,用於瞭解爬蟲底層實現的具體流程,現在各種好用的爬蟲庫(如requests,httpx...等)都是基於此進行封裝的。
PS:本文只作為實現請求的程式碼記錄,基礎部分不做過多闡述。

一、什麼是socket

簡稱:套接字
大白話陳述一下:網路由類似無數終端(計算機裝置)組成,每一臺計算機裝置上面都有很多的程式,而每個程式都有其埠號,ip+埠號形成唯一標識,應用程式可以透過它傳送或接收資料,可對其進行像對檔案一樣的開啟、讀寫和關閉等操作。PS:個人總結的,可能不是很準確看看就行

二、socket實現http請求

http請求方式:get和post
要使用socket首先要實現其例項化:scoket(family,type[,protocal])
其中第一個引數family表示地址族,常用的協議族有:AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等,預設值為AF_INET,通常使用這個即可。
第二個參數列示Socket型別,這裡使用的值有三個:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW。
SOCK_STREAM:TCP型別,保證資料順序以及可靠性;
SOCK_DGRAM:UDP型別,不保證資料接收的順序,非可靠連線;
SOCK_RAW:原始型別,允許對底層協議如IP、ICMP進行直接訪問,基本不會用到。
預設值是第一個。
第三個引數是指定協議,這個是可選的,通常賦值為0,由系統選擇。

GET /article-types/6/ HTTP/1.1
Host: www.zhangdongshengtech.com
Connection: close

第一行:請求方式+空格+請求的資源地址+空格+http協議版本
第二行:明確host
第三行:定義了Connection的值是close,如果不定義,預設是keep-alive。
PS:這裡Connection設定為close是為了方便獲取完響應後直接斷開連線,如果是keep-alive再獲取完響應資料後不會斷開連線。
報文示例如下:目標url=http://www.baidu.com
GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n

報文解析

一個HTTP請求報文由請求行(request line)、請求頭部(header)、空行和請求資料4個部分組成
請求行:由請求方法欄位、URL欄位和HTTP協議版本欄位3個欄位組成,它們用空格分隔。例如,GET /index.html HTTP/1.1
請求頭部:請求頭部由關鍵字/值對組成,每行一對,關鍵字和值用英文冒號“:”分隔。例如,User-Agent:產生請求的瀏覽器型別
空行:最後一個請求頭之後是一個空行,傳送回車符和換行符,通知伺服器以下不再有請求頭。
請求資料:請求資料不在GET方法中使用,而是在POST方法中使用。POST方法適用於需要客戶填寫表單的場合。與請求資料相關的最常使用的請求頭是Content-Type和Content-Length。

GET:示例

# -*- coding: utf-8 -*-
# @Time    : 2023/1/8 15:12
# @Author  : 紅後
# @Email   : [email protected]
# @blog    : https://www.cnblogs.com/Red-Sun
# @File    : socket_http_get.py
# @Software: PyCharm

import socket

url = 'www.baidu.com'
port = 80

# 建立TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 連線指定服務端
sock.connect((url, port))
# 建立請求訊息頭髮送的請求報文(詳情見瀏覽器裡面的請求頭原文)
request_url = 'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n'
# 傳送請求(send的引數為位元組格式所以進行了編碼)
sock.send(request_url.encode())
response = b''
# 接收返回的資料
rec = sock.recv(1024)
# 由於是TCP協議所以是分片傳遞,用while迴圈獲取資料
while rec:
    response += rec
    rec = sock.recv(1024)
# 返回響應的文字是位元組格式需要解碼
print(response.decode())

Socket爬蟲:Python版
由上個演示可知響應中有Content-Length存在因此就可以知到響應資料的位元組長度了。
所以上述示例程式碼可以修改為透過判斷接受報文總位元組長度達標後斷開連線。

自動判斷響應長度示例程式碼

# -*- coding: utf-8 -*-
# @Time    : 2023/1/8 15:56
# @Author  : 紅後
# @Email   : [email protected]
# @blog    : https://www.cnblogs.com/Red-Sun
# @File    : socket_http_get_upgrade.py
# @Software: PyCharm
import socket

url = 'www.baidu.com'
port = 80

# 建立TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 連線指定服務端
sock.connect((url, port))
# 建立請求訊息頭髮送的請求報文(詳情見瀏覽器裡面的請求頭原文)
request_url = 'GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n'
# 傳送請求(send的引數為位元組格式所以進行了編碼)
sock.send(request_url.encode())
# 接收返回的資料
rec = sock.recv(1024)
# 獲取訊息頭與訊息體分割的索引位置
index = rec.find(b'\r\n\r\n')
# 換行前為響應頭報文
response_head = rec[:index]
# 索引位後移4為刪除換行獲取響應文字主體
response_body = rec[index+4:]
# 獲取Content-Length,start_index:索引開始位,end_index:索引結束位
start_index = response_head.find(b'Content-Length')
end_index = response_head.find(b'\r\n', start_index)
content_length = int(response_head[start_index:end_index].split(b' ')[1])
# 由於是TCP協議所以是分片傳遞,用while迴圈獲取資料
while len(response_body) < content_length:
    rec = sock.recv(1024)
    response_body += rec
# 關閉套接字
sock.close()
# 返回響應的文字是位元組格式需要解碼
print(response_body.decode())

POST:示例

# -*- coding: utf-8 -*-
# @Time    : 2023/1/9 14:33
# @Author  : 紅後
# @Email   : [email protected]
# @blog    : https://www.cnblogs.com/Red-Sun
# @File    : socket_http_post.py
# @Software: PyCharm
import socket

from urllib.parse import urlparse

url = 'http://www.baidu.com/'
# 作為一個懶癌患者加上了一個主域名解析,就不需要自己提取了,直接把目標網站扔裡面結束。
host_url = urlparse(url).hostname
# http請求大部分都是80埠
port = 80
# 建立TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 連線指定服務端
sock.connect((host_url, port))
request_header = b'''POST / HTTP/1.1
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: close
Content-Length: 傳遞引數的長度
Content-Type: application/x-www-form-urlencoded
Host: www.baidu.cn
Origin: http://www.baidu.cn
Pragma: no-cache
Referer: http://www.baidu.cn/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
'''
data = b'傳遞的引數(原型格式)'
# 傳送請求(send的引數為位元組格式所以進行了編碼)
sock.send(request_header + b'\r\n' +data)
response = b''
# 接收返回的資料
rec = sock.recv(1024)
# 由於是TCP協議所以是分片傳遞,用while迴圈獲取資料
while rec:
    response += rec
    rec = sock.recv(1024)
# 返回響應的文字是位元組格式需要解碼
print(response.decode())


PS:避免例項網站被搞,這裡就用百度代替,此程式碼只是模板連結無法直接執行。
url:目標網站地址
request_header:發起的請求頭(懶人做法直接去瀏覽器抓包後複製請求頭的值就行,直接貼上就能用),這個模板要改幾個值,Connection需要是close,瀏覽器預設keep-alive,如果Accept-Encoding屬性中有gzip最好移除,不然列印容易報錯。

三、socket實現https請求

http跟https的一些區別:
埠:http預設80,https預設443
傳遞:HTTP協議是位於第四層協議TCP之上完成的應用層協議, 端到端都是明文傳送,別人一下就能獲取到資料,不太安全。HTTPS是基於SSL加密傳輸的,這樣別人截獲你的資料包破解的機率要小一點,比HTTP安全一些。
PS:個人看法,http跟https相比就差了一個ssl加密所以用ssl.wrap_socket (sock[, **opts])包裝現有套接字實現https請求。

https的GET例項

# -*- coding: utf-8 -*-
# @Time    : 2023/1/9 15:34
# @Author  : 紅後
# @Email   : [email protected]
# @blog    : https://www.cnblogs.com/Red-Sun
# @File    : socket_https_get.py
# @Software: PyCharm
import socket
import ssl

url = 'www.baidu.com'
port = 443
# 勇1ssl.wrap_socket封裝socket
sock = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM))
# 連線指定服務端
sock.connect((url, port))
# 建立請求訊息頭髮送的請求報文(詳情見瀏覽器裡面的請求頭原文)
request_url = b'GET https://www.baidu.com/ HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n'
# 傳送請求(send的引數為位元組格式所以進行了編碼)
sock.send(request_url)
response = b''
# 接收返回的資料
rec = sock.recv(1024)
# 由於是TCP協議所以是分片傳遞,用while迴圈獲取資料
while rec:
    response += rec
    rec = sock.recv(1024)
# 返回響應的文字是位元組格式需要解碼
print(response.decode())

https的POST實現:就是對上面http的post實現就行ssl封裝這裡就不在做過多闡述了。

懶人直接用指令碼

個人封裝的指令碼,主要作為學習熟練編寫還有很多能封裝改進的地方,暫時簡化如此了,要用的話看一下注意事項
注意事項:

  1. url要帶http或https
  2. post傳參的data資料前面記得加個r避免\斜槓的轉義效果導致報錯
  3. 複製的request_header貼上之後要看看有沒有多餘的空行有時候貼上會自動回車
  4. 訪問失敗可以嘗試刪減request_header中的引數,有時候無用引數會導致這類問題

PS:暫時就想到這麼多等以後遇見問題在補充吧,畢竟也只是學習筆記,慢慢學吧。

# -*- coding: utf-8 -*-
# @Time    : 2023/1/9 15:37
# @Author  : 紅後
# @Email   : [email protected]
# @blog    : https://www.cnblogs.com/Red-Sun
# @File    : SocketSpider.py
# @Software: PyCharm
import socket
import ssl
from urllib.parse import urlparse


class SocketSpider:
    '''
    socket爬蟲指令碼(PS:模仿requests寫的僅作為學習所用)
    '''

    def format_socket(self, url):
        '''
        格式化建立socket
        '''
        host_url = urlparse(url).hostname
        port = (80, 443)['https' == url[:5]]
        if port == 80:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        else:
            sock = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM))
        sock.connect((host_url, port))
        return sock

    def format_header(self, request_header):
        '''
        對請求頭中的資料進行處理
        '''
        message = b''
        for header_data in request_header.split(b'\n'):
            if b'gzip' in header_data:
                continue
            message += header_data + b'\r\n'
        # message += b'\r\n'
        return message.replace(b'Connection: keep-alive', b'Connection: close')

    def get(self, url: str, request_header: str):
        '''
        get請求實現
        url: 必須要帶http或https
        request_header: 直接從瀏覽器抓包複製出請求頭就行
        '''
        sock = self.format_socket(url=url)
        message = self.format_header(request_header.encode())
        sock.send(message)
        response = b''
        # 接收返回的資料
        rec = sock.recv(1024)
        # 由於是TCP協議所以是分片傳遞,用while迴圈獲取資料
        while rec:
            response += rec
            rec = sock.recv(1024)
        # 返回響應的文字是位元組格式需要解碼
        return response.decode()

    def post(self, url:str, request_header: str, data: str):
        '''
        post請求實現
        url: 必須要帶http或https
        request_header: 直接從瀏覽器抓包複製出請求頭就行
        data: 同樣直接從瀏覽器複製出來就行(前面最好加上r避免\斜槓的轉義效果)
        '''
        sock = self.format_socket(url=url)
        message = self.format_header(request_header.encode()) + data.encode()
        sock.send(message)
        response = b''
        # 接收返回的資料
        rec = sock.recv(1024)
        # 由於是TCP協議所以是分片傳遞,用while迴圈獲取資料
        while rec:
            response += rec
            rec = sock.recv(1024)
        # 返回響應的文字是位元組格式需要解碼
        return response.decode()


if __name__ == '__main__':
    url = 'https://www.baidu.com/'
    request_header = '''GET / HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Cookie: PSTM=1664529301; BIDUPSID=5D9256DF2D0A67E0B97C22A9AC33CB09; BAIDUID=B54EAE77A79A6008061229C7A4AB5387:FG=1; MCITY=-224%3A; BD_UPN=12314753; BAIDUID_BFESS=B54EAE77A79A6008061229C7A4AB5387:FG=1; __bid_n=1845a26a63492b76484207; FEID=v10-1cec01417c6a9c7b08b333c03eb5425b40157d14; __xaf_fpstarttimer__=1672974585279; __xaf_thstime__=1672974585296; FPTOKEN=1oofVJhUqwLSMyqmyHnXJSxhD+K991pfK+BFWKI9hSqaoAEyXVZYBO320OdxlRccUo1KnxIfZK0C7Fpcc5ED9MHoxVt7PAEblzQAudnC7Di7Ic5it9yUwuAcV2iTjxygfDUcIQI5H0vbTllVkUpaX+xEyctdNFuFm0Py4wQ6EFX7bAjD0I5muVjmnRdsC/6qHg8fINmN3dEID5+n6zpiNVs+bLYePtIeO9SI/HvE7mnQxcd/HbOwf0to5q/5tcNyzO8riRarry1ryI7wocSoAnYc6HTyEaVY2xSdDYeXPNLCbMOwZvfAejftHAfvTCNRWO/K3GV0PYU/+yhEXkejECxAMOnFsbfPyhHd13AyEY0IwyNNYthFeO5qSazdfMZIVFEqenGxifjUCLsejVbcHg==|4j1jFPC8s2KJ3Bd5vfyJK+BhAEWpSgq+RbmgPQ512ro=|10|b66cd1b11d1528a08711913e6ce61baf; __xaf_fptokentimer__=1672974585330; BA_HECTOR=24ag80a4ah2la5ak05242lqk1hrnc8s1k; ZFY=gDQKa:Bq4ufkSfg5tno3jSIyvxsB0KQkWtgWw1CAvnO4:C; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; COOKIE_SESSION=4_0_9_9_20_16_1_1_9_8_1_2_1125999_0_3_0_1673244971_0_1673244968%7C9%231882534_14_1670467865%7C4; BDRCVFR[t1epCe_UAtt]=OjjlczwSj8nXy4Grjf8mvqV; BD_HOME=1; delPer=0; BD_CK_SAM=1; PSINO=5; ab_sr=1.0.1_MjQ0YjU1ODM4ODAzOTE5ODQxM2U5ZTRhYjFlMDIwMzkwZGIzYjc0MjVmZmExMTZmMTA1ZmEzMmUzNGMyMGQ3ZThmNzliNmJhZGFjMDM3NDE4YzQ4NGMxMWI0YzIxY2VkMWFhNTcyMTQ1NWVlNDMyY2VkZmY5MTMwZjJmNDVhOTkwNzY1YTQwOTI4M2I2Yzg1ZTYwODBmYzhiZTgzMzA1MA==; BDRCVFR[sOxo1TgcNNt]=OjjlczwSj8nXy4Grjf8mvqV; BDRCVFR[bIKc3BPCk_C]=OjjlczwSj8nXy4Grjf8mvqV; H_PS_645EC=4e43o0N5BrE9ePh0PWMUS4TfxCmDRlE97BtwBPR4eHZmdOKlWsRxZaGgaEJYE2x9oTHCsGqhpL13; H_PS_PSSID=36555_37647_37625_36920_38035_37989_37931_26350_38009_37881
Host: www.baidu.com
Pragma: no-cache
Referer: https://cn.bing.com/
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
'''
    a = SocketSpider()
    a.get(url=url, request_header=request_header)