重要更新:在@老赵的提醒下,如果在请求中加入“Accept-Encoding: gzip, deflate”,下面的问题就会自动消失。具体见文章末尾。
两年前我用C#写了一个爬虫类,一直在用。今天终于出错了。让我代码出错的页面是:http://www.hacker.org/challenge/solvers.php?id=1
这个页面非常之强大,好多简单的爬虫都失效了,比如这段C#代码:
WebClient webClient = new WebClient(); webClient.DownloadData("http://www.hacker.org/challenge/solvers.php?id=1")
还有php的curl(参考示例),以及Python的opener等,如果直接调用,都会中枪。
当然,一般的浏览器都能毫无压力正常打开这个页面。
用上述简易方式下载网页,最后都只能下载到 72k 的内容,但是实际上,这个页面有200多K。剩下的这个内容为什么不能直接抓取到呢?下面来分析一下这朵奇葩,并且给出解决方案。
用wget下载这个页面时,出现了这样的提示:
30% [============> ] 73,389 30.6K/s in 2.3s
2012-07-05 16:46:37 (30.6 KB/s) – Connection closed at byte 73389. Retrying.
–2012-07-05 16:46:38– (try: 2) http://www.hacker.org/challenge/solvers.php?id=1
Connecting to www.hacker.org|173.236.190.252|:80… connected.
HTTP request sent, awaiting response… 206 Partial Content
Length: 243584 (238K), 170195 (166K) remaining [text/html]
Saving to: `solvers.php?id=1.2′
30% [+++++++++++++ ] 73,629 –.-K/s in 1.9s
2012-07-05 16:46:43 (126 B/s) – Connection closed at byte 73629. Retrying.
–2012-07-05 16:46:45– (try: 3) http://www.hacker.org/challenge/solvers.php?id=1
Connecting to www.hacker.org|173.236.190.252|:80… connected.
HTTP request sent, awaiting response… 206 Partial Content
Length: 243584 (238K), 169955 (166K) remaining [text/html]
Saving to: `solvers.php?id=1.2′
60% [+++++++++++++============> ] 147,285 22.2K/s in 3.2s
2012-07-05 16:46:52 (22.2 KB/s) – Connection closed at byte 147285. Retrying.
……
一般用wget抓取网页的时候,只需要一次就可以下载完成了,而这个网页一共重试了 5 次才下载完(上面只列了前 3 次)。看起来 hacker.org 的服务器用了特殊的方法,限制每个请求只能下载 70k 左右的数据。
那如何才能抓取后面几部分的数据呢?多线程下载工具常用的方法是在HTTP头上加入Range字段,告诉服务器,我要从第xxx字节开始下载。这里也一样,如果发现下载的内容还没到content-length就结束了,就从刚才的位置继续下载。
下面就是一个我实现的示例代码,估计还有很多边界情况没考虑到,不过暂时够用了。
const int Net_Timeout = 60000; private CookieContainer cookie = new CookieContainer(40); public byte[] Download(string Url) { Listret = new List (); byte[] buffer = new byte[4096]; long totalLength = -1; while (totalLength < 0 || ret.Count < totalLength) { while (true) { try { HttpWebRequest loHttp = (HttpWebRequest)WebRequest.Create(Url); loHttp.Timeout = Net_Timeout; loHttp.ServicePoint.Expect100Continue = false; if (ret.Count != 0) loHttp.AddRange(ret.Count); loHttp.CookieContainer = cookie; HttpWebResponse loWebResponse = (HttpWebResponse)loHttp.GetResponse(); if (totalLength < 0) totalLength = loWebResponse.ContentLength; loWebResponse.Cookies = cookie.GetCookies(loHttp.RequestUri); Stream input = loWebResponse.GetResponseStream(); int len = 0; while ((len = input.Read(buffer, 0, buffer.Length)) > 0) { for (int i = 0; i < len; i++) ret.Add(buffer[i]); } break; } catch (Exception e) { Console.WriteLine(e.Message); } } } return ret.ToArray(); }
最后说一下@老赵 给的建议(见评论),HTTP请求如果加入gzip/deflate的头,一般都会以压缩的形式返回,大大节省流量了啊。强烈建议爬虫要加压缩。具体的C#代码见下方,直接加入到上面代码的合适位置即可。不过需要注意的是,Apache 服务器据说是一边压缩一边返回数据的,在返回头中不会提示ContentLength。上述代码 ContentLength 相关的代码也需要删除。
loHttp.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;