紹
網頁抓取是一種重要的技術,經常在許多不同的環境中使用,尤其是數據科學和數據挖掘。 Python在很大程度上被認為是網絡抓取的首選語言,其原因在于Python的內嵌電池特性。 使用Python,您可以在大約15分鐘內使用不到100行代碼創建一個簡單的抓取腳本。 因此,無論何種用途,網頁抓取都是每個Python程序員必須具備的技能。
在我們開始動手之前,我們需要退后一步,考慮什么是網頁抓取,什么時候應該使用它,何時避免使用它。
如您所知,網頁抓取是一種用于從網站自動提取數據的技術。 重要的是要理解,網頁抓取是一種從各種來源(通常是網頁)中提取數據的粗略技術。 如果網站的開發人員足夠慷慨地提供API來提取數據,那么訪問數據將是一種更加穩定和健壯的方式。 因此,根據經驗,如果網站提供API以編程方式檢索其數據,請使用它。 如果API不可用,則只能使用網絡抓取。
請務必遵守有關您使用的每個網站的網頁抓取的任何規則或限制,因為有些網站不允許這樣做。 有了這個清楚的認識,讓我們直接進入教程。
在本教程中,我們將抓取http://quotes.toscrape.com/,這是一個列出著名作家名言的網站。
網頁抓取管道
我們可以將web-scraping理解為包含3個組件的管道:
下載:下載HTML網頁
解析:解析HTML并檢索我們感興趣的數據
存儲:以特定格式將檢索到的數據存儲在本地計算機中
下載HTML
從網頁中提取任何數據,從邏輯上講,我們首先要下載它。 我們有兩種方法可以做到這一點:
1.使用瀏覽器自動化庫
您可以使用Selenium等瀏覽器自動化庫從網頁下載HTML。 Selenium允許您打開瀏覽器,比方說Chrome,并根據需要控制它。 您可以在瀏覽器中打開網頁,然后使用Selenium自動獲取該頁面的HTML代碼。
但是,這種方法有一個很大的缺點 - 它明顯變慢。 原因是運行瀏覽器并在瀏覽器中呈現HTML的開銷。 此方法僅應用于特殊情況 - 我們要抓取的內容在瀏覽器中使用JavaScript代碼,或者要求我們單擊按鈕/鏈接以獲取數據,Selenium可以為我們執行此操作。
2.使用HTTP庫
與第一種方法不同,HTTP庫(例如Requests模塊或Urllib)允許您發送HTTP請求,完全不需要打開任何瀏覽器。 這種方法應該始終是首選,因為它比Selenium快得多。
現在讓我告訴您如何使用Selenium和Requests庫實現管道這個組件:
使用Requests
使用以下命令安裝Requests模塊:
現在您可以在代碼中使用它,如下所示:
這里,對URL進行HTTP GET請求,這幾乎與下載網頁同義。 然后,我們可以通過訪問requests.get方法返回的結果對象來獲取頁面的HTML源代碼。
使用Selenium
您可以通過pip安裝selenium模塊:
在這里,我們首先創建一個表示瀏覽器的webdriver對象。 這樣做會在運行代碼的計算機上打開Chrome瀏覽器。 然后,通過調用webdriver對象的get方法,我們可以打開URL。 最后,我們通過訪問webdriver對象的page_source屬性來獲取源代碼。
在這兩種情況下,URL的HTML源都作為字符串存儲在page變量中。
解析HTML和提取數據
不必深入計算機科學理論,我們可以將解析定義為分析字符串的過程,以便我們可以理解其內容,從而輕松訪問其中的數據。
在Python中,有兩個庫可以幫助我們解析HTML:BeautifulSoup和Lxml。 Lxml是一個比BeautifulSoup更低級的框架,我們可以在BeautifulSoup中使用Lxml作為后端,因此對于簡單的HTML解析,BeautifulSoup將是首選的庫。
但在我們深入分析之前,我們必須分析網頁的HTML,看看我們想要抓取的數據是如何構建和定位的。只有當我們掌握了這些信息時,我們才能從解析的HTML中獲取我們想要的信息。但幸運的是,我們不必在編輯器中打開源代碼,并手動理解每個HTML元素并將其與渲染頁面中的相應數據相關聯。大多數瀏覽器都提供了一個檢查器,比如Chrome的開發人員工具,它使我們只需單擊它們即可快速查看任何元素的HTML代碼。
要在Chrome中執行此操作,請在Chrome中打開網頁,然后右鍵單擊要抓取的數據,然后選擇“檢查”。在Firefox中,此選項稱為Inspect Element - 這是在做相同的事情,但只是名稱不同。
您會注意到Chrome窗口底部打開了一個窗格,其中包含您單擊的元素的源代碼。 瀏覽一下源代碼,了解我們想要抓取的數據是如何在HTML代碼中構建的。
經過一些檢查后你可以理解,http://quotes.toscrape.com/上的每個引用都包含在一個帶有class =“quote”屬性的div中。 在該div中,引用的文本在class =“text”的范圍內,作者的名稱在class =“author”的小標簽中。 當我們實際解析HTML并提取數據時,將需要此信息。
現在,讓我們開始使用BeautifulSoup解析HTML頁面。 但首先,我們必須安裝它:
安裝好之后,可以像下面這樣在代碼中調用:
首先,我們通過將頁面傳遞給BeautifulSoup類構造函數來創建頁面的解析版本。 如您所見,我們還將第二個參數html.parser傳遞給構造函數。 這是Beautiful Soup將用于解析傳遞給它的字符串的解析器的名稱。 你可以使用我們之前談到過的解析器lxml,因為你已經安裝了Lxml庫。
然后,我們提取包含class =“quote”的頁面中的所有div標簽,因為我們知道這些是包含引用的div。 為此,Beautiful Soup 4提供了find_all功能。 我們將標記名稱和類名稱傳遞給find_all函數,并返回滿足條件的所有標記,即包含引用的標記。
這里需要注意的一件重要事情是,我們在這里使用樹結構。 變量soup以及引用的每個元素都是樹。 在某種程度上,引用的元素是較大的soup樹的一部分。 無論如何,為避免進入不同的討論,讓我們繼續。
我們知道引用的文本是帶有class =“text”的span標記,而作者是帶有class =“author”的小標記。 要從quote元素中提取它們,我們再次使用類似的函數find。 find函數使用與find_all函數相同的參數。 唯一的區別是它返回滿足條件的第一個標記,而find_all返回標記列表。 此外,我們希望訪問返回對象的text屬性,該對象包含該標記中包含的文本。
因此,正如您在代碼中看到的那樣,我們遍歷列表引用的所有元素,并提取引用文本和作者名稱,將它們存儲在名稱為scraped的列表。 在控制臺上打印時,已抓取的列表如下所示:
存儲檢索的數據
一旦我們獲得了數據,我們就可以以任何我們想要的格式存儲它,例如CSV文件,SQL數據庫或NoSQL數據庫。 嚴格來說,這一步不應算作抓取過程的一部分,但為了完整起見,我將簡要介紹它。
我想說最流行的存儲抓取數據的方法是將它們存儲為CSV電子表格,所以我將簡要介紹如何做到這一點。 我不會詳細介紹,因為您應該參考官方的Python文檔。 所以,不用多說,讓我們看看代碼。
我們可以看到,代碼非常明顯。 我們從打開的quotes.csv文件創建一個CSV編寫器對象,然后使用writerow函數逐個寫入引用。 很明顯,writerow函數接受一個列表作為輸入,然后將其作為一行寫入CSV。
結論和后續步驟
本教程應該幫助您了解在學習自己實現簡單的scraper時基本上是什么。 這種抓取應該足以實現簡單的自動化或小規模數據檢索。 但是如果你想有效地提取大量數據,你應該研究一下抓取框架,特別是Scrapy。 它可以幫助您使用幾行代碼編寫非常快速,高效的scraper。 無論你使用什么樣的框架,在那個閃亮的表面下面,框架也使用這些非常基本的抓取原則,所以理解本教程應該可以幫助你為開始抓取的探險建立基礎知識。
英文原文:https://stackabuse.com/introduction-to-web-scraping-with-python/
譯者:javylee
在前面的章節中 我們學習了構建一個分布式系統所必需的各種基本知識和技能 比如分布式系統的基礎理論、網絡編程技術、 RP 架構、內存計算 分布式文件系統、分布式計算框架等,但僅僅掌握這些內容還是遠遠不夠的 我們還需要學習和掌握分布式系統中常用的一些中間件 這些中間件主要用于分布式系統中常見的一些業務場景 數據全文檢索、日志和消息處理、數據庫的分片、網站的負載均衡等。由于篇幅有限, 本章只對全文檢索與消息隊列這兩個用途廣泛又相對復雜的中間件進行全面介紹。
我們已經習慣以網上搜索的方式來快速學習知識并解決技術問題了,這就需要互聯網搜索引擎。如何在海量網頁(文本)信息中準確且快速地找到包含我們所搜索的關鍵字的所有網頁并合理排序展示,的確是一個很有挑戰的難題。
除了我們日常工作使用的搜索引擎,大量互聯網應用都需要具備關鍵字檢索(即全文檢索)功能。要理解關鍵字檢索的價值,我們需要先了解關系型數據庫索引的局限性。我們在SQL查詢語句中使用like "%keyword%"這種查詢條件時,數據庫的索引是不起作用的。此時,搜索就變成類似于一頁頁翻書的遍歷過程了,幾乎全部都是IO操作,因此對性能的負面影響很大;如果需要對多個關鍵詞進行模糊匹配,比如 like"%keyword1%" and like "%keyword2%",此時的查詢效率也就可想而知了。
關鍵字檢索在本質上是將一系列文本文件的內容以“詞組(關鍵詞)”為單位進行分析并生成對應的索引記錄。索引存儲了關鍵詞到文章的映射關系,在映射關系中記錄了關鍵詞所在的文章編號、出現次數、出現頻率等關鍵信息,甚至包括了關鍵詞在文章中出現的起始位置于是我們有機會看到關鍵字“高亮展示”的查詢結果頁面。
關鍵字檢索的第一步是對整個文檔(Document)進行分詞,得到文本中的每個單詞,這對于英文來說毫無困難,因為一個英文語句中的單詞乙間是通過空格子付大杰力開B,但十人口句中的字與詞是兩個概念,所以中文分詞就成了一個很大的問題,比如利. 北爾人女目如何分詞呢?是“北京、天安門”還是“北、京、天安、門”﹖解決這個問題的最好辦法是將中文詞庫結合中文分詞法,其中比較知名的中文分詞法有IK(IKAnalyzer)或庖丁(PaodingAnalyzcr),配合開源Lucene使用起來非常方便。
Java生態圈中有名的全文檢索開源項目是 Apache Lucene(后簡稱Lucene),它在2001年成為Apache的開源項目。Lucene最初的貢獻者Doug Cutting是全文檢索領域的一位資深專家,曾經是V-Twin搜索引擎(蘋果的Copland操作系統的成就之一)的主要開發者,他貢獻Lucene的目的是為各種中小型應用程序加入全文檢索功能。目前Apache官方維護的Lucene相關的開源項目如下。
為了對一個文檔進行索引,Lucene提供了5個基礎類,分別是Document、Field、Index Writer、Analyzer和 Directory。首先,Document用來描述任何待搜索的文檔,例如HTML頁面、電子郵件或文本文件。我們知道,一個文檔可能有多個屬性,比如一封電子郵件有接收日期、發件人、收件人、郵件主題、郵件內容等屬性,每個屬性都可以用一個Field對象來描述。此外,我們可以把一個Document對象想象成數據庫中的一條記錄,而每個Field對象就是這條記錄的個字段。其次,在一個 Document 能被查詢之前,我們需要對文檔的內容進行分詞以找出文檔包含的關鍵字,這部分工作由 Analyzer對象實現。Analyzer把分詞后的內容交給IndexWriter建立索引。IndexWriter是Lucene用來創建索引(Index)的核心類之一,用于把每個Document對象都加到索引中來,并且把索引對象持久化保存到Directory中。Directory代表了Lucene索引的存儲位置,目前有兩個實現:第1個是 FSDirectory,表示在文件系統中存儲;第2個是RAMDirectory,表示在內存中存儲。
在明白了建立 Lucene索引所需要的這些類后,我們就可以對任意文檔創建索引了。下面給出了對指定文件目錄下的所有文本文件建立索引的源碼:
//索引文件目錄
Directory indexDir = FSDirectory.open (Paths.get ("index-dir"));Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig config = new IndexWriterConfig(analyzer);IndexWriter indexWriter = new Indexwriter (indexDir, config);//需要被索引的文件目錄
String dataDir=". ";
File[] dataFiles = new File(dataDir).listFiles();long startTime - new Date() .getTime();
for(int i= 0; i<dataFiles.length; i++){
if(dataFiles[i].isFile () && dataFiles[i].getName ().endsWith(".txt"))(
System .out.println ("Indexing file"+
dataFiles[i]-getCanonicalPath());
Reader txtReader = new FileReader (dataFiles[i]);Document doc - new Document();
//文檔的文件名也被作為一個Field,從而定位到具體的文件
doc.add(new StringField("filename", dataFiles[i].getName (),
Field.store.YES));
doc.add(new TextField("body", txtReader));indexwriter.addDocument (doc);
}
}
indexWriter.close();
long endTime - new Date( .getTime();
System.out.println("It takes " +(endTime - startTime)
+ " milliseconds to create index for the files in directory "+dataDir);
你可以把包含英文句子的任意文本(比如英文歌詞)都放到項目的根目錄下,運行上面的程序完成索引的創建過程,如果一切正常,則會出現類似于如下所示的提示:
Indexing file D:\project\leader-study-search\lemon-tree.txt
It takes 337 milliseconds to create index for the files in directory .
接下來我們嘗試查詢關鍵字,查詢內容(對應 body字段)包括“good”的所有文檔并輸出結果。為此,我們首先需要打開指定的索引文件,然后構造Query對象并執行查詢邏輯,最后輸出查詢結果。下面是對應的源碼:
//打開指定的索引文件
Directory indexDir = FSDirectory.open (Paths.get("index-dir"));IndexReader reader = DirectoryReader.open (indexDir);
IndexSearcher searcher = new IndexSearcher(reader);//查詢
String querystr = "good";
Analyzer analyzer = new StandardAnalyzer();
QueryParser parser = new QueryParser ( "body", analyzer);Query a- parser.parse(querystr);
int hitsPerPage = 10;
TopDocs docs=searcher.search(q, hitsPerPage);ScoreDoc[] hits = docs.scoreDocs;
//輸出查詢結果
System.out.println("Found " +hits.length + " hits.");for (int i =0;i< hits.length; ++i){
int docid = hits[i] .doc;
Document d = searcher.doc(docId);
System.out.println((i +1)+ "." +d.get ("filename "));
}
如果你搜索的關鍵字恰好在某個文本文件中,則運行這段代碼,控制臺會輸出類似于下面的內容:
Found 1 hits.1. lemon-tree.txt
通過對上面例子的學習,我們已經初步掌握了Lucene 的基本用法,Lucene編程的整個流程如下圖所示。
Lucene編程的整個流程可以總結為如下三個獨立步驟。
Lucene還普遍與網絡爬蟲技術相結合,提供基于互聯網資源的全文檢索功能,比如有不少提供商品比價和最優購物的信息類網站,通過爬蟲去抓取各個電商平臺上的商品信息并將其錄入Lucene索引庫里,然后提供用戶檢索服務,如下所示為此類系統的一個典型架構圖。
如果把Lucene與MySQL做對比,你會發現Lucene像MySQL的某個存儲引擎,比如InnoDB或者MyISAM。Lucene只提供了基本的全文檢索相關的API,還不是一個獨立的中間件,功能不夠豐富,API也比較復雜,不太方便使用。除此之外,Lucene還缺乏一個更為關鍵的特性—分布式,當我們要檢索的文檔數量特別龐大時,必然會遇到宕機的瓶頸,所以有了SolrElasticSearch,它們都是基于Lucene的功能豐富的分布式全文檢索中間件。
如下所示是Solr的架構示意圖。我們看到,Solr在 Lucene的基礎上開發了很多企業級增強功能:提供了一套強大的Data Schema來方便用戶定義document的結構;增加了高效靈活的緩存功能;增加了基于Web的管理界面以提供集中的配置管理功能;可以將Solr的索引數據分片存儲到多個節點上,并且通過多副本復制的方式來提升系統的可靠性。
Solr的分布式集群模式也被稱為SolrCloud,這是一種很靈活的分布式索引和檢索系統。SolrCloud也是一種具有去中心化思想的分布式集群,在集群中并沒有特殊的 Master 節點,而是依靠ZooKeeper來協調集群。SolrCloud中一個索引數據(Collection)可以被劃分為多個分片(Shard)并存儲在不同的節點上(Solr Core或者Core),在索引數據分片的同時,SolrCloud也可以實現分片的復制(Replication)功能以提升集群的可用性。SolrCloud集群的所有狀態信息都被放在ZooKeeper中統一維護,客戶端在訪問SolrCloud集群時,首先要向ZooKeeper查詢索引數據(Collection)所在的Core節點的地址列表,然后就可以連接到任意Core 節點上來完成索引的所有操作(CRUD)了。
如下圖所示給出了一個 SolrCloud 參考部署方案,本方案中的索引數據(Collection)被分為兩個分片,同時每個 Shard分片的數據都有3份,其中一份所在的Core節點被稱為Leader,其他兩個Core節點被稱為Replica。所有索引數據都分布在8個Core 中,他們位于獨立的3臺服務器上,所以其中任何一臺機器宕機,都不會影響到系統的可用性。如果某個服務器在運行中宕機,那么SolrCloud 會自動觸發Leader的重新選舉行為,這是通過ZooKeeper提供的分布式鎖功能來實現的。
之前說到,SolrCloud中的每個Shard分片都是由一個Leader 與N個Replica組成的,而且客戶端可以連接到任意一個Core節點上進行索引數據的操作,那么,此時索引數據是如何實現多副本同步的呢?下圖給出了背后的答案。
如果客戶端連接的Core不是Leader,則此節點會把請求轉發給所在Shard分片的Leader節點。
Leader 會把數據(Document)路由到所在Shard分片的每個Replica節點。如果文檔分片路由規則計算出目標Shard分片是另外一個分片,則 Leader會把數據轉發給該分片對應的Leader節點去處理。
接下來談談另外一個重要問題,這個問題就是SolrCloud采用了什么算法進行索引數據的分片Shard?為了選擇合適的分片算法,SolrCloud提出了以下兩個關鍵要求。
(1)分片算法的計算速度必須快,因為在建立索引及訪問索引的過程中都頻繁用到分片算法。
(2)分片算法必須保證索引數據能均勻地分布到每一個分片上,SolrCloud 的查詢是先分后總的過程,如果某個分片中的索引文檔(Document)的數量遠大于其他分片,那么在查詢此分片時所花的時間就會明顯多于其他分片,也就是說最慢分片的查詢速度決定了整體的查詢速度。
基于以上兩點,SolrCloud選擇了一致性哈希算法來實現索引分片。
本節最后說說SolrCloud 所支持的“近實時搜索”這個高級特性。近實時搜索就是在較短的時間內使得新添加的 Document可見可查,這主要基于Solr的 Soft Commit機制。在8.1.2節中講到,Lucene在創建索引時數據是在提交時被寫入磁盤的,這就是Hard Commit,它確保了即便停電也不會丟失數據,但會增加延時。同時,對于之前已經打開的Searcher 來說,新加入的Document也是不可見的。Solr為了提供更實時的檢索能力,提供了Soft Commit的新模式,在這種模式下僅把數據提交到內存,此時并沒有將其寫入磁盤索引文件中,但索引Index可見,Solr 會打開新的Searcher 從而使新的 Document可見。同時,Solr會進行預熱緩存及查詢以使得緩存的數據也是可見的。為了保證數據最終會被持久化保存到磁盤上,可以每1~10分鐘自動觸發Hard Commit而每秒鐘自動觸發Soft Commit.Soft Commit也是一把雙刃劍,一方面Commit越頻繁,查詢的實時性越高,但同時增加了Solr 的負荷,因為Commit越頻繁,越會生成小且多的索引段(Segment),于是Solr Merge 的操作會更加頻繁。在實際項目中建議根據業務的需求和忍受度來確定Soft Commit 的頻率。
ElasticSearch(后簡稱ES)并不是 Apache出品的,它與Solr類似,也是基于Lucene的一個分布式索引服務中間件。ES 的出現晚于Solr,但從當前的發展狀況來看,它的勢頭和流行度要超過前輩許多。值得一提的是在日志分析領域,以ES為核心的ELK三件套(ELK Stack)成為事實上的標準。ELK其實并不是一款軟件,而是一整套解決方案,是三款軟件即ES、Logstash和Kibana首字母的縮寫。這三款軟件都是開源軟件,通常配合使用,又先后歸于Elastic.co 公司名下,故被簡稱為ELK Stack。在 Google上有文章提到,ELK Stack每個月的下載量達到50萬次,已經成為世界上最流行的日志管理平臺,而在當前流行的基于Docker 與 Kubernetes 的PaaS平臺上,ELK也是標配之一,非 Apache 出品的ES之所以能后來居上,與ELK的流行和影響力也有著千絲萬縷的聯系。
實際上,在所有分布式系統中最需要全文檢索的就是日志模塊了。如果嘗試對節點數超過5個的分布式系統做Trouble Shooting,你就會明白日志集中收集并提供全文檢索功能的重要性和緊迫性了。在沒有類似于ELK Stack這樣一套日志子系統的情況下,我們不得不登錄每個主機來查詢日志,并且“拼接”所有相關的查詢結果,以定位和分析故障出現的環節及前因后果,這項工作看起來并不復雜,但實際上很耗費精力,因為在每個主機上都可能有多個日志文件需要分析,僅僅定位某個時間點的日志就讓人很頭疼了。
如下所示是ELK Stack 的一個架構組成圖。Logstash是一個有實時管道能力的數據收集引擎,用來收集日志數據并且作為索引數據寫入ES集群中,我們也可以開發自定義的日志采集探頭并按照ELK的日志索引格式寫入ES集群中;Kibana則為ES提供了數據分析及數據可視化的Web平臺,它可以在ES的索引中查找數據并生成各種維度的表圖。
ES通過簡單的RESTful API來隱藏Lucene的復雜性,從而讓全文搜索變得簡單,它提供了近實時的索引、搜索、分析等功能。我們可以這樣理解和描述ES。
在ES中增加了Type這個概念,如果我們把Index類比為Database,Type就相當于Table,但這個比喻不是很恰當,因為我們知道不同 Table 的結構完全不同,而一個Index中所有Document 的結構是高度一致的。ES 中的Type其實是Document 中的一個特殊字段,用來在查詢時過濾不同的 Document,比如我們在做一個 B2C的電商平臺時,需要對每個店鋪的商品進行索引,則可以用Type區分不同的商鋪。實際上,Type的使用場景非常少,這是我們需要注意的。
與SolrCloud一樣,ES也是分布式系統,但ES并沒有采用ZooKeeper作為集群的協調者,而是自己實現了一套被稱為Zen Discovery 的模塊,該模塊主要負責集群中節點的自動發現和Master節點的選舉。Master節點維護集群的全局狀態,比如節點加入和離開時進行Shard的重新分配,集群的節點之間則使用P2P的方式進行直接通信,不存在單點故障的問題。ES不使用ZooKeeper 的一個好處是系統部署和運維更加簡單了,壞處是可能出現所謂的腦裂問題。要預防腦裂問題,我們需要重視的一個參數是 discovery.zen.minimum_master_nodes,它決定了在選舉 Master節點的過程中需要有多少個節點通信,一個基本原則是這里需要設置成N/2+1,N是集群中節點的數量。例如在一個三節點的集群中,minimum_master_nodes應該被設為3/2+1 =2(四舍五入),當兩個節點的通信失敗時,節點1會失去它的主狀態,同時節點2不會被選舉為Master節點,沒有一個節點會接收索引或搜索的請求,沒有一個分片會處于不一致狀態。ES在Zen Discovery算法上做了不少改進以解決腦裂問題,GitHub上關于腦裂的Issue后來在2014年被關閉了,如下所示是相關說明:
2. Zen Discovery
Pinging after master loss (no local elects)Fixes the split brain issue:#2488
Batching join requests
More resilient joining process (wait on a publish from master)
ES 的集群與SolrCloud還有一個重大差別,即ES集群中的節點類型不止一種,有以下幾種類型。
如下所示是Tribe節點連接多個ES集群展示日志的ELK部署方案,據說魅族就采用了這種方案來解決各個IDC機房日志的集中展示問題。
本節最后,我們一起安裝ES并編寫一些簡單的例子來加深對ES 的理解并初步掌握其API的用法。我們可以去ES官網下載 ES 的二進制版本(在Windows上運行時可以下載ZIP包),解壓后在其 bin目錄下有可執行的腳本,比如 elasticsearch.bat。在執行啟動腳本后,在瀏覽器里訪問http://localhost:9200,會顯示如下信息,表明ES啟動正常:
{
"name" : "Y8 klCx",
"cluster name" :"elasticsearch",
"cluster uuid" :"X3tmO4iXSKa8l_ ADWNh_g","version" :{
"number" :"5.3.0",
"build hash" :"3adb13b",
"build date" : "2017-03-23T03:31:50.652Z","build snapshot" :false,
"lucene version" :"6.4.1",
},
"tagline" :"You Know, for Search"
}
ES提供了REST接口以讓我們很方便地將一個Document加入索引中,為了學會這個API首先,我們需要知道在ES中一個Document由以下3個字段唯一確定。
現在我們就容易理解Document CRUD的REST接口的URL的寫法了: http:/localhost:9200/<index>/<type>/[<id>]。
此外,在ES中一個Document是用JSON結構體來表示的,由于JSON的結構本身就有字段類型的暗示,比如字符串與數字的屬性是用不同方式表示的,因此 ES可以實現JSO到Document Schema t日以刻鳳ES被稱為Schema-less 系統的原因。但Schema-less i進個的方你想要的字段類型映別,心T配的Schema不滿意,則也可以使用自定義映射(MappTgO nNo Schema,如果對ES自動匹配的Schema不滿意,則也可以使用自定義映射(Mapping)的方式來設計更為合理的Schema。
從5.0版本開始,ES開發了一個全新的Java客戶端API,這個API的最大目標是移除對ES 及 Lucene類庫的依賴,變得更加輕量級,同時采用了分層設計的思路,底層僅僅包括一個HTTP通信層及一個Sniffer用于發現集群中的其他節點,其他層則包括Query DSL等功能,我們在本節中就使用這個新的Java API來完成Document的操作。
我們需要在Maven中引用這個API:
<dependency>
<groupId>org.elasticsearch.client</groupId><artifactId>sniffer</artifactId>
<version>5.3.0</version>
</dependency>
下面這段代碼的作用是獲取ES的健康信息,類似于我們在瀏覽器中訪問地址http://localhost:9200/_cluster/health:
RestClient client = RestClient
.builder(new HttpHost("localhost",9200)).build();
Response response = client.performRequest(
"GET", "/ cluster/health", Collections.singletonMap ("pretty"
"true"));
HttpEntity entity =response.getEntity();
System. out.println(EntityUtils.tostring(entity));
在運行后會輸出下面這樣一段內容,其中 status屬性比較重要,green表示集群很健康:
"cluster name" :"elasticsearch",
"status" :"green",
"timed out" : false,"number of nodes" :1,
"number of_data_nodes" :1,
"active_primary_shards" :0,"active shards" :0,
"relocating_shards" :0,"initializing_shards" :0,"unassigned shards" :0,
"delayed unassigned_shards" :0,"number of pending tasks" :0,"number of in_flight_fetch" :0,
"task max _waiting_in_queue_millis" :0,"active_shards percent as_number" :100.0
}
接下來,我們在名為 blogs 的 Index里插入一個 Document,Document 的 type為 blogId為1。下面是Document的JSON內容:
{
"user" : "Leader us",
"post date" :"2017-12-12",
"message" : "Mycat 2.0 is coming ! "
}
對應的代碼如下:
//index a document
String docJson= "{An"+
" "user\ " :\"Leader us\ ",n"+
" "post datel ": "2017-12-12\",n" +
" \"messaael" :\ "Mycat 2.0 is coming!\"\n"+"}";
System. out.println(docJson);
entity = new NStringEntity(docJson,ContentType.APPLICATION_JSON);Response indexResponse = client.performRequest("PUT","/blogs/blog/1",
Collections.singletonMap ( "pretty", "true"), entity);
System.out.println (Entityotils.tostring (indexResponse.getEntity()));
運行上述代碼后,在ES中成功插入一個索引文檔,控制臺會輸出如下信息:
{
" index" :"blogs",
"_type" : "blog","_id" :"1",
"_version" :1,"result" : "created"," _shards" :{
"total" :2,
"successful" :1,
"failed" :0
},
"created" : true
}
如果第2次運行上面的代碼,則輸出信息中的result值會從created變為updated,表明是更新Document 的操作,同時_version會累加。我們用下面的代碼繼續增加100個用于測試的Document:
String[] products= {"Mycat ", "Mydog", "Mybear","MyAllice"};
for (int i=0;i<100;i++)
{
//index a document
String docJson= "{\n"+
"\ "user\ ":\ "Leader us ",\n" +
\"post_date\" :\""+(2017+i)+"-12-12\",\n"+
"\ "messagel" :\""+products[i%products.length] +i+" is
coming! "\n" +
"]";
HttpEntity entity = new NStringEntity(docJson, ContentType.APPLICATIONM_ JSOtResponse indexResponse = client.performRequest ("PUT","/blos/blog/"+i,
Collections.singletonMap ("pretty", "true"),
entity);
System.out.println(EntityUtils.toString(indexResponse.getEntity()));
}
此時打開瀏覽器,輸入查詢指令:
http://localhost:9200/blogs/blog/_search?pretty=true
則會出現下面的查詢信息:
{
"took" :14,
"timed_ out" :false," shards" :{
"total":5,
"successful" :5,"failed" :0
),
"hits" :{
"total" :100,
"max_score" : 1.0,"hits" :[
{
" index" :"blogs"," type" :"blog",
"id" : "19"," score" :1.0," source":{
"user" : "Leader us",
"postdate" :"2036-12-12",
"message" :"MyAllice19 is coming ! "
},
{
" index" : "blogs","type" : "blog" ," id" :"22"," score" :1.0," source" :{
"user":"Leader us",
"post date" :"2039-12-12",
"message" :"Mybear22 is coming!"
}
},
根據上述信息,我們得知 blogs這個Index的分片數量為5個,hits部分為匹配查詢條件的Document列表,總共有100個符合條件的文檔,_source部分為我們錄入的原始Document的信息。如果查詢某個特定Document的內容,則只要在URL中指定文檔的ID即可,比如http://localhost:9200/blogs/blog/1。
如果我們要查詢包含某個關鍵字的文檔,則該怎么辦?ES提供了一個Query DSL 的語法,采用JSON格式描述,用起來也比較方便,比如下面這段DSL 語句表明查詢任意字段值中包含mycat這個關鍵字的Document:
{
"query":{
"query string": {
" query" :"mycat"
}
}
}
我們只要將上述DSL作為JSON內容Post到某個索引的URL地址即可,下面是具體的代碼:
//search document
String dsl- "{"query":{"+
"\ " query string \": {"+
" "auery ": \ "mycat\""+
"] ]}";
System.out.println (dsl);
HttpEntity entity = new NStringEntity(dsl,
ContentType.APPLICATION_JSON);
Response response =
client.performRequest("POST", "/blogs/blog/search",
Collections.singletonMap ("pretty", "true"),entity);
System.out.println (EntityUtils.toString (response.getEntity()));
在這里就不再深入討論ES的其他編程內容了,我們主要圍繞Query DSL的語法細節進行講解,比如高亮顯示匹配結果、過濾查詢結果、控制結果集緩存及聯合查詢等內容。
據觀世界
網頁信息爬取已經成為現在科學研究獲取數據的重要方式之一,也是基于大數據的人工智能技術發展的基礎之一。在本教程中,我們將介紹如何自動從網站獲取數據。大多數網站都是以受眾為中心創建的,您可以使用搜索引擎或在網絡瀏覽器中輸入網址,來查看頁面上顯示的信息。有時候,我們希望自動提取和處理這些數據,這時就需要網絡抓取幫助我們免于無聊的重復勞動。我們可以創建自定義爬蟲程序來訪問網站,提取特定數據并以特定方式處理這些數據。
本文將僅演示從bbc.com新聞網站中提取新聞數據,但您應該能夠通過一些試驗和糾錯來調整它以從任何您想要的網站中提取信息。
例如,您可能需要:
Web抓取還有許多其他用例。但是,您還應注意,版權法和網絡抓取法律很復雜,因國家/地區而異。雖然人們通常不介意你復制他們的內容甚至是為了商業利益進行網絡信息爬取,但是有一些法律案件涉及了從LinkedIn抓取數據、從OKCupid抓取數據等,網絡抓取可能會違反法律、違反特定網站的服務條款或違反道德準則。所以在抓取信息之前,要確定其是否合法。
在本教程中我們將介紹:
我們將使用在線編程環境repl.it,因此您無需在本地安裝任何軟件即可逐步進行操作。如果您想根據自己的需要對本教程中的代碼進行調整,您可以在repl.it按照注冊流程創建一個免費帳戶。
毫無疑問,我們都使用過Web瀏覽器訪問過網頁。網站有兩種形式:
網站通常使用三種計算機語言進行編碼:HTML,CSS和JavaScript。這本身就是一個龐大而復雜的領域,基本了解其中的一些工作原理是有效進行自動化網頁抓取的必要條件。您在瀏覽器中打開任何網站并右鍵單擊網站的某個頁面,您將看到一個菜單,其中應包含"查看源"選項。
如下圖所示:左側是普通網頁和打開的頁面右鍵菜單(通過右鍵單擊頁面顯示)。單擊查看源碼按鈕會生成右側的結果,我們可以看到包含Web瀏覽器顯示完整頁面所需的所有數據和支持信息的代碼。雖然左邊的頁面易于閱讀和使用,并且看起來很好,但右邊的頁面則看起來像是怪物,一般人較難理解,但是如果我們想編寫自定義Web抓取器,了解它是必要的。
網頁與源碼對比
首先要弄清楚這兩個頁面的對應方式:正常顯示的網站的哪些部分與代碼的哪些部分相匹配。您可以在源代碼視圖中使用"查找"(Ctrl + F)來查找在普通視圖中可見的特定文本片段以幫助完成此操作。在左邊的文章中,我們可以看到以"Getting a TV job"這個短語開頭。 如果我們在代碼視圖中搜索此短語,我們可以在代碼中找到相應的文本,在第805行。
源碼查找
高亮顯示部分之前的<p class ="story-body__introduction">是HTML代碼,用于指定段落(HTML中的<p>)從此處開始,并且這是一種特殊的段落(故事簡介)。該段繼續到</p>符號。您不必完全理解HTML,但您應該知道它包含的構成新聞文章的文本數據和有關如何顯示文章的其他數據。網頁抓取的很大一部分工作是查看網頁代碼,以識別我們感興趣的數據,并將其與其他代碼分開。
在大多數頁面中,有很多代碼用于定義網頁的結構,布局,交互性和其他功能,而相對較少的代碼包含我們通常查看的實際文本和圖像。對于特別復雜的頁面,即使在查找功能的幫助下,也很難找到負責頁面特定部分的代碼。為了解決這個問題,大多數Web瀏覽器都帶有所謂的"開發人員工具",主要針對程序員協助創建和維護Web站點,但這些工具對于Web抓取也非常有用。
您可以從主菜單打開瀏覽器的開發者工具,左側顯示Google Chrome,右側顯示Mozilla Firefox。 如果您使用的是其他網絡瀏覽器,應該能夠找到類似的設置。
開發者工具
打開開發人員工具會在Web瀏覽器中顯示一個新面板,通常位于底部或右側。 該工具包含一個"Inspector"面板和一個選擇器工具,可以通過按下下面的紅色圖標來選擇。 選擇器工具激活后,您可以單擊網頁的某些部分以查看相應的源代碼。 在下圖中,我們在普通視圖中選擇了相同的第一段,我們可以在下面的面板中再次看到<p class = story-body__introduction">代碼。
Selector
開發人員工具比使用簡單的源代碼查找工具更強大,但它們也更復雜。 您應該根據自己的經驗和嘗試分析的頁面的復雜性選擇一種方法。
現在我們已經看到了關于如何在瀏覽器中構建網頁的內容,我們可以開始使用Python檢索和操作它們。 Python不是Web瀏覽器,因此我們不會通過Python獲得網頁的"正常"表示。 相反,我們只能檢索和操作HTML源代碼。
我們將使用requests庫通過一個Python Repl執行此操作。打開repl.it并選擇創建一個新的Python repl。
repl.it
這將帶您進入一個可以編寫和運行Python代碼的Python編譯環境。首先,我們將從BBC新聞主頁下載內容,并打印出HTML源代碼的前1000個字符。
您可以使用以下四行Python來完成此操作:
import requests url = "https://bbc.com/news" response = requests.get(url) print(response.text[:1000])
將此代碼放在Repl自動為您創建的main.py文件中,然后按"Run"按鈕。在短暫的延遲之后,您便可在輸出窗格中看到輸出的HTML源代碼,類似于我們在上面的Web瀏覽器中查看的內容。
獲取網頁
代碼分析:
我們現在已經自動爬取了一個網頁,并可以顯示部分內容。我們不太可能對網頁的完整源代碼轉儲感興趣(除非我們出于存檔的原因存儲它),所以讓我們提取頁面的一些符合我們需要的部分而不是前1000個字符。
我們現在想在BBC新聞主頁上找到所有新聞文章,并獲取他們的網址。如果我們查看下面的主頁面,我們會看到主頁上有很多故事,使用"檢查"工具將光標置于任何標題之上,我們可以看到每個故事都有一個唯一的網址,它將我們帶到那個新聞報道。例如,在下圖中標題為"US and Canada agree new trade deal"的新聞的網址是https://www.bbc.com/news/business-45702609。
如果我們使用瀏覽器的開發人員工具檢查該元素,我們可以看到它是一個<a>元素,它是鏈接的HTML,其中<href>標簽指向URL。請注意,href部分僅指向URL的后半部分,省略了https://www.bbc.com部分。由于我們已經在BBC上,因此該網站可以使用相對URL而不是絕對URL。這意味著當您單擊該鏈接時,您的瀏覽器將確定該URL未完成,并在其前面加上https://www.bbc.com。
新聞URL
我們可以嘗試使用Python的內置文本搜索功能(如find()或正則表達式)從BBC頁面中提取所有URL,但實際上不可能可靠地執行此操作。幸運的是,有一個功能強大且易于使用的HTML解析庫,名為BeautifulSoup,它將幫助我們從給定的HTML片段中提取所有鏈接。 我們可以通過修改Repl中的代碼來使用它,如下所示:
import requests from bs4 import BeautifulSoup url = "https://bbc.com/news" response = requests.get(url) html = response.text soup = BeautifulSoup(html, "html.parser") links = soup.findAll("a") for link in links: print(link.get("href"))
如果您運行此代碼,您將看到它輸出了幾十個URL,每行一個。您可能會注意到代碼現在比以前運行起來時間要長得多,BeautifulSoup不是內置于Python中,而是第三方模塊。 這意味著在運行代碼之前,Repl必須去獲取此庫并為您安裝它。后續運行會更快。
URL獲取
代碼分析:
如果我們查看輸出窗格中的URL,我們會看到很多結果。我們有絕對URL(以"http"開頭)和相對URL(以"/"開頭)。他們中的大多數都轉到一般頁面而不是特定的新聞文章。我們需要在我們感興趣的鏈接中找到一個模式(轉到新聞文章),這樣我們才能只提取我們感興趣的那些。
再次嘗試和糾錯是最好的方法。如果我們訪問BBC新聞主頁并使用開發人員工具來檢查新聞文章的鏈接,我們會發現它們都有類似的模式。它們是以"/news"開頭并以一串數字結尾的相對URL,例如/news/newsbeat-45705989。
我們可以對代碼進行一些小改動,只輸出與此模式匹配的URL。用以下四行替換上述代碼的最后兩行:
for link in links: href = link.get("href") if href.startswith("/news") and href[-1].isdigit(): print(href)
代碼運行結果:
URL篩選
現在我們已經獲取到BBC新聞主頁上的每篇文章的鏈接,可以進一步獲取這些文章每一篇的數據。作為一個實驗項目,讓我們從每篇文章中提取專有名詞(人物,地點等)并打印出最常見的名詞,以了解今天新聞所討論的內容。
調整我們的代碼如下:
import requests import string from collections import Counter from bs4 import BeautifulSoup url = "https://bbc.com/news" response = requests.get(url) html = response.text soup = BeautifulSoup(html, "html.parser") links = soup.findAll("a") news_urls = [] for link in links: href = link.get("href") if href.startswith("/news") and href[-1].isdigit(): news_url = "https://bbc.com" + href news_urls.append(news_url) all_nouns = [] for url in news_urls[:10]: print("Fetching {}".format(url)) response = requests.get(url) html = response.text soup = BeautifulSoup(html, "html.parser") words = soup.text.split() nouns = [word for word in words if word.isalpha() and word[0] in string.ascii_uppercase] all_nouns += nouns print(Counter(all_nouns).most_common(100))
這段代碼比我們之前編寫的代碼更復雜,所以如果你不了解所有這些代碼,請不要擔心。主要變化是:
在頂部,我們添加了兩個新的導入。一個用于字符串,它是標準的Python模塊,包含一些有用的單詞和字母快捷方式。我們將使用它來識別字母表中的所有大寫字母。第二個模塊是一個計數器,一旦我們建立了所有名詞的列表,我們就可以在列表中找到詞頻最高的名詞。
我們在第一個for循環的頂部添加了news_urls = []。我們將其標識為"news URL"后,不是打印出每個URL,而是將其添加到此列表中,以便我們以后可以下載每個頁面。在for循環內部向下兩行,我們將根域("http://bbc.com")與每個href屬性組合在一起,然后將完整的URL添加到news_urls列表中。
然后我們進入另一個for循環,在那里我們遍歷前10個新聞URL(如果你有更多時間,你可以刪除[:10]部分來遍歷所有新聞頁面,但為了提高效率,我們只是用前10個)。
打印出我們正在獲取的URL(因為下載每個頁面需要一秒左右,顯示一些反饋很好,這樣我們可以看到該程序正在運行的進度)。然后我們像上文一樣獲取頁面并將其轉換為Soup。使用words = soup.text.split()進行分詞。
下一行循環遍歷該給定文章中的所有單詞,并僅保留由數字字符組成的單詞,并以大寫字母開頭(string.ascii_uppercase只是大寫字母)。這也是一種非常粗略的提取名詞的方式,我們會得到很多單詞(比如句子開頭的那些單詞),這些單詞實際上并不是專有名詞,但現在又是一個很好的近似值。
最后,我們將所有看起來像名詞的單詞添加到我們的all_nouns列表中,然后轉到下一篇文章來做同樣的事情。下載解析所有頁面之后,我們可以使用Python中內置的Counter來統計詞頻。
您應該看到類似于下圖的輸出(盡管您的文字會有所不同,因為新聞每隔幾個小時就會發生變化)。我們有最常見的名詞,然后計算這個名詞出現在我們所看到的所有10篇文章中的頻率。
名詞獲取
我們可以看到我們的原始提取和解析方法遠非完美,由于每篇文章底部的社交媒體鏈接,大多數文章都會出現??像"Twitter"和"Facebook"這樣的詞,因此它們的存在并不意味著Facebook和Twitter本身就在今天的新聞中。類似地,諸如"From"之類的單詞不是名詞,并且還包括諸如"BBC","Optimizely"和"Business"之類的其他單詞,因為它們主要出現在頁面上文章之外的文本上。
我們已經學習了網頁信息抓取的基礎知識,并研究了網絡的工作原理,如何從網頁中提取信息,以及如何進行一些非常基本的文本提取。你可能想要做一些與BBC提取單詞不同的東西!您可以從https://repl.it/@GarethDwyer1/beginnerwebscraping中fork此Repl并對其進行修改以更改它所搜索的站點以及它提取的內容。
好用的Python網頁抓取工具推薦:
還有許多其他工具,只需將它們相互結合使用即可完成很多工作。如果您有好的建議或問題,請留言評論。
END
*請認真填寫需求信息,我們會在24小時內與您取得聯系。