維基百科中的資料科學:手把手教你用Python讀懂全球最大百科全書

大資料文摘發表於2018-10-24

沒人否認,維基百科是現代最令人驚歎的人類發明之一。

幾年前誰能想到,匿名貢獻者們的義務工作竟創造出前所未有的巨大線上知識庫?維基百科不僅是你寫大學論文時最好的資訊渠道,也是一個極其豐富的資料來源。

自然語言處理到監督式機器學習,維基百科助力了無數的資料科學專案。

維基百科的規模之大,可稱為世上最大的百科全書,但也因此稍讓資料工程師們感到頭疼。當然,有合適的工具的話,資料量的規模就不是那麼大的問題了。

本文將介紹“如何程式設計下載和解析英文版維基百科”。

在介紹過程中,我們也會提及以下幾個資料科學中重要的問題:

1、從網路中搜尋和程式設計下載資料

2、運用Python庫解析網路資料(HTML, XML, MediaWiki格式)

3、多程式處理、並行化處理

這個專案最初是想要收集維基百科上所有的書籍資訊,但我之後發現專案中使用的解決方法可以有更廣泛的應用。這裡提到的,以及在Jupyter Notebook裡展示的技術,能夠高效處理維基百科上的所有文章,同時還能擴充套件到其它的網路資料來源中。

本文中運用的Python程式碼的筆記放在GitHub,靈感來源於Douwe Osinga超棒的《深度學習手冊》。前面提到的Jupyter Notebooks也可以免費獲取。

GitHub連結:https://github.com/WillKoehrsen/wikipedia-data-science/blob/master/notebooks/Downloading%20and%20Parsing%20Wikipedia%20Articles.ipynb

免費獲取地址:

https://github.com/DOsinga/deep_learning_cookbook

程式設計搜尋和下載資料

任何一個資料科學專案第一步都是獲取資料。我們當然可以一個個進入維基百科頁面打包下載搜尋結果,但很快就會下載受限,而且還會給維基百科的伺服器造成壓力。還有一種辦法,我們透過dumps.wikimedia.org這個網站獲取維基百科所有資料的定期快照結果,又稱dump。

用下面這段程式碼,我們可以看到資料庫的可用版本:

import requests # Library for parsing HTML from bs4 import BeautifulSoup base_url = 'https://dumps.wikimedia.org/enwiki/' index = requests.get(base_url).text soup_index = BeautifulSoup(index, 'html.parser') # Find the links on the page dumps = [a['href'] for a in soup_index.find_all('a') if         a.has_attr('href')] dumps ['../', '20180620/', '20180701/', '20180720/', '20180801/', '20180820/', '20180901/', '20180920/',  'latest/']

這段程式碼使用了BeautifulSoup庫來解析HTML。由於HTML是網頁的標準標識語言,因此就處理網路資料來說,這個庫簡直是無價瑰寶。

本專案使用的是2018年9月1日的dump(有些dump資料不全,請確保選擇一個你所需的資料)。我們使用下列程式碼來找到dump裡所有的檔案。

dump_url = base_url + '20180901/' # Retrieve the html dump_html = requests.get(dump_url).text # Convert to a soup soup_dump = BeautifulSoup(dump_html, 'html.parser') # Find list elements with the class file soup_dump.find_all('li', {'class': 'file'})[:3] [<li><a href="/enwiki/20180901/enwiki-20180901-pages-articles-multistream.xml.bz2">enwiki-20180901-pages-articles-multistream.xml.bz2</a> 15.2 GB</li>, <li><a href="/enwiki/20180901/enwiki-20180901-pages-articles-multistream-index.txt.bz2">enwiki-20180901-pages-articles-multistream-index.txt.bz2</a> 195.6 MB</li>,  <li><a href="/enwiki/20180901/enwiki-20180901-pages-meta-history1.xml-p10p2101.7z">enwiki-20180901-pages-meta-history1.xml-p10p2101.7z</a> 320.6 MB</li>]

我們再一次使用BeautifulSoup來解析網路找尋檔案。我們可以在https://dumps.wikimedia.org/enwiki/20180901/頁面裡手工下載檔案,但這就不夠效率了。網路資料如此龐雜,懂得如何解析HTML和在程式中與網頁互動是非常有用的——學點網站檢索知識,龐大的新資料來源便觸手可及。

考慮好下載什麼

上述程式碼把dump裡的所有檔案都找出來了,你也就有了一些下載的選擇:文章當前版本,文章頁以及當前討論列表,或者是文章所有歷史修改版本和討論列表。如果你選擇最後一個,那就是萬億位元組的資料量了!本專案只選用文章最新版本。

所有文章的當前版本能以單個文件的形式獲得,但如果我們下載解析這個文件,就得非常費勁地一篇篇文章翻看,非常低效。更好的辦法是,下載多個分割槽文件,每個文件內容是文章的一個章節。之後,我們可以透過並行化一次解析多個文件,顯著提高效率。

“當我處理文件時,我更喜歡多個小文件而非一個大文件,這樣我就可以並行化執行多個文件了。”

分割槽文件格式為bz2壓縮的XML(可擴充套件標識語言),每個分割槽大小300~400MB,全部的壓縮包大小15.4GB。無需解壓,但如果你想解壓,大小約58GB。這個大小對於人類的全部知識來說似乎並不太大。

維基百科中的資料科學:手把手教你用Python讀懂全球最大百科全書維基百科壓縮檔案大小下載檔案

Keras 中的get_file語句在實際下載檔案中非常好用。下面的程式碼可透過連結下載檔案並儲存到磁碟中:

from keras.utils import get_file saved_file_path = get_file(file, url)

下載的檔案儲存在~/.keras/datasets/,也是Keras預設儲存設定。一次性下載全部檔案需2個多小時(你可以試試並行下載,但我試圖同時進行多個下載任務時被限速了)

解析資料

我們首先得解壓檔案。但實際我們發現,想獲取全部文章資料根本不需要這樣。我們可以透過一次解壓執行一行內容來迭代文件。當記憶體不夠執行大容量資料時,在檔案間迭代通常是唯一選擇。我們可以使用bz2庫對bz2壓縮的檔案迭代。

不過在測試過程中,我發現了一個更快捷(雙倍快捷)的方法,用的是system utility bzcat以及Python模組的subprocess。以上揭示了一個重要的觀點:解決問題往往有很多種辦法,而找到最有效辦法的唯一方式就是對我們的方案進行基準測試。這可以很簡單地透過%%timeit Jupyter cell magic來對方案計時評價。

迭代解壓檔案的基本格式為:

data_path = '~/.keras/datasets/enwiki-20180901-pages-articles15.xml-p7744803p9244803.bz2 # Iterate through compressed file one line at a time for line in subprocess.Popen(['bzcat'],                              stdin = open(data_path),                              stdout = subprocess.PIPE).stdout:    # process line

如果簡單地讀取XML資料,並附為一個列表,我們得到看起來像這樣的東西:

維基百科中的資料科學:手把手教你用Python讀懂全球最大百科全書

維基百科文章的源XML

上面展示了一篇維基百科文章的XML檔案。每個檔案裡面有成千上萬篇文章,因此我們下載的檔案裡包含百萬行這樣的語句。如果我們真想把事情弄複雜,我們可以用正規表示式和字串匹配跑一遍文件來找到每篇文章。這就極其低效了,我們可以採取一個更好的辦法:使用解析XML和維基百科式文章的定製化工具。

解析方法

我們需要在兩個層面上來解析文件:

1、從XML中提取文章標題和內容

2、從文章內容中提取相關資訊

好在,Python對這兩個都有不錯的應對方法。

解析XML

解決第一個問題——定位文章,我們使用SAX(Simple API for XML) 語法解析器。BeautifulSoup語句也可以用來解析XML,但需要記憶體載入整個文件並且建立一個文件物件模型(DOM)。而SAX一次只執行XML裡的一行字,完美符合我們的應用場景。

基本思路就是我們對XML文件進行搜尋,在特定標籤間提取相關資訊。例如,給出下面這段XML語句:

<title>Carroll F. Knicely</title> <text xml:space="preserve">\'\'\'Carroll F. Knicely\'\'\' (born c. 1929 in [[Staunton, Virginia]] - died November 2, 2006 in [[Glasgow, Kentucky]]) was [[Editing|editor]] and [[Publishing|publisher]] of the \'\'[[Glasgow Daily Times]]\'\' for nearly 20 years (and later, its owner) and served under three [[Governor of Kentucky|Kentucky Governors]] as commissioner and later Commerce Secretary.\n' </text>

我們想篩出在<title>和<text>這兩標籤間的內容(這個title就是維基百科文章標題,text就是文章內容)。SAX能直接讓我們實現這樣的功能——透過parser和ContentHandler這兩個語句來控制資訊如何透過解析器然後被處理。每次掃一行XML句子進解析器,Content Handler則幫我們提取相關的資訊。

如果你不嘗試做一下,可能理解起來有點難度,但是Content handler的思想是尋找開始標籤和結束標籤之間的內容,將找到的字元新增到快取中。然後將快取的內容儲存到字典中,其中相應的標籤作為對應的鍵。最後我們得到一個鍵是標籤,值是標籤中的內容的字典。下一步,我們會將這個字典傳遞給另一個函式,它將解析字典中的內容。

我們唯一需要編寫的SAX的部分是Content Handler。全文如下:

在這段程式碼中,我們尋找標籤為title和text的標籤。每次解析器遇到其中一個時,它會將字元儲存到快取中,直到遇到對應的結束標籤(</tag>)。然後它會儲存快取內容到字典中--  self._values。文章由<page>標籤區分,如果Content Handler遇到一個代表結束的 </page> 標籤,它將新增self._values 到文章列表(self._pages)中。如果感到疑惑了,實踐觀摩一下可能會有幫助。

下面的程式碼顯示瞭如何透過XML檔案查詢文章。現在,我們只是將它們儲存到handler._pages中,稍後我們將把文章傳送到另一個函式中進行解析。

# Object for handling xml handler = WikiXmlHandler() # Parsing object parser = xml.sax.make_parser() parser.setContentHandler(handler) # Iteratively process file for line in subprocess.Popen(['bzcat'],                              stdin = open(data_path),                              stdout = subprocess.PIPE).stdout:    parser.feed(line)        # Stop when 3 articles have been found    if len(handler._pages) > 2:         break

如果我們觀察 handler._pages,我們將看到一個列表,其中每個元素都是一個包含一篇文章的標題和內容的元組:

handler._pages[0] [('Carroll Knicely',   "'''Carroll F. Knicely''' (born c. 1929 in [[Staunton, Virginia]] - died November 2, 2006 in [[Glasgow, Kentucky]]) was [[Editing|editor]] and [[Publishing|publisher]] ...)]

此時,我們已經編寫的程式碼可以成功地識別XML中的文章。現在我們完成了解析檔案一半的任務,下一步是處理文章以查詢特定頁面和資訊。再次,我們使用專為這項工作而建立的一個工具。

解析維基百科文章

維基百科執行在一個叫做MediaWiki的軟體上,該軟體用來構建wiki。這使文章遵循一種標準格式,這種格式可以輕易地用程式設計方式訪問其中的資訊。雖然一篇文章的文字看起來可能只是一個字串,但由於格式的原因,它實際上編碼了更多的資訊。為了有效地獲取這些資訊,我們引進了強大的 mwparserfromhell, 一個為處理MediaWiki內容而構建的庫。

如果我們將維基百科文章的文字傳遞給 mwparserfromhell,我們會得到一個Wikicode 物件,它含有許多對資料進行排序的方法。例如,以下程式碼從文章建立了一個wikicode物件,並檢索文章中的 wikilinks()。這些連結指向維基百科的其他文章:

import mwparserfromhell # Create the wiki article wiki = mwparserfromhell.parse(handler._pages[6][1]) # Find the wikilinks wikilinks = [x.title for x in wiki.filter_wikilinks()] wikilinks[:5] ['Provo, Utah', 'Wasatch Front', 'Megahertz', 'Contemporary hit radio', 'watt']

有許多有用的方法可以應用於wikicode,例如查詢註釋或搜尋特定的關鍵字。如果您想獲得文章文字的最終修訂版本,可以呼叫:

wiki.strip_code().strip() 'KENZ (94.9 FM,  " Power 94.9 " ) is a top 40/CHR radio station broadcasting to Salt Lake City, Utah '

因為我的最終目標是找到所有關於書籍的文章,那麼是否有一種方法可以使用解析器來識別某個類別中的文章呢?幸運的是,答案是肯定的——使用MediaWiki templates。

文章模板

模板(templates)是記錄資訊的標準方法。維基百科上有無數的模板,但與我們的目的最相關的是資訊框( Infoboxes)。有些模板編碼文章的摘要資訊。例如,戰爭與和平的資訊框是:

維基百科中的資料科學:手把手教你用Python讀懂全球最大百科全書維基百科上的每一類文章,如電影、書籍或廣播電臺,都有自己的資訊框。在書籍的例子中,資訊框模板被命名為Infobox book。同樣,wiki物件有一個名為filter_templates()的方法,它允許我們從一篇文章中提取特定的模板。因此,如果我們想知道一篇文章是否是關於一本書的,我們可以透過book資訊框去過濾。展示如下:

# Filter article for book template wiki.filter_templates('Infobox book')

如果匹配成功,那我們就找到一本書了!要查詢你感興趣的文章類別的資訊框模板,請參閱資訊框列表。

如何將用於解析文章的mwparserfromhell 與我們編寫的SAX解析器結合起來?我們修改了Content Handler中的endElement方法,將包含文章標題和文字的值的字典,傳送到透過指定模板搜尋文章文字的函式中。如果函式找到了我們想要的文章,它會從文章中提取資訊,然後返回給handler。首先,我將展示更新後的endElement 。

def endElement(self, name):    """Closing tag of element"""    if name == self._current_tag:        self._values[name] = ' '.join(self._buffer)    if name == 'page':        self._article_count += 1        # Send the page to the process article function        book = process_article(**self._values,                               template = 'Infobox book')        # If article is a book append to the list of books        if book:              self._books.append(book)

一旦解析器到達文章的末尾,我們將文章傳遞到函式 process_article,如下所示:

def process_article(title, text, timestamp, template = 'Infobox book'):    """Process a wikipedia article looking for template"""      # Create a parsing object    wikicode = mwparserfromhell.parse(text)    # Search through templates for the template    matches = wikicode.filter_templates(matches = template)    if len(matches) >= 1:        # Extract information from infobox        properties = {param.name.strip_code().strip(): param.value.strip_code().strip()                      for param in matches[0].params                      if param.value.strip_code().strip()}        # Extract internal wikilinks

雖然我正在尋找有關書籍的文章,但是這個函式可以用來搜尋維基百科上任何類別的文章。只需將模板替換為指定類別的模板(例如 Infobox language是用來尋找語言的),它只會返回符合條件的文章資訊。

我們可以在一個檔案上測試這個函式和新的ContentHandler 。

Searched through 427481 articles. Found 1426 books in 1055 seconds.

讓我們看一下查詢一本書的結果:

books[10] ['War and Peace', {'name': 'War and Peace',  'author': 'Leo Tolstoy',  'language': 'Russian, with some French',  'country': 'Russia',  'genre': 'Novel (Historical novel)',  'publisher': 'The Russian Messenger (serial)',  'title_orig': 'Война и миръ',  'orig_lang_code': 'ru',  'translator': 'The first translation of War and Peace into English was by American Nathan Haskell Dole, in 1899',  'image': 'Tolstoy - War and Peace - first edition, 1869.jpg',  'caption': 'Front page of War and Peace, first edition, 1869 (Russian)',  'release_date': 'Serialised 1865–1867; book 1869',  'media_type': 'Print',  'pages': '1,225 (first published edition)'}, ['Leo Tolstoy',  'Novel',  'Historical novel',  'The Russian Messenger',  'Serial (publishing)',  'Category:1869 Russian novels',  'Category:Epic novels',  'Category:Novels set in 19th-century Russia',  'Category:Russian novels adapted into films',  'Category:Russian philosophical novels'], ['https://books.google.com/?id=c4HEAN-ti1MC',  'https://www.britannica.com/art/English-literature',  'https://books.google.com/books?id=xf7umXHGDPcC',  'https://books.google.com/?id=E5fotqsglPEC',  'https://books.google.com/?id=9sHebfZIXFAC'],  '2018-08-29T02:37:35Z']

對於維基百科上的每一本書,我們把資訊框中的資訊整理為字典、書籍在維基百科中的wikilinks資訊、書籍的外部連結和最新編輯的時間戳。(我把精力集中在這些資訊上,為我的下一個專案建立一個圖書推薦系統)。你可以修改process_article 函式和WikiXmlHandler類,以查詢任何你需要的資訊和文章!

如果你看一下只處理一個檔案的時間,1055秒,然後乘以55,你會發現處理所有檔案的時間超過了15個小時!當然,我們可以在一夜之間執行,但如果可以的話,我不想浪費額外的時間。這就引出了我們將在本專案中介紹的最後一種技術:使用多處理和多執行緒進行並行化。

並行操作

與其一次一個解析檔案,不如同時處理其中的幾個(這就是我們下載分割槽的原因)。我們可以使用並行化,透過多執行緒或多處理來實現。

多執行緒與多處理

多執行緒和多處理是同時在計算機或多臺計算機上執行許多工的方法。我們磁碟上有許多檔案,每個檔案都需要以相同的方式進行解析。一個簡單的方法是一次解析一個檔案,但這並沒有充分利用我們的資源。因此,我們可以使用多執行緒或多處理同時解析多個檔案,這將大大加快整個過程。

通常,多執行緒對於輸入/輸出繫結任務(例如讀取檔案或發出請求)更好(更快)。多處理對於cpu密集型任務更好(更快)。對於解析文章的過程,我不確定哪種方法是最優的,因此我再次用不同的引數對這兩種方法進行了基準測試。

學習如何進行測試和尋找不同的方法來解決一個問題,你將會在資料科學或任何技術的職業生涯中走得更遠。

相關報導:https://towardsdatascience.com/wikipedia-data-science-working-with-the-worlds-largest-encyclopedia-c08efbac5f5c

相關文章