整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          「Python」教你編寫網(wǎng)絡(luò)爬蟲

          .網(wǎng)絡(luò)爬蟲何時(shí)有用

          假設(shè)我有一個(gè)鞋店,并且想要及時(shí)了解競爭對手的價(jià)格。我可以每天訪問他們的網(wǎng)站,與我店鋪中鞋子的價(jià)格進(jìn)行對比。但是,如果我店鋪中的鞋類品種繁多,或是希望能夠更加頻繁地查看價(jià)格變化的話,就需要花費(fèi)大量的時(shí)間,甚至難以實(shí)現(xiàn)。再舉一個(gè)例子,我看中了一雙鞋,想等它促銷時(shí)再購買。我可能需要每天訪問這家鞋店的網(wǎng)站來查看這雙鞋是否降價(jià),也許需要等待幾個(gè)月的時(shí)間,我才能如愿盼到這雙鞋促銷。上述這 兩個(gè)重復(fù)性的手工流程,都可以利用網(wǎng)絡(luò)爬蟲技術(shù)實(shí)現(xiàn)自動(dòng)化處理。

          理想狀態(tài)下,網(wǎng)絡(luò)爬蟲并不是必須品,每個(gè)網(wǎng)站都應(yīng)該提供API,以結(jié)構(gòu)化的格式共享它們的數(shù)據(jù)。然而現(xiàn)實(shí)情況中,雖然一些網(wǎng)站已經(jīng)提供了這種API,但是它們通常會(huì)限制可以抓取的數(shù)據(jù),以及訪問這些數(shù)據(jù)的頻率。另外,對于網(wǎng)站的開發(fā)者而言,維護(hù)前端界面比維護(hù)后端API接口優(yōu)先級更高。總之,我們不能僅僅依賴于API去訪問我們所需的在線數(shù)據(jù),而是應(yīng)該學(xué)習(xí)一些網(wǎng)絡(luò)爬蟲技術(shù)的相關(guān)知識。

          2. 網(wǎng)絡(luò)爬蟲是否合法

          網(wǎng)絡(luò)爬蟲目前還處于早期的蠻荒階段,“允許哪些行為”這種基本秩序還處于建設(shè)之中。從目前的實(shí)踐來看,如果抓取數(shù)據(jù)的行為用于個(gè)人使用,則不存在問題;而如果數(shù)據(jù)用于轉(zhuǎn)載,那么抓取的數(shù)據(jù)類型就非常關(guān)鍵了。

          世界各地法院的一些案件可以幫助我們確定哪些網(wǎng)絡(luò)爬蟲行為是允許的。在Feist Publications, Inc.起訴Rural Telephone Service Co.的案件中,美國聯(lián)邦最高法院裁定抓取并轉(zhuǎn)載真實(shí)數(shù)據(jù)(比如,電話清單)是允許的。而在澳大利亞,Telstra Corporation Limited起訴Phone Directories Company Pty Ltd這一類似案件中,則裁定只有擁有明確作者的數(shù)據(jù),才可以獲得版權(quán)。此外,在歐盟的ofir.dk起訴home.dk一案中,最終裁定定期抓取和深度鏈接是允許的。

          這些案件告訴我們,當(dāng)抓取的數(shù)據(jù)是現(xiàn)實(shí)生活中的真實(shí)數(shù)據(jù)(比如,營業(yè)地址、電話清單)時(shí),是允許轉(zhuǎn)載的。但是,如果是原創(chuàng)數(shù)據(jù)(比如,意見和評論),通常就會(huì)受到版權(quán)限制,而不能轉(zhuǎn)載。

          無論如何,當(dāng)你抓取某個(gè)網(wǎng)站的數(shù)據(jù)時(shí),請記住自己是該網(wǎng)站的訪客,應(yīng)當(dāng)約束自己的抓取行為,否則他們可能會(huì)封禁你的IP,甚至采取更進(jìn)一步的法律行動(dòng)。這就要求下載請求的速度需要限定在一個(gè)合理值之內(nèi),并且還需要設(shè)定一個(gè)專屬的用戶代理來標(biāo)識自己。在下面的小節(jié)中我們將會(huì)對這些實(shí)踐進(jìn)行具體介紹。

          關(guān)于上述幾個(gè)法律案件的更多信息可以參考下述地址:

          • http://caselaw.lp.findlaw.com/scripts/getcase. pl?court=US&vol=499&invol=340
          • http://www.austlii.edu.au/au/cases/cth/FCA/2010/44.html
          • http://www.bvhd.dk/uploads/tx_mocarticles/S_og_Handelsrettens_afg_relse_i_Ofir-sagen.pdf

          3. 背景調(diào)研

          在深入討論爬取一個(gè)網(wǎng)站之前,我們首先需要對目標(biāo)站點(diǎn)的規(guī)模和結(jié)構(gòu)進(jìn)行一定程度的了解。網(wǎng)站自身的robots.txt和Sitemap文件都可以為我們提供一定的幫助,此外還有一些能提供更詳細(xì)信息的外部工具,比如Google搜索和WHOIS。

          3.1 檢查robots.txt

          大多數(shù)網(wǎng)站都會(huì)定義robots.txt文件,這樣可以讓爬蟲了解爬取該網(wǎng)站時(shí)存在哪些限制。這些限制雖然僅僅作為建議給出,但是良好的網(wǎng)絡(luò)公民都應(yīng)當(dāng)遵守這些限制。在爬取之前,檢查robots.txt文件這一寶貴資源可以最小化爬蟲被封禁的可能,而且還能發(fā)現(xiàn)和網(wǎng)站結(jié)構(gòu)相關(guān)的線索。關(guān)于robots.txt協(xié)議的更多信息可以參見http://www.robotstxt.org。下面的代碼是我們的示例文件robots.txt中的內(nèi)容,可以訪問http://example.webscraping.com/robots.txt獲取。

              # section 1
              User-agent: BadCrawler
              Disallow: /
          
              # section 2
              User-agent: *
              Crawl-delay: 5
              Disallow: /trap
          
              # section 3
              Sitemap: http://example.webscraping.com/sitemap.xml

          在section 1中,robots.txt文件禁止用戶代理為BadCrawler的爬蟲爬取該網(wǎng)站,不過這種寫法可能無法起到應(yīng)有的作用,因?yàn)閻阂馀老x根本不會(huì)遵從robots.txt的要求。本章后面的一個(gè)例子將會(huì)展示如何讓爬蟲自動(dòng)遵守robots.txt的要求。

          section 2規(guī)定,無論使用哪種用戶代理,都應(yīng)該在兩次下載請求之間給出5秒的抓取延遲,我們需要遵從該建議以避免服務(wù)器過載。這里還有一個(gè)/trap鏈接,用于封禁那些爬取了不允許鏈接的惡意爬蟲。如果你訪問了這個(gè)鏈接,服務(wù)器就會(huì)封禁你的IP一分鐘!一個(gè)真實(shí)的網(wǎng)站可能會(huì)對你的IP封禁更長時(shí)間,甚至是永久封禁。不過如果這樣設(shè)置的話,我們就無法繼續(xù)這個(gè)例子了。

          section 3定義了一個(gè)Sitemap文件,我們將在下一節(jié)中了解如何檢查該文件。

          3.2 檢查網(wǎng)站地圖

          網(wǎng)站提供的Sitemap文件(即網(wǎng)站地圖)可以幫助爬蟲定位網(wǎng)站最新的內(nèi)容,而無須爬取每一個(gè)網(wǎng)頁。如果想要了解更多信息,可以從http://www.sitemaps.org/protocol.html獲取網(wǎng)站地圖標(biāo)準(zhǔn)的定義。下面是在robots.txt文件中發(fā)現(xiàn)的Sitemap文件的內(nèi)容。

              <?xml version="1.0" encoding="UTF-8"?>
              <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
                <url><loc>http://example.webscraping.com/view/Afghanistan-1
                  </loc></url>
                <url><loc>http://example.webscraping.com/view/Aland-Islands-2
                 </loc></url>
                <url><loc>http://example.webscraping.com/view/Albania-3</loc>
                  </url>
                ...
              </urlset>

          網(wǎng)站地圖提供了所有網(wǎng)頁的鏈接,我們會(huì)在后面的小節(jié)中使用這些信息,用于創(chuàng)建我們的第一個(gè)爬蟲。雖然Sitemap文件提供了一種爬取網(wǎng)站的有效方式,但是我們?nèi)孕鑼ζ渲?jǐn)慎處理,因?yàn)樵撐募?jīng)常存在缺失、過期或不完整的問題。

          3.3 估算網(wǎng)站大小

          目標(biāo)網(wǎng)站的大小會(huì)影響我們?nèi)绾芜M(jìn)行爬取。如果是像我們的示例站點(diǎn)這樣只有幾百個(gè)URL的網(wǎng)站,效率并沒有那么重要;但如果是擁有數(shù)百萬個(gè)網(wǎng)頁的站點(diǎn),使用串行下載可能需要持續(xù)數(shù)月才能完成,這時(shí)就需要使用第4章中介紹的分布式下載來解決了。

          估算網(wǎng)站大小的一個(gè)簡便方法是檢查Google爬蟲的結(jié)果,因?yàn)镚oogle很可能已經(jīng)爬取過我們感興趣的網(wǎng)站。我們可以通過Google搜索的site關(guān)鍵詞過濾域名結(jié)果,從而獲取該信息。我們可以從http://www.google.com/advanced_search了解到該接口及其他高級搜索參數(shù)的用法。

          圖1所示為使用site關(guān)鍵詞對我們的示例網(wǎng)站進(jìn)行搜索的結(jié)果,即在Google中搜索site:example.webscraping.com。

          從圖1中可以看出,此時(shí)Google估算該網(wǎng)站擁有202個(gè)網(wǎng)頁,這和實(shí)際情況差不多。不過對于更大型的網(wǎng)站,我們會(huì)發(fā)現(xiàn)Google的估算并不十分準(zhǔn)確。

          在域名后面添加URL路徑,可以對結(jié)果進(jìn)行過濾,僅顯示網(wǎng)站的某些部分。圖2所示為搜索site:example.webscraping.com/view的結(jié)果。該搜索條件會(huì)限制Google只搜索國家頁面。

          圖1

          圖2

          這種附加的過濾條件非常有用,因?yàn)樵诶硐肭闆r下,你只希望爬取網(wǎng)站中包含有用數(shù)據(jù)的部分,而不是爬取網(wǎng)站的每個(gè)頁面。

          3.4 識別網(wǎng)站所用技術(shù)

          構(gòu)建網(wǎng)站所使用的技術(shù)類型也會(huì)對我們?nèi)绾闻廊‘a(chǎn)生影響。有一個(gè)十分有用的工具可以檢查網(wǎng)站構(gòu)建的技術(shù)類型——builtwith模塊。該模塊的安裝方法如下。

              pip install builtwith

          該模塊將URL作為參數(shù),下載該URL并對其進(jìn)行分析,然后返回該網(wǎng)站使用的技術(shù)。下面是使用該模塊的一個(gè)例子。

              >>> import builtwith
              >>> builtwith.parse('http://example.webscraping.com')
              {u'javascript-frameworks': [u'jQuery', u'Modernizr', u'jQuery UI'],
               u'programming-languages': [u'Python'],
               u'web-frameworks': [u'Web2py', u'Twitter Bootstrap'],
               u'web-servers': [u'Nginx']}

          從上面的返回結(jié)果中可以看出,示例網(wǎng)站使用了Python的Web2py框架,另外還使用了一些通用的JavaScript庫,因此該網(wǎng)站的內(nèi)容很有可能是嵌入在HTML中的,相對而言比較容易抓取。而如果改用AngularJS構(gòu)建該網(wǎng)站,此時(shí)的網(wǎng)站內(nèi)容就很可能是動(dòng)態(tài)加載的。另外,如果網(wǎng)站使用了ASP.NET,那么在爬取網(wǎng)頁時(shí),就必須要用到會(huì)話管理和表單提交了。

          3.5 尋找網(wǎng)站所有者

          對于一些網(wǎng)站,我們可能會(huì)關(guān)心其所有者是誰。比如,我們已知網(wǎng)站的所有者會(huì)封禁網(wǎng)絡(luò)爬蟲,那么我們最好把下載速度控制得更加保守一些。為了找到網(wǎng)站的所有者,我們可以使用WHOIS協(xié)議查詢域名的注冊者是誰。Python中有一個(gè)針對該協(xié)議的封裝庫,其文檔地址為https://pypi.python.org/pypi/python-whois,我們可以通過pip進(jìn)行安裝。

              pip install python-whois

          下面是使用該模塊對appspot.com這個(gè)域名進(jìn)行WHOIS查詢時(shí)的返回結(jié)果。

              >>> import whois
              >>> print whois.whois('appspot.com')
              {
                ...
                "name_servers": [
                  "NS1.GOOGLE.COM",
                  "NS2.GOOGLE.COM",
                  "NS3.GOOGLE.COM",
                  "NS4.GOOGLE.COM",
                  "ns4.google.com",
                  "ns2.google.com",
                  "ns1.google.com",
                  "ns3.google.com"
                ],
              "org": "Google Inc.",
              "emails": [
                  "abusecomplaints@markmonitor.com",
                  "dns-admin@google.com"
                ]
              }

          從結(jié)果中可以看出該域名歸屬于Google,實(shí)際上也確實(shí)如此。該域名是用于Google App Engine服務(wù)的。當(dāng)我們爬取該域名時(shí)就需要十分小心,因?yàn)镚oogle經(jīng)常會(huì)阻斷網(wǎng)絡(luò)爬蟲,盡管實(shí)際上其自身就是一個(gè)網(wǎng)絡(luò)爬蟲業(yè)務(wù)。

          4. 編寫第一個(gè)網(wǎng)絡(luò)爬蟲

          為了抓取網(wǎng)站,我們首先需要下載包含有感興趣數(shù)據(jù)的網(wǎng)頁,該過程一般被稱為爬取(crawling)。爬取一個(gè)網(wǎng)站有很多種方法,而選用哪種方法更加合適,則取決于目標(biāo)網(wǎng)站的結(jié)構(gòu)。我們首先會(huì)探討如何安全地下載網(wǎng)頁,然后會(huì)介紹如下3種爬取網(wǎng)站的常見方法:

          • 爬取網(wǎng)站地圖;
          • 遍歷每個(gè)網(wǎng)頁的數(shù)據(jù)庫ID;
          • 跟蹤網(wǎng)頁鏈接。

          4.1 下載網(wǎng)頁

          要想爬取網(wǎng)頁,我們首先需要將其下載下來。下面的示例腳本使用Python的urllib2模塊下載URL。

              import urllib2
              def download(url):
                  return urllib2.urlopen(url).read()

          當(dāng)傳入U(xiǎn)RL參數(shù)時(shí),該函數(shù)將會(huì)下載網(wǎng)頁并返回其HTML。不過,這個(gè)代碼片段存在一個(gè)問題,即當(dāng)下載網(wǎng)頁時(shí),我們可能會(huì)遇到一些無法控制的錯(cuò)誤,比如請求的頁面可能不存在。此時(shí),urllib2會(huì)拋出異常,然后退出腳本。安全起見,下面再給出一個(gè)更健壯的版本,可以捕獲這些異常。

              import urllib2
          
              def download(url):
                  print 'Downloading:', url
                  try:
                      html = urllib2.urlopen(url).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                  return html

          現(xiàn)在,當(dāng)出現(xiàn)下載錯(cuò)誤時(shí),該函數(shù)能夠捕獲到異常,然后返回None。

          1.重試下載

          下載時(shí)遇到的錯(cuò)誤經(jīng)常是臨時(shí)性的,比如服務(wù)器過載時(shí)返回的503 Service Unavailable錯(cuò)誤。對于此類錯(cuò)誤,我們可以嘗試重新下載,因?yàn)檫@個(gè)服務(wù)器問題現(xiàn)在可能已解決。不過,我們不需要對所有錯(cuò)誤都嘗試重新下載。如果服務(wù)器返回的是404 Not Found這種錯(cuò)誤,則說明該網(wǎng)頁目前并不存在,再次嘗試同樣的請求一般也不會(huì)出現(xiàn)不同的結(jié)果。

          互聯(lián)網(wǎng)工程任務(wù)組(Internet Engineering Task Force)定義了HTTP錯(cuò)誤的完整列表,詳情可參考https://tools.ietf.org/html/rfc7231#section-6。從該文檔中,我們可以了解到4xx錯(cuò)誤發(fā)生在請求存在問題時(shí),而5xx錯(cuò)誤則發(fā)生在服務(wù)端存在問題時(shí)。所以,我們只需要確保download函數(shù)在發(fā)生5xx錯(cuò)誤時(shí)重試下載即可。下面是支持重試下載功能的新版本 代碼。

              def download(url, num_retries=2):
                  print 'Downloading:', url
                  try:
                      html = urllib2.urlopen(url).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                      if num_retries > 0:
                          if hasattr(e, 'code') and 500 <= e.code < 600:
                              # recursively retry 5xx HTTP errors
                              return download(url, num_retries-1)
                  return html

          現(xiàn)在,當(dāng)download函數(shù)遇到5xx錯(cuò)誤碼時(shí),將會(huì)遞歸調(diào)用函數(shù)自身進(jìn)行重試。此外,該函數(shù)還增加了一個(gè)參數(shù),用于設(shè)定重試下載的次數(shù),其默認(rèn)值為兩次。我們在這里限制網(wǎng)頁下載的嘗試次數(shù),是因?yàn)榉?wù)器錯(cuò)誤可能暫時(shí)還沒有解決。想要測試該函數(shù),可以嘗試下載http://httpstat.us/500,該網(wǎng)址會(huì)始終返回500錯(cuò)誤碼。

          >>> download('http://httpstat.us/500')
          Downloading: http://httpstat.us/500
          Download error: Internal Server Error
          Downloading: http://httpstat.us/500
          Download error: Internal Server Error
          Downloading: http://httpstat.us/500
          Download error: Internal Server Error

          從上面的返回結(jié)果可以看出,download函數(shù)的行為和預(yù)期一致,先嘗試下載網(wǎng)頁,在接收到500錯(cuò)誤后,又進(jìn)行了兩次重試才放棄。

          2.設(shè)置用戶代理

          默認(rèn)情況下,urllib2使用Python-urllib/2.7作為用戶代理下載網(wǎng)頁內(nèi)容,其中2.7是Python的版本號。如果能使用可辨識的用戶代理則更好,這樣可以避免我們的網(wǎng)絡(luò)爬蟲碰到一些問題。此外,也許是因?yàn)樵?jīng)歷過質(zhì)量不佳的Python網(wǎng)絡(luò)爬蟲造成的服務(wù)器過載,一些網(wǎng)站還會(huì)封禁這個(gè)默認(rèn)的用戶代理。比如,在使用Python默認(rèn)用戶代理的情況下,訪問http://www.meetup.com/,目前會(huì)返回如圖3所示的訪問拒絕提示。


          圖3

          因此,為了下載更加可靠,我們需要控制用戶代理的設(shè)定。下面的代碼對download函數(shù)進(jìn)行了修改,設(shè)定了一個(gè)默認(rèn)的用戶代理“wswp”(即Web Scraping with Python的首字母縮寫)。

              def download(url, user_agent='wswp', num_retries=2):
                  print 'Downloading:', url
                  headers = {'User-agent': user_agent}
                  request = urllib2.Request(url, headers=headers)
                  try:
                      html = urllib2.urlopen(request).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                      if num_retries > 0:
                          if hasattr(e, 'code') and 500 <= e.code < 600:
                              # retry 5XX HTTP errors
                              return download(url, user_agent, num_retries-1)
                  return html

          現(xiàn)在,我們擁有了一個(gè)靈活的下載函數(shù),可以在后續(xù)示例中得到復(fù)用。該函數(shù)能夠捕獲異常、重試下載并設(shè)置用戶代理。

          4.2 網(wǎng)站地圖爬蟲

          在第一個(gè)簡單的爬蟲中,我們將使用示例網(wǎng)站robots.txt文件中發(fā)現(xiàn)的網(wǎng)站地圖來下載所有網(wǎng)頁。為了解析網(wǎng)站地圖,我們將會(huì)使用一個(gè)簡單的正則表達(dá)式,從<loc>標(biāo)簽中提取出URL。下面是該示例爬蟲的代碼。

              def crawl_sitemap(url):
                  # download the sitemap file
                  sitemap = download(url)
                  # extract the sitemap links
                  links = re.findall('<loc>(.*?)</loc>', sitemap)
                  # download each link
                  for link in links:
                      html = download(link)
                      # scrape html here
                      # ...

          現(xiàn)在,運(yùn)行網(wǎng)站地圖爬蟲,從示例網(wǎng)站中下載所有國家頁面。

          >>> crawl_sitemap('http://example.webscraping.com/sitemap.xml')
          Downloading: http://example.webscraping.com/sitemap.xml
          Downloading: http://example.webscraping.com/view/Afghanistan-1
          Downloading: http://example.webscraping.com/view/Aland-Islands-2
          Downloading: http://example.webscraping.com/view/Albania-3
          ...

          可以看出,上述運(yùn)行結(jié)果和我們的預(yù)期一致,不過正如前文所述,我們無法依靠Sitemap文件提供每個(gè)網(wǎng)頁的鏈接。下面我們將會(huì)介紹另一個(gè)簡單的爬蟲,該爬蟲不再依賴于Sitemap文件。

          4.3 ID遍歷爬蟲

          本節(jié)中,我們將利用網(wǎng)站結(jié)構(gòu)的弱點(diǎn),更加輕松地訪問所有內(nèi)容。下面是一些示例國家的URL。

          • http://example.webscraping.com/view/Afghanistan-1
          • http://example.webscraping.com/view/Australia-2
          • http://example.webscraping.com/view/Brazil-3

          可以看出,這些URL只在結(jié)尾處有所區(qū)別,包括國家名(作為頁面別名)和ID。在URL中包含頁面別名是非常普遍的做法,可以對搜索引擎優(yōu)化起到幫助作用。一般情況下,Web服務(wù)器會(huì)忽略這個(gè)字符串,只使用ID來匹配數(shù)據(jù)庫中的相關(guān)記錄。下面我們將其移除,加載http://example.webscraping.com/view/1,測試示例網(wǎng)站中的鏈接是否仍然可用。測試結(jié)果如圖4所示。

          圖4

          從圖4中可以看出,網(wǎng)頁依然可以加載成功,也就是說該方法是有用的。現(xiàn)在,我們就可以忽略頁面別名,只遍歷ID來下載所有國家的頁面。下面是使用了該技巧的代碼片段。

              import itertools
              for page in itertools.count(1):
                  url = 'http://example.webscraping.com/view/-%d' % page
                  html = download(url)
                  if html is None:
                      break
                  else:
                      # success - can scrape the result
                      pass

          在這段代碼中,我們對ID進(jìn)行遍歷,直到出現(xiàn)下載錯(cuò)誤時(shí)停止,我們假設(shè)此時(shí)已到達(dá)最后一個(gè)國家的頁面。不過,這種實(shí)現(xiàn)方式存在一個(gè)缺陷,那就是某些記錄可能已被刪除,數(shù)據(jù)庫ID之間并不是連續(xù)的。此時(shí),只要訪問到某個(gè)間隔點(diǎn),爬蟲就會(huì)立即退出。下面是這段代碼的改進(jìn)版本,在該版本中連續(xù)發(fā)生多次下載錯(cuò)誤后才會(huì)退出程序。

              # maximum number of consecutive download errors allowed
              max_errors = 5
              # current number of consecutive download errors
              num_errors = 0
              for page in itertools.count(1):
                  url = 'http://example.webscraping.com/view/-%d' % page
                  html = download(url)
                  if html is None:
                      # received an error trying to download this webpage
                      num_errors += 1
                      if num_errors == max_errors:
                          # reached maximum number of
                          # consecutive errors so exit
                          break
                  else:
                      # success - can scrape the result
                      # ...
                      num_errors = 0

          上面代碼中實(shí)現(xiàn)的爬蟲需要連續(xù)5次下載錯(cuò)誤才會(huì)停止遍歷,這樣就很大程度上降低了遇到被刪除記錄時(shí)過早停止遍歷的風(fēng)險(xiǎn)。

          在爬取網(wǎng)站時(shí),遍歷ID是一個(gè)很便捷的方法,但是和網(wǎng)站地圖爬蟲一樣,這種方法也無法保證始終可用。比如,一些網(wǎng)站會(huì)檢查頁面別名是否滿足預(yù)期,如果不是,則會(huì)返回404 Not Found錯(cuò)誤。而另一些網(wǎng)站則會(huì)使用非連續(xù)大數(shù)作為ID,或是不使用數(shù)值作為ID,此時(shí)遍歷就難以發(fā)揮其作用了。例如,Amazon使用ISBN作為圖書ID,這種編碼包含至少10位數(shù)字。使用ID對Amazon的圖書進(jìn)行遍歷需要測試數(shù)十億次,因此這種方法肯定不是抓取該站內(nèi)容最高效的方法。

          4.4 鏈接爬蟲

          到目前為止,我們已經(jīng)利用示例網(wǎng)站的結(jié)構(gòu)特點(diǎn)實(shí)現(xiàn)了兩個(gè)簡單爬蟲,用于下載所有的國家頁面。只要這兩種技術(shù)可用,就應(yīng)當(dāng)使用其進(jìn)行爬取,因?yàn)檫@兩種方法最小化了需要下載的網(wǎng)頁數(shù)量。不過,對于另一些網(wǎng)站,我們需要讓爬蟲表現(xiàn)得更像普通用戶,跟蹤鏈接,訪問感興趣的內(nèi)容。

          通過跟蹤所有鏈接的方式,我們可以很容易地下載整個(gè)網(wǎng)站的頁面。但是,這種方法會(huì)下載大量我們并不需要的網(wǎng)頁。例如,我們想要從一個(gè)在線論壇中抓取用戶賬號詳情頁,那么此時(shí)我們只需要下載賬號頁,而不需要下載討論貼的頁面。本文中的鏈接爬蟲將使用正則表達(dá)式來確定需要下載哪些頁面。下面是這段代碼的初始版本。

              import re
          
              def link_crawler(seed_url, link_regex):
                  """Crawl from the given seed URL following links matched by link_regex
                  """
                  crawl_queue = [seed_url]
                  while crawl_queue:
                      url = crawl_queue.pop()
                      html = download(url)
                      # filter for links matching our regular expression
                      for link in get_links(html):
                          if re.match(link_regex, link):
                              crawl_queue.append(link)
          
              def get_links(html):
                  """Return a list of links from html
                  """
                  # a regular expression to extract all links from the webpage
                  webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']',
                      re.IGNORECASE)
                  # list of all links from the webpage
                  return webpage_regex.findall(html)

          要運(yùn)行這段代碼,只需要調(diào)用link_crawler函數(shù),并傳入兩個(gè)參數(shù):要爬取的網(wǎng)站URL和用于跟蹤鏈接的正則表達(dá)式。對于示例網(wǎng)站,我們想要爬取的是國家列表索引頁和國家頁面。其中,索引頁鏈接格式如下。

          • http://example.webscraping.com/index/1
          • http://example.webscraping.com/index/2

          國家頁鏈接格式如下。

          • http://example.webscraping.com/view/Afghanistan-1
          • http://example.webscraping.com/view/Aland-Islands-2

          因此,我們可以用/(index|view)/這個(gè)簡單的正則表達(dá)式來匹配這兩類網(wǎng)頁。當(dāng)爬蟲使用這些輸入?yún)?shù)運(yùn)行時(shí)會(huì)發(fā)生什么呢?你會(huì)發(fā)現(xiàn)我們得到了如下的下載錯(cuò)誤。

              >>> link_crawler('http://example.webscraping.com',
                  '/(index|view)')
              Downloading: http://example.webscraping.com
              Downloading: /index/1
              Traceback (most recent call last):
                  ...
              ValueError: unknown url type: /index/1

          可以看出,問題出在下載/index/1時(shí),該鏈接只有網(wǎng)頁的路徑部分,而沒有協(xié)議和服務(wù)器部分,也就是說這是一個(gè)相對鏈接。由于瀏覽器知道你正在瀏覽哪個(gè)網(wǎng)頁,所以在瀏覽器瀏覽時(shí),相對鏈接是能夠正常工作的。但是,urllib2是無法獲知上下文的。為了讓urllib2能夠定位網(wǎng)頁,我們需要將鏈接轉(zhuǎn)換為絕對鏈接的形式,以便包含定位網(wǎng)頁的所有細(xì)節(jié)。如你所愿,Python中確實(shí)有用來實(shí)現(xiàn)這一功能的模塊,該模塊稱為urlparse。下面是link_crawler的改進(jìn)版本,使用了urlparse模塊來創(chuàng)建絕對路徑。

              import urlparse
              def link_crawler(seed_url, link_regex):
                  """Crawl from the given seed URL following links matched by link_regex
                  """
                  crawl_queue = [seed_url]
                  while crawl_queue:
                      url = crawl_queue.pop()
                      html = download(url)
                      for link in get_links(html):
                          if re.match(link_regex, link):
                              link = urlparse.urljoin(seed_url, link)
                              crawl_queue.append(link)

          當(dāng)你運(yùn)行這段代碼時(shí),會(huì)發(fā)現(xiàn)雖然網(wǎng)頁下載沒有出現(xiàn)錯(cuò)誤,但是同樣的地點(diǎn)總是會(huì)被不斷下載到。這是因?yàn)檫@些地點(diǎn)相互之間存在鏈接。比如,澳大利亞鏈接到了南極洲,而南極洲也存在到澳大利亞的鏈接,此時(shí)爬蟲就會(huì)在它們之間不斷循環(huán)下去。要想避免重復(fù)爬取相同的鏈接,我們需要記錄哪些鏈接已經(jīng)被爬取過。下面是修改后的link_crawler函數(shù),已具備存儲(chǔ)已發(fā)現(xiàn)URL的功能,可以避免重復(fù)下載。

              def link_crawler(seed_url, link_regex):
                  crawl_queue = [seed_url]
                  # keep track which URL's have seen before
                  seen = set(crawl_queue)
                  while crawl_queue:
                      url = crawl_queue.pop()
                      html = download(url)
                      for link in get_links(html):
                          # check if link matches expected regex
                          if re.match(link_regex, link):
                              # form absolute link
                              link = urlparse.urljoin(seed_url, link)
                              # check if have already seen this link
                              if link not in seen:
                                  seen.add(link)
                                  crawl_queue.append(link)

          當(dāng)運(yùn)行該腳本時(shí),它會(huì)爬取所有地點(diǎn),并且能夠如期停止。最終,我們得到了一個(gè)可用的爬蟲!

          高級功能

          現(xiàn)在,讓我們?yōu)殒溄优老x添加一些功能,使其在爬取其他網(wǎng)站時(shí)更加有用。

          解析robots.txt

          首先,我們需要解析robots.txt文件,以避免下載禁止爬取的URL。使用Python自帶的robotparser模塊,就可以輕松完成這項(xiàng)工作,如下面的代碼所示。

          >>> import robotparser
          >>> rp = robotparser.RobotFileParser()
          >>> rp.set_url('http://example.webscraping.com/robots.txt')
          >>> rp.read()
          >>> url = 'http://example.webscraping.com'
          >>> user_agent = 'BadCrawler'
          >>> rp.can_fetch(user_agent, url)
          False
          >>> user_agent = 'GoodCrawler'
          >>> rp.can_fetch(user_agent, url)
          True

          robotparser模塊首先加載robots.txt文件,然后通過can_fetch()函數(shù)確定指定的用戶代理是否允許訪問網(wǎng)頁。在本例中,當(dāng)用戶代理設(shè)置為 BadCrawler 時(shí),robotparser模塊會(huì)返回結(jié)果表明無法獲取網(wǎng)頁,這和示例網(wǎng)站robots.txt的定義一樣。

          為了將該功能集成到爬蟲中,我們需要在crawl循環(huán)中添加該檢查。

              ...
              while crawl_queue:
                  url = crawl_queue.pop()
                  # check url passes robots.txt restrictions
                  if rp.can_fetch(user_agent, url):
                      ...
                  else:
                      print 'Blocked by robots.txt:', url
          支持代理

          有時(shí)我們需要使用代理訪問某個(gè)網(wǎng)站。比如,Netflix屏蔽了美國以外的大多數(shù)國家。使用urllib2支持代理并沒有想象中那么容易(可以嘗試使用更友好的Python HTTP模塊requests來實(shí)現(xiàn)該功能,其文檔地址為http://docs.python-requests.org/)。下面是使用urllib2支持代理的代碼。

              proxy = ...
              opener = urllib2.build_opener()
              proxy_params = {urlparse.urlparse(url).scheme: proxy}
              opener.add_handler(urllib2.ProxyHandler(proxy_params))
              response = opener.open(request)

          下面是集成了該功能的新版本download函數(shù)。

              def download(url, user_agent='wswp', proxy=None, num_retries=2):
                  print 'Downloading:', url
                  headers = {'User-agent': user_agent}
                  request = urllib2.Request(url, headers=headers)
          
                  opener = urllib2.build_opener()
                  if proxy:
                      proxy_params = {urlparse.urlparse(url).scheme: proxy}
                      opener.add_handler(urllib2.ProxyHandler(proxy_params))
                  try:
                      html = opener.open(request).read()
                  except urllib2.URLError as e:
                      print 'Download error:', e.reason
                      html = None
                      if num_retries > 0:
                          if hasattr(e, 'code') and 500 <= e.code < 600:
                          # retry 5XX HTTP errors
                          html = download(url, user_agent, proxy,
                              num_retries-1)
                  return html
          下載限速

          如果我們爬取網(wǎng)站的速度過快,就會(huì)面臨被封禁或是造成服務(wù)器過載的風(fēng)險(xiǎn)。為了降低這些風(fēng)險(xiǎn),我們可以在兩次下載之間添加延時(shí),從而對爬蟲限速。下面是實(shí)現(xiàn)了該功能的類的代碼。

              class Throttle:
                  """Add a delay between downloads to the same domain
                  """
                  def __init__(self, delay):
                      # amount of delay between downloads for each domain
                      self.delay = delay
                      # timestamp of when a domain was last accessed
                      self.domains = {}
          
                  def wait(self, url):
                      domain = urlparse.urlparse(url).netloc
                      last_accessed = self.domains.get(domain)
          
                      if self.delay > 0 and last_accessed is not None:
                          sleep_secs = self.delay - (datetime.datetime.now() -
                              last_accessed).seconds
                          if sleep_secs > 0:
                              # domain has been accessed recently
                              # so need to sleep
                              time.sleep(sleep_secs)
                      # update the last accessed time
                      self.domains[domain] = datetime.datetime.now()

          Throttle類記錄了每個(gè)域名上次訪問的時(shí)間,如果當(dāng)前時(shí)間距離上次訪問時(shí)間小于指定延時(shí),則執(zhí)行睡眠操作。我們可以在每次下載之前調(diào)用Throttle對爬蟲進(jìn)行限速。

              throttle = Throttle(delay)
              ...
              throttle.wait(url)
              result = download(url, headers, proxy=proxy,
                  num_retries=num_retries)
          避免爬蟲陷阱

          目前,我們的爬蟲會(huì)跟蹤所有之前沒有訪問過的鏈接。但是,一些網(wǎng)站會(huì)動(dòng)態(tài)生成頁面內(nèi)容,這樣就會(huì)出現(xiàn)無限多的網(wǎng)頁。比如,網(wǎng)站有一個(gè)在線日歷功能,提供了可以訪問下個(gè)月和下一年的鏈接,那么下個(gè)月的頁面中同樣會(huì)包含訪問再下個(gè)月的鏈接,這樣頁面就會(huì)無止境地鏈接下去。這種情況被稱為爬蟲陷阱

          想要避免陷入爬蟲陷阱,一個(gè)簡單的方法是記錄到達(dá)當(dāng)前網(wǎng)頁經(jīng)過了多少個(gè)鏈接,也就是深度。當(dāng)?shù)竭_(dá)最大深度時(shí),爬蟲就不再向隊(duì)列中添加該網(wǎng)頁中的鏈接了。要實(shí)現(xiàn)這一功能,我們需要修改seen變量。該變量原先只記錄訪問過的網(wǎng)頁鏈接,現(xiàn)在修改為一個(gè)字典,增加了頁面深度的記錄。

              def link_crawler(..., max_depth=2):
                  max_depth = 2
                  seen = {}
                  ...
                  depth = seen[url]
                  if depth != max_depth:
                      for link in links:
                          if link not in seen:
                              seen[link] = depth + 1
                              crawl_queue.append(link)

          現(xiàn)在有了這一功能,我們就有信心爬蟲最終一定能夠完成。如果想要禁用該功能,只需將max_depth設(shè)為一個(gè)負(fù)數(shù)即可,此時(shí)當(dāng)前深度永遠(yuǎn)不會(huì)與之相等。

          最終版本

          這個(gè)高級鏈接爬蟲的完整源代碼可以在https://bitbucket.org/ wswp/code/src/tip/chapter01/link_crawler3.py下載得到。要測試這段代碼,我們可以將用戶代理設(shè)置為BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那個(gè)用戶代理。從下面的運(yùn)行結(jié)果中可以看出,爬蟲果然被屏蔽了,代碼啟動(dòng)后馬上就會(huì)結(jié)束。

          >>> seed_url = 'http://example.webscraping.com/index'
          >>> link_regex = '/(index|view)'
          >>> link_crawler(seed_url, link_regex, user_agent='BadCrawler')
          Blocked by robots.txt: http://example.webscraping.com/

          現(xiàn)在,讓我們使用默認(rèn)的用戶代理,并將最大深度設(shè)置為1,這樣只有主頁上的鏈接才會(huì)被下載。

          >>> link_crawler(seed_url, link_regex, max_depth=1)
          Downloading: http://example.webscraping.com//index
          Downloading: http://example.webscraping.com/index/1
          Downloading: http://example.webscraping.com/view/Antigua-and-Barbuda-10
          Downloading: http://example.webscraping.com/view/Antarctica-9
          Downloading: http://example.webscraping.com/view/Anguilla-8
          Downloading: http://example.webscraping.com/view/Angola-7
          Downloading: http://example.webscraping.com/view/Andorra-6
          Downloading: http://example.webscraping.com/view/American-Samoa-5
          Downloading: http://example.webscraping.com/view/Algeria-4
          Downloading: http://example.webscraping.com/view/Albania-3
          Downloading: http://example.webscraping.com/view/Aland-Islands-2
          Downloading: http://example.webscraping.com/view/Afghanistan-1

          和預(yù)期一樣,爬蟲在下載完國家列表的第一頁之后就停止了。

          本文節(jié)選自《用Python寫網(wǎng)絡(luò)爬蟲》

          本書講解了如何使用Python來編寫網(wǎng)絡(luò)爬蟲程序,內(nèi)容包括網(wǎng)絡(luò)爬蟲簡介,從頁面中抓取數(shù)據(jù)的三種方法,提取緩存中的數(shù)據(jù),使用多個(gè)線程和進(jìn)程來進(jìn)行并發(fā)抓取,如何抓取動(dòng)態(tài)頁面中的內(nèi)容,與表單進(jìn)行交互,處理頁面中的驗(yàn)證碼問題,以及使用Scarpy和Portia來進(jìn)行數(shù)據(jù)抓取,并在最后使用本書介紹的數(shù)據(jù)抓取技術(shù)對幾個(gè)真實(shí)的網(wǎng)站進(jìn)行了抓取,旨在幫助讀者活學(xué)活用書中介紹的技術(shù)。

          本書適合有一定Python編程經(jīng)驗(yàn),而且對爬蟲技術(shù)感興趣的讀者閱讀。

          明:SVG 雖然也是標(biāo)簽,但它不是 HTML5,標(biāo)題加了 HTML5 只是為了與 canvas 放到一起。

          一、為什么要學(xué) SVG ?

          SVG 意為可縮放矢量圖形(Scalable Vector Graphics),使用 XML 格式定義矢量圖形。其他的圖像格式都是基于像素的,但是 SVG 沒有單位的概念,它的20只是表示1的20倍,所以 SVG 繪制的圖形放大或縮小都不會(huì)失真。

          與其他圖像比較,SVG 的優(yōu)勢有以下幾點(diǎn):

          1. SVG 可以被多個(gè)工具讀取和修改。
          2. SVG 與其他格式圖片相比,尺寸更小,可壓縮性強(qiáng)。
          3. SVG 可任意伸縮。
          4. SVG 圖像可以隨意地高質(zhì)量打印。
          5. SVG 圖像可以添加文本和事件,還可搜索,適合做地圖。
          6. SVG 是純粹的 XML,不是 HTML5。
          7. SVG是W3C標(biāo)準(zhǔn)

          二、SVG 形狀元素

          2.1、svg 標(biāo)簽

          SVG 的代碼都放到 svg 標(biāo)簽?zāi)兀琒VG 中的標(biāo)簽都是閉合標(biāo)簽,與html中標(biāo)簽用法一致。svg的屬性有:

          • 有width和height,指定了svg的大小。

          eg:畫一條直線,完整代碼如下:

          <!DOCTYPE html>
          <html lang="en">
          <head>
           <meta charset="UTF-8">
           <meta name="viewport" content="width=device-width, initial-scale=1.0">
           <title>Document</title>
          </head>
          <body style="height:600px;">
           <svg width="300" height="300">
            <line x1="0" y1="0" x2="100" y2="100" stroke="black" stroke-width="20"></line>    
           </svg> 
          </body>
          </html>

          上述 svg 設(shè)置的寬高沒有帶單位,此時(shí)默認(rèn)是像素值,如果需要添加單位時(shí),除了絕對單位,也可以設(shè)置相對單位。

          • viewBox 屬性

          使用語法:<svg viewBox=" x1,y1,width,height "></svg>

          四個(gè)參數(shù)分別是左上角的橫縱坐標(biāo)、視口的寬高。表示只看SVG的某一部分,由上述四個(gè)參數(shù)決定。

          使用 viewBox 之后,相當(dāng)于svg整體大小不變,只能看到 viewBox 設(shè)置部分,視覺上被放大。

          2.2、SVG 如何嵌入 HTML

          SVG 的代碼可以直接嵌入到 html 頁面中,也可以通過 html 的embed、object、iframe嵌入到html中。嵌入的時(shí)候嵌入的是 SVG 文件,SVG 文件必須使用 .svg 后綴。分別介紹各種方法如何使用?

          2.2.1、embed 嵌入:

          使用語法:<embed src="line.svg" type="image/svg+xml"></embed>

          src是SVG文件路徑,type 表示 embed 引入文件類型。

          優(yōu)點(diǎn):所有瀏覽器都支持,并允許使用腳本。

          缺點(diǎn):不推薦 html4 和 html 中使用,但 html5 支持。

          2.2.2、object 嵌入:

          使用語法:<object data="line.svg" type="image/svg+xml"></object>

          data 是 SVG 文件路徑,type 表示 object 引入文件類型。

          優(yōu)點(diǎn):所有瀏覽器都支持,支持 html、html4 和 html5。

          缺點(diǎn):不允許使用腳本。

          2.2.3、iframe 嵌入:

          使用語法:<iframe width="300" height="300" src="./line.svg" frameborder="0"></iframe>

          src是 SVG 文件路徑,width、height、frameborder 設(shè)置的大小和邊框。

          優(yōu)點(diǎn):所有瀏覽器都支持,并允許使用腳本。

          缺點(diǎn):不推薦 html4 和 html 中使用,但 html5 支持。

          2.2.4、html中嵌入:

          svg 標(biāo)簽直接插入 html 內(nèi)容內(nèi),與其他標(biāo)簽用法一致。

          2.2.5、連接到svg文件:

          使用 a 標(biāo)簽,直接鏈接到 SVG 文件。

          使用語法:<a href="line.svg">查看SVG</a>

          三、SVG形狀元素

          3.1、 - line

          使用語法:
          <svg width="300" height="300" >  
           <line x1="0" y1="0" x2="300" y2="300" stroke="black" stroke-width="20"></line>
          </svg>

          使用line標(biāo)簽創(chuàng)建線條,(x1,y1)是起點(diǎn),(x2,y2)是終點(diǎn),stroke繪制黑線,stroke-width是線寬。

          3.2、矩形 - rect

          //使用語法:
          <svg width="300" height="300" >
          <rect 
           width="100" height="100"  //大小設(shè)置
           x="50" y="50"  //可選 左上角位置,svg的左上角默認(rèn)(0,0)
           rx="20" ry="50" //可選 設(shè)置圓角
           stroke-width="3" stroke="red" fill="pink" //繪制樣式控制
          ></rect>
          </svg>

          上述參數(shù) width、height是必填參數(shù),x、y是可選參數(shù),如不設(shè)置的時(shí)候,默認(rèn)為(0,0),也就是svg的左上角開始繪制。rx、ry是可選參數(shù),不設(shè)置是矩形沒有圓角。fill定義填充顏色。

          3.3、圓形 - circle

          // 使用語法
          <svg width="300" height="300" >
           <circle 
            cx="100" cy="50" // 定義圓心 ,可選
            r="40" // 圓的半徑
            stroke="black" stroke-width="2" fill="red"/> //繪制黑框填充紅色
          </svg>

          上述(cx,xy)定義圓心的位置,是可選參數(shù),如果不設(shè)置默認(rèn)圓心是(0,0)。r是必需參數(shù),設(shè)置圓的半徑。

          3.4、橢圓 - ellipse

          橢圓與圓相似,不同之處在于橢圓有不同的x和y半徑,而圓兩個(gè)半徑是相同的。

          // 使用語法
          <svg width="300" height="300" >
           <ellipse 
            rx="20" ry="100" //設(shè)置橢圓的x、y方向的半徑
            fill="purple" // 橢圓填充色
            cx="150" cy="150" //設(shè)置橢圓的圓心 ,可選參數(shù)
           ></ellipse>
          </svg>

          上述橢圓的兩個(gè)rx、ry兩個(gè)方向半徑是必須參數(shù),如果rx=ry就表示是圓形,(cx,cy)是橢圓的圓心,是可選參數(shù),如果不設(shè)置,則默認(rèn)圓心為(0,0)。

          3.5、折線 - polyline

          // 使用語法
          <svg width="300" height="300" style="border:solid 1px red;">
            <!-- 繪制出一個(gè)默認(rèn)填充黑色的三角形 -->
           <polyline 
            points=" //點(diǎn)的集合
             0 ,0, // 第一個(gè)點(diǎn)坐標(biāo)
             100,100, // 第二個(gè)點(diǎn)坐標(biāo)
             100,200 // 第三個(gè)點(diǎn)坐標(biāo)
              " 
            stroke="green" 
           ></polyline>
          <!-- 繪制一個(gè)臺(tái)階式的一條折線 -->
           <polyline 
            points="0,0,50,0,50,50,100,50,100,100,150,100,150,150" 
            stroke="#4b27ff" fill="none"
           ></polyline>
          </svg>

          上述代碼執(zhí)行結(jié)果如圖所示:

          需要注意的是 points 中包含了多個(gè)點(diǎn)的坐標(biāo),但不是一個(gè)數(shù)組。

          3.6、多邊形 - polygon

          polygon 標(biāo)簽用來創(chuàng)建不少于3個(gè)邊的圖形,多邊形是閉合的,即所有線條連接起來。

          // 使用語法
          <svg width="300" height="300" style="border:solid 1px red;">
           <polygon 
            points="
              0,0,   //多邊形的第一點(diǎn)
             100,100,  //多邊形的第二點(diǎn)
              0,100  //多邊形的第三點(diǎn)
            " 
          	stroke="purple"
          	stroke-width="1"
          	fill="none"
           ></polygon>
          </svg>

          polygon繪制的時(shí)候與折線有些類似,但是polygon會(huì)自動(dòng)閉合,折線不會(huì)。

          3.7、路徑 - path

          path 是SVG基本形狀中最強(qiáng)大的一個(gè),不僅能創(chuàng)建其他基本形狀,還能創(chuàng)建更多其他形狀,如貝塞爾曲線、2次曲線等。

          點(diǎn)個(gè)關(guān)注,下篇更精彩!

          活中犯錯(cuò)誤是正常的,沒有人不會(huì)犯錯(cuò)誤,更何況是開發(fā)人員呢?今天我們就來卡看看開發(fā)人員在編寫 HTML 和 CSS 時(shí)最常犯的六大錯(cuò)誤有哪些。

          作者 | Stas Melnikov

          譯者 | 彎月,責(zé)編 | 劉靜

          出品 | CSDN(ID:CSDNnews)

          以下為譯文:

          用placeholder屬性代替label元素

          開發(fā)人員經(jīng)常用placeholder屬性代替label元素。但是,在這種寫法下,使用屏幕閱讀器的用戶無法填寫字段,因?yàn)槠聊婚喿x器無法從placeholder屬性中讀取文本。

          <input type="email" placeholder="Enter your email">

          因此,我建議用label元素顯示字段名稱,而placeholder應(yīng)該作為例子顯示在用戶需要填充的數(shù)據(jù)中。

          <label>
          <span>Enter your email</span>
          <input type="email" placeholder="e.g. example@gmail.com">
          </label>

          用img元素標(biāo)記裝飾用的圖片

          我經(jīng)常看到開發(fā)人員混淆裝飾圖片和內(nèi)容圖片。例如,他們會(huì)使用img元素來顯示社交圖標(biāo)。

          <a href="https://twitter.com" class="social">
          <img class="social__icon" src="twitter.svg" alt>
          <span class="social__name">Twitter</span>
          </a>

          然而,社交圖標(biāo)是裝飾性圖標(biāo),其目的是幫助用戶迅速理解元素的含義,而無需閱讀文本。即便我們刪除這些圖標(biāo),元素的含義也不會(huì)消失,所以我們應(yīng)該使用background-image屬性。

          <a href="https://twitter.com" class="social">
          <span class="social__name">Twitter</span>
          </a>
          .social::before {
          background-image: url("twitter.svg");
          }

          使用resize屬性

          如果利用resize屬性來禁止textarea調(diào)整大小,那么你就破壞了可訪問性。因?yàn)橛脩魺o法舒適地輸入數(shù)據(jù)。

          textarea {
          width: 100%;
          height: 200px;
          resize: none;
          }

          你應(yīng)該使用min-width、max-width、min-height以及max-height屬性,這些屬性可以限制元素的大小,而且用戶也可以舒舒服服地輸入數(shù)據(jù)。

          textarea {
          min-width: 100%;
          max-width: 100%;
          min-height: 200px;
          max-height: 400px;
          }

          同時(shí)使用display: block和position: absolute(fixed)

          我經(jīng)常看見開發(fā)人員像下面這樣使用display和position屬性:

          .button::before {
          content: "";
          display: block;
          position: absolute;
          top: 0;
          left: 0;
          }

          但是,瀏覽器會(huì)默認(rèn)設(shè)置block。因此,你無需為absolute或fixed的元素設(shè)置這個(gè)值。也就是說,以下代碼的結(jié)果與上述代碼完全相同。

          .button::before {
          content: "";
          position: absolute;
          top: 0;
          left: 0;
          }

          Outline屬性的none值

          無法通過鍵盤訪問網(wǎng)站;鏈接打不開;無法注冊等等。出現(xiàn)這些情況是因?yàn)殚_發(fā)人員將outline屬性設(shè)置成了none值,因此元素?zé)o法聚焦。

          .button:focus {
          outline: none;
          }

          /* or */

          .button:focus {
          outline: 0;
          }

          如果你需要禁用默認(rèn)的聚焦,那么也別忘了指定取而代之的聚焦?fàn)顟B(tài)。

          .button:focus {
          outline: none;
          box-shadow: 0 0 3px 0 blue;
          }

          空元素

          開發(fā)人員經(jīng)常使用HTML空元素來調(diào)整元素的樣式。例如,利用空div或span元素來顯示導(dǎo)航欄菜單。

          <button class="hamburger">
          <span></span>
          <span></span>
          <span></span>
          </button>

          .hamburger {
          width: 60px;
          height: 45px;
          position: relative;
          }

          .hamburger span {
          width: 100%;
          height: 9px;

          background-color: #d3531a;
          border-radius: 9px;

          position: absolute;
          left: 0;
          }

          .hamburger span:nth-child(1) {
          top: 0;
          }

          .hamburger span:nth-child(2) {
          top: 18px;
          }

          .hamburger span:nth-child(3) {
          top: 36px;
          }

          其實(shí),你可以使用 ::before和 ::after偽元素達(dá)成同樣的效果。

          <button class="hamburger">
          <span class="hamburger__text">
          <span class="visually-hidden">Open menu</span>
          </span>
          </button>

          .hamburger {
          width: 60px;
          height: 45px;
          position: relative;
          }

          .hamburger::before,
          .hamburger::after,
          .hamburger__text::before {
          content: "";
          width: 100%;
          height: 9px;

          background-color: #d3531a;
          border-radius: 9px;

          position: absolute;
          left: 0;
          }

          .hamburger::before {
          top: 0;
          }

          .hamburger::after {
          top: 18px;
          }

          .hamburger__text::before {
          top: 36px;
          }

          .visually-hidden {
          position: absolute !important;
          clip: rect(1px, 1px, 1px, 1px);
          width: 1px !important;
          height: 1px !important;
          overflow: hidden;
          }

          原文:https://dev.to/melnik909/the-6-most-common-mistakes-developers-when-writing-html-and-css-f92

          本文為 CSDN 翻譯,轉(zhuǎn)載請注明來源出處。

          【END】


          主站蜘蛛池模板: 波多野结衣一区在线| 久久一区二区三区免费播放| 亚洲一区日韩高清中文字幕亚洲 | 亚洲av无码一区二区三区乱子伦| 麻豆天美国产一区在线播放| 久久久久人妻一区精品果冻| 国产成人精品无码一区二区老年人 | 日韩AV在线不卡一区二区三区| 亚洲av乱码一区二区三区香蕉| 丰满爆乳一区二区三区| 免费观看日本污污ww网站一区 | 日韩人妻不卡一区二区三区| 中文字幕一区视频一线| 青娱乐国产官网极品一区| 国产欧美色一区二区三区| 亚洲一区二区三区国产精华液| 精品国产乱码一区二区三区| 人妻少妇精品视频一区二区三区| 精品日产一区二区三区手机| 在线观看国产区亚洲一区成人| 国产另类ts人妖一区二区三区| 亚洲第一区在线观看| www亚洲精品少妇裸乳一区二区 | 国产91一区二区在线播放不卡| 日本高清天码一区在线播放| 国产精品一区二区无线| 日本一区中文字幕日本一二三区视频 | 亚洲欧美一区二区三区日产| 国产高清一区二区三区| 在线成人一区二区| 亚洲AV成人精品日韩一区 | 中文字幕日韩欧美一区二区三区| 亚洲av无一区二区三区| 人妻少妇精品视频三区二区一区 | 麻豆一区二区三区蜜桃免费| 一区二区三区免费在线视频| 亚洲中文字幕丝袜制服一区| 国产日韩一区二区三免费高清| 性色AV一区二区三区| 中文字幕人妻第一区| 日韩在线一区二区三区免费视频|