前言
其實這篇文章早就想寫了,因為自己太懶,到現在才更新。雖然這三個例子都是最簡單的算法,但是不得不說,相比較暴力的做法,確實提升了效率,也節省了程序員的時間。三個例子中用到的分別是二分查找、二維平均卷積、異步改同步。
二分查找
應用背景
給定一個URL資源接口,如XXX_001.mp4代表視頻的第一秒、XXX_002.mp4代表視頻的第二秒,以此類推;如何寫一個多線程爬蟲,把整個視頻資源快速爬下來?
對照算法
容易想到的就是順序爬取,直接假設資源具有999個,將URL生成好存在隊列里,開50個線程依次獲取,當有線程返回404時,通知隊列清空,其他排在之后的線程也停止工作,等待未下載完畢的線程處理,之后程序退出。
存在的問題
- 假設資源只有20個,50個線程很明顯一瞬間打出來30個404,這對網站也是一種攻擊行為,很容易被反爬策略限制。
- 各個線程之間的調度需要有效處理,假設2號線程返回404,在它之后從隊列里拿取URL的所有線程都需要被停止,雖然Python的隊列是線程安全的,但是需要操作已經運行的其他線程仍存在一定的問題。
- 因為網絡io的不穩定問題,很可能接口返回異常值如500,這時候需要處理重試,同時還需要及時確定資源是否已爬取完畢。
解決方案
假設我們一開始就可以知道資源的總數,那么很容易得到隊列里應有的URL,組織多線程爬取也就變得簡單,只需要超時重試這一個操作即可。
這里可以對URL做一個抽象,我們進行二分查找的對象可以視為一個前半部分為1(200),后半部分為0(404)的數組:{1, 1, 1, 1, 0, 0, 0},於是問題變為,用最小的數組訪問次數,找出來最右邊的一個1。
代碼方案
這里是完整的代碼實現:
queue = []
max_num = 1000
for i in range(max_num):
ts = url.replace('{2}', '%03d' % i)
save = save_dir + '/%03d.ts' % i
queue.append((ts, save))
left = 0
right = max_num - 1
mid = 0
r = requests.get(queue[mid][0])
if r.status_code != 200:
print(str(video) + ' total: %d' % mid)
os.removedirs(save_dir)
return
while left <= right:
mid = (left + right) // 2
q = queue[mid]
u = q[0]
r = requests.get(u)
if r.status_code == 200:
left = mid + 1
with open(q[1], 'wb') as f:
f.write(r.content)
else:
right = mid - 1
r = requests.get(queue[mid][0])
if r.status_code == 200:
if not spider.file_exists_or_has_content(queue[mid][1]):
with open(queue[mid][1], 'wb') as f:
f.write(r.content)
mid += 1
queue = queue[:mid]
print(str(video) + ' total: %d' % mid)
可以看出,主要代碼部分就是下面的二分查找,使用這樣的臨界處理,可以找出最右邊的元素,以最小的訪問次數獲取URL資源總數,處理完畢后queue里就是所有的資源了,其中在中間階段已經將爬取的部分視頻保存,方便后面的線程不重復發起網絡請求。這樣我們就可以愉快的爬小黃網了(誤
二維平均卷積
應用背景
給定一個原圖像,一個輸出框和一個代表原圖像顯著性分布的顯著性圖像(即畫面主體的灰度圖、越顯著灰度值越高),如何調整輸出框的位置,使得框住的圖像顯著性最高(即裁剪問題)?
對照算法
暴力解決的話很容易想到dp的方法,將輸出框左上角從(0, 0)開始位移,第一次計算所有像素灰度的sum,每次移動都加上新覆蓋的一行(列)的灰度並減去取消覆蓋的一行(列)的灰度。
存在的問題
- 耗時,是非常耗時,這種密集的cpu操作,io極短,相當於一直在讓cpu做加法、減法。
- 假設我們需要對N對圖片都進行這樣的處理,即對視頻的每一幀處理,算法的執行時間會成為嚴重性能瓶頸。
解決方案
有一種加速類似運算的操作叫做卷積,一般我們選擇的卷積核是為了提取圖像關鍵部分、或為了圖像增強,但是在這里,可以有效的利用卷機的硬件加速效果實現我們的算法加速;同時采用平均卷積避免加和出來的結果過大導致計算放慢。
代碼方案
這里是完整的代碼實現:
def crop_fix(sframe, sheight, swidth):
skenerl = np.ones((sheight, swidth)) / (sheight * swidth)
s = signal.convolve2d(sframe, skenerl, mode='valid')
m = np.argmax(s)
r, c = divmod(m, s.shape[1])
return r, c
簡潔易懂,這大概我2020上半年寫的最優雅的代碼了吧,有效利用硬件加速效果,全部使用庫里優化過的函數。於是就可以愉快的省下時間划水啦(誤
異步改同步
這個理論上不算算法的解決方案,但是也屬於代碼的小trick,一並介紹了。
應用背景
假如你有一個JTree(Java Swing知識、未獲取前置知識點的同學請看書),即一個樹形菜單,其中每個節點的展開都會觸發Expand事件(委托事件模型、Swing的nb之處),其會啟動一個線程用以發起網絡請求,動態加載樹的子節點;現在新增了一個需求,需要完全展開這個樹,同時保證請求數不至於爆炸,怎么實現?
對照算法
正常來說展開這個樹,我們會使用遞歸算法,展開一個節點,遍歷其子節點依次調用這個算法,當這個節點是子節點時停止。
存在的問題
- 由於節點的展開與節點內容的獲取時異步的,遞歸算法不知道需要等待多長時間再開始遍歷其子節點,導致樹的展開不徹底。
- 由於算法使用遞歸,很難控制一個有效的延遲時間,使得每秒請求數不至於過高。
解決方案
將源代碼中異步的方法嘗試改為同步,但又不影響原來的代碼邏輯;這里想到了Java的synchronized和ReentrantLock,這里選用前者實現,因為后者還需要在類中維護一個變量,較為麻煩。
代碼方案
首先我們在異步方法里加上這一句:
synchronized (ChxGUI.this) {
ChxGUI.this.notify();
}
這樣既不會影響原來代碼的邏輯,又方便了我們獲取進程執行結束的信息。之后實現我們需要的業務代碼即可:
allButton.addActionListener(e -> new Thread(() -> {
ChxGUI gui = ChxGUI.this;
gui.label.setText("請不要操作 稍等一會");
gui.tree.setEnabled(false);
synchronized (ChxGUI.this) {
try {
gui.tree.expandRow(0);
Thread.sleep((int) (1000 * Math.random()));
ChxGUI.this.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
TreeNode root = (TreeNode) gui.treeModel.getRoot();
for (int i = root.getChildCount() - 1; i >= 0; i--) {
TreeNode course = root.getChildAt(i);
synchronized (ChxGUI.this) {
try {
gui.tree.expandPath(ChxUtility.getPath(course));
Thread.sleep((int) (1000 * Math.random()));
ChxGUI.this.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
for (int j = course.getChildCount() - 1; j >= 0; j--) {
TreeNode lesson = course.getChildAt(j);
synchronized (ChxGUI.this) {
try {
gui.tree.expandPath(ChxUtility.getPath(lesson));
Thread.sleep((int) (2000 * Math.random()));
ChxGUI.this.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
gui.tree.setEnabled(true);
gui.label.setText("就緒。");
}).start()
);
這里的重點就是使用同一個同步量進行兩者的同步,這樣可以把代碼從異步執行改為同步執行,請求API的次數也就變得安全可控。於是就可以愉快的刷網課啦(誤
后記
這些應該是些常見的優化思路,寫在這里是因為我確實運用這些解決了實際問題,也取得了不錯的效果,coding改變世界,我堅信這一點,也希望分享這段經歷給更多的人,學而不已、闔棺乃止,今天是國家公祭日,願逝者安息,願生者奮發,願祖國昌盛,致敬英雄!