寒假作業2/2


這個作業屬於哪個課程 2021春軟件工程實踐|W班
這個作業要求在哪里 寒假作業2/2
這個作業的目標 閱讀《構建之法》提出問題
編寫程序,使用PSP進行時間管理與總結
其他參考文獻 The Most Efficient Way To Find Top K Frequent Words In A Big Word Sequence

閱讀《構建之法》並提問

如果你是病人, 你希望你的醫生是下面的那一種呢?

​ a) 剛剛在書上看到你的病例, 開刀的過程中非常認真嚴謹, 時不時還要停下來翻書看看…

​ b) 富有創新意識, 開刀時突然想到一個新技術, 新的刀法, 然后馬上在你身上試驗…

​ c) 已經處理過很多類似的病例, 可以一邊給你開刀, 一邊和護士聊天說昨天晚上放的 《非誠勿擾》的花絮…

​ d) 此醫生無正式文憑或醫院, 但是號稱有秘方, 可治百病。

​ e) 還有這一類, 給你開刀到一半的時候, 出去玩去了, 快下班的時候, 他們匆匆趕回來, 胡搞一氣, 給你再縫好, 打了很多麻葯,就把你送出了院, 說“治好了”!

我認為現在互聯網已經進入了"存量競爭",能脫穎而出的往往是那些富有創新意識, 開刀時突然想到一個新技術的項目之后在加上穩定的運營

有一種意見認為作坊只能獨立存在,和其他機構都合不來。其實不然,在龐大的企業內部,
也有一些人構建了一個小作坊,自己做主,做自己感興趣的事

我感覺大機構反而可能是小作坊創新的阻礙。比如facebook,在他還是小作坊的時候,就是靠創新吸引了最初的用戶,但是現在,為了商業利益,他對於擁有新技術的小作坊,首先是收購,如果收購不成功,就仿照一個與之對應的軟件來壓垮小作坊,一定程度上是阻礙了創新。

白箱:在設計測試的過程中,設計者可以“看到”軟件系統的內部結構,並且使用軟件的內部知識來指導測試數據及方法的選擇。“白箱”並不是一個精確的說法,因為把箱子塗成白色,同樣也看不見箱子里的東西。有人建議用“玻璃箱”來表示。

如果白箱測試要知道程序的結構,那測試人員是不是要熟悉整個系統,不然不能進行白箱測試。

職業發展

幾種方法:

· PSP

· 考級

· Steve McConnell Construx

· Corporate Career Model

· Pragmatic Approach

正如文中所說,是很難量化一個工程師的技術和能力,但是我覺得上面的這些方法在國內有些不適用。之前也讀過這篇文章:怎樣花兩年時間去面試一個人。但是能做到文中的做法的公司,也是極少的,感覺在國內,衡量一個人的技術和能力還是靠他的項目經歷。

Lotus 1-2-3 占據了大部分市場份額, 不過, 它的日期計算功能有一個小Bug,就是把1900 年當作閏年。這類軟件在內部把日期保存為“從1900/1/1 到當前日期的天數”這樣的一個整數。Excel 作為后來者,要支持 Lotus 1-2-3 的數據文件格式,這樣才能正確處理別的軟件產生的格式文件。 這個錯誤就這么延續下來了,每一版本都有人報告,但是都沒有改正。

我自己在使用一些軟件的時候,或是寫代碼的時候,會遇到一些讓人有“為什么要這樣實現”的感覺,然后在網上尋找答案的時候常常是說為了“向后兼容”。有些軟件因為過重的歷史包袱導致被那些新進的軟件超越,而有一些軟件卻因“不能向后兼容”導致被用戶拋棄。那要如何在這兩者之間找到一個平衡點?

WordCount編程

Github項目地址

PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划
• Estimate • 估計這個任務需要多少時間 3Day 4Day
Development 開發
• Analysis • 需求分析 (包括學習新技術) 60 90
• Design Spec • 生成設計文檔 20 10
• Design Review • 設計復審 10 20
• Coding Standard • 代碼規范 (為目前的開發制定合適的規范) 20 10
• Design • 具體設計 60 70
• Coding • 具體編碼 240 360
• Code Review • 代碼復審 20 10
• Test • 測試(自我測試,修改代碼,提交修改) 60 180
Reporting 報告
• Test Repor • 測試報告 60 120
• Size Measurement • 計算工作量 20 20
• Postmortem & Process Improvement Plan • 事后總結, 並提出過程改進計划 30 10
合計 600 1000

代碼規范制定鏈接

codestyle.md

解題思路

​ 看過一遍題目,一開始我認為可以將代碼分為幾個大模塊

  • WordReader模塊:讀取合法單詞

  • TopK模塊:統計topK

  • Core模塊,將上兩個模塊整合,實現統計文檔信息和TopK的需求

​ 思路就是Core通過WordReader模塊,讀取出合法單詞,統計文檔的字符數、單詞數和行數,同時將這個合法單詞存入TopK模塊統計TopK,最后將從TopK模塊中獲取出現頻率前十的數,並和文檔的統計信息一並輸出。

​ 之后想了想,應該在讀取模塊中就可以統計文檔信息,那么Core模塊實際上就起到了一個將WordReader模塊和TopK模塊整合的功能

image-20210227133550157

模塊關系

​ 最后在網上查資料的時候,有看到在有限內存下對多數據(1億條左右)實現求TopK的思路,所以我希望我的程序也能對大文件進行處理。

設計與實現過程

整體設計

​ 由於學習了Spring Boot框架,覺得它的IoC的思想確實能減少不同模塊之間的耦合,所以我的Core模塊默認是沒有對WordReader模塊的定義,而是通過set方式將其"注入"。

​ 而如果我是通過在Core中new一個WordReader來實現的,這次的要求是從文件中讀取,那么我必然要在Core模塊中寫一個方法傳入File類來構造WordReader,如果需求改為要從數據庫讀取,那Core模塊還是要修改出一個傳遞數據庫Connect的方法,這樣對WordReader的修改傳遞到了Core,程序之間耦合度較高。

​ 以上是我在整體設計的時候的思考。

WordReader模塊

​ 首先,由於作業要求能把這個功能放到不同的環境中去,所以WordReader模塊不應只能從文件中讀取數據,所以我將設計了一個WordReader的接口,並實現了一個從InputStream中讀取單詞的實現類。

public interface WordReader {
    //返回下一個有效的單詞
	public String nextWord();
	public boolean setInputStream(InputStream is);
	public long getLineNumber();
	public long getWordNumber();
	public long getCharNumber();
	public void clear();
}

​ 設置讀入流的工作交給了main函數,main函數將文件路徑轉換成流,通過流新建一個WordReader,之后set進Core中。這樣可以很容易復用這個功能。

public static void main(String[] args){
	...
    InputStream is = new FileInputStream(file);
    InputStreamWordReader wr = new InputStreamWordReader(is);
	Core core = new Core();
	core.setWordReader(wr);
	core.start();
	...
}

​ 而在實現的時候,我第一個想到的就是用Scanner類來讀取單詞,它提供了豐富的讀取功能,但是它按單詞讀取的時候並不能統計換行,而如果按行讀取,對於大文件來說,如果數據都在一行,那么就不能將內存控制在可以控制的訪問內

​ 綜上缺點,我決定還是自己手寫一個從流中獲取有效單詞的類。

​ 我實現了一個InputStreamWordReader類,由於我源代碼中有詳細的注解,這里就不放代碼來解釋了,而這個類有以下的特點

  • 繼承自WordReader類,可以整合到Core中
  • 統計了讀出文件的字符數、單詞數和行數
  • 可以通過next方法獲取下一個有效的單詞,即作業要求中對單詞的定義
  • 可以統計有效的行數,跳過只包含空字符的行,如果最后一行沒有換行也可以統計到
  • 這個類的實現是完全通過流的思想,不會緩存整個文件、一整行的字符串,最多緩存一個單詞,以控制整個程序的內存使用,也因為這個功能的實現,所以整體邏輯比較復雜

TopK模塊

​ 剛開始查閱資料的時候,發現大概的思路是將單詞存入HashMap或TrieTree中,在統計頻率最高的單詞時,通過小頂堆來維護出現頻率最高的k個字符串,最后返回結果。

​ 結合這個作業的實際來看,他除了26個字母,數字也是合法的,如果使用TrieTree所以可能導致數的深度很深,而且每個節點的字節點也很多,如果在稀疏的情況下,TrieTree實際上的內存用量更多。所以我使用了HashMap+PriorityQueue來實現TopK模塊。

​ 由於使用的都是java封裝的算法,實際代碼實現也很簡短,所以在這方面也不過多贅述。

性能改進

性能改進的方法

​ 上面說到,我希望我的程序也能對大文件進行處理。而要實現這一要求,有兩個檻:

  1. 在讀入的時候必須嚴格按流讀入,不能加載整個文件到內存中,這一要求我上面已經實現了
  2. 在處理TopK的時候,也不能直接處理整個文件,不然也是內存不足,這個是要改進的地方

​ 而之前查詢到的思路是將整個文件的單詞按hash分到不同的小文件中,之后再對每個小文件進行處理。因為是按hash分割的,所以相同的單詞必然在同一個文件之中,所以最后只要取出每個小文件的前k高出現頻率的詞,再取出k*n的詞中最高的k個,就用有限的內存實現了功能。

​ 我本人是不喜歡直接貼大段代碼的,所以我這里只貼方法名並且說說我他的流程。

public class HugeDataCore{
	...
    //分割的每個小文件
	private File files[];
    //緩存的文件夾
	private File cacheDir;
    //是否啟用分割文件
	private boolean div=false;
    //如果分割文件,記錄要分割成幾份文件
    private int divNum;
    //獲取最高詞頻單詞的個數
	private long topCount =10;
    //文件最大閾值
	private int eachFileSize;
    //優先隊列
    private PriorityQueue<Node> pq = new PriorityQueue<>();
    ...
        

	//通過文件來構造,這樣閾值默認是50mb
	public HugeDataCore(File inputFile){...}
    //可以設置閾值的構造
	public HugeDataCore(File inputFile,int eachFileSize){...}
    
    //判斷並分割文件,超過閾值則啟用分割文件,反之不啟用
	private void divFile(){...}
    //開始處理的函數,在開始之前判斷並分割文件
	public void start() throws ExecutionException, InterruptedException {
		divFile();
		...
	}
    //設置獲取的TopK的個數K
	public void setTopCount(int topCount) {...}
    //設置緩存文件夾,結束會自動刪除文件
	public void setCacheDir(String cacheDir) {...}

    //獲取各種信息
	public long getWordNumber() {...}
	public long getCharNumber() {...}
	public long getLineNumber() {...}
    
    ...
}
  • 實現了一個HugeDataCore類,這個類只能通過File來構建,因為大文件只能從文件中讀取。
  • 在開始調用start來開始處理文件,處理之前會先判斷文件是否超過一個閾值,當然這個閾值可以你自己來設置。如果超過閾值就分割文件,如果不超過就不分割文件。分割文件可以自己設置緩存的路徑。
  • 因為分割文件要讀入一次文件,所以統計字符數、單詞數和函數再分割文件中記錄。
  • 分割完文件后,會將分割的文件存到一個File數組中,之后再start中順序處理生成的文件。
  • 這里處理小文件的邏輯和之前實現的不分割處理文件的Core的邏輯是一樣的,所以直接用文件構的流構造Core,復用了代碼。
  • 最后也是優先隊列類處理處理每個小文件的前k個詞頻最高的詞,獲取到頻率最高的k個詞。

​ 所以理論上,這個程序在空間上是可以對非常大的文件進行處理的

性能優化后的結果

​ 在github上找到了一個隨機生成字符串的Java代碼:wordnet-random-name,我是用他來生成隨機字符串的。因為他是用詞庫的,而且詞庫有限,所以認為如果生成非常大量的字符串,生成的字符串是非常稠密的。

​ 對於隨機生成的150mb大小的文件,大概1000w個單詞,我分別用了不分割文件分割文件的方法跑了一遍:

image-20210226170528118

不分割文件

image-20210227130110730

分割文件

​ 這里解釋一下這個圖,上面的柱狀圖是這段時間內分配的內存,而下面折線圖這是實際使用的內存,橙色豎條則是

​ 可以看到,如果不分割文件,內存在處理TopK的時候是到達了1g左右,而分割文件就內存則被控制在了200mb左右。這還是在我按流讀入的情況下,我讓其他同學用他緩存整個文件后進行處理的讀入實現運行,在讀入階段的內存使用就到達了1g左右。在分割文件的情況下,程序大概跑了9秒。

​ 同樣的,我生成了一個1500mb左右的數據,大概1億個單詞,在生成數據的時候,在每個單詞后面添加一個1000以下的隨機數,讓數據變得比較稀疏:

image-20210226170639739

不分割文件

image-20210226170801422

不分割文件CPU使用圖

image-20210227130258936

分割文件

image-20210227130228765

分割文件CPU使用圖

​ 可以看到,如果是不分割文件,內存使用了接近4g,而且一直觸發gc導致cpu占用率也非常高。而分割了文件后,內存占用不超過400mb,而且程序也不會因為一直gc而陷入死鎖。在分割文件情況下,程序大概跑了2分半。

​ 最后,因為對文件進行了分割,所以肯定是比不上直接對文件進行處理的耗時短,而且這還與機器硬盤的讀取速寫有關,所以性能是沒有優勢的。但是我認為在這里用一定的時間換取空間是非常划算的。

單元測試

z

對於判斷單詞合法性的單元測試

@Test
void isWord(){
	//一定是一個只包含字母和數字的字符串
	Assert.assertEquals(true,wr.isWord("abce"));
	Assert.assertEquals(false,wr.isWord("abc1e"));
	Assert.assertEquals(false,wr.isWord("12abce"));
	Assert.assertEquals(false,wr.isWord("3"));
	Assert.assertEquals(false,wr.isWord(""));
	Assert.assertEquals(true,wr.isWord("asodjf123abce"));
}

​ 分別是測試了

  • 連續四個字母為合法
  • 數字對連續字符的隔斷
  • 數字開頭
  • 純數字
  • 空字符
  • 一個字母+數字的合法字符

對有效行數的統計

@Test
void getLineNumber() {
	String string = new String("\n    \naisodjo \t  \n ???---\n\ni\nrr");
	wr.setInputStream(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)));
	while (wr.nextWord()!=null);
	Assert.assertEquals(4,wr.getLineNumber());
}

​ 分別是測試了

  • 開頭空行
  • 空白字符換行
  • 字符+空白字符+換行
  • 非空白字符非有效字符的換行
  • 連續換行
  • 最后無換行的一行

異常處理說明

​ 這個程序異常較少,主要是文件的異常和用戶輸入的異常。

​ 如果用戶輸入的文件不存在或者權限不足,會拋出FileNotFoundException

​ 同樣的,在分割文件時,文件夾的權限不足也會拋出這個異常。

心路歷程與收獲

心路歷程

​ 剛開始感覺這個程序比起之前的什么管理系統簡單的太多,但是當我真的想要按上述的要求編寫程序時,還是發現了許多以前我沒有注意到的點。在之前,我編寫程序的時候,感覺很難控制每個方法的粒度,但是這次在寫單元測試的時候,發現要做到'單元'的話,我之前設計的方法粒度可能太大。

收獲

​ 在之前的編程中,我從來沒有用過單元測試,基本上都是手動輸入樣例然后人工對比結果。這次學到了單元測試的使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM