面向聰明小白的程式設計入門教程 做點有意思的東西(Part 7)
做一個文件轉換器應用,雖然用不了太多程式碼,但是要涉及到程序、執行緒、自定義Qt訊號等太過抽象的東西。在做這個應用前,我們還是先來點簡單的吧。之前的教程好像有些無聊,這節課,我們來學著做點有意思的東西。
有意思的東西,要有有意思的內容。如果自己沒有內容,就到網際網路上致敬一些吧。
1. 做一個IP地址檢視器
說實話,檢視電腦的IP,也挺無聊的,但是夠簡單,所以就從這裡開始吧。IP地址在作業系統裡就可以直接檢視。但是除了IP地址,我們也想通過IP獲取地理地址和網路運營商情況。IP地址和地理地址並沒有固定的關係,所以我們需要藉助網路上的資料庫,或者說藉助第三方的服務來查詢。這裡,我們選用IP.CN 提供的IP地址查詢服務。
from PyQt5.Qt import ( QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QSizePolicy ) def fetch_ip(): from urllib.request import urlopen, Request return urlopen(Request("https://ip.cn", headers={"User-Agent": "curl/7"})) \ .read().decode().strip().replace("來自", "\n來自") app = QApplication([]) lbl = QLabel() lbl.setStyleSheet("background: teal; color: lime; font-size: 72px;" "qproperty-alignment: AlignCenter;" "qproperty-text: 'Ready.'") lbl.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)) btn = QPushButton() btn.setStyleSheet("* { background: seagreen; color: aqua; font-size: 72px; border: none }" "* { qproperty-text: 獲取IP地址 }" "*:hover { background:darkgreen }" "*:pressed { background: olive }") btn.setSizePolicy(lbl.sizePolicy()) btn.clicked.connect(lambda: lbl.setText(fetch_ip())) box = QVBoxLayout() box.addWidget(lbl) box.addWidget(btn) box.setStretch(0, 2) box.setStretch(1, 1) box.setSpacing(0) box.setContentsMargins(0, 0, 0, 0) wnd = QWidget() wnd.setWindowTitle("IP地址察看器") wnd.resize(777, 777 * 0.618) wnd.setLayout(box) wnd.show() app.exec()
執行以上程式,點選按鈕,大約卡頓半秒後,文字標籤處就會顯示我們電腦的IP
地址、地理
地址和ISP
資訊。
這個程式涉及到了不少新的知識點,我來依次解釋一下:
-
def 函式名(...引數):
這種語法是用來定義函式的。Lambda
表示式定義的是匿名函式,def
定義的是有名字的函式。函式接受0個或多個輸入,處理後返回0個或多個輸出。 -
冒號之後的下一行開始是函式體。函式體左邊的四個空格不能省略。
Python
為了簡潔,沒有提供特殊的符號來給函式定界。Python
用Tab
(製表符)或空格來給函式定界。Tab
在不同平臺下寬度可能不一樣,所以程式程式碼中的空白一般用空格。理論上任意個空格都可以,但使用4個空格已經是事實上的標準了。 - 函式名稱裡面執行的邏輯,不一定要跟函式名有關係。在函式體裡頭,可以執行我們想執行的任意邏輯。
-
函式體可以什麼都不做。但是
Python
規定函式體不能為空。所以,表示什麼都不做,要用語句pass
-
函式體通過
return
關鍵字結束執行,並將return
後邊跟著的資料(如果有的話)返回。沒有返回語句的函式,會執行到函式尾部,返回None
-
用來匯入模組的
import
語句,除了放在程式碼頭部外,也可以用在函式體裡面。但是出了這個函式,import
進來的東西就訪問不到了 - Python是一種指令碼語言,意思是Python程式碼會從頭到尾一行一行地順序執行。所以,用到的模組要提前匯入,用到的函式要提前定義
-
使用Python程式碼也可以訪問網頁。Python內建的
urllib
模組提供了這個功能。 -
URL就是我們通常說的網址。常見的網址可能使用
http
協議,也可能使用https
協議。所以,在程式碼中,我們要明確指出 -
urlopen
函式可以將網頁下載回來。不同的網頁,下載回來的格式也不一樣。可能是普通的HTML網頁(最常見的網頁型別),可能是純文字文件(在Windows下俗稱記事本文件),也可能是圖片、視訊、壓縮包等電腦上可以儲存的任意檔案格式。 -
urlopen
函式下載網頁消耗的時間是不確定的。Python
程式碼要一行一行執行,下載網頁時,程式要等待下載完成才能執行其他程式碼(包括響應使用者的點選事件)。所以,下載時會導致軟體假死,點選按鈕沒反應。 -
Python語言經常被用來做爬蟲(用來自動化批量下載網頁),而
urllib
是Python官方的可以做爬蟲的模組。所以,直接使用urllib
,會被ip.cn
識別為爬蟲而拒絕服務(返回HTTP狀態碼403)。所以,我們需要將我們的HTTP請求偽裝成瀏覽器或者其他使用者代理(User-Agent, 一般使用者不會直接使用HTTP協議訪問網頁,而要藉助瀏覽器代為訪問,瀏覽器代理使用者訪問網頁,這時瀏覽器的角色就是使用者代理)。不過,我們這次不偽裝成瀏覽器,而要偽裝成cURL
(可以當作一個命令列下的網頁瀏覽器)。因為IP.CN
對瀏覽器返回的是一個HTML
網頁,對cURL
返回的是一個包含了IP
資訊的字串。為了省卻解析HTML
網頁獲取我們關心的IP
資訊,我們決定偽裝成cURL
,一步到位獲取。 -
要偽裝成
cURL
,我們需要修改HTTP請求的頭部Header
。HTTP
規範定義了HTTP頭部的User-Agent
欄位表示使用者代理。我們修改這個欄位即可。經過我的測試,IP.CN
對cURL
的識別策略是User-Agent
欄位以curl
開頭,後面跟斜槓和curl
版本號。我們用curl/7
就行。 -
urllib.request.urlopen
返回的資料型別是urllib.response.Response
物件,這是urllib
對HTTP響應的封裝。Response.read()
方法可以讀取響應內容。由於HTTP響應可能是張圖片,所以不能用字串來表示。read
方法讀到的是位元組碼,位元組碼可以表示任何資料型別,也可以表示任何檔案型別。從位元組碼轉換到字串,需要解碼,即呼叫decode()
方法。這張,我們便得到了一個表示IP
地址資訊的字串,格式類似於當前 IP: 115.171.212.227 來自: 北京市 電信\n
。 -
字串呼叫
strip()
方法可以去除首位的空白字元,比如換行符號。 -
字串呼叫
replace()
方法,可以替換字串中的指定子串為其他文字。我們用replace()
方法來給字串中間新增一個換行符。 -
控制元件的文字、對齊方式等屬性也可以通過樣式表來設定,比如
qproperty-text
表示文字,qproperty-alignment
表示對齊方式。 -
Qt的樣式表(QSS, Qt Style Sheet)中,
*
是萬用字元,表示任意控制元件。 -
QSS
中,:hover
表示滑鼠懸浮狀態,:pressed
表示滑鼠按下狀態。 -
除了
QBoxLayout.addWidget()
方法,我們還可以通過QBoxLayout.setStretch(索引,比重)
來調節子控制元件在佈局中的拉伸因子。
注意,IP
地址是商品,是可以用來買賣的,IP
地址的歸屬地和歸屬運營商(ISP)也是動態變化的,不一定準確。
2. 看段子
我們剛做的IP地址檢視器,在查詢IP時會假死一會兒。這個問題比較複雜,我們留到後面處理。現在,我們該做一個看段子的軟體。我們的目標是,點選按鈕可以隨機刷新出幾條段子,點選段子名稱可以閱讀段子的內容。
當然,我們不是在做AI
(人工智慧),不過就算是做AI
,要想編個能看的段子,一般的電腦CPU怕是一時半會算不出來。我們希望找一個免費的可以提供隨機段子的介面。這裡說的介面是HTTP
介面,介面類似於執行在其他電腦上的一個遠端的函式,我們可以呼叫這個函式(這個介面)獲取我們需要的東西。介面的格式一般比較固定,裡面的資料一般不會經過特殊處理,可以直接讀取解析出來。
不過,免費的介面可不好找。我搜了半天也沒有搜到。看來我們得自立更生,找一個可以提供隨機段子的網站,將網頁裡的隨機段子手動扒出來了。這種程式學名叫做爬蟲。現在教爬蟲有點過早,但是為了我們的程式有內容可看,只能先硬著頭皮上了。
我發現一個網站叫段子網 (此處不是在做廣告,數數本文的留言人數,就知道我沒騙你啦),首頁每次重新整理,都會在側邊欄生成5個隨機的段子。不過,這5個段子只有標題。看來,我們的程式又得多幾行程式碼了。
from PyQt5.Qt import ( QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QSizePolicy, QHBoxLayout ) from urllib.request import urlopen from parsel import Selector import textwrap app = QApplication([]) lbl = QLabel("Ready.") lbl.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) lbl.setWordWrap(True) lbl.setStyleSheet("font-size: 36px") btn = QPushButton("重新整理段子") lft = QVBoxLayout() lft.setSpacing(1) for i in range(1, 6): wgt = QPushButton(f"段子{i}") wgt.setStyleSheet("* { font-size: 36px; background: steelblue }" "*:hover { background: cadetblue }") wgt.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) lft.addWidget(wgt) top = QHBoxLayout() top.addLayout(lft) top.addWidget(lbl) box = QVBoxLayout() box.addLayout(top) box.addWidget(btn) box.setSpacing(0) box.setContentsMargins(0, 0, 0, 0) wnd = QWidget() wnd.setWindowTitle("五個段子") wnd.resize(999, 999 * 0.618) wnd.setLayout(box) wnd.show() app.setStyleSheet( "* { font-size: 72px; color: lime; background: darkturquoise }" "QLabel { background: teal; qproperty-alignment: AlignCenter }" "QPushButton { background: darkgreen; border: none }" "QPushButton:hover { background: seagreen }" "QPushButton:pressed { background: green }" ) def fetch_joke(url): htm = urlopen(url).read().decode() title = Selector(htm).xpath("//article//h1/text()").get() content = "\n".join(Selector(htm).xpath("//article/section/p/text()").getall()) return f"{title}\n{content}" def refresh_jokes(): htm = urlopen("https://duanziwang.com").read().decode() jks = [x.attrib for x in Selector(htm).xpath("//div/li/a")] for idx, jok in enumerate(jks): btn: QPushButton = lft.itemAt(idx).widget() btn.setText(textwrap.fill(jok["title"][:24], 12)) btn.setProperty("url", jok["href"]) btn.clicked.connect(lambda: lbl.setText(fetch_joke(app.sender().property("url")))) btn.clicked.connect(refresh_jokes) app.exec()
執行以上程式碼,可以看到我們的段子軟體的功能已經完成。這個程式涉及到了不少新的知識點,我大概解釋一下:
-
QApplication.setStyleSheet
可以設定應用的全域性樣式表 -
QLabel.setWordWrap
可以啟用或禁用自動換行(預設關閉狀態) -
for x in y
這種語法是用來遍歷容器型別資料(比如列表)的。y
是容器,x
是元素,在每次迭代中會更新。 -
range(a, b)
函式可以用來生成整數範圍[a, b-1]
。範圍不是列表,不儲存元素,只是描述了元素的生成規則。 -
for
也可以用來遍歷迭代器。range
函式返回的範圍屬於Python中的迭代器。 -
在函式中定義的變數是區域性變數,出了函式就訪問不到了。要在函式體中定義或訪問全域性變數,得用
global
關鍵字 -
點選不同的段子標題,要下載不同的段子。所以,我們需要在槽函式裡獲知被點選的段子。我們考慮將段子的網址儲存到按鈕中,在槽函式中只要考慮如何獲取哪個按鈕被點選就行了。
Qt
不會將點選事件的訊號源(按鈕物件)傳遞給槽函式,但是,Qt
提供了QObject.sender()
例項方法來獲取最新的訊號源。 -
這個
QObject.sender()
是個例項方法,意思是這個方法只能在QObject
類的例項物件上呼叫。 -
QObject
是Qt
系統中所有型別的根類。Qt
中的所有類都繼承自QObject
。所以,每個Qt
中的物件都是QObject
類的物件 -
QObject.sender()
方法可以在任意Qt
物件上呼叫。我們可以直接用app.sender()
來獲得最新的訊號源,即使用者最新點選的按鈕。 -
QObject.setProperty
可以在Qt
物件上儲存資料。比如,我們可以將段子的地址儲存在關聯的按鈕中。 -
textwrap.fill
方法可以以指定的段長切割字串,每個切點放一個換行符 -
常見的網頁是一種HTML文件,是一種基於標籤的樹形結構,格式類似於
<html><head></head><body><a href="www.github.io">CLICK</a></body></html>
。手動解析這種文件,一般要用到正則表示式
,非常麻煩。所以,我們一般用現成的解析器來解析HTML文件。在這裡,我們使用parsel
(pip install parsel
)這個模組來解析HTML。 -
Selector(HTML程式碼)
建構函式用來生成HTML解析器,Selector.xpath()
方法用指定的路徑(XPath)來解析HTML,返回的物件還是Selector
。Selector.get
方法獲取匹配到的第一個元素,Selector.getall
方法獲取匹配到的全部元素。//div/li/a
表示HTML
文件中任意位置的div
標籤下的直接子li
標籤下的直接子a
標籤。在我們下載的網頁中,這個XPath
語句正好匹配到我們需要的5個段子的超連結。要獲取標籤的文字,需要使用text()
。 -
[function(x) for x in y]
這種語法叫生成器,可以用來將一個列表中的元素經過處理一一對映到另一個列表。我們也可以利用這種語法,將需要寫兩行的for x in y: function(x)
語法壓縮到一行。 -
"\n".join(字串列表)
這種語法可以用特定的字元連線多個字串。