Quantcast
Channel: Darkthread
Viewing all 428 articles
Browse latest View live

MemoryStream 不可擴展錯誤

$
0
0

記錄自己遇到的蠢問題一枚。

抽象類別 Stream 常被當成輸入輸出參數 ,如此資料可以來自檔案、網路、記憶體或使用者自訂來源,還可套用裝飾者模式(Decorator Pattern)壓縮加密一次完成,提供強大彈性。實務上我常應用的情境是 ClosedXML/OpenXML SDK 之類原本要讀寫 Excel、Word 檔案的場合,函式接受檔案路徑或 Stream 以開啟現有檔案。遇到檔案保存於資料庫,取出的資料格式為 byte[],沒必要寫成暫存檔再開啟,我習慣用 new MemoryStream(byte[] data) 建立 MemoryStream,使用起來跟 FileStream 沒有兩樣。過去這個技巧我主要用在 ClosedXML 讀取 Excel 檔案,最近搬到 OpenXML SDK 操作修改 Word 內容,才發現這個程式寫法有問題。

如下圖所示:

使用 new MemoryStream(byte[]) 建立的 Stream 可正確讀取 Word 文件檔,但在試圖新增內容時遇上「記憶體資料流是不可擴展的 / MemoryStream is not expandable」錯誤。

爬文發現我在這裡犯了一個低級錯誤,依據 MSDN 文件:

MemoryStream Constructor (Byte[])

Initializes a new non-resizable instance of the MemoryStream class based on the specified byte array.

傳入 byte[] 建構的 MemoryStream 大小是固定的,用於唯讀沒什麼問題,甚至只置換少量內容資料空間未變時也還過得去,但遇上要需要擴充大小就必死無疑。

var ms = new MemoryStream();
ms.Write(bin, 0, bin.Length);

搞清楚問題根源,要修好不是難事! 如以上範例,先用 new MemoryStream() 建構可擴展的 MemoryStream,再用 .Write() 寫入 byte[] 內容,搞定收工!


全文檢索筆記 - Lucene.Net (1)

$
0
0

網站專案的規格提到了網站內容的全文檢索,不要求比美 Google 的速度與精準度,提供最基本的關鍵字查詢就成。陸續評估了一些解決方案,整理成筆記備忘兼分享。

談到在 .NET 做全文檢索,不能不提 Lucene.Net這個開源全文檢索引擎!

如果你對 Lucene.Net 很陌生,推薦 CSDN 有篇不錯的入門指引: 使用Lucene.Net实现全文检索

剛開始接觸 Lucene.Net 被一堆術語搞得昏頭轉向,尤其是建立索引欄位時,參數裡有一堆 ANALYZE、NORMS、POSITION、OFFSETS,搞得我一頭霧水。以下整理使用 Lucene 程式庫時可能用到的一些術語:

  • Document 文件
    文件是一堆欄位( Field )的集合,也搜索結果項目的基本單位,一般會對應到實際的文件、檔案、網頁... 等。
  • Filed 欄位
    可分為四類: 參考
    1. Keyword – 不分析但要建立索引,例如: URL、檔案路徑、人名、身分證號、電話號碼… 等。
    2. Unindexed – 不分析也不建索引,呈現結果時一併顯示,但不作為查詢條件。
    3. UnStored –分析並建索引但不保存,可當成查詢條件,但不需出現在查詢結果,例如: 文件內容。
    4. Text - 分析、建索引且保存完整內容可顯示於查詢結果。
  • Term 詞彙
    搜尋條件的基本單位,包含欄位名稱及關鍵字內容。分詞器解析文字內容拆成一個個 Term,搜索時透過 Term 比對才能快速找到目標,分析出的 Term 會儲存於索引檔方便日後查詢。
  • Analyzer 分析器/分詞器
    分詞器負責將文字內容拆解成一個個 Term 以建立索引,搜索時以 Term 為對象遠比 LIKE '%...%' 有效率(Index Seek vs Index Scan)。分詞過程是先將文字內容單元化(Tokenization),提取出 Token 後結合欄位名稱形成 Term。參考
  • 常見中文分詞器
    WhitespaceAnalyzer只去除空白,不支援中文
    SimpleAnalyzer去除字母以外的符號,不支援中文
    StopAnalyzer比 SimpleAnalyzer 再多去除 the, a, this 等虛詞,不支援中文。
    StandardAnalyzer英文部分比照 StopAnalyzer,但支援中文單字切割,每個中文字元一個 Term。
    CJKAnalyzer支援中日韓文字,但中文支援不理想。
    SmartChineseAnalyzer比較標準的中文分詞,但效果也不是很好。
    PanGuAnalyzer盤古分詞器,專為中文研發,有字典檔提高準確度。可用 NuGet 安裝,是蠻流行的中文分詞器。參考1 參考2
    MMSeg也可用 NuGet 直接安裝,以演算法為核心,快速且簡單,但準確率不如盤古。 
    IKAnalyzer中國研發,頗熱門且持續在發展的分詞器,效果看起來很少,但 .NET 版本難尋(Github 有一些棄守的 .NET 版半成品,大陸網站找到的下載連結多已失效,唯一找到的 DLL 版來自 2008 年相容性存疑,放棄未試)
  • Boost 是什麼?
    簡單地說,Boost 是賦與欄位不同權重讓查詢結果更符合預期的機制,可想像成 Google 搜索關鍵字時決定那一筆優先顯示的邏輯,Lucene 透過 Boost 決定查詢結果符合度。
    Boost 可以當名詞用,例如 Document Boost、Field Boost、Query Boost,是一個浮點數,可用於查詢結果排序。預設值為1.0,再乘上多個因子得到最後結果,數字愈大代表愈相關愈重要。Lucence 查詢過程包含布林符合比對以及排序分數計算,Boost 用於排序比分階段。
    Boost 當動詞用時意指增加某個項目的 Boost 值,例如 Boost 某個 Field 代表增加該欄位的 Boost 值,Boost 某個 Documentd 代表增加該文件的 Boost 值。
    當我們對預設的查詢結果不滿意,便可透過 Boost 某個欄位或文件讓結果符合預期。
    例如: 我們可以將標題欄位的權重設定比內文欄位高,如此,標題包含關鍵字與內文包含關鍵字相比,標題有關鍵字應視為更符合查詢條件,我們可以 Boost 標題欄位,使其優先顯示在查詢結果中。Field.SetBoost()可設定Boost值。
    Boost 分為 Index Time Boost 及Query Time Boost。Index Time Boost 指在建立索引時就計算好欄位或文件的 Boost 值。Query Time Boost 則是查詢時賦與搜尋條件不同的 Boost 值以影響顯示結果。 參考
  • Norms 是什麼?
    Norms 是 Index Time Boost 保存 Boost 因子的方法,在建立索引階段產生 Boost 數字儲存下來。Norms 的計算依據是欄位內容長短,符合條件且字數愈少的結果愈優先。
    建立索引時啟用 Norms 會耗用一些運算並佔用每個欄位 1 Byte 的空間,如不需要可停用它以省效能。參考

要了解上述術語的原因是我們在建立 Field 時有一堆參數要指定,初上手常會搞不清楚該怎麼設定,即使讀了參數說明文件若不懂 NORMS、POSITION、OFFSET 等術語還是不知如何下手,以下做個簡單整理:
public Field(string name, string value, Field.Store store, Field.Index index, Field.TermVector termVector);

  • Field.Store
    • YES 或 NO。
      當文字內容很長且不需顯示在查詢結果中,可選擇不存文字本體只留索引以節省空間。
  • Field.Index
    • ANALYZED - 建立索引並分詞(適用內文、標題、摘要等)
    • NOT_ANALYZED - 索引但不分詞,使用 NORMS
    • ANALYZED_NO_NORMS - 索引並分詞,不使用NORMS
    • NOT_ANALYZED_NO_NORMS - 索引不分詞且不用NORMS
    • NOT - 不索引
  • Field.TermVector 參考 Position 與 Offset 差異
    • YES - 統計詞彙(Term)出現頻率
    • WITH_POSITIONS - 統計頻率,記錄出現位置(以詞彙為單位)
    • WITH_OFFSETS - 統計頻率,記錄起迄字元位置
    • WITH_POSITIONS_OFFSETS - 統計頻率,記錄出現位置及起迄字元位置
    • NO - 完全不統計

文章一開始提到的介紹文針對不同應用整理了簡單的欄位參數建議:

結論

Lucene.Net 提供很高的擴充空間與客製彈性,嚴格來說一個框架而非完整解決方案,開發者可以自行調整權重、索引邏輯,滿足各式刁鑽需求。由其運作原理,中文文件的分詞將是搜尋結果符合預期與否的關鍵,盤古分詞器很簡便易用,但既有的分詞及查詢結果離一般對中文檢索期待仍有距離,評估要投入不少的時間優化改善,值不值投資見仁見智,下篇文章再進一步闡述。

全文檢索筆記 - Lucene.Net (2) 盤古分詞

$
0
0

前一篇筆記談完 Lucene.Net 術語與基本觀念,感覺用盤古中文分詞器是不錯的主意。先來個最簡單的「盤古中文分詞->建立索引->查詢關鍵字」 Lucene.Net 範例:

privatestaticstring IndexPath = "E:\\LuceneIndex";
publicstaticvoid SimpleDemo()
{
//指定索引資料儲存目錄
    var fsDir = FSDirectory.Open(IndexPath);
 
//建立IndexWriter
using (var idxWriter = new IndexWriter(
        fsDir, //儲存目錄
new PanGuAnalyzer(), //使用盤古分詞器
true, //清除原有索引,重新建立
        IndexWriter.MaxFieldLength.UNLIMITED //不限定欄位內容長度
        ))
 
    {
//示範為兩份文件建立索引
        var doc = new Document();
//每份文件有兩個Field: Source、Word
        doc.Add(new Field("Source", "阿甘正傳", Field.Store.YES, Field.Index.ANALYZED));
        doc.Add(new Field("Word", "人生就像一盒巧克力,你永遠也不會知道你將拿到什麼。", 
            Field.Store.YES, Field.Index.ANALYZED));
        idxWriter.AddDocument(doc);
 
        doc = new Document();
        doc.Add(new Field("Source", "Spider Man", Field.Store.YES, Field.Index.ANALYZED));
        doc.Add(new Field("Word", "Remember, with great power, comes great responsibility.", 
            Field.Store.YES,
            Field.Index.ANALYZED));
        idxWriter.AddDocument(doc);
 
//建立索引
        idxWriter.Commit();
        idxWriter.Optimize();
    }
 
//查詢示範
 
//若不需刪除文件或修改Norms,第二個參數傳入true採唯讀方式效能較好
    var searcher = new IndexSearcher(fsDir, true); 
//指定欄位名傳入參數
    QueryParser qp = new QueryParser(Version.LUCENE_30, "Word", new PanGuAnalyzer());
    Query q = qp.Parse("巧克力");
    var hits = searcher.Search(q, 10); //查詢前10筆
    Debug.WriteLine($"找到{hits.TotalHits}筆");
foreach (var doc in hits.ScoreDocs)
    {
        Debug.WriteLine($"{searcher.Doc(doc.Doc).Get("Word")}");
    }
}

使用盤古分詞器建立索引,試著查詢「巧克力」,不孚眾望,真的找到了!

不過再多試幾下,就被澆了冷水。改查詢"永遠"... 登楞! 找不到。

分詞搜尋跟傳統印象中 Word/Excel/Notepad 尋找不太相同。分詞器會將整段文字分成一個個 Term,"永"、"遠"、跟 "永遠" 是不同的東西,使用 Luke.Net觀察建立的索引,盤古分詞的真實分詞結果如下。永遠被拆成了永跟遠,而查詢「永遠」PanGuAnalyzer 會判定沒有相符合內容。

用盤古分詞再多做了一些測試:

有些詞被拆成單字或斷錯位置,預期如果直接查詢"永遠"、"什麼"、"不會"、"馬蹄"、"收拾"、"賤人"、"百分之九十九"等詞將不會得到符合的結果:

生命/就/像/一盒/巧克力/你/永/遠/也不/會/知道/你/將/拿到/什/麼
我/達/達/的/馬/蹄/是美麗/的/錯/誤/我不是/歸/人/是個過客
天才/是/百分之一/的/靈/感/加上/百分之/九十九/的/汗水
賤/人/就是/矯/情
再/冷/也不/能拿別人/的/血/暖/自己
我/對/你/的/敬仰/真是/如/滔滔/江水/連綿不絕/又/有如/黃河泛濫一發/不可收拾
對/對/本/為/消遣/作/樂/今日/穿腸兄/居然/對到嘔出幾十兩血/謂/空前/絕/後/小弟/佩服/佩服
未/傳/你/你/就站出來/要不是/做賊心虛/就是/身上/有/屎/你/說/你/是不是/犯/賤

由此可知,中文分詞器決定"查詢效率與準確性",愈是精準將文字解析成單字,索引檔愈小,愈能快速查到正確結果。英文有空白,很容易精確切割詞與詞,將沒有標點的連續中文正確切成詞彙明顯難上許多。字典檔是找出詞彙的捷徑,但仍存在白痴造句法陷阱,例如: 這書本來就不是給小孩、啤酒不如果汁好喝。由於難以 100% 掌握字句原意,有些分詞器會透過針對同一段文字列舉不同組合提高命中率(多元分詞,或稱為最細粒度分詞),例如: 我是程式設計師,拆解成: 我/是/程式/程式設計/設計師/程式設計師。另一個思考方向是乾脆將文字拆成單字或較小的詞彙,例如: 我/是/程/式/設/計/師,查詢「程式設計」相當於找尋同時出現 程/式/設/計 四個詞彙,但如此查詢效能勢必要打折扣。

我找到一個替代做法在這個盤古分詞範例中查到「永遠」- 用 PhraseQuery 拼裝多個字元:

不過,用這招查詢 "永" "遠" "也" 會失敗,原因是分詞結果中的 Term 是 "也不","也" 比對不符! 除非字典檔夠完整,能讓盤古分詞產生更理想的結果,遇到分詞不正確或被拆成單詞都會導致查不到預期結果。(也可能我錯過什麼簡便做法,懇請十方大德賜教)

在盤古分詞器踩到一些坑之後,我回頭改用 Lucene.Net 內附的 StandardAnalyzer,結果好多了! 只要文字相連,就可以查到,不管關鍵字是否為有意義(例如: 「到什」),其邏輯接近 LIKE '%關鍵字%',但預期搜尋效能不如字彙分詞。至於跳幾個字組裝出的「永不知」及順序顛倒的「力巧克」則如預期沒有吻合項目。

經過以上簡單測試,若不考慮效能跟索引空間,看起來 StandardAnalyzer 比盤古分詞簡單可靠,滿足最基本的全文檢索要求,算是已立於不敗之地,確認用 Lucene.Net 不致開天窗。至於中文分詞器的運作細節,就留待下篇筆記再來探討。

全文檢索筆記 - Lucene.Net (3) 自動分詞 vs 詞庫分詞

$
0
0

前篇筆記試用了盤古分詞器跟 StadnardAnalyzer,繼續研究其他分詞器選擇。

英文能依據空白快速精準分詞,中文沒這麼幸運,必須借助演算法,邏輯複雜許多。中文分詞主要有兩個方向: 第一種是自動分詞,依循固定規則自動切分,例如: 一元分詞、二元分詞;第二種則是詞庫分詞,查詢詞庫識找出已知詞彙;也有分詞器選擇兩種做法兼用,以求互補。

一元分詞與二元分詞的優點是做法簡單,不需維護詞庫,但其索引幾乎跟原文一樣大,查詢效率也較差;詞庫分詞的索引可縮小到原文的 30%(參考),但詞庫完整性是成敗關鍵,需要持續訓練(甚至要考慮借助機器學習、人工智慧)提高精準度,要投注的心力不容小覷。

大陸對 Lucene 中文分詞的研究較多,我找到一篇 Lucene中文分析器的中文分词准确性和性能比较實測多種分詞器,看實例比較容易搞清楚什麼是一元分詞、二元分詞與詞庫分詞:

原文

2008年8月8日晚,举世瞩目的北京第二十九届奥林匹克运动会开幕式在国家体育场隆重举行。

StandardAnalyzer 一元分詞

2008/年/8/月/8/日/晚/举/世/瞩/目/的/北/京/第/二/十/九/届/奥/林/匹/克/运/动/会/开/幕/式/在/国/家/体/育/场/隆/重/举/行/

ChineseAnalyzer 一元分詞但去除英文字

年/月/日/晚/举/世/瞩/目/的/北/京/第/二/十/九/届/奥/林/匹/克/运/动/会/开/幕/式/在/国/家/体/育/场/隆/重/举/行/

CJKAnalyzer 二元分詞,不管合不合理,兩兩組合就算一個詞

2008/年/8/月/8/日晚/举世/世瞩/瞩目/目的/的北/北京/京第/第二/二十/十九/九届/届奥/奥林/林匹/匹克/克运/运动/动会/会开/开幕/幕式/式在/在国/国家/家体/体育/育场/场隆/隆重/重举/举行/

PaodingAnalyzer 庖丁分詞器,細粒度全切,字典查不到就二元分詞

2008/年/8/月/8/日/晚/举世/瞩目/举世瞩目/目的/北京/二/第二/十/二十/第二十/九/十九/二十九/九届/奥林/奥林匹克/运动/运动会/奥林匹克运动会/开幕/开幕式/国家/体育/体育场/隆重/举行/隆重举行/

IK_CAnalyzer 細粒度全切,字典查不到的二元分詞

2008年/2008/年/8月/8/月/8日/8/晚/举世瞩目/举世/瞩目/目的/北京/第二十九届/第二十九/第二十/第二/二十九/二十/十九/九届/九/奥林匹克运动会/奥林匹克/奥林/运动会/运动/开幕式/开幕/在国/国家/国/体育场/体育/隆重举行/隆重/举行/行/

MIK_CAnalyzer 最大匹配與細粒度全切搭配

2008年/8月/8日/晚/举世瞩目/目的/北京/第二十九届/奥林匹克运动会/开幕式/在国/国家/体育场/隆重举行/

MMAnalyzer 字典查不到時一元分詞

2008/年/8/月/8/日/晚/举世瞩目/北京/第二十/九届/奥林匹克运动会/开幕式/国家/体育场/隆重举行/

中文分詞是門深奧學問,有膨脹率(原文跟索引大小比率)、準確率、召回率、F值、消歧義... 等等議題值得探討,但我的目標是尋找現成全文檢索解決方案,一頭裁進去還要不要驗收? 但對於有興趣深入了解的同學,附上我找到的幾篇文章:

回到中文分詞器選擇上,各家中文分詞器都有自己的設計哲學,除了考量命中率,也必須考慮建立索引耗用資源及產生的詞彙數,詞彙數愈多命中率上升,但要付出索引檔變大及查詢效能下降的代價。庖丁分詞、IKAnalyzer、MMAnalyzer 可靠字典檔找出詞彙,找不到的部分則用二元或一元分詞,避免如盤古分詞拆錯就回天乏術的缺點。但使用 Lucene.Net 要有心理準備,有些好用的中文分詞器只有 Java 版,未移植到 .NET,看得到不一定吃得到,Lucene.Net 可用的選項沒那麼多。

所以我們再把焦點要放在 Lucene.Net 現成可用中文分詞器的比較上。

除了盤古分詞跟 StandardAnalyzer,我還找到兩個 NuGet 可下載安裝的中文分詞器:

MMSeg 是不少人推崇的演算法,簡單、快速、有效。而 CWSharp 將詞庫分詞、一元分詞、二元分詞一網打盡,基本上有了這兩個分詞器已囊括本次評估的主要分詞演算法。經過一番摸索,我成功使用 MMSegAnalyzer、SimpleAnalyzer、ComplexAnalyzer、CWSharp 詞庫、CWSharp 一元分詞、CWSharp 二元分詞完成索引及查詢,測試範例如下。

class Program
    {
staticvoid Main(string[] args)
        {
            AnalyzerTest("盤古分詞", "D:\\PanGuIndex", new PanGuAnalyzer());
            AnalyzerTest("標準分詞", "D:\\StdAnalyzerIndex", 
new StandardAnalyzer(Version.LUCENE_30));
            AnalyzerTest("MMSeg MaxWord", "D:\\MMSegIndex", new MMSegAnalyzer());
            AnalyzerTest("MMSeg Simple", "D:\\MMSegSimpIndex",
new Lucene.Net.Analysis.MMSeg.SimpleAnalyzer());
            AnalyzerTest("MMSeg Complex", "D:\\MMSegCompIndex",
new Lucene.Net.Analysis.MMSeg.ComplexAnalyzer());
            AnalyzerTest("CWSharp詞庫分詞", "D:\\CWStdIndex", 
new CwsAnalyzer(
new Yamool.CWSharp.StandardTokenizer(
new FileStream("cwsharp.dawg", FileMode.Open))));
            AnalyzerTest("CWSharp一元分詞", "D:\\CWUniIndex", 
new CwsAnalyzer(new UnigramTokenizer()));
            AnalyzerTest("CWSharp二元分詞", "D:\\CWBiIndex", 
new CwsAnalyzer(new BigramTokenizer()));
            Console.Read();
        }
 
publicstaticvoid AnalyzerTest(string title, 
string indexPath, Analyzer analyzer)
        {
//指定索引資料儲存目錄
            var fsDir = FSDirectory.Open(indexPath);
 
//建立IndexWriter
using (var idxWriter = new IndexWriter(
                fsDir, //儲存目錄
                analyzer, 
true, //清除原有索引,重新建立
                IndexWriter.MaxFieldLength.UNLIMITED //不限定欄位內容長度
            ))
 
            {
//示範為兩份文件建立索引
                var doc = new Document();
//每份文件有兩個Field: Source、Word
                doc.Add(new Field("Word", 
"生活就像一盒巧克力,你永遠也不會知道你將拿到什麼。",
                    Field.Store.YES, Field.Index.ANALYZED));
                idxWriter.AddDocument(doc);
 
//建立索引
                idxWriter.Commit();
                idxWriter.Optimize();
            }
 
 
            var searcher = new IndexSearcher(fsDir, true);
//指定欄位名傳入參數
            QueryParser qp = new QueryParser(Version.LUCENE_30, 
"Word", analyzer);
 
            Action<string> testQuery = (kwd) =>
            {
                var q = qp.Parse(kwd);
                var hits = searcher.Search(q, 10);
                Console.WriteLine($"查詢「{kwd}」找到{hits.TotalHits}筆");
            };
            Console.WriteLine($"{title}測試");
            testQuery("生活");
            testQuery("就像");
            testQuery("一盒");
            testQuery("巧克力");
            testQuery("永遠");
            testQuery("不會知道");
            testQuery("拿到");
            testQuery("什麼");
            Console.WriteLine("========================================");
        }
    }

使用內建詞庫或範例詞庫,實測結果如下:

盤古分詞測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到0筆
查詢「不會知道」找到0筆

查詢「拿到」找到1筆
查詢「什麼」找到0筆
========================================
標準分詞測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到1筆
查詢「不會知道」找到1筆
查詢「拿到」找到1筆
查詢「什麼」找到1筆
========================================
chars loaded time=89986ms,line=12638,on file=C:\Lab\LuceneTest\bin\Debug\data\chars.dic
words loaded time=2465010ms,line=149852,on file=words.dic
load all dic user time=2980004ms
unit loaded time=0ms,line=22,on file=C:\Lab\LuceneTest\bin\Debug
MMSeg MaxWord測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到1筆
查詢「不會知道」找到0筆
查詢「拿到」找到1筆
查詢「什麼」找到1筆
========================================
MMSeg Simple測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到1筆
查詢「不會知道」找到0筆
查詢「拿到」找到1筆
查詢「什麼」找到1筆
========================================
MMSeg Complex測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到1筆
查詢「不會知道」找到0筆
查詢「拿到」找到1筆
查詢「什麼」找到1筆
========================================
CWSharp詞庫分詞測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到1筆
查詢「不會知道」找到1筆
查詢「拿到」找到1筆
查詢「什麼」找到1筆
========================================
CWSharp一元分詞測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到1筆
查詢「不會知道」找到1筆
查詢「拿到」找到1筆
查詢「什麼」找到1筆
========================================
CWSharp二元分詞測試
查詢「生活」找到1筆
查詢「就像」找到1筆
查詢「一盒」找到1筆
查詢「巧克力」找到1筆
查詢「永遠」找到1筆
查詢「不會知道」找到1筆
查詢「拿到」找到1筆
查詢「什麼」找到1筆
========================================

以「生活就像一盒巧克力,你永遠也不會知道你將拿到什麼。」為例,分別查詢「生活」、「就像」、「一盒」、「巧克力」、「永遠」、「不會知道」、「拿到」、「什麼」。其中盤古分詞最不理想,找不到「永遠」、「不會知道」、「什麼」;MMSeg 的三種實作只錯過「不會知道」;CWSharp 跟內建的 StandardAnalyzer 則全部都查得到。

單以查詢結果涵蓋度,CWSharp 跟 StandardAnalyzer 全過,MMSeg 錯失不算標準常用詞彙的「不會知道」稍稍遜色。StandardAnalyzer 全部拆成單詞,查詢原理不同先不納入比較,進一步研究都有使用詞庫的 CWSharp 及 MMSeg,會發現查不查得到跟詞庫分詞結果有關。MMSeg 之所以查不到「不會知道」是因為依詞庫拆成「也不」「會」「知道」,三個詞彙組不出「不會知道」。而 CWSharp 的詞庫拆成「也」「不」「會」「知道」,可以由後三者組出「不會知道」。

依據這個原理,如果我們查詢「活就像」這種不合理詞彙,所有使用詞庫的分詞器(盤古、MMSeg、CWSharp語庫法)全軍覆沒,只有一元分詞跟二元分詞能過關。

盤古分詞測試
查詢「活就像」找到0筆
========================================
標準分詞(一元分詞)測試
查詢「活就像」找到1筆
========================================
MMSeg MaxWord測試
查詢「活就像」找到0筆
========================================
MMSeg Simple測試
查詢「活就像」找到0筆
========================================
MMSeg Complex測試
查詢「活就像」找到0筆
========================================
CWSharp詞庫分詞測試
查詢「活就像」找到0筆
========================================
CWSharp一元分詞測試
查詢「活就像」找到1筆
========================================
CWSharp二元分詞測試
查詢「活就像」找到1筆
========================================

由上述測試,對 Lucene.Net 分詞器選擇可做個粗淺結論: 如果不在乎索引大小且對搜尋效能要求不嚴嚴苛,可選擇一元分詞或二元分詞;選擇詞庫分詞,索引較小且查詢效能較好,但遇到詞庫未涵蓋的詞彙會出現查不到的狀況,而找不到又分為詞彙本身不合理以及詞庫未函蓋兩種狀況,前者需向使用者解釋查詢無效字彙不在系統支援範圍,後者則需要持續增補詞庫。要選哪一種策略,甚至二者並用,可視專案的需求而定。

全文檢索筆記 – Lucent.Net (4) 詞庫校正

$
0
0

體會過自動分詞(一元分詞、二元分詞)與詞庫分詞的特性差異,但是到目前為止有個問題一直被忽略,我測試用的詞庫直接下載自網路,內容是簡體中文,拆解精準度大有問題。

以 CWSharp 詞庫分詞為例,使用 Github 下載的 cwsharp.dawg 詞庫檔分析這句中文「競選活動已日趨白熱化,參選人莫不全力尋求廠商支援,其中以鄭少秋勝算最大。」,使用 Luke.net 查看分詞結果如下:

雖然還是能查到關鍵字,但分詞結果並不好,幾乎都拆成單一字元,跟一元分詞沒什麼兩樣。這意味詞庫命中率極低,其根本原因在於我們用的詞庫是簡體。將原文換成簡體 - 「竞选活动已日趋白热化,参选人莫不全力寻求厂商支持,其中以郑少秋胜算最大。」 再測一次即可看出差異。大部分的動名詞都被挑出來,連人名「鄭少秋」都能被識別成單一詞彙,這才是應有的結果:

由此可知,直接用簡體字庫僅能命中簡繁體用字相同的詞彙,準確率有限,要有效解析繁體中文內容,詞庫必須轉成繁體,或更進一步改用繁體專屬詞庫。在此以 CWSharp 為例,示範如何轉換及製作詞庫。CWSharp 使用名為 DAWG(Directed Acyclic Word Graph) 的檔案格式保存詞庫(其查詢效率狂勝清單及二元樹,參考),DAWG 是由詞彙清單及字元頻率表轉換而成,Github有簡體範例詞庫的原始檔 – cwsharp.dic, cwsharp.freq,以及轉換程式範例

借用 NuGet 可得的微軟簡繁轉換元件 ChineseConverter將簡體轉成繁體,就能轉換出繁體中文版詞庫檔 - cwsharp-tw.dawg。

staticvoid BuildDawgFile()
        {
            var rootPath = ".\\DAWG";
            var wordUtil = new WordDict();
//加载默认的词频
using (var sr = new StreamReader(rootPath + @"\cwsharp.freq", Encoding.UTF8))
            {
string line = null;
while ((line = sr.ReadLine()) != null)
                {
if (line == string.Empty) continue;
                    line = ChineseConverter.Convert(line, //簡體轉繁體
                        ChineseConversionDirection.SimplifiedToTraditional);
                    var array = line.Split(' ');
                    wordUtil.Add(array[0], int.Parse(array[1]));
                }
            }
//加载新的词典
using (var sr = new StreamReader(rootPath + @"\cwsharp.dic", Encoding.UTF8))
            {
string line = null;
while ((line = sr.ReadLine()) != null)
                {
if (line == string.Empty) continue;
                    line = ChineseConverter.Convert(line, //簡體轉繁體
                        ChineseConversionDirection.SimplifiedToTraditional);
                    wordUtil.Add(line);
                }
            }
//保存新的dawg文件
            wordUtil.SaveTo(new FileStream(".\\cwsharp-tw.dawg", FileMode.Create));
        }

改用 cwsharp-tw.dawg 重測,大部分詞彙就被正確識別出來了!

理論上,詞庫愈專業愈完整愈貼近檢索文章的用語習慣,建立索引及搜尋效能愈好。中研院有數萬甚至百萬筆等級的中文詞庫,堪稱最完整最權威的來源,不過它們不是開放資料,需考量授權可用性與成本。採 CC 3.0 授權的教育部國語辭典,則是另一個專業且可行的來源。需記住一點,詞庫分詞真要做到犀利精準,不可能單靠現成詞庫,看的是長期養成培育的功夫,持續補充新名詞(例如: 比特幣、聖結石),不斷學習及優化,是條不歸路。

考量飼養詞庫的成本,若全文檢索非網站的核心價值所在,文件數不多且對搜尋效能要求不高,二元分詞(索引及效率較一元分詞稍好)會是較省事的選擇。

Chrome 網頁中文變醜之謎

$
0
0

我習慣將 Chrome 標準字型設成思源黑體字型, 除非網頁硬將 font-family 指定成細明體(例如: Mobile01),換了字型讓網頁質感變好,比新細明體賞心悅目許多。

Pocket是我慣用的稍後再讀服務,在 FB 或爬文時看到不急著看但值得花時間讀的相關文章,我會先丟進 Queue 裡收藏,有空再讀。在使用 Pocket 網頁介面閱讀文章時我注意到一件事 – 文字閱讀模式(不開啟原始網頁,改用 Pocket 自訂樣式呈現文章內容) 下,標題字型是 Chrome 預設的思源黑體沒錯,但內文部分中文變得很醜,但不是新細明體。

原以為是 CSS 被設成某種特殊中文字型,但使用 F12 工具檢查,內文區的 font-family 是 inherit,Rendered Fonts 則顯示最後用的是 SimSun 新宋體(相當於簡體中文界的新細明體吧)。inherit 理論上該使用 Chrome 的預設字型,應該是 Noto Sans CJK TC Regular 才對。

爬文找到相關文章 Chrome 18 以上中文字體變醜的原因及暫時解法 - Yu-Cheng Chuang’s Blog,提到 Chrome 遇到 lang="zh" 會視為簡體中文(lang="zh-tw" 才是繁體中文)的行為。檢查 HTML,果然在內文 <div> 發現 lang="zh",而標題沒有,這樣就能解釋標題跟內文字型為何不同,二者的 font-family 都是 inherit,但內文因 lang="zh" 被視為簡體,故用了簡體中文的預設字型 SimSun。

使用 Chrome Adavanced Font Settings外掛可找到 Chrome 針對簡體中文的進階字型設定,三種字型風格中的 Serif 被設定新宋體,Standard/Sans-Serif 則是微軟雅黑。將三者都改成思源黑體,就可以解決 lang="zh" 讓字型變醜的問題了。

另外,Pocket 閱讀介面可選擇使用 Serif 或 Sans Serif 字型,若不改進階字型設定,選 Sans Serif 會改用微軟雅黑體,也可避開醜醜的新宋體。但如果不想在其他網站也因 lang="zh" 中文變醜,修改簡體中文預設字型是根本解決之道。

【茶包射手日記】網頁在某支手機無法使用

$
0
0

同事貢獻新鮮茶包一枚。查到最後發現是低級錯誤,但念在用電話跟 LINE 遠端偵錯耗了三個小時,值得記錄並列為日後問題排除參考。

最初的報案內容是某位使用者剛換了 iPhone 8 新手機,要連上某個例行工作網站查資料,輸入帳號密碼卻無法登入。我們試了自己的手機及平板檢測正常,原以為是使用者個人帳號被鎖或失效導致登入失敗,但檢查帳號狀態正常,歷經一陣同鴨講追問細節後才搞清楚,其實登入有成功,而是畫面不正常且無法操作。(跟報案內容大不相同,隔空抓藥好刺激呀)

為對照比較,商請使用者借其他人的手機測試結果正常,這下確認問題出在新手機上,集中火力嚴加拷問,終於查到關鍵:

問題手機的 JavaScript 被停用了被停用了被停用了!

JavaScript 啟用後問題果然煙消雲散,但留下兩個謎團:

  1. 停用 JavaScript 後會有一大堆網站無法使用,何以使用者未察覺?
    推測是新手機的關係,使用者平日以電話、簡訊及 LINE 為主,還沒太多機會用瀏覽器上網,因此沒注意到 JavaScript 被停用。
  2. JavaScript 啟用選項藏在 Safari 進階設定選單的深處,非一般人會想到要更改的設定(使用者本身也不知有此選項),誤觸可能性極低。JavaScript 到底是被誰關閉的,真的是謎。

總之,由這個案例學到一些經驗:

  1. 先使用對照法釐清問題與帳號資料還是裝置有關,有助於加速破案。
  2. 行動裝置的 App 使用比率高,瀏覽器非日常必備,JavaScript 停用未必會被察覺,應列入行動裝置瀏覽網站異常之優先檢查項目。
  3. 如網頁缺少 JavaScript 就無法運作,可考慮加註警示以便在第一時間示警,最簡單的做法是用 <noscript>標籤加註文字。
    例如: <noscript>快把他X的JavaScript給我打閞,不然別碰我!</noscript>(誤)

使用 CSS 實現標題單行置中多行靠左

$
0
0

跟同事討論到一個需求,要在顯示文章的網頁實現「標題只有一行時置中顯示;若文字較多折行時則靠左對齊」的效果。起初程序員大腦想到的做法是用 JavaScript 依文字長度動態調整 text-align 樣式,但由於折行與否是瀏覽器依字型大小、容器寬度自行裁量,難以依據字數直接推算,於是我開始揣摩由文字元素高度偵測行數的雞鳴狗盜招術...

爬文後才發現我把事情想得太複雜了,這個需求用 CSS 就能搞定,一行程式都不用寫。做法是用 <div> 包住 display: inline-block 的 <span>,將 <div> 設成 text-align: center,<span> 設成 text-align: left。

<style>
.box {
  border: 1px solid gray;
  margin: 12px;
  padding: 6px;
  width: 420px;
}
.flex-align-title {
  text-align: center;
  width: 100%;
}
.flex-align-title span {
  text-align: left;
  display: inline-block;
}
</style>
<divclass="box">
<divclass="flex-align-title">
<span>網路盛傳連中樂透頭彩 黑大:子虛烏有</span>
</div>
</div>
<divclass="box">
<divclass="flex-align-title">
<span>資訊史重大里程碑 某部落客成功實現GUID碰撞實驗 專家:機率比被隕石爆頭還低</span>
</div>
</div>

搞定收工,就這麼簡單!

想動手實測的同學可試玩 CodePen Live Demo

註: GUID 重複與樂透梗來自這裡


筆記:比特幣挖礦在挖什麼?

$
0
0

區塊鏈跟比特幣最近熱到發燙,沒幻想過靠它致富(甚至覺得仰賴鉅量能源運作的虛擬貨幣很不環保),倒是對其原理奧義充滿興趣。先前看過不少深淺文章,限於慧根,對其運作原理仍一知半解,知道所謂礦工挖礦類似暴力破解雜湊(Hash)函式,對為什麼驗證交易真實性會扯上破解雜湊值毫無概念。

今天看完一部介紹短片(其實不算短,26分20秒)豁然開朗,欣喜之餘推薦給跟我一樣有興趣了解比特幣挖礦在挖什麼碗粿的同學:(記得開中文字幕。影片還算淺白,但真心覺得要懂公私鑰、雜湊碼,有粗淺密碼學基礎才能下嚥)

影片

隨手筆記重點如下:

  • 加密貨幣(以下以比特幣為主)的基礎是一份公開流傳的帳冊,記載全市場每一筆交易記錄(例如Alice給Bob 100元)
  • 在帳冊加入交易記錄時,交易者需以其私有金鑰對該記錄加上數位簽章,以產生不可否認性
  • 交易記錄包含時間戳記及唯一序號,即使兩筆記錄的交易對象與金額相同,數位簽章也不會相同,故無法靠複製產生重複交易記錄
  • 帳冊被拆成多個區塊,每個區塊附有依其內容產生的雜湊值(SHA256),下一個區塊需包含前一區塊的雜湊值,串連成所謂的區塊鏈
  • 篡改某個區塊內容,會改變該區塊的雜湊值,連帶下一個區塊跟下下一個區塊的雜湊值也要修改
  • 為讓篡改難如登天,建立區塊時需加入一段內容讓SHA256雜湊值的前N個(例如60個)位元組剛好是0,這只能靠嘗試各種數字組合暴力破解
  • 礦工負責傾聽網路上的交易訊息,設法用最短的時間嘗試在包含新交易記錄的區塊加入不同內容組合,讓區塊SHA256前60位為零,符合此一條件就算區塊建立完成,馬上廣播出去,第一個建立區塊的礦工可以獲得一定數目的比特幣做為獎勵
  • 礦工建立區塊的過程就稱為挖礦,由於只有第一個建立區塊的礦工獲得獎勵,即使擁有強大的運算力,也要憑運氣拼人品,還真的跟挖礦淘金沒兩樣
    延伸閱讀: 挖比特幣的礦工都賺瘋了:直擊中國四川、東北、內蒙古的超級大礦場 - TechOrange
  • GPU原本用於3D圖形運算,其運算原理剛好與SHA256吻合,用來破解SHA256效率高成本又比用CPU低,故成為挖礦機主流
  • 為防止有人偽造交易,比特幣有個防止偽造交易的終極武器—當區塊鏈出現多個版本時,以較長的一份為準
  • 建立區塊需要可觀的運算能力(等同暴力破解SHA256),除非篡改區塊者擁有的超過全球礦工總和一半以上的運算能力,才可能維持偽造版本的區塊鏈長度不被直實版本超越,一旦被超越,篡改版作廢便白忙一場
  • 比特幣規定每10分鐘產生一個區塊,當參與的礦工數愈來愈多、計算機能力愈來愈強大,可透過提高建立區塊SHA256所需的起始0長度(例如從60個提高到72個),藉以調節挖礦難度
  • 2009年開始時建立一個區塊礦工可得到50枚比特幣獎勵,2012 Nov起是25枚,2016 Jul起12.5枚,2020 Feb起6.25枚,每4年減半,故總數2100萬個,永遠不會超過
  • 區塊建立獎勵會不斷遞減,礦工的另一個收入來源來自支付者額外支付交易手續費

同場加映:另一部有趣的相關影片,破解SHA256到底有多難?

影片

SHA256相當於40億相乘8次,假設:

最頂級的GPU每秒可以算10億次SHA256,我們在電腦塞進多顆GPU,做成一台一秒可完成40億次SHA256計算的超級電腦。
假設Google所有伺服器運算能力的1000倍才能跟40億台上述超級電腦不相上下。
假設地球的人口有一半的人,每個人擁有1000家Google的運算能力。
假設銀河系1%的恆星有一顆像地球的行星,剛好有4億人口每個人持有1000家Google的運算力。
假設宇宙碰巧就有40億個像銀河系這樣有一堆地球且人人有Google的星系。
請以上40億個星系上的所有地球,人人都拿出自己的1000倍Google運算能力,一起算上5070億年(差不多是宇宙年齡的37倍),將會有40億分之1的機會可以破解SHA256。

2017 觀音山馬

$
0
0

山路跑滿跑好的小而美觀音山馬,連續第三年。(20162015)

氣象預報週五放晴一天,週末兩日又再陰雨濕冷。週六一早雨勢不小,心想不妙,今年「跑馬總有好天氣」運勢已劃上句點了嗎?週日一早起床,啊哈! 雨停了,感謝老天。

七點才起跑不用摸黑早起真好,六點半抵達微風運河,會場跟上一場根除小兒麻痺扶輪社公益路跑相同,只是今天有硬斗的山路等著我... (抖)

遠方天空滿是黑鴉鴉的烏雲,但沒下雨已屬萬幸~

蘆洲地方特色—神將,大會口號:「跑馬有神助,輕鬆跑山路」。連跑三年從沒感覺山路輕鬆過,可能是跑前忘了跟神將合照,下回試試。

長官、民代各方人馬致詞時,四軸空拍機在頭上飛呀飛,空拍似乎已成大型戶外活動的趨勢。

七點準時起跑,全馬參賽人數不及七百人,又是地方路跑社團主辦,是我最愛的小而美比賽。

起跑後先繞微河運河一圈約3.5K後進入河濱道,途經觀音坑溪橋、獅子頭釣魚台(釣魚台是我們的!)、關渡橋一路北行。這條路線是三重、土城、蘆洲、板橋一帶路跑的必經賽道,加上不到一個月前才剛跑過,早無新鮮感,但是為什麼一看到關渡大橋,我還是會舉起相機呢?

過了關渡大橋沒多久,左轉過天橋彎進小巷繞了一陣後開始爬坡,可怕的山路來了~

路旁人家養的鴨子,大清早驚見大批人馬跑來跑去看傻了,一整群動也不動,很有趣。

遠方山頭被雲籠罩,今天有機會回味跑進雲裡的感覺囉~

果不其然,還不到觀音山遊客中心已身處雲霧中。

一群人在雲霧中奔跑,別有趣味。

最高點:硬漢嶺步道口。天候不佳遊客少,難得拍照不用避開路人。

前途一片白茫茫,追著遠方跑者的背影前進,又是另一番意境。

今年路線微調,起跑前大會宣告全馬折返點有驚喜,是一處鮮為人知的祕境。喝! 居然有霸王龍擋道?

祕境是兩塊嶙峋怪石包夾一塊滿是綠意的狹小腹地,景色稱不上壯闊倒也別緻,是小而美的秘境~

折返後轉往八里方向下山。大會貼心,行至此地若被山路折磨到精神失常,可直送八里療養院。(喂)

繞回觀音山風景區入口,天氣漸晴,路途也接近尾聲。

回程經過關渡大橋,不爭氣的我又... 拍了一張。(跪地吶喊) 為什麼我意志力這麼薄弱呢?

淡水河邊有好多人在釣魚,跑著遇到兩個年輕人拎著釣竿要回家,好奇一問「有釣到嗎?」,對方搖搖頭,我再問「是經常釣不到還是偶爾會釣不到」,對方楞了足足五秒,才勉強擠出一句「經常釣不到」。現在回想,我好像做了一個在傷口撒鹽加搓揉的動作,槓龜哥(喂),對不起!

花了 5:22:32 回到會場,霸王龍已在終點久候(該不會跑回來的吧?),開心跟牠擊掌,再下一馬。

本屆由澎澎裙美眉們負責掛牌。

補上獎牌照

      

我的 Windows 10 倉頡中文輸入筆記

$
0
0

使用倉頡輸入超過二十年,當年升級 Windows 8 時最震驚的莫過於「新倉頡輸入法」被移除,回頭改用必須選字的「倉頡輸入法」內心有萬頭羚羊狂奔。(但是另外也有很多人因為必須選字的「ㄅ半」注音輸入被移除哀嚎;輸入法這玩意跟信仰一樣,大家各有所愛且難以撼動)

Windows 8 時代要裝回新倉頡跟ㄅ半很麻煩,還需要複製安裝輸入法檔案(參考: 如何在 Windows 8 中新增注音 (ㄅ半)、新倉頡、新速成輸入法 ),Windows 8.1 起新倉頡等輸入法改回內建但隱藏,修改機碼(Registry)即可開啟(參考: 在 Windows 8.1 中新增注音 (ㄅ半)、新倉頡、新速成輸入法 (僅適用桌面模式))。做法是將以下這段文字存成「新倉頡.reg」,直接點兩下匯入或按右鍵選「合併」,輸入法清單就會有新倉頡可選。

Windows Registry Editor Version 5.00
 
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP\{B115690A-EA02-48D5-A231-E3578D2FDF80}\LanguageProfile\0x00000404\{F3BA907A-6C7E-11D4-97FA-0080C882687E}]
"Description"="Microsoft New ChangJie"
"Display Description"=hex(2):40,00,25,00,53,00,79,00,73,00,74,00,65,00,6d,00,\
  52,00,6f,00,6f,00,74,00,25,00,5c,00,53,00,59,00,53,00,54,00,45,00,4d,00,33,\
  00,32,00,5c,00,69,00,6e,00,70,00,75,00,74,00,2e,00,64,00,6c,00,6c,00,2c,00,\
  2d,00,35,00,30,00,39,00,33,00,00,00
"IconFile"=hex(2):25,00,53,00,79,00,73,00,74,00,65,00,6d,00,52,00,6f,00,6f,00,\
  74,00,25,00,5c,00,73,00,79,00,73,00,74,00,65,00,6d,00,33,00,32,00,5c,00,49,\
  00,4d,00,45,00,5c,00,49,00,4d,00,45,00,54,00,43,00,5c,00,49,00,6d,00,54,00,\
  43,00,54,00,69,00,70,00,2e,00,44,00,4c,00,4c,00,00,00
"IconIndex"=dword:00000002
"Enable"=dword:00000000
"ProfileFlags"=dword:00000004

不過,使用密技招喚的新倉頡有些限制,它只能用於傳統桌面程式,在 Windows 8/10 App 跟新一代的 Windows 系統介面(例如下圖)敲不出中文:

另外,我常遇到新倉頡切換視窗後失效,在 Windows Live Writer 之類的老 Windows Form 程式被停用(如下圖),必須改用其他中文輸入法或重啟程式。

除了新倉頡,我也試過幾個替代方案:

  • demoshop 新自然輸入法特別版
    新自然的字庫選字及學習功能很強大,demo 的佛心版擁有付費版才支援的倉頡輸入,我在 Windows 8 時代用得很開心,可惜與 Windows 10 不相容,一直狂當,只得棄用。
  • PIME 輸入法平台內附的酷倉輸入法
    新酷音威力強大,但酷倉相對陽春許多,不支援自動選字也無法先輸一段文字再調整選字,除了標點符號便捷輸入,功能相當於 Windows 內建的倉頡輸入法。

最後,在 Windows 10 還是新倉頡最順手,遇到新介面及 App 則改用酷倉,至於快速輸入及切換,磨合一陣子試出順手的設定,整理如下:

  1. 新倉頡全形標點快速鍵
    ,= Ctrl+,
    、= Ctrl+'
    。= Ctrl+.
    ?= Ctrl+Shift+/
    != 先按`再按1
    ;= Ctrl+;
    := Ctrl+Shift+;
    【= Ctrl+[
    】= Ctrl+]
    「」『』... = Ctrl+[ 與 Ctrl+] 再按上下鍵選取
    通用技巧: 先按`再輸入符號可選取相關符號,例如: 輸入`@ 可選取@⊕⊙㊣﹫
    ()= `( 及 `)
    += `+
    -= `-
    /= `/
    ÷= `/再按向下鍵選取
    ×= `*再按向下鍵選取
    ± = `+再按向下鍵選取
    —(波折號) = `-再按向下鍵選取
    …(刪節點) = 輸入倉頡字根 Z難日中(ZXAL)
    符號螢幕小鍵盤 Ctrl+Alt+,
  2. 酷倉全形標點快速鍵
    Ctrl+標點 帶出選取視窗(新酷音則是 Shift+標點) 各鍵對應的符號可自訂
  3. 中英文輸入切換
    中文輸入時按 Shift 可切換輸入中文或英文,切到英文模式雖然輸入英文沒問題,但標點符號 Ctrl 快速鍵會跟部分軟體打架。例如 Visual Studio 預設使用 Ctrl+. 帶出智慧標籤會跟句號「。」相衝,雖然可以修改 Visual Studio 設定避開,但相衝按鍵不只一組比較麻煩,故我會按 Alt-Shift 關閉中文輸入比較省事。
    後來,我找到最順手的解法是新增一個「英文(美國)」語系,並使用跟中文輸入法相似的 Ctrl-Shift+N 快速鍵切換到英文輸入法,詳細做法可參考下一點。
  4. 輸入法切換快速鍵
    Windows 預設使用 Ctrl-Shift 切換不同中文輸入法,開啟關閉中文輸入則靠 Alt-Shift,我找到一個更直覺快速的做法,為新倉頡、酷倉、新酷音設定 Ctrl-Shift-1/2/3 快速鍵,另外新增英文語系英文輸入法 設為 Ctrl-Shift-0,用一致的按鍵組合切換新倉頡、酷倉、新酷音及純英文。
    快速鍵的設定選單藏在「控制台\語言\進階設定\變更語言列快速鍵」

    為各輸入法指定 Ctrl-Shift-數字 組合,切換輸入一鍵到位。比起切中文 Ctrl-Shift 循環打檔,切純英文改按 Alt-Shift 更順手。

【茶包射手筆記】Chrome 開發者工具看不到 Form Data

$
0
0

使用 Chrom F12 開發者工具偵察 Web Form 送回內容,正常情況應如下圖所示,Content-Type 為 application/x-www-form-urlencoded,Request Headers 下方應有一區 Form Data 可檢視 Post 送回內容:

我所偵察的 ASP.NET 網頁,遇特定條件會透過 Resonse.Redirect() 轉址,此時 Response 收到 HTTP Status 302 很合理,但向下想查傳回內容,卻發現 Form Data 資訊區不見了!

爬文在 stackoverflow 查到相關討論,這是仍存在於當前 Chrome 穩定版(2017年12月,v62) 的 Bug (推估從 v61 起就有),目前測試中的 v64/v63 已修正,預計月底穩定版更新到 v63 後會解決。

【茶包射手日記】Notepad 改 config 後程式掛點

$
0
0

倉頡輸入筆記文網友 s793016 留言提到 PRIME(中州韻輸入法) 內含倉頡輸入,簡單試用挺驚豔的(心得容後再寫),不過有個問題:必須新增簡體中文語系才能用,解法是修改 ime.json 檔將語系改為 zh-TW 重新註冊 PIMETextService.dll (參考: 在 Windows 10 下安裝最新版的 PRIME 中州韻輸入法方法 - Hiraku Dev)。修改 Program Files 目錄下的檔案需要管理權限,我選擇用管理者權限開 CMD,切到指定目錄下指令 notepad ime.json 用筆記本簡單修改後存檔,接著下指令跑 regsvr32 跑完程序,不料踩到 Notepad 地雷一枚。

反註冊沒什麼問題,但註冊 PIMETextService.dll 時程式崩潰,註冊失敗~

再做了測試,發現如不修改 ime.json,regsvr32 反註冊及註冊都沒問題。反覆測試了幾次,某次改用 Notepad++ 修改,居然註冊成功了!兇手現身,立刻拘提 Notepad 到案嚴刑拷打。

測試使用 Notepad 更動 ime.json 的一個字元,理論上檔案大小不變。但比對修改前後,發現檔案差了 3 個 Byte。

使用 Notepad++ 開啟修改後的 ime-notepad.json,右下角 UTF8-BOM 足以解釋 3 個 Byte 從何而來。

ime.json 原本的格式是不包含 BOM的 UTF8,Notepad 存檔時卻自做主張在檔案前方補上 BOM(0xEF 0xBB 0xBF),用 Notepad++ 的 Hex Editor 外掛可以看得很清楚:

換句話說,問題點在於 Notepad 修改 ime.json 時雞婆為檔案補上 BOM,而恰巧 PIME 程式無法識別包含 BOM 的檔案格式且未捕捉到例外,然後... BOOM! 程式就爆炸了~

Notepad 為什麼要雞婆加上 BOM?在一篇十年前文章(BOM BOM BOM - 就是愛程式)找到詳細說明,Notepad 會加 BOM 的行為由來以久,只是我今天才遇上。又長見識了。

最後提一下 PRIME,中州韻輸入法引擎連續輸入整句話字根(不用敲空白)再用詞庫解析的做法感覺相當聰慧,還有自我學習能力,開源開放甚至允許你發明自己的輸入法令人耳目一新。可惜測試期間好幾次因切換輸入法讓應用程式(Chrome、Live Writer)崩潰閃退,穩定性讓人擔憂,暫時是無緣了。

C# 小技巧 - 不必再靠 switch case 副檔名決定 ContentType 囉

$
0
0

由 ASP.NET 伺服器端傳回檔案內容,需指定適當的 ContentType,瀏覽器才會將其視為圖檔、HTML、CSS 或 JavaScript 處理。過去我都是土法煉鋼,取得副檔名再用 switch … case 針對已知檔案種類列舉對應 ContentType,像這樣:

string contentType = "";
switch (fileName.Split('.').Last())
{
case"jpg":
        contentType="image/jpeg";
break;
case"gif":
        contentType="image/gif";
break;
case"png":
        contentType="image/png";
break;
case"htm":
        contentType="text/html";
break;
case"css":
        contentType="text/css";
break;
case"js":
        contentType="text/javascript";
break;
default:
thrownew ApplicationException("Not supperted file type!");
}

隨便搜尋我的舊文章就能找到應用案例: 淺嚐Data URIHTML5 Canvas的Origin-Clean安全原則

最近發現好東西,.NET 4.5 起 System.Web 內建的 MimeMapping.GetMimeMapping()可以直接將檔名(注意: 是檔名不是副檔名)對應成 ContentType,不用再自己徒手硬刻:

所以,文章開頭的程式邏輯可以簡化成:

if (!"jpg,gif,png,htm,css,js".Split(',').Contains(fileName.Split('.').Last())
thrownew ApplicationException("Not supperted file type!");
var contentType=MimeMapping.GetMimeMapping(fileName);

順便補充,從 ASP.NET WebForm 傳回 jpg、png 等圖檔,ContentType 已指定為 image/jpeg、image/png,若希望瀏覽器不要直接顯示而是下載另存檔案,可透過 Content-Disposition Header 搞定:
Response.AddHeader("Content-Disposition", "attachment; filename=\"" + Server.UrlEncode(fileName) + "\"");
如果是 ASP.NET MVC Action,直接使用 return File(byteArray, contentType, fileName),MVC 會在背後搞定 ContentType 及 ContentDisposition Header,是最省事的做法。

延伸閱讀:

Coding4Fun–自動產生副檔名轉 ContentType 對照表

$
0
0

昨天的文章提到 .NET 4.5 內建 MimeMapping.GetMimeMapping(),可省去自己用 switch … case 逐一列舉副檔名對應ContentType 的工夫。

不過,這項福利僅限於 .NET 4.5+,如果程式使用的是 .NET 3.5 或 .NET 4.0,只能乖乖自己處理。理論上,新開發的程式用 .NET 4.5.2+ 名正言順(參考:  蛤,微軟停止.NET 4.0-4.5-4.5.1的技術支援?會對我的系統造成影響嗎?) BUT! 生活周遭總還是有 .NET 4.0/3.5/2.0 甚至 1.1 的線上系統,雖如風中蟾蜍,卻仍還需要擴充維護,所以... (我懂)

既然免不了要自己寫邏輯,除了查文件硬刻跟抄別人的程式,其實有個方法可以自動產生副檔名跟 ContentType 對照表。既然瀏覽器、.NET 4.5 MimeMapping 跟作業系統都知道什麼副檔案要對應什麼 ContentType,借用他們的資料來源自動產生即可。臉書專頁上有同學討論起針對 .NET 4.0 以下的做法,手癢難耐,決定動手實作玩一玩。

在 Windows 系統的 HKEY_CLASS_ROOT\MIME\Database\ContentType 機碼下有一份完整 ContentType 列表,其中 Extension 有該 ContentType 對應的副檔名,可視為權威資料來源:

因此,跑個迴圈讀 Registry,不費什麼力氣就能產生完整的 ContentType 與副檔名對應表囉!

class Program
    {
staticvoid Main(string[] args)
        {
            var mimeReg = Registry.ClassesRoot
                    .OpenSubKey(@"MIME\Database\Content Type");
            var mimeMappings = new Dictionary<string, string>();
 
            mimeReg
            .GetSubKeyNames()
            .Where(o => mimeReg.OpenSubKey(o).GetValue("Extension") != null)
            .ToList()
            .ForEach(o =>
            {
                var ext = mimeReg.OpenSubKey(o).GetValue("Extension")
                          .ToString().Substring(1);
if (mimeMappings.ContainsKey(ext))
                {
                    Console.WriteLine("[{0}] 重複設定 {1} & {2}", 
                        ext, mimeMappings[ext], o);
                }
else
                {
                    mimeMappings.Add(ext, o);
                }
            });
 
foreach (var key in mimeMappings.Keys.OrderBy(o => o))
            {
                Console.WriteLine($"{key} -> {mimeMappings[key]}");
            }
            Console.ReadLine();
        }
    }

程式裡有個小地方要特別處理:存在多個 ContentType 指向同一副檔名的情形,但反向對應時只能從中擇一。我的做法是保留第一筆並印出重複項目檢視。

執行結果發現 46 筆重複,取用第一筆的值應該也通用:

[pptx] 重複設定 application/vnd.ms-powerpoint.12 & application/vnd.openxmlformats-officedocument.presentationml.presentation
[xlsx] 重複設定 application/vnd.ms-excel.12 & application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
[docx] 重複設定 application/vnd.ms-word.document.12 & application/vnd.openxmlformats-officedocument.wordprocessingml.document
[dotx] 重複設定 application/vnd.ms-word.template.12 & application/vnd.openxmlformats-officedocument.wordprocessingml.template
[asx] 重複設定 application/x-mplayer2 & application/x-wmplayer
[cer] 重複設定 application/pkix-cert & application/x-x509-ca-cert
[zip] 重複設定 application/x-zip-compressed & application/zip
[mid] 重複設定 audio/mid & audio/midi
[m4a] 重複設定 audio/mp4 & audio/MP4A-LATM
[mp3] 重複設定 audio/mp3 & audio/mpeg
[mp3] 重複設定 audio/mp3 & audio/mpg
[ac3] 重複設定 audio/ac3 & audio/vnd.dolby.dd-raw
[aiff] 重複設定 audio/aiff & audio/x-aiff
[m4a] 重複設定 audio/mp4 & audio/x-m4a
[mid] 重複設定 audio/mid & audio/x-mid
[mid] 重複設定 audio/mid & audio/x-midi
[mp3] 重複設定 audio/mp3 & audio/x-mp3
[mp3] 重複設定 audio/mp3 & audio/x-mpeg
[m3u] 重複設定 audio/mpegurl & audio/x-mpegurl
[mp3] 重複設定 audio/mp3 & audio/x-mpg
[wav] 重複設定 audio/wav & audio/x-wav
[jpg] 重複設定 image/jpeg & image/pjpeg
[png] 重複設定 image/png & image/x-png
[mid] 重複設定 audio/mid & midi/mid
[p10] 重複設定 application/pkcs10 & pkcs10
[p7s] 重複設定 application/pkcs7-signature & pkcs7-signature
[cer] 重複設定 application/pkix-cert & pkix-cert
[crl] 重複設定 application/pkix-crl & pkix-crl
[vcf] 重複設定 text/directory & text/directory;profile=vCard
[vcf] 重複設定 text/directory & text/vcard
[vcf] 重複設定 text/directory & text/x-vcard
[xml] 重複設定 application/xml & text/xml
[mpeg] 重複設定 video/mpeg & video/mpg
[avi] 重複設定 video/avi & video/msvideo
[mpeg] 重複設定 video/mpeg & video/x-mpeg
[mpeg] 重複設定 video/mpeg & video/x-mpeg2a
[asx] 重複設定 application/x-mplayer2 & video/x-ms-asf
[asx] 重複設定 application/x-mplayer2 & video/x-ms-asf-plugin
[avi] 重複設定 video/avi & video/x-msvideo
[sst] 重複設定 application/vnd.ms-pki.certstore & vnd.ms-pki.certstore
[pko] 重複設定 application/vnd.ms-pki.pko & vnd.ms-pki.pko
[cat] 重複設定 application/vnd.ms-pki.seccat & vnd.ms-pki.seccat
[p12] 重複設定 application/x-pkcs12 & x-pkcs12
[p7b] 重複設定 application/x-pkcs7-certificates & x-pkcs7-certificates
[p7r] 重複設定 application/x-pkcs7-certreqresp & x-pkcs7-certreqresp
[cer] 重複設定 application/pkix-cert & x-x509-ca-cert

在我的 Windows 10 上,共找到 145 筆已知對應:

3g2 -> audio/3gpp2
3gp -> audio/3gpp
3gp2 -> video/3gpp2
3gpp -> video/3gpp
aac -> audio/aac
ac3 -> audio/ac3
accda -> application/msaccess.addin
accdb -> application/msaccess
accdc -> application/msaccess.cab
accde -> application/msaccess.exec
accdr -> application/msaccess.runtime
accdt -> application/msaccess.template
accdw -> application/msaccess.webapplication
accft -> application/msaccess.ftemplate
adts -> audio/vnd.dlna.adts
aiff -> audio/aiff
amr -> audio/amr
appcontent-ms -> application/windows-appcontent+xml
application -> application/x-ms-application
asx -> application/x-mplayer2
au -> audio/basic
avi -> video/avi
cat -> application/vnd.ms-pki.seccat
cer -> application/pkix-cert
contact -> text/x-ms-contact
crl -> application/pkix-crl
css -> text/css
dds -> image/vnd.ms-dds
dib -> image/bmp
doc -> application/msword
docm -> application/vnd.ms-word.document.macroEnabled.12
docx -> application/vnd.ms-word.document.12
dotm -> application/vnd.ms-word.template.macroEnabled.12
dotx -> application/vnd.ms-word.template.12
dtcp-ip -> application/x-dtcp1
dvr-ms -> video/x-ms-dvr
dwfx -> model/vnd.dwfx+xps
easmx -> model/vnd.easmx+xps
ec3 -> audio/ec3
edrwx -> model/vnd.edrwx+xps
emf -> image/x-emf
eprtx -> model/vnd.eprtx+xps
epub -> application/epub+zip
fif -> application/fractals
flac -> audio/x-flac
gif -> image/gif
gz -> application/x-gzip
hqx -> application/mac-binhex40
hta -> application/hta
htc -> text/x-component
html -> text/html
ico -> image/x-icon
ics -> text/calendar
iqy -> text/x-ms-iqy
jnlp -> application/x-java-jnlp-file
jpg -> image/jpeg
jtx -> application/x-jtx+xps
latex -> application/x-latex
lpcm -> audio/l16
m3u -> audio/mpegurl
m4a -> audio/mp4
m4r -> audio/x-m4r
m4v -> video/x-m4v
man -> application/x-troff-man
mid -> audio/mid
mka -> audio/x-matroska
mkv -> video/x-matroska
mov -> video/quicktime
mp3 -> audio/mp3
mp4 -> video/mp4
mpeg -> video/mpeg
nix -> application/x-mix-transfer
odc -> text/x-ms-odc
odp -> application/vnd.oasis.opendocument.presentation
ods -> application/vnd.oasis.opendocument.spreadsheet
odt -> application/vnd.oasis.opendocument.text
one -> application/msonenote
osdx -> application/opensearchdescription+xml
p10 -> application/pkcs10
p12 -> application/x-pkcs12
p7b -> application/x-pkcs7-certificates
p7c -> application/pkcs7-mime
p7m -> pkcs7-mime
p7r -> application/x-pkcs7-certreqresp
p7s -> application/pkcs7-signature
pdf -> application/pdf
pko -> application/vnd.ms-pki.pko
png -> image/png
potm -> application/vnd.ms-powerpoint.template.macroEnabled.12
potx -> application/vnd.openxmlformats-officedocument.presentationml.template
ppam -> application/vnd.ms-powerpoint.addin.macroEnabled.12
ppsm -> application/vnd.ms-powerpoint.slideshow.macroEnabled.12
ppsx -> application/vnd.openxmlformats-officedocument.presentationml.slideshow
ppt -> application/vnd.ms-powerpoint
pptm -> application/vnd.ms-powerpoint.presentation.macroEnabled.12
pptx -> application/vnd.ms-powerpoint.12
ps -> application/postscript
pub -> application/vnd.ms-publisher
rqy -> text/x-ms-rqy
sit -> application/x-stuffit
sldm -> application/vnd.ms-powerpoint.slide.macroEnabled.12
sldx -> application/vnd.openxmlformats-officedocument.presentationml.slide
solitairetheme8 -> application/x-compressed
spl -> application/futuresplash
sst -> application/vnd.ms-pki.certstore
svg -> image/svg+xml
swf -> application/x-shockwave-flash
tar -> application/x-tar
thmx -> application/vnd.ms-officetheme
tif -> image/tiff
tts -> video/vnd.dlna.mpeg-tts
txt -> text/plain
uvu -> video/vnd.dece.mp4
vcf -> text/directory
vsd -> application/vnd.ms-visio.viewer
vsto -> application/x-ms-vsto
wav -> audio/wav
wax -> audio/x-ms-wax
wdp -> image/vnd.ms-photo
website -> application/x-mswebsite
wm -> video/x-ms-wm
wma -> audio/x-ms-wma
wmd -> application/x-ms-wmd
wmf -> image/x-wmf
wmv -> video/x-ms-wmv
wmx -> video/x-ms-wmx
wmz -> application/x-ms-wmz
wpl -> application/vnd.ms-wpl
wsc -> text/scriptlet
wtv -> video/wtv
wvx -> video/x-ms-wvx
xaml -> application/xaml+xml
xbap -> application/x-ms-xbap
xht -> application/xhtml+xml
xlam -> application/vnd.ms-excel.addin.macroEnabled.12
xls -> application/vnd.ms-excel
xlsb -> application/vnd.ms-excel.sheet.binary.macroEnabled.12
xlsm -> application/vnd.ms-excel.sheet.macroEnabled.12
xlsx -> application/vnd.ms-excel.12
xltm -> application/vnd.ms-excel.template.macroEnabled.12
xltx -> application/vnd.openxmlformats-officedocument.spreadsheetml.template
xml -> application/xml
xps -> application/vnd.ms-xpsdocument
z -> application/x-compress
zip -> application/x-zip-compressed

掌握這個技巧,稍微包裝及調整一下,事先存成文字檔備查,或是即時查詢 Registry 建立 Dictionary<string, string> 都成,就能輕鬆寫出 .NET 2.0/3.5/4.0 版 GetMimeMapping() 囉~


CODE-使用 Stack + yield 取代遞迴

$
0
0

分享最近學到的遞迴邏輯的替代寫法。

舉個實例比較容易說明,假設公司組織樹狀結構以部門資料物件形式呈現:

publicclass Dept
    {
publicstring Name;
public List<Dept> Children = new List<Dept>();
    }

組織架構範例如下:

{
"Name": "總經理",
"Children": [
    {
"Name": "行政部",
"Children": [
        { "Name": "人資組" },
        { "Name": "總務組" }
      ]
    },
    {
"Name": "資訊部",
"Children": [
        { "Name": "網路組" },
        { "Name": "研發組" }
      ]
 
    },
    {
"Name": "業務部",
"Children": [
        {
"Name": "海外組",
"Children": [
            { "Name": "海外一科" },
            { "Name": "海外二科" }
          ]
        },
        {
"Name": "通路組",
"Children": [
            { "Name": "行銷科" },
            { "Name": "電銷科" }
          ] 
        }
      ] 
    }
  ] 
}

若要列舉所有部門名稱,過去我慣用遞迴(Recursive,在函式中呼叫自己)來解,身為程式老鳥,不爬文不查書徒手寫遞迴是基本功,難不倒我:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
 
namespace LinqRecursive
{
class Program
    {
staticvoid Main(string[] args)
        {
            var root = JsonConvert.DeserializeObject<Dept>(File.ReadAllText("Org.json"));
            var deptNames = new List<string>();
            ExploreOrg(root, deptNames);
            Console.WriteLine(string.Join(",", deptNames.ToArray()));
            Console.Read();
        }
 
staticvoid ExploreOrg(Dept dept, List<string> list)
        {
            list.Add(dept.Name);
            dept.Children.ForEach(o => ExploreOrg(o, list));
        }
    }
 
publicclass Dept
    {
publicstring Name;
public List<Dept> Children = new List<Dept>();
    }
 
}

執行成功:

最近再遇到相同案例,突發奇想有沒有更巧妙的做法? 爬文查得 LINQ 美技一枚– 為 IEnumerable<T> 新增擴充方法 Traverse(),參數為可取得子物件 IEnumerable 集合的 Lambda 運算式,即可產出樹狀結構下所有節點的 IEnumerable<T> 集合繼續串接 LINQ 方法,十分巧妙:

publicstaticclass LinqExt
    {
//REF: https://stackoverflow.com/a/20975582/288936
publicstatic IEnumerable<T> Traverse<T>(this IEnumerable<T> items,
            Func<T, IEnumerable<T>> childSelector)
        {
            var stack = new Stack<T>(items);
while (stack.Any())
            {
                var next = stack.Pop();
yieldreturn next;
foreach (var child in childSelector(next))
                    stack.Push(child);
            }
        }

有了萬用 Traverse 擴充方法,不必花時間另寫遞迴函式,程式更簡潔,而傳回型別為 IEnumerable<T>,能與其他 LINQ 操作無縫接軌,極為方便。

staticvoid Main(string[] args)
        {
            var root = JsonConvert.DeserializeObject<Dept>(File.ReadAllText("Org.json"));
            Console.WriteLine(string.Join(",", 
new List<Dept>() { root }
                .Traverse<Dept>(o => o.Children)
                .Select(o => o.Name).ToArray()));
            Console.Read();
        }

回頭看 Traverse 方法,利用 Stack<T> 資料結構,一方面取得子元素集合推入 Stack<T>,另一方面則從 Stack<T> 取出元素查詢其子元素,堆與取之間把所有元素都巡過一輪。還有一個巧妙處在於 yield,可以無腦地在迴圈裡遇到條件符合就 return 結果,交由 .NET 在背後蒐集結果彙整成 IEnumerable<T>。若不用 yield 也可以,替代做法是準備一個 List<T> 蒐集結果最後再傳回,像這個樣子:

publicstatic IEnumerable<T> Traverse<T>(this IEnumerable<T> items,
            Func<T, IEnumerable<T>> childSelector)
        {
            var stack = new Stack<T>(items);
            var results = new List<T>();
while (stack.Any())
            {
                var next = stack.Pop();
                results.Add(next);
foreach (var child in childSelector(next))
                    stack.Push(child);
            }
return results;
        }

由此可見 yield 簡化邏輯的效果,收入工具箱,未來遇類似情境多了新武器可用。對 yield 的原理有興趣深入了解的同學,推薦安德魯的系列文:

【茶包射手日記】Windows 10 每天十點多固定醒來

$
0
0

家裡 Windows 10 的使用率不高,平時長期處於睡眠狀態,但偶爾會發現無故醒來,我知道有部分 Windows 排程具有喚醒電腦能力,正常情況醒來做完事閒置一陣子會再回去睡覺,發現醒著多半是閒置休眠機制失靈,倒也沒特別調查。今天心血來潮挖了一下,發現一個祕密—原來我的 Windows 10 固定每天早上十點多都會起床夢遊,有趣的是,起床時間還不固定,甚至有最遲 10:57 才起來的記錄:

檢視事件詳細內容追到一個排程 NT TASK\Microsoft\Windows\rempl\shell

在「工作排程器」Microsoft、Windows、rempl 資料夾的確有個 shell 排程,被設定成「喚醒電腦以執行此工作」,而上次執行時間 10:57:30,距離電腦被喚醒時間不到 30 秒。罪證確鑿,豈容狡辯! (怒拍驚堂木) 來人吶...

至於醒來時間不是十點整,每天不固定亂跳是怎麼回事?是洋葱,是排程有延遲 0 到 1 小時的隨機設定。

確認喚醒行為後,下個疑問是「rempl/shell 排程」是什麼鬼?可以關掉嗎?

以下是我找到幾篇相關文章:

MS Answers 論壇文章 windows task wakes up computer
SuperUser 文章 如何停用排程的喚醒能力

研究心得如下:

  1. rempl/shell 是 Windows 10 1607 更新加入的每日固定檢查,跟 Windows Update 更新有關。每天把在睡覺電腦挖起來檢查更新很符合資安精神,有無矯枉過正倒見仁見智。 (由事件檢視器觀察 rempl/shell 喚醒電腦後多半只有幾分鐘的活動記錄,懷疑它有處理完就叫電腦回去睡的能力,但僅為猜測尚未實驗證實)
  2. 檢查目前有沒有排定的喚醒時程,可使用指令 powercfg /waketimers
    下圖兩次執行結果為啟用 rempl/shell「喚醒電腦以執行此工作」選項前後的差異
  3. 要停用排程的喚醒能力,可以從電源管理下手
  4. 要檢查哪些排程被設成可喚醒電腦,有個好用 PowerShell 指令 - Get-ScheduledTask | where {$_.settings.waketorun}

【茶包射手日記】預訂五十年後執行的排程

$
0
0

近來異常充實,專案火燒屁股,大小茶包報案照常受理,生活好不精彩。遇到一枚奇妙茶包,追了好一會兒,謎底卻令人莞爾,為枯躁生活平添一絲趣味,特記上一筆。

同事報案,表單系統在歸檔時有個錯誤重試機制,出錯時自動休眠 30 分鐘再試,另外,系統亦接受程式指定於特定時間(稱為喚醒時間)重試。

監看報表出現多筆詭異喚醒時間,排定在 2058、2075、2052、2081、2068、2057、2055... 等 40-70 年後的日期,預訂於遙不可及的未來執行。嘗試分析詭異喚醒時間與初次執行的關聯,找不出任何規律。

初步檢查歸檔程式,確實有一段邏輯在系統出錯時傳回 DateTime.Now.AddMinutes(120),明確指定兩小時後重試,喚醒時間沒理由亂跳。同事甚至一度懷疑是資料庫錯亂導致,但僅有單一欄位資料不對,其餘資料正常的機會微乎其微(機率應不會比 GUID 碰撞高)。

從 Log 檔找到歸檔程式傳回 2058-06-27 20:00:54 的鐵據(俗話說:Log 寫得好,除錯沒煩惱 :P ),直接排除資料庫涉案的嫌疑,辦案重心回到歸檔程式本身,仔細翻找,看到一段程式碼,忍不住笑了出來:

if (theDate != DateTime.Today)
    {
        Random rnd = new Random(Guid.NewGuid().GetHashCode());
//當日非營業日,休眠至次日並稍作延遲再嘗試重新歸檔
        wrc.Sleep(theDate.AddSeconds(rnd.Next()), 
"非營業日,將於次日嘗試重新歸檔:" + 
            theDate.ToString("yyyy/MM/dd"));
        Response.End();
    }

程式原意是遇非營業日延後一天再試並加上亂數延遲,避免大量排程擠在同一時間執行,但此處犯了兩個錯誤:第一是漏了 AddDays(1) 未將時間延後一天,第二是製造延遲誤用 Random.Next(),誤以為 Random.Next() 會比照 VBScript Rnd() 或 Math.random() 傳回介於 0 與 1 間的小數,但 .NET Random.Next() 傳回的是 0 – int.MaxValue(約20億) 間的隨機正整數,變成延遲 0 - 63 年不等,就是 2058、2081 等神奇未來年份加無規律的由來。程式寫好多年,因非營業日執行的機率極低,這支 Bug 才得以隱身至今。

回到程式碼,可改寫如下:

if (theDate != DateTime.Today)
    {
//建構式不需傳入種子,直接以當下時間隨機決定
        Random rnd = new Random();
//當日非營業日,休眠至次日並稍作延遲再嘗試重新歸檔
//使用 Random.Next(N) 產生大於等於0但小於N的隨機整數
//另外也可用 Random.Next(M, N) 產生大於等於M但小於N的隨機整數
        wrc.Sleep(theDate.AddDays(1).AddSeconds(rnd.Next(3600)), 
"非營業日,將於次日嘗試重新歸檔:" + 
            theDate.ToString("yyyy/MM/dd"));
        Response.End();
    }

就醬,結束一次被茶包逗樂的難得經驗。

CODE-從 JSON 提取文字內容

$
0
0

從檔案萃取文字部分建立索引是全文檢索的必要程序,先前介紹過為 PDF、Office 檔案產生文字索引的做法,實際開發則遇到為 JSON 建立文字索引的需求。借用上回遞迴文章的組織資料當實例,假設 JSON 格式如下:

{
"Name": "總經理",
"Children": [
    {
"Name": "行政部",
"Children": [
        { "Name": "人資組" },
        { "Name": "總務組" }
      ]
    },
    {
"Name": "資訊部",
"Children": [
        { "Name": "網路組" },
        { "Name": "研發組" }
      ]
 
    },
    {
"Name": "業務部",
"Children": [
        {
"Name": "海外組",
"Children": [
            { "Name": "海外一科" },
            { "Name": "海外二科" }
          ]
        },
        {
"Name": "通路組",
"Children": [
            { "Name": "行銷科" },
            { "Name": "電銷科" }
          ] 
        }
      ] 
    }
  ] 
}

其中 Name、Children 是屬性名稱而非內容本體,不應納入搜尋範圍,故建索引時只需提取「總經理、行政部、人資組、總務組...」等純部門名稱就好。理論上應該找得到現成的 IFilter 或程式庫,但感覺原理不難,與其花時間尋找,不如自已寫個「JSON 文字資料抽取函式」練練功也好。(對啦對啦,我就是手癢愛亂寫啦,來咬我呀)

最早的想法是採遞迴 (或是上回的 Stack<T> + yield 奇技) 巡遍每一個 Property 讀取成字串。遇到物件就探索所有屬性、遇到陣列就跑完每一個元素,層層遞迴抓出所有內容。試寫了一陣子,閃過一個好點子,何不把 JSON 轉成 XML 再讀取 InnerText?秒殺搞定!!

Json.NET 果然不負所望,內建的 JsonConvert.DeserializeXmlNode Method (String, String)方法可將 JSON 字串直接轉成 XmlNode,得來全不費功夫~

staticstring ExtractJsonText(string raw)
        {
            var node = JsonConvert.DeserializeXmlNode(raw, "Root");
return node.InnerText;
        }
 
staticvoid Main(string[] args)
        {
            var json = System.IO.File.ReadAllText("Org.json");
            Console.WriteLine(ExtractJsonText(json));
            Console.Read();
        }

簡單幾行程式得到結果如下:

總經理行政部人資組總務組資訊部網路組研發組業務部海外組海外一科海外二科通路組行銷科電銷科

但有個問題。文字是提取出來了,卻全黏在一起,不利於分詞器正確切詞。有個簡單的改善做法設法在各屬性值間插入空白,我想到改用 InnerXml 加 Regex 置換 XML 標籤的做法可以實現。另外還有個狀況是 DeserializeXmlNode() 會將整個 JSON 視為一整個 Object,必須以 { 開頭 } 結尾,遇到 JSON 以 [、] 開頭及結尾會出錯,但這也難不倒我,偵測到主體是陣列時,外面再套一層 { "Array": [ … ] } 即可搞定。

改良版本如下:

staticstring ExtractJsonText(string raw)
{
    var json = raw.TrimStart(' ', '\t', '\r', '\n');
if (json.StartsWith("["))
        json = $@"{{ ""Array"": {json} }}";
    var node = JsonConvert.DeserializeXmlNode(json, "Root");
return//將Element Tag換成空白,再去除連續空白
string.Join(" ",
            Regex.Replace(node.InnerXml, "<[^>]+>", " ")
                .Split(newchar[] {' '}, StringSplitOptions.RemoveEmptyEntries)
                .ToArray());
}

執行結果:

總經理 行政部 人資組 總務組 資訊部 網路組 研發組 業務部 海外組 海外一科 海外二科 通路組 行銷科 電銷科

完美! Json.NET 好威啊~

CODE-C# 程式讀取 Exchange 共用行事曆

$
0
0

工作遇到新需求:辦公室自動化服務希望讀取使用者行事曆,整合顯示於個人資訊頁。

EWS Managed API封裝了複雜又囉嗦的 Exchange Web Service SOAP 細節,改以 .NET 程式庫形式提供電子郵件、連絡人、行事曆、公用資料夾的存取管道,是 C# 開發 Exchange 相關程式的首選。(意外發現 EWS Managed API 從 2014 起轉為 Github 開源專案,有原始碼在手,搞不懂走不通都有救,用起來格外讓人放心,微軟真的愈來愈開放)

官方文件有篇詳細介紹文,示範如何使用 EWS Mananged API 查詢行事曆取得個人約會資訊。基本原理是利用 CalendarFolder.Bind() 連上個人行事曆,接著建立一個 CalendarView 指定查詢期間、取回筆數,CalendarView.PropertySet 則傳入要讀取的欄位(主旨、開始/結束時間… 等),接著呼叫 CalendarFolder.FindAppointments() 就取得 Appointment 的集合,很簡單,這部分可直接參考官方範例。好消息是 EWS Managed API 用 NuGet 就可以下載:

不過我的需求多了一些變化,系統會使用統一中間程式查詢不同使用者的行事曆,而我們不可能為此要使用者交出 AD 帳號密碼。因此,透過行事曆共用是較可行做法。

行事曆共用可使用 Outlook 操作, 找到自己的行事曆(一般會有兩份,要選名稱有個人郵件地址位於 Exchange Server 的那一份),右鍵選單開啟內容:

程式使用 EWS Managed API 會以特定 AD 帳號執行(測試時可用開發者自己的帳號,正式營運則會申請專用帳號),要開放行事曆供程式存取,使用者必須授與該帳號讀取權限,授權完成可使用 Outlook 檢查是否看到測試對象的約會做為驗證。但有一點要留意,除了授權讀取空閒/忙碌時間、主旨、地點外,其他區域有個「可看到資料夾」一定要勾選。未勾選時 Oulook 可查看共享行事曆,但 EWS Managed API 不行,我花了不少時間才發現這個眉角。

若要求省事,也可請共用行事曆的使用者直接選取「權限等級:檢閱者」;若使用者很介意約會細節外流,只想透露主旨、地點,甚至只打算開放/空閒忙碌資訊,只勾選自己想開放範圍也成,但記得一定要勾選「可看到資料夾」,不然程式沒戲唱:

程式範例如下。查詢他人行事曆時,有個關鍵是使用 new FolderId(WellKnownFloderName.Calendar, "對方的Email") 取得資料夾代號,若對方未授與「可看到資料夾」權限,程式會在 FindAppointments 時噴出找不到資料夾的錯誤。

privatestaticvoid QuerySharedCalender()
{
    var ewsUrl = "https://the-exchange-server/ews/Exchange.asmx";
    ExchangeService ews = new ExchangeService(ExchangeVersion.Exchange2007_SP1);
    ews.Credentials = new WebCredentials(userId, pwd, domainName);
    ews.Url = new Uri(ewsUrl);
    FolderId folderToAccess = 
new FolderId(WellKnownFolderName.Calendar, "someone@company.com");
//指定日期區間與資料筆數
    var view = new CalendarView(
new DateTime(2017, 12, 10),
new DateTime(2017, 12, 17), 
        1024);
    view.PropertySet = new PropertySet(
                AppointmentSchema.Subject, 
                AppointmentSchema.Start, 
                AppointmentSchema.End);
    FindItemsResults<Appointment> apps = ews.FindAppointments(folderToAccess, view);
foreach (var app in apps)
    {
        Console.WriteLine(
            $"{app.Start:MM-dd HH:mm} - {app.End:MM-dd HH:mm} {app.Subject}");
    }
}

假設行事曆如下:

測試成功!

Viewing all 428 articles
Browse latest View live