一个让WebClient(C#)、curl(PHP)、opener(Python)等各种抓取代码失效的网页及抓取办法

  重要更新:在@老赵的提醒下,如果在请求中加入“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下载这个页面时,出现了这样的提示:

$ wget http://www.hacker.org/challenge/solvers.php?id=1
–2012-07-05 16:46:30– http://www.hacker.org/challenge/solvers.php?id=1
Resolving www.hacker.org… 173.236.190.252
Connecting to www.hacker.org|173.236.190.252|:80… connected.
HTTP request sent, awaiting response… 200 OK
Length: 243584 (238K) [text/html]
Saving to: `solvers.php?id=1.2′

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) {
    List<byte> ret = new List<byte>();
    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;

3 评论

  1. 不知道你的爬虫是不是在请求里加了gzip/deflate头,现在基本是个服务器都支持吧,而且就算不支持也最多跟你现在一样,但绝大部分情况下是会得到压缩的,这就可以收到少得多的数据了。200K的网页绝对压缩到70K以内……

老赵进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注