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

大數據文摘出品

編譯:狗小白、李佳、張弛、魏子敏

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

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

從自然語言處理到監督式機器學習,維基百科助力了無數的數據科學項目。

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

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

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

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。這個大小對於人類的全部知識來說似乎並不太大。

維基百科壓縮文件大小

下載文件

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數據,並附爲一個列表,我們得到看起來像這樣的東西:

維基百科文章的源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)。有些模板編碼文章的摘要信息。例如,戰爭與和平的信息框是:

維基百科上的每一類文章,如電影、書籍或廣播電臺,都有自己的信息框。在書籍的例子中,信息框模板被命名爲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

【今日機器學習概念】

Have a Great Definition

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章