從測試300萬個超鏈接接學到的
Stack Exchange上有超過三百萬個不同的鏈接。經過很長時間,許多鏈接已經不能用了。
最近我花時間編寫了一個工具,它能判斷哪些是壞鏈,能幫助我們來修復壞鏈。
我們是怎么做的?
首先,我們要對他人的網站心存敬意。
做一個好的網民
● 對每個域名限制請求
我們采用自動過期的set,來確保十秒鐘內對單個域名不會請求多過一次。當我們覺得需要對某些鏈接進行更多的測試時,我們也做了特殊處理。
- public class AutoExpireSet<T>
- {
- Dictionary<T, DateTime> items = new Dictionary<T, DateTime>();
- Dictionary<T, TimeSpan> expireOverride =
- new Dictionary<T, TimeSpan>();
- int defaultDurationSeconds;
- public AutoExpireSet(int defaultDurationSeconds)
- {
- this.defaultDurationSeconds =
- defaultDurationSeconds;
- }
- public bool TryReserve(T t)
- {
- bool reserved = false;
- lock (this)
- {
- DateTime dt;
- if (!items.TryGetValue(t, out dt))
- {
- dt = DateTime.MinValue;
- }
- if (dt < DateTime.UtcNow)
- {
- TimeSpan span;
- if (!expireOverride.TryGetValue(t, out span))
- {
- span =
- TimeSpan.FromSeconds(defaultDurationSeconds);
- }
- items[t] = DateTime.UtcNow.Add(span);
- reserved = true;
- }
- }
- return reserved;
- }
- public void ExpireOverride(T t, TimeSpan span)
- {
- lock (this)
- {
- expireOverride[t] = span;
- }
- }
- }
● 健壯的驗證函數
我們的驗證函數包括了許多我認為非常重要的概念。
- public ValidateResult Validate(
- bool useHeadMethod = true,
- bool enableKeepAlive = false,
- int timeoutSeconds = 30 )
- {
- ValidateResult result = new ValidateResult();
- HttpWebRequest request = WebRequest.Create(Uri)
- as HttpWebRequest;
- if (useHeadMethod)
- {
- request.Method = "HEAD";
- }
- else
- {
- request.Method = "GET";
- }
- // always compress, if you get back a 404 from a HEAD
- // it can be quite big.
- request.AutomaticDecompression = DecompressionMethods.GZip;
- request.AllowAutoRedirect = false;
- request.UserAgent = UserAgentString;
- request.Timeout = timeoutSeconds * 1000;
- request.KeepAlive = enableKeepAlive;
- HttpWebResponse response = null;
- try
- {
- response = request.GetResponse() as HttpWebResponse;
- result.StatusCode = response.StatusCode;
- if (response.StatusCode ==
- HttpStatusCode.Redirect ||
- response.StatusCode ==
- HttpStatusCode.MovedPermanently ||
- response.StatusCode ==
- HttpStatusCode.SeeOther ||
- response.StatusCode ==
- HttpStatusCode.TemporaryRedirect)
- {
- try
- {
- Uri targetUri =
- new Uri(Uri, response.Headers["Location"]);
- var scheme = targetUri.Scheme.ToLower();
- if (scheme == "http" || scheme == "https")
- {
- result.RedirectResult =
- new ExternalUrl(targetUri);
- }
- else
- {
- // this little gem was born out of
- // http://tinyurl.com/18r
- // redirecting to about:blank
- result.StatusCode =
- HttpStatusCode.SwitchingProtocols;
- result.WebExceptionStatus = null;
- }
- }
- catch (UriFormatException)
- {
- // another gem ... people sometimes redirect to
- // http://nonsense:port/yay
- result.StatusCode =
- HttpStatusCode.SwitchingProtocols;
- result.WebExceptionStatus =
- WebExceptionStatus.NameResolutionFailure;
- }
- }
● 從***天開始就設置正確的User Agent字符串
如果什么地方出錯了,你希望他人能夠聯系到你。我們的鏈接爬蟲的user agent字符串為: Mozilla/5.0 (compatible; stackexchangebot/1.0; +http://meta.stackoverflow.com/q/130398)。
● 處理302, 303, 307等頁面跳轉
盡管302和303跳轉非常常見,307卻不多見。它被作為一種針對瀏覽器的錯誤表現的解決方法被引入,解釋見此處。
307***的例子是http://www.haskell.org。我非常不贊同在首頁就跳轉地做法,URL重寫以及其他的工具可以解決這個問題,而不需要有多余的跳轉;但是,首頁跳轉仍舊存在。
當你跳轉時,你需要繼續測試。我們的鏈接測試機會測試最多五層。你需要設置層次上限,否則你會陷入無限循環。
跳轉有時很奇怪,網站有時會把你導向到about:config或一個不存在的URL。檢驗跳轉的頁面信息很重要。
● 當你獲得所需要的信息時,請及時中斷請求
在TCP協議中,包收到時,特殊的狀態會被標記。當客戶端發送給服務器的包中標記了FIN的話,連接會早早的中止。調用request.Abort你可以避免在404時從服務器端下載大量數據。
當測試鏈接時,你經常需要避免HTTP keepalive。因為我們的測試機沒必要給服務器造成不必要得連接負擔。
中斷可以減少壓縮,但我非常贊成啟用壓縮。
● 先使用HEAD請求,再用GET請求
一些服務器不使用HEAD。例如,Amazon完全禁止了,對HEAD請求返回405。在ASP.NET MVC中,人們經常顯式設置路由經過的verb屬性。程序員們在規定使用HttpVerbs.Get時往往沒有使用HttpVerbs.Head。所以當你失敗時(沒有獲得200響應),你需要重新使用GET verb來測試。(譯者:這一段不是很懂,如有錯誤請指正。)
● 忽略robots.txt
開始我打算做一個好網民,解析了所有的robots.txt文件,遵守排除和爬蟲頻率。但事實上許多網站如GitHub, Delicious和Facebook都有針對爬蟲的白名單。所有的爬蟲都被屏蔽了,除了那些著名的允許爬蟲的網站(如Google, Yahoo和Bing)。因為鏈接測試機是不會抓取網頁,關注robots.txt也不現實,所以我建議忽略robots.txt。這在Meta Stack Overflow也有討論。
● 使用合理的超時
測試時,我們給網站30s來響應,但有些網站需要更長時間。你當然不想讓一個惡意的網站讓你的測試機停止。所以我們采用30s作為最長的響應時間。
● 用很多線程來測試鏈接
我用在悉尼的開發電腦來做鏈接測試,顯然串行的三百萬次訪問不知道會占用多長時間。所以我用了30個線程。
并發當然也會帶來一些技術挑戰。你也不想在等待一個域名釋放資源的時候讓一個線程阻塞。
我采用Async類來管理隊列。相對于微軟的任務并行庫(Microsoft Task Parallel Library),我更喜歡Async,因為使用它來限制線程池中的線程數量非常簡單,而且API也簡單易用。
● 一次實效不代表***失效
我仍舊在調整判斷一個鏈接是壞鏈的算法。一次失效有可能是偶然事件。一個星期內的數次失效可能是服務器壞掉或者不幸的巧合。
現在隔天的兩次失效看起來比較可靠 – 我們沒有去尋找最***的算法,而是讓用戶告訴我們什么時候出錯了,但我們相信出錯率不高。
同樣的我們仍舊需要確定在一次成功測試之后多久藥重新測試。我想每隔三個月測一次就足夠了。
測試鏈接的一些有趣發現
Kernel.org被黑了
2011年9月1日,Kernel.org被黑了。你要問,這和測試鏈接有什么關系呢?
事實證明有人破壞了所有的文檔鏈接,這些鏈接今天仍舊不能用。例如http://www.kernel.org/pub/software/scm /git/docs/git-svn.html 在Stack Overflow的150個左右的帖子里出現過,現在它們會將你導向到404頁面,而它的新地址應該在:http://git-scm.com/docs /git-svn。在所有我碰到的壞鏈中,git文檔的壞鏈是最嚴重的。將近影響了6000個帖子。采用Apache的重寫功能來處理它是非常容易的。
有的網站的URL不能給你任何信息
http://www.microsoft.com/downloads/details.aspx?familyid=e59c3964-672d-4511-bb3e-2d5e1db91038displaylang=en 是個壞鏈,在60個左右的帖子中出現。想象下,如果這個鏈接類似于http://www.microsoft.com/downloads/ie- developer-toolbar-beta-3,那么就算微軟打算移走這個鏈接,我們仍舊克一猜測它可能帶我們去到什么頁面。
將你的404頁面做的別致和有用–從GitHub學到的
在所有的404頁面中,GitHub的讓我最生氣。
你問為什么?
它看起來很酷,有相當不錯的視覺效果。有些人就是看什么都不順眼。
嗯,事實上是:
https://github.com/dbalatero/typhoeus 在50個左右的帖子里被引用,而它已經轉移到https://github.com/typhoeus。GitHub沒有使用任何的跳轉,僅僅將你轉到404頁面。
對url采用最基本的解析以確定真正想要去的頁面是非常小的開銷:
對不起,我們沒有找到你鏈接到的頁面。用戶經常會改變賬戶導致鏈接失效。”typhoeus”庫也存在于:https://github.com/typhoeus
是的,沒有任何信息告訴我我犯了個錯誤。GitHub應該讓404頁面變得更有用。對我來說GitHub 404頁面最讓我氣憤地是我花了很多力氣而找不到結果。不要給我漂亮的頁面,能提供一些有用的信息嗎。
你可以做多一步,跳轉到他們新的首頁去,我理解賬號是非常有技巧的,但它看起來在GitHub上是多么不可思議的常見錯誤啊。
在Stack Overflow上我們花了很多時間來優化這種情況,例如“你最喜歡的程序員笑話是什么?”,討論區認為這個問題不會持續很久,所以我們盡可能解釋為什么要移除它,以及哪里可以找到它。
Oracle的問題
Oracle收購Sun對Java生態圈來說是個永遠的沉重的打擊。Oracle的任務是重新樹立品牌,重構Java 生態圈,但這是錯誤的引導。大量的文檔都沒有被正確定向。就連最近的在dev.java.net下的所有項目都沒有正確的跳轉頁面。Hudson這個 Java持續集成的服務器曾經使用https://hudson.dev.java.net/ (譯者注:也失效了),Stack Overflow中150個帖子都引用了它。
個人的教訓
href 標題的重要性
在短鏈的世界里,看起來在URI里使用任何合理的標題不再那么被鼓勵了。事實上過去的三年里你訪問的5%的鏈接都失效了。我相信我的博客中也有許多壞鏈。修復壞鏈是個困難的任務,尤其在沒有上下文的情況下,這項任務變得更加困難。
所以我決定為我的鏈接都加上合理的標題。不僅因為能讓搜索引擎更好地搜索結果,也能讓用戶知道受損的圖片下是什么內容,同時在處理壞的勢后能幫我修復它。
超鏈接是很脆弱的
當我們使用Google時,我們從來沒得到404。它確保我們在雜亂無章的網絡中高效的搜索。測試很多的鏈接告訴你現實并沒有那么的好。那么意味著我要避免使用鏈接嗎?當然不是,知道問題的存在能夠幫我思考我寫下的內容。我會避免寫出失去意義的文章。在Stack Overflow我們經常看到如下的回復:
See this blog post over here. 看看這里的文章。
當外部資源鏈接失效的時候,這種答案就沒有了意義。
原文鏈接: samsaffron.com