banner
肥皂的小屋

肥皂的小屋

github
steam
bilibili
douban
tg_channel

搭建本地IP代理池(完結)

事情起因#

代辦事項之 - 搭建電腦本地 ip 代理池

最終希望實現的效果:設計一個GUI客戶端,能方便地爬取代理,隨機使用代理,換代理,清空代理等等,點一下換一下ip

素材獲取:在Python 目錄中查看《多線程爬取西刺高匿代理並驗證可用性》這篇文章

改寫爬取西刺代理的 py 代碼#

發現三個月前寫的代碼自己都難以接受,遂總結如下 (不會總結的程序員不是好的安全小白):

問題改進方法
用了不合適的進程庫 Pool ().apply_async 導致多進程時不能把存儲的列表留住所以採用了文件讀寫的方式使得代碼非常雜亂重新理解進程和線程的適用性並採用了多線程隊列
使用大量 I/O 操作導致不同環境的適配問題採用 MongoDB 資料庫存儲
代碼可重利用性不高在類中使爬蟲操作和資料庫插入操作分開,且資料庫插入函數可重複利用,使得代碼簡潔許多
沒有處理代碼異常在容易出錯的地方增加了代碼異常處理 try except

參考教程:

全代碼:

#!/usr/bin/python
# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 多線程爬取西刺高匿代理並存儲到mongodb資料庫
@time: 2019-04-20
'''
import pymongo  # mongodb資料庫操作
from threading import Thread  # 多線程
from fake_useragent import UserAgent  # 假的useragent
import requests  # 請求站點
from lxml import etree  # 解析站點
import re  # re解析庫
import telnetlib  # telnet連接測試代理有效性
import timeit  # 計算用時


class multi_threaded_crawl_proxies(object):
    def __init__(self):
        try:
            # 連接mongodb,得到連接對象
            client = pymongo.MongoClient('mongodb://localhost:27017/')
            self.db = client.proxies  # 指定proxies資料庫,這個類都使用這個資料庫
            print("連接資料庫成功!")
        except Exception as e:
            print("資料庫連接失敗:{}".format(e))
        self.ua = UserAgent()  # 用於生成User-Agent
        self.crawl_progress()

    def crawl_progress(self):
        # 爬取操作
        try:
            # 可添加多個爬取的函數的啟動函數在此處
            self.xici_nn_proxy_start()
        except Exception as e:
            print("程序運行錯誤:{}\n程序退出!".format(e))
            exit(0)

    def xici_nn_proxy_start(self):
        xici_t_cw_l = []  # 爬取線程列表
        self.xici_proxies_l = []  # 用於存放驗證過的代理字典,字典包含ip、端口、ip類型、地址
        for i in range(1, 21):  # 爬取20頁代理
            t = Thread(target=self.xici_nn_proxy, args=(i,))
            xici_t_cw_l.append(t)  # 添加線程到線程列表
            t.start()
        for t in xici_t_cw_l:  # 等待所有線程完成退出主線程
            t.join()
        self.db_insert(self.xici_proxies_l, "xici")  # 插入資料庫

    def xici_nn_proxy(self, page):
        # 西刺代理爬取函數
        url = "https://www.xicidaili.com/nn/{}".format(page)
        # 這裡不加user-agent會返回狀態碼503
        req = requests.get(url, headers={"User-Agent": self.ua.random})
        if req.status_code != 200:
            print("ip被封了!此頁爬取失敗!")
            exit(0)
        else:
            print("正在爬取第{}頁的內容...".format(page))
            content = req.content.decode("utf-8")
            tree = etree.HTML(content)
            # 用xpath獲得總的ip_list
            tr_nodes = tree.xpath('.//table[@id="ip_list"]/tr')[1:]
            for tr_node in tr_nodes:
                td_nodes = tr_node.xpath('./td')  # 用xpath獲得單個ip的標籤
                speed = int(re.split(r":|%", td_nodes[6].xpath(
                    './div/div/@style')[0])[1])  # 獲得速度的值
                conn_time = int(re.split(r":|%", td_nodes[7].xpath(
                    './div/div/@style')[0])[1])  # 獲得連接時間的值
                if(speed <= 85 | conn_time <= 85):  # 如果速度和連接時間都不理想,就跳過這個代理
                    continue
                ip = td_nodes[1].text
                port = td_nodes[2].text
                ip_type = td_nodes[5].text.lower()
                td_address = td_nodes[3].xpath("a/text()")
                address = 'None'
                if td_address:  # 有的地址為空,默認置為空,獲取到則置為對應地址
                    address = td_address[0]
                proxy = "{}:{}".format(ip, port)
                try:
                    # 用telnet連接一下,能連通說明代理可用
                    telnetlib.Telnet(ip, port, timeout=1)
                except:
                    pass
                else:
                    self.xici_proxies_l.append(
                        {"ip": ip, "port": port, "ip_type": ip_type, "address": address})

    def db_insert(self, proxies_list, collection_name):
        if proxies_list:
            # 傳入的列表為空則退出
            collection = self.db['{}'.format(collection_name)]  # 選擇集合,沒有會自動創建
            collection.insert_many(proxies_list)  # 插入元素為字典的列表
        else:
            print("代理列表為空!\n程序退出!")
            exit(0)


if __name__ == "__main__":
    start_time = timeit.default_timer()
    multi_threaded_crawl_proxies()
    end_time = timeit.default_timer()
    print("程序運行結束,總用時:{}".format(end_time-start_time))

效果如下:

image

image


[19-04-26 更新]

修改註冊表參數#

那麼代理有了,我們需要修改 windows 本地的代理,搜了一下,了解到:

  • win能通過IE瀏覽器的代理設置來代理訪問外網
  • 網上大部分文章介紹的 IE 代理位置是在
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings
  • 但是據參考文章 1 的作者說,真正修改的地方應該是
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections

也就是上面那個位置中的Connections項,我們在 IE 代理中設置一個127.0.0.1:80的代理:

image

打開代理是這樣的:

image

註冊表導出是這樣的:

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections]
"DefaultConnectionSettings"=hex:46,00,00,00,0a,00,00,00,01,00,00,00,00,00,00,\
  00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\
  00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00
"SavedLegacySettings"=hex:46,00,00,00,51,00,00,00,0b,00,00,00,0c,00,00,00,31,\
  32,37,2e,30,2e,30,2e,31,3a,38,30,07,00,00,00,3c,6c,6f,63,61,6c,3e,00,00,00,\
  00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\
  00,00,00,00,00,00,00,00
"Netkeeper"=hex:46,00,00,00,31,2c,30,30,0b,00,00,00,0c,00,00,00,31,32,37,2e,30,\
  2e,30,2e,31,3a,38,30,07,00,00,00,3c,6c,6f,63,61,6c,3e,00,00,00,00,00,00,00,\
  00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\
  00,00,00,00
"greenvpn"=hex:46,00,00,00,02,00,00,00,01,00,00,00,00,00,00,00,00,00,00,00,00,\
  00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\
  00,00,00,00,00,00,00,00,00,00

看到除了 ip 地址和端口,還有一些奇怪的十六進制字符串,根據參考文章 1 的介紹,大致參數如下:(圖片來自參考文章 1)

image

再次感謝SolomonXie大佬的參考文章 1:

總結下也就這麼幾位:

46 00 00 00 開關 00 00 00 IP長度 00 00 00 IP地址 00 00 00 是否跳過本地代理 21 00 00 00 PAC地址

不知道大佬的是IE幾,我的是IE11,與這個略有不同,大概如下:

46 00 00 00 自增位 00 00 00 開關 00 00 00 IP長度 00 00 00 IP地址 00 00 00 是否跳過本地代理
  • 每個信息的分隔符是三個 00,即 00 00 00。

  • 開關:主要代表 IE 設置中複選框的選中情況。使用代理為 03,不使用為 01,對本地使不使用代理與這個開關無關,只取決於最後的是否跳過本地代理部分。你也可以設置好你的設置然後打開註冊表查看

  • 自增位:不知道是從哪個值開始自增,就算設置不改變的情況,重新點擊確定這個自增值都會開始自增,索性直接設置為 00 即可,後來的效果也證實了把這個自增位設置為 00 是毫無影響的

  • IP 長度:十六進制,包括。和:,比如我設置127.0.0.1:80是 12 位的,註冊表中的值為0C

  • IP 地址:直接把 IP 按照每個字符轉十六進制就好了。

  • 是否跳過本地代理:如果沒勾選,則全為 0,如果勾選了選項,值為:

07 00 00 00 3c 6c 6f 63 61 6c 3e

此處注意,如果你看到這裡並且已經開始編碼,那麼注意註冊表中導入的十六進制值是沒有空格和逗號的

  • 這段除了前面的 07 剩餘的意思為:,這是固定值,無需修改

  • 最後全補為 0 即可,設置了 ip 地址的項的總長度為 224,沒設置的總長度為 167

所以根據以上內容,按照我的情況:設置代理對本地不代理的註冊表值應該如下:

46 00 00 00 00 00 00 00 03 00 00 00 IP長度 00 00 00 IP地址 00 00 00 07 00 00 00 3c 6c 6f 63 61 6c 3e

只需 ip 地址轉為十六進制字符串,算一下長度傳入即可

註冊表修改部分的代碼:

注意註冊表中導入的十六進制值是沒有空格和逗號的

#!/usr/bin/python
# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 利用修改註冊表設置系統代理
@time: 19-04-26
'''
import IPy  # 判斷ip是否合法
import re
import subprocess  # 執行cmd命令


def setproxy(hex_value):
    # 傳入整理好的十六進制代理設置
    try:
        vpn_name = "Netkeeper"  # 你的專用網絡的名字
        subprocess.run('REG ADD "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" /v "{}" /t REG_BINARY /d "{}" /f'.format(
            vpn_name, hex_value), shell=True, stdout=subprocess.PIPE)
        print("導入註冊表成功!")
    except Exception as e:
        print("修改註冊表報錯:{}\n程序退出!".format(e))
        exit(0)


def registry_value_construction(ip, port, proxy_switch, local_proxy_switch):
    # 註冊表值構造
    switch_options = {"1": "03", "0": "01"}  # 代理開關選項
    local_switch_options = {
        "1": "070000003c6c6f63616c3e", "0": ""}  # 本地代理開關選項
    # 端口合法性檢查的正則表達式
    port_regular_expression = r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'
    if not re.search(port_regular_expression, port):  # 如果端口不在0-65535之間,則報錯
        print("端口不符合類型\n程序退出!")
        exit(0)
    if not ip:  # ip為空的情況,就忽略端口的設置,直接全部置為0
        former = "4600000000000000{}00000000000000{}".format(switch_options.get(
            proxy_switch), local_switch_options.get(local_proxy_switch))  # 填充代理開關選項和本地代理開關選項進入
        # 補全一部分00,不然導入不進去,下同
        value = former + "00"*int((112-int(len(former)))/2)
        print("ip為空的註冊表參數值構建完成!")
    else:
        try:
            IPy.IP(ip)  # 註冊表中能隨便填ip和端口,但是我們不允許,如果ip不正確或為空則會直接報錯退出
            # hex()方法轉換出來的數字是0x開頭的且ip長度站兩位
            ip_len = hex(len(ip)+len(port)+1).split("x")[-1].zfill(2)
            ip = bytes(ip, 'ascii').hex()  # 獲得ip的十六進制字符串
            port = bytes(port, 'ascii').hex()
            former = "4600000000000000{}000000{}000000{}3A{}{}".format(switch_options.get(
                proxy_switch), ip_len, ip, port, local_switch_options.get(local_proxy_switch)).lower()
            value = former+"00"*int((150-int(len(former)))/2)
            print("註冊表參數值構建完成!")
        except Exception as e:
            print("程序報錯:{}\n程序退出!".format(e))
            exit(0)
    return value


if __name__ == "__main__":
    ip = "127.0.0.1"  # 設置代理ip
    port = "80"  # 設置代理端口
    proxy_switch = "1"  # 設置代理開關,"1"為開啟代理,"0"為不開啟代理
    local_proxy_switch = "1"  # 設置本地代理開關,"1"為開啟,對本地不適用代理,"0"為關閉,對本地使用代理
    hex_value = registry_value_construction(
        ip, port, proxy_switch, local_proxy_switch)  # 這個函數返回代理設置參數的十六進制字符串
    setproxy(hex_value)  # 將十六進制字符串傳入函數去設置

[19-05-03:修改了其中的 ip_len 語句]

部分功能解釋:

  • 由於 IPy 調用的 ip 合法性檢測功能在 ip 為空時也會報錯,所以先判斷 ip 是否為空;非空的話則忽略端口值,除了開關以外都置為 0

  • 代碼中使用的檢測端口合法性的正則表達式檢測的是0-65535,備忘錄一下1024-65535的正則表達式如下:

^(1(02[4-9]|0[3-9][0-9]|[1-9][0-9]{2})|[2-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$

效果演示如下 (電腦點擊看大圖):

image


[19-05-03 完結]

寫了幾天深度學習的項目,思緒有點亂,今天摸個魚把前面寫好的 win 本地代理客戶端的編寫思路整理一下

UI 設計#

前情提要:前面我已經完成了多線程爬取西刺代理的代碼改寫以及通過註冊表改系統代理這兩部分

一直使用命令行很不爽,這個功能用 GUI 實現起來會很適合,經過多方比較選擇了pyqt5(才不是因為有可視化設計界面比較方便)

安裝#

我的環境:Win10+Anaconda3+pip/conda清華源

好像spyder這個庫是需要pyqt5這個庫做支持的,直接安裝pyqt5會提示你更新到的版本太高,spyder不高興了,因此:

pip install spyder
pip install pyqt5

基本頁面介紹#

安裝完了pyqt5,如果你沒有在安裝Anaconda3時勾選添加到環境變量,那麼根據你自己python包安裝的位置自行添加環境變量

如果你Anaconda默認添加了環境變量,直接在命令行輸入designer即可打開,界面及基本介紹如下 (電腦點擊圖片看大圖):

image

image

UI 全靠自己設計,建議在MainWindow上把你的幾個功能區用Containers部件中的Frame部件分隔開,這樣有利於後面你處理每個功能區

速成可參考:PyQT5 速成教程 - 2 Qt Designer 介紹與入門

這個翻滾吧挨踢男大佬的文章還是很通俗易懂的:Python 菜鳥教程全目錄

signal&slot#

PyQt5 有一個獨特的 signal&slot (信號槽) 機制來處理事件。信號槽用於對象間的通信。signal 在某一特定事件發生時被觸發,slot 可以是任何可調用對象。當 signal 觸發時會調用與之相連的 slot。

也就是說你需要設計誰是發送方,誰是接收方,這裡舉個例子:

** 在designer中按F3F4切換 UI 設計界面和信號和槽編輯界面 **

拖動信號發送方到信號接收方上就會彈出信號和槽編輯界面,我們這裡拖動的是一個combobox到一個textbrowser上:

image

** soapffz的建議,初期每個不同部件的常用方法都點一遍,導出Python代碼查看語句構建,後期只設計 UI,其他的所有信號和槽都在邏輯部分的代碼中自行編寫,實現 UI 與邏輯分離 **

參考文章:還是翻滾吧挨踢男大佬的文章:PyQt5 學習筆記 05----Qt Designer 信號槽

代碼導出#

那麼基本的 UI 設計完了,我們給代理爬取的下拉框添加一個信號槽作為例子來說明導出的代碼的構成,如圖所示:

image

在界面右下角也可看到:

image

我的環境安裝完pyqt5之後在cmd即可執行pyuic5 -o xx.py xx.ui指令

如果你測試pyuic5不能執行,自行百度,演示如下:

image

導出的代碼構成是這樣的:

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'local_prixies.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(640, 480)
        MainWindow.setMinimumSize(QtCore.QSize(640, 480))
        MainWindow.setMaximumSize(QtCore.QSize(640, 480))
        各種部件的大小,位置,長寬高等屬性
        self.retranslateUi(MainWindow)
        添加的信號和槽在這裡
            self.comboBox_proxychoose_crawl.currentTextChanged['QString'].connect(self.textBrowser_disp.append)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "win本地代理設置 - by soapffz"))
        self.label_funcchoose.setText(_translate("MainWindow", "功能選擇"))
        各種部件內的內容

導出的代碼只有一個類,肯定要實例化這個對象才能啟動,此處需要引入重要內容:UI 與代碼邏輯分離

UI 與代碼邏輯分離#

1. 不分離狀態

在導出的 py 文件中添加如下代碼 (確保你的類名字也是 UI_MainWindow):

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    MyWindow = Ui_MainWindow()
    MyWindow.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

即可在導出的py文件中啟動該界面 (大 GIF):

image

2. 分離狀態

新建一個 mainwindow.py文件,主要實現界面的實例化以及界面所有邏輯實現以實現 UI 與邏輯的分離,在其中添加如下代碼 (假定你導出的py文件的名字叫做ui.py):

from PyQt5 import QtCore, QtGui, QtWidgets
from ui import Ui_MainWindow


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        # 實例化UI界面
        self.setupUi(self)
        # 實例化之後,self擁有UI_Mainwindow的所有屬性
        # 在此處編輯所有的信號槽以及剩下的UI部分,實現UI與代碼邏輯分離

新建一個main.py文件,主要用於啟動,在其中添加如下代碼:

from PyQt5 import QtCore, QtGui, QtWidgets
from mainwindow import MainWindow
from sys import argv
from sys import exit as sys_exit

if __name__ == "__main__":
    app = QtWidgets.QApplication(argv)  # 獲取命令行參數
    mainWindow = MainWindow()  # 創建界面實例
    mainWindow.show()  # 顯示界面
    sys_exit(app.exec_())

然後執行main.py即可看到界面,小建議:

  • 熟悉之後在designer中只編寫UI,不寫任何信號和槽,全部在mainwindow.py中註釋的地方添加
  • 初始化界面時需要的參數也放在mainwindow.py中,此處舉個例子:

在代理爬取部分的combobox中,我們需要添加西刺高匿其他暫無這兩個下拉選項,我們不用在UI設計時就寫進去,可以在mainwindow.py中通過初始化時將代理名稱列表添加進去來實現,代碼如下:

mainwindow.py

from PyQt5 import QtCore, QtGui, QtWidgets
from ui import Ui_MainWindow


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    # 為了剝離ui和邏輯,基本的邏輯操作我們都在這個類中完成
    def __init__(self):
        # 初始化的部分直接放在這個函數裡
        super().__init__()
        # 實例化UI界面
        self.setupUi(self)
        # 代理的名字,如果有新的代理函數在此加入名字
        self.proxyname_l = ["西刺高匿", "其他暫無"]
        self.ui_remaining_part()

    def ui_remaining_part(self):
        # 除了基本的ui界面,在這個函數中設置每個部件的初始值以及實例化每個對象
        # 將代理的名稱列表插入兩個代理的combobox中
        self.comboBox_proxychoose_crawl.addItems(self.proxyname_l)
        self.comboBox_proxychoose_setup.addItems(self.proxyname_l)

所以初始化UI時需要的其他參數以及實例化其他功能型類的語句我都是寫在mainwindow.py在實例化了setupUI之後的

** 這樣我們就只需要在設計UI時修改重要部件的別名,就能在mainwindow.py中愉快地編輯對應部件的剩下的屬性以及信號和槽,即使UI界面稍微有所變動,也不會影響到原來的代碼邏輯,從而實現了UI與代碼邏輯的分離 **

參考文章:PyQt5 如何讓界面和邏輯分離簡介

信號和槽傳遞額外參數#

信號和槽傳遞額外參數的重要性不亞於上面提到的UI和代碼邏輯分離

上面我們已經說了,在設計UI時只管設計就好,剩下的信號和槽我們都在代碼邏輯部分實現,那麼就要求我們能熟練掌握每個部件的所能觸發的事件以及有哪些槽,部件觸發事件傳遞信號和槽的格式基本如下:

self.部件.事件.connect(槽)

比如我們上面的 gif 演示的改變代理爬取的下拉框,就把下拉框變化的內容輸出到顯示區域對應的語句如下:

self.comboBox_proxychoose_crawl.currentTextChanged['QString'].connect(self.textBrowser_disp.append)

.connect裡面的能收到來自前面的事件所發出的信號,那麼接收到的信號是什麼呢?舉幾個常見例子:

  • 如果是按鈕.按鈕被點擊, 那麼槽接收到的是 ``
  • 如果是下拉框。檢測下拉框改變選項,那麼槽接收到的就是下拉框改變選項後下拉框中的值 `
  • 如果是單選框checkbox.狀態改變,那麼槽接收到的是單選框狀態改變到的狀態的信號數值,2是選中,0是未選中,1是半選

這些是我寫代碼中查詢資料得到的,那麼如果用到了從來沒用過的部件,想迅速知道某個部件某個事件改變後傳遞的信號有哪些,你可以這樣做:

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    # 為了剝離ui和邏輯,基本的邏輯操作我們都在這個類中完成
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.部件.事件.connect(self.自定義的函數)

    def 自定義的函數(self, parameter):
        print(parameter)

這樣你每次觸發你選中的部件的對應事件,傳遞的參數就會被打印出來

也就是說每個部件在觸發某個事件的時候,都会帶著自身的某個參數,但是經過實驗,後面的 connect 只能是沒帶括號的某個函數

也就是說是不能帶其他參數的,那麼如果我們想要自定義這個傳遞的參數怎麼辦呢?

** 我們使用lambda就可以帶參數了 **

舉個例子,我現在有兩個按鈕bt1bt2,當我點擊其中一個之後,我想知道我點擊的是 bt 幾,代碼如下:

        self.pushbutton_1.clicked.connect(lambda:self.whichbt(1))
        self.pushbutton_2.clicked.connect(lambda:self.whichbt(2))
    def whichbt(slef,i):
        print("我現在打印了{}號按鈕".format(i))

這樣.connect後面的函數就能帶括號且在括號中可以帶多個參數了,你也可以傳遞它自身:

        self.checkBox_1.stateChanged.connect(lambda: self.which_checkbox(self.checkBox_1, 1))
        self.checkBox_2.stateChanged.connect(lambda: self.which_checkbox(self.checkBox_2, 2))

    def which_checkbox(slef, part, i):
        part.setChecked(False)
        print("checkbox{}的狀態改變了,但我現在把它置為未選中了".format(i))

但是又要傳遞本來的參數,又要傳遞額外的參數,我不知道怎麼實現,知道的大佬可以留言告知一下

參考文章:pyqt 信號和槽傳遞額外參數

重要的部分我們介紹完了,來介紹幾個我編寫代碼中遇到的小問題以及解決方案

行輸入#

我除了設置隨機代理之外還添加了自定義代理,那麼用戶自定義輸入ipport,肯定需要做第一步的驗證,在尋找中找到了一篇優質文章:PyQt5 基本控件詳解之 QLineEdit(四)

從中學習到了對文本框輸入限制的設置方法,最終ipport部分代碼如下:

# 設置ip默認不可寫,ip地址掩碼;但是此處只是限制輸入類型為數字,還需驗證ip合法性
self.lineEdit_customizeip.setInputMask('000.000.000.000;_')
self.lineEdit_customizeip.setReadOnly(True)
# 設置端口默認不可寫,以及限制端口為0-65535
# 設置文本允許出現的內容
port_reg = QtCore.QRegExp(r"^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$")
# 自定義文本驗證器
pportregValidator = QtGui.QRegExpValidator(self)
# 設置屬性
pportregValidator.setRegExp(port_reg)
# 設置驗證器
self.lineEdit_customizeport.setValidator(pportregValidator)
self.lineEdit_customizeport.setReadOnly(True)

ip設置後一打開就能看到掩碼的輸入框:

image

因為我輸入除了我限制的部分以外都輸不進去,也不會在文本框中顯示,就不放實操效果了

兩個 checkbox 的互斥#

我使用的是Check Box來選擇代理方式是隨機代理還是自定義代理,默認是不互斥的

參考文章:PyQt5 系列教程(15):單選按鈕

單選按鈕默認為 autoExclusive(自動互斥)。如果啟用了自動互斥功能,則屬於同一個父窗口小部件的單選按鈕的行為就屬於同一個互斥按鈕組的一部分。當然加入 QButtonGroup 中能實現多組單選按鈕互斥。

此處為了實現隨機代理和自定義代理兩個按鈕的互斥,我們把它添加到一個 QButtonGroup (找不到說 checkbox 也能用 QButtonGroup 的那篇文章了) 以實現互斥,代碼如下:

# 把兩個checkbox放到一個互斥的QButtonGroup裡面,起到單選效果
self.btgp_mutuallyexclusive = QtWidgets.QButtonGroup(self.groupBox_setting)
self.btgp_mutuallyexclusive.addButton(self.checkBox_randomproxy)
self.btgp_mutuallyexclusive.addButton(self.checkBox_customizeproxy)

看一下沒添加到QButtonGroup之前的效果:

image

看一下添加之後的:

image

給窗口關閉時綁定事件#

我希望在退出時能觸發清空代理函數,這樣即使程序關閉也不影響我們正常使用,找到一篇參考文章:

PyQt5 編程 (17):窗口事件

他排版很亂,實現的效果是當點擊關閉按鈕時,觸發彈框問你是不是確認要關閉

通過單擊窗口標題欄中的關閉按鈕或調用 close () 方法來關閉窗口時,closeEvent (self,event) 方法被調用。 通過 event 參數可獲得 QCloseEvent 類的一個對象。 為了防止窗口關閉,必須通過該對象調用 ignore () 方法,否則調用 accept () 方法。
下面的例子為:單擊關閉按鈕將顯示一個標準對話框,要求確認是否關閉該窗口。 如果用戶單擊 “是” 按鈕,則關閉窗口;如果用戶單擊 “否” 按鈕,則僅關閉對話框,窗口不會被關閉。

代碼如下:

import sys
from PyQt5 import QtWidgets


class MyWindow(QtWidgets.QWidget):
    def init(self):
        QtWidgets.QWidget.init(self)
        self.resize(300, 100)

    def closeEvent(self, e):
        result = QtWidgets.QMessageBox.question(
            self, "關閉窗口確認", "真的要關閉窗口?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
        if result == QtWidgets.QMessageBox.Yes:
            e.accept()
            QtWidgets.QWidget.closeEvent(self, e)
        else:
            e.ignore()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())

實現的效果如下:

image

那麼我們綁上點擊關閉時清空設置的函數即可:

def closeEvent(self, e):
    # 點擊關閉按鈕時清空設置
    QtWidgets.QWidget.closeEvent(self, self.pbp_of_clearsetup())

功能性代碼優化#

雖然功能性代碼我早就完成了,但這次在寫代碼邏輯時發現了原來代碼不完善的地方以及一些功能還沒有實現,這裡補充說明一下

爬取代理及資料庫操作#

寫爬取代理的時候只寫了爬取和存入資料庫的部分,但是我們還需要的功能如下:

點擊開始代理時,需要從資料庫中讀取ip端口並存入對應代理專門用來存儲讀取數據的列表中,點擊換一個時,丟棄當前代理,從列表中再隨機選一個,當換了代理集時把存儲列表清空,下次點擊開始代理時重新從資料庫中讀取並存到對應列表,這就保證了我們新爬取的代理能被用到

我把爬取代理和從資料庫讀取代理兩個功能放在了一個函數,這樣避免了資料庫連接等代碼的重複,部分代碼如下:

try:
    # 讀取對應集合中ip和port信息
    collection_dict_l = self.db["{}".format(collection_name)].find({}, {
        "_id": 0, "ip": 1, "port": 1})
    for item in collection_dict_l:
        # 去重操作
        if item not in proxies_l:
            # 添加到對應的集合列表中,列表中元素形式為ip和port的字典
            proxies_l.append(item)
    return proxies_l
except Exception as e:
    return("程序報錯:{}".format(e))

參考文章:Python Mongodb 查詢文檔

其他參考文章:

設置代理部分#

這個真的被自己坑到了

在原來的代碼的構建註冊表函數中,在過濾了 ip 為空和端口不合法性之後,我們構建參數值的代碼如下:

try:
    IPy.IP(self.ip)  # 註冊表中能隨便填ip和端口,但是我們不允許,如果ip不正確或為空則會直接報錯退出
    ip_len = hex(len(self.ip)+len(self.port)+1).replace('x', '')
    ip = bytes(self.ip, 'ascii').hex()  # 獲得ip的十六進制字符串
    port = bytes(self.port, 'ascii').hex()
    former = "4600000000000000{}000000{}000000{}3A{}{}".format(switch_options.get(
        self.proxy_switch), ip_len, ip, port, local_switch_options.get(self.local_proxy_switch)).lower()
    hex_value = former+"00"*int((150-int(len(former)))/2)
    print("註冊表參數值構建完成!")
except Exception as e:
    print("程序報錯:{}\n程序退出!".format(e))
    exit(0)

其中這行構建ip長度的十六進制值的代碼:

ip_len = hex(len(self.ip)+len(self.port)+1).replace('x', '')

沒有處理好在 ip+:+ 端口的長度超過 15 位的情況:

  • 127.0.0.1:80:12 位,ip_len=0C
  • 110.110.110.110:65535:21 位,ip_len=015

參數值有如下變化:

代理開關0000000C
代理開關000000015

中間就多了個 0!導致傳進去之後會亂掉,參數值由4600...會莫名其妙地變為04600...,導致代理不生效

於是我們將ip_len語句改為:

# hex()方法轉換出來的數字是0x開頭的且ip長度站兩位
ip_len = hex(len(ip)+len(port)+1).split("x")[-1].zfill(2)

就解決了問題,這個故事告訴我們一定要細心

完結#

最終效果如下

1.. 爬取代理部分:

資料庫為空,且 ip 最近被封的狀態 (電腦點擊看大圖):

image

注:此為交替暫停開始錄製的效果,抽取了大部分幀以減小 gif 體積,實際事件參照軟件顯示區域時間戳

2. 代理部分 (電腦點擊看大圖):

image

  • 為了保護自己,原 ip 打碼了
  • 代碼目前還沒完全完善,可能會碰到連接速度不好的 ip,此 gif 為遇到了連接速度不錯的代理,為最理想狀態,在發布github項目時會加入先驗證在連接的功能
  • 注:此為交替暫停開始錄製的效果,抽取了大部分幀以減小 gif 體積,實際事件參照軟件顯示區域時間戳

全代碼太多了就不放了,等這幾天的深度學習的項目寫完再把這個項目開源到github作為自己的第一個開源項目

大家也可以去給我點個star,提個issues什麼的

最後po一張自己在構思時用畫圖畫的UI設計圖:

image

現在的軟件界面:

image

本文完~

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。