1. 程式人生 > >《C# 爬蟲 破境之道》:第二境 爬蟲應用 — 第三節:處理壓縮資料

《C# 爬蟲 破境之道》:第二境 爬蟲應用 — 第三節:處理壓縮資料

續上一節內容,本節主要講解一下Web壓縮資料的處理方法。

在HTTP協議中指出,可以通過對內容壓縮來減少網路流量,從而提高網路傳輸的效能。

那麼問題來了,在HTTP中,採用的是什麼樣的壓縮格式和機制呢?

 

首先呢,先說壓縮格式,主要有三種:

  • DEFLATE,是一種使用 Lempel-Ziv 壓縮演算法(LZ77)和哈夫曼編碼的資料壓縮格式。定義於 RFC 1951 : DEFLATE Compressed Data Format Specification;
  • ZLIB,是一種使用 DEFLATE 的資料壓縮格式。定義於 RFC 1950 : ZLIB Compressed Data Format Specification;
  • GZIP,是一種使用 DEFLATE 的檔案格式。定義於 RFC 1952 : GZIP file format specification;

我們這裡就不細琢磨了,格式裡面又有演算法,又有規則什麼的,我也搞不清楚,說多了,捱罵……理解上,就相當於我們常用的Zip、7Zip、RAR等壓縮格式;

但是需要注意的是,ZLIB和GZIP都是使用的DEFLATE,這就有點兒意思了,後面再說:)

 

說完壓縮格式,再來說機制,分為兩條路子(請求、回覆):

  • 請求:在request header中指定Accept-Encoding。例如:Accept-Encoding: gzip, deflate, compress, br;Accept-Encoding在Headers中是可選的,可以不指定;當然,其中還有一些規則,後面我們結合回覆一起給出;
  • 回覆:在response header中指定Content-Encoding。例如:Content-Encoding: gzip;Content-Encoding在Headers中也是可選的,可以不指定;不過現在大多數站點都會對內容進行壓縮,不過通常不會對圖片及視訊等已經經過壓縮的資源進行壓縮,因為得不償失啊;

來解釋一下,首先客戶端(比如說瀏覽器)發出請求,我們在使用瀏覽器的過程中,一般就只是輸入一個網址或點選某個連線,不會刻意去填寫一下Accept-Encoding,但是瀏覽器會為我們新增;這個Accept-Encoding,就是告訴網站伺服器端,我(瀏覽器)可以解釋這幾種壓縮格式(一個列表),你(網站伺服器)要是壓縮,就給我這幾種格式,否則,就不要壓縮了;網站伺服器端收到請求後,進行解析,看看有沒有自己能夠使用的壓縮格式,如果有,那麼就進行壓縮,如果有多個可以使用,那就要看優先順序,選擇優先順序最高的格式進行壓縮(後面列出規則),並將使用的壓縮格式填入Content-Encoding中傳送回客戶端;客戶端(瀏覽器)收到回覆以後,就看Content-Encoding有沒有值,如果有並且自己也認識,那麼就可以正常解壓,顯示在介面上了。

這個就是壓縮的機制了,一切看起來那麼的和諧,但在網際網路的世界,總是不缺乏“驚喜”,即使客戶端不指定任何Accept-Encoding,伺服器端也會根據情況返回Content-Encoding,這就迫使瀏覽器,還必須得有兩把刷子,否則就傻眼了。

HTTP Header中Accept-Encoding 是瀏覽器發給伺服器,宣告瀏覽器支援的編碼型別[1] 
常見的有
Accept-Encoding: compress, gzip          //支援compress 和gzip型別
Accept-Encoding:                               //預設是identity
Accept-Encoding: *                            //支援所有型別
Accept-Encoding: compress;q=0.5, gzip;q=1.0//按順序支援 gzip , compress
Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 // 按順序支援 gzip , identity
伺服器返回的對應的型別編碼header是 content-encoding.伺服器處理accept-encoding的規則如下所示:
1. 如果伺服器可以返回定義在Accept-Encoding 中的任何一種Encoding型別, 那麼處理成功(除非q的值等於0, 等於0代表不可接受) 
2. * 代表任意一種Encoding型別 (除了在Accept-Encoding中顯示定義的型別) 
3. 如果有多個Encoding同時匹配, 按照q值順序排列 
4. identity總是可被接受的encoding型別(除非明確的標記這個型別q=0) 

如果Accept-Encoding的值是空, 那麼只有identity是會被接受的型別
如果Accept-Encoding中的所有型別伺服器都沒法返回, 那麼應該返回406錯誤給客戶端
如果request中沒有Accept-Encoding 那麼伺服器會假設所有的Encoding都是可以被接受的。
如果Accept-Encoding中有identity 那麼應該優先返回identity (除非有q值的定義,或者你認為另外一種型別是更有意義的)
注意:
如果伺服器不支援identity 並且瀏覽器沒有傳送Accept-Encoding,那麼伺服器應該傾向於使用HTTP1.0中的 "gzip" and "compress" , 伺服器可能按照客戶端型別傳送更適合的encoding型別
大部分HTTP1.0的客戶端無法處理q值

Accept-Encoding與Content-Encoding的規則
Accept-Encoding 與 Content-Encoding 的對應規則

 

另外,需要額外說明的是,在Accept-Encoding中指定的delfate,可不一定是DEFLATE壓縮格式,按照官方的說法:

  • gzip,一種由檔案壓縮程式「Gzip,GUN zip」產生的編碼格式,描述於 RFC 1952。這種編碼格式是一種具有 32 位 CRC 的 Lempel-Ziv 編碼(LZ77);
  • deflate,由定義於 RFC 1950 的「ZLIB」編碼格式與 RFC 1951 中描述的「DEFLATE」壓縮機制組合而成的產物;

也就是說,deflate其實對應的應該是ZLIB壓縮格式,而它的名字,又與DEFLATE格式重名(估計這位同仁會被祭天了吧),導致很多瀏覽器廠商不知道究竟該用哪種格式來解釋Content-Encoding: deflate,因為不論你選擇哪種,都會有例外發生,這就尷尬了。所以,儘管deflate的壓縮效果要比gzip好,但還是會被不少Web-Server放棄或者降低優先順序。這也就是為什麼我們會經常看到Content-Encoding: gzip而很少能看到Content-Encoding: deflate的原因;所以,我們在做爬蟲的時候,也應該儘量避免使用deflate,減少不必要的麻煩。

 

話鋒一轉,回到我們的爬蟲,也會遇到上面瀏覽器遇到的尷尬場面,所以,就必須得事先準備好常用的解壓縮方式,要不然,資料抓下來了,讀不出來,你說氣不氣~

本節中,我們就來繼續改造我們的爬蟲框架,讓它也有兩把刷子:)

[Code 2.3.1]

 1 public static byte[] DecompressStreamData(Stream sourceStream, String contentEncoding)
 2 {
 3     var _stream = sourceStream;
 4     switch ((contentEncoding ?? string.Empty).ToLower())
 5     {
 6         case "gzip":
 7             _stream = new GZipStream(sourceStream, CompressionMode.Decompress);
 8             break;
 9         case "deflate":
10             _stream = new DeflateStream(sourceStream, CompressionMode.Decompress);
11             break;
12         default:
13             break;
14     }
15     using (var memory = new MemoryStream())
16     {
17         int length = 256;
18         Byte[] buffer = new Byte[length];
19         int bytesRead = _stream.Read(buffer, 0, length);
20         while (bytesRead > 0)
21         {
22             memory.Write(buffer, 0, bytesRead);
23             bytesRead = _stream.Read(buffer, 0, length);
24         }
25         return memory.ToArray();
26     }
27 }
DecompressStreamData 靜態方法

這是一個公共靜態方法,其目的就是將原資料流中的資料轉換為byte[]陣列,其中,如果指定了壓縮格式,就會使用適當的方法進行解壓。這裡只提供了最常見的gzip和不推薦的deflate兩種格式,可以自行擴充套件。

 

接下來,就是對工蟻(WorkerAnt)進行改造了。

[Code 2.3.2]

 1 private void GetResponse(JobContext context)
 2 {
 3     context.Request.BeginGetResponse(new AsyncCallback(acGetResponse =>
 4     {
 5         var contextGetResponse = acGetResponse.AsyncState as JobContext;
 6         using (contextGetResponse.Response = contextGetResponse.Request.EndGetResponse(acGetResponse))
 7         using (contextGetResponse.ResponseStream = contextGetResponse.Response.GetResponseStream())
 8         using (contextGetResponse.Memory = new MemoryStream())
 9         {
10             // 此處省略N行……
11 
12             if (TaskStatus.Running == contextGetResponse.JobStatus)
13             {
14                 if (!String.IsNullOrEmpty(contextGetResponse.Response.Headers["Content-Encoding"]))
15                 {
16                     contextGetResponse.Memory.Seek(0, SeekOrigin.Begin);
17                     contextGetResponse.Buffer = DecompressStreamData(contextGetResponse.Memory
18                         , contextGetResponse.Response.Headers["Content-Encoding"]);
19                     //contextGetResponse.Buffer = contextGetResponse.Memory.ToArray();
20                 }
21                 else
22                     contextGetResponse.Buffer = contextGetResponse.Memory.ToArray();
23 
24                 contextGetResponse.JobStatus = TaskStatus.RanToCompletion;
25                 NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, });
26             }
27 
28             contextGetResponse.Buffer = null;
29         }
30     }), context);
31 }
改造WorkerAnt的GetResponse方法

註釋中是原來使用的方法,現在用上面的DecompressStreamData替換掉了。

 

這樣我們在收到採集完成事件通知時,就可以得到解壓縮後的資料了:

[Code 2.3.3]

 1 switch (args.Context.JobStatus)
 2 {
 3     // 此處省略N行……
 4     case TaskStatus.RanToCompletion:
 5         if (null != args.Context.Buffer && 0 < args.Context.Buffer.Length)
 6         {
 7             Task.Factory.StartNew(oBuffer =>
 8             {
 9                 var content = new UTF8Encoding(false).GetString((byte[])oBuffer);
10                 richOutput.EndInvoke(richOutput.BeginInvoke(new MethodInvoker(() => { richOutput.Text = content; })));
11             }, args.Context.Buffer, TaskCreationOptions.LongRunning);
12         }
13         if (null != args.Context.Watch)
14             Console.WriteLine("/* ********************** using {0}ms / request  ******************** */"
15                 + Environment.NewLine + Environment.NewLine, (args.Context.Watch.Elapsed.TotalMilliseconds / 100).ToString("000.00"));
16         break;
17     // 此處省略N行……
18     default:/* Do nothing on this even. */
19         break;
20 }
改造應用中對事件的處理

至於為何在Complete事件的位置處理解壓縮,而不在Running事件的位置,這是gzip的限制,它具有CRC校驗位,CRC的演算法,大家可以在網上搜索,大體上說,就是遍歷一遍所有資料,進行與或計算,最終得到一個校驗位,來保證資料的完整性與正確性。這也導致我們無法對中間資料進行解壓,因為沒有校驗位,對末尾資料解壓,又因資料不全,CRC計算結果也不會對。

 

至此,我們就完成了對HTTP協議內容部分已壓縮資料的處理,拋磚引玉,可以實現更多種壓縮格式的處理;

 

節外生枝:

  • 本節講述的資料壓縮,指的是HTTP協議中,對協議內容部分的壓縮,在HTTP 2.x的版本中,增加了對協議頭部的壓縮(更確切的說是快取)的機制,用空間換時間,由於2.x版本Schema為HTTPS,處理起來,另有蹊蹺,本節先不做深入介紹了,可作為延伸內容,有興趣的童鞋可以搜尋相關主題;
  • 為了方便以後的做更多更好的案例,原始碼中增加了一個WinForm專案,這樣在切換Uri的時候,就更方便一些;

 

喜歡本系列叢書的朋友,可以點選連結加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑問的時候可以及時給我個反饋。同時,也算是給各位志同道合的朋友提供一個交流的平臺。
需要原始碼的童鞋,也可以在群檔案中獲取最新原始碼。