使用NLP和ML來提取和構造Web資料

磐創 AI發表於2020-10-29

作者|Conner Brew 編譯|VK 來源|Towards Data Science

介紹

在本文中,我們將建立一個基於戰爭研究所(ISW)的結構化文件資料庫。ISW為外交和情報專業人員提供資訊產品,以加深對世界各地發生的衝突的瞭解。

要檢視與本文相關聯的原始程式碼和Notebook,請訪問以下連結:https://colab.research.google.com/drive/1pTrOXW3k5VQo1lEaahCo79AHpyp5ZdfQ?usp=sharing

要訪問Kaggle上託管的最終結構化資料集,請訪問以下連結:https://www.kaggle.com/connerbrew2/isw-web-scrape-and-nlp-enrichment

本文將是一個關於web抽取、自然語言處理(NLP)和命名實體識別(NER)的練習。對於NLP,我們將主要使用開源Python庫NLTK和Spacy。

本文旨在演示web提取和NLP的一個用例,而不是關於這兩種技術使用的全面初學者教程。如果你是NLP或web提取的新手,我建議你遵循不同的教程,或者瀏覽Spacy、BeautifulSoup和NLTK文件頁面。

# 匯入庫

import requests
import nltk
import math
import re
import spacy
import regex as re
import pandas as pd
import numpy as np
import statistics as stats
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import json

# 你需要從NLTK下載一些包。

from bs4 import BeautifulSoup 
from nltk import *
nltk.download('stopwords')
nltk.download('punkt')
from nltk.corpus import stopwords

# #在大多數環境中,你需要安裝NER-D。

!pip install ner-d
from nerd import ner

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.cluster import MiniBatchKMeans
from sklearn.feature_extraction.text import TfidfVectorizer

初始化變數

首先,我們將初始化最終結構化資料中需要的資料欄位。對於每個文件,我要提取標題、釋出日期、人名、地名和其他各種資訊。我們還將增強文件中已經存在的資訊—例如,我們將使用文件中的地名來獲取相關的座標,這對於以後視覺化資料非常有用。

# 初始化最終資料集的資料欄位

dates=[]
titles=[]
locations=[]
people=[]
key_countries=[]
content_text=[]
links=[]
coord_list=[]
mentioned_countries=[]
keywords=[]
topic_categories=[]

# 為後面的主題模型初始化簇變數

cluster_keywords=[]
cluster_number=[]

# 使用SPACY庫初始化NLP物件
nlp = spacy.load("en_core_web_sm")

提取href

我們將從ISW的生產庫中提取文件。首先,我們將抓取“瀏覽”頁面以獲取每個產品的單獨href連結。然後我們將這些連結儲存在一個列表中,供提取函式稍後訪問。

# #從ISW瀏覽頁面獲取產品連結

urls=['http://www.understandingwar.org/publications?page={}'.format(i) for i in range(179)]
hrefs=[]

def get_hrefs(page,class_name):
  page=requests.get(page)
  soup=BeautifulSoup(page.text,'html.parser')
  container=soup.find_all('div',{'class':class_name})
  container_a=container[0].find_all('a')
  links=[container_a[i].get('href') for i in range(len(container_a))]
  for link in links:
    if link[0]=='/':
      hrefs.append('http://www.understandingwar.org'+link)

for url in urls:
  get_hrefs(url,'view-content')

Web爬取

我們將要編寫的前幾個函式是相當簡單的文字提取。本教程不是關於BeautifulSoup用法的教程,要了解Python中的web爬取,請檢視這裡的文件:https://www.crummy.com/software/BeautifulSoup/bs4/doc/

獲得日期

對於我們的第一個函式,我們將提取釋出日期。它掃描從產品網頁中提取的html文件,並找到一個類為“submitted”的欄位。這是我們的生產日期。

獲得標題

接下來,我們需要產品名稱。同樣,這個欄位被方便地標記為“title”類。

獲取所有文字

最後,我們將提取文件的全文。當我提取文字時,我通常遵循“先提取,後過濾”的web提取方式。這意味著,在最初的文字提取中,我對文字執行最少的過濾和處理。我更願意在以後的分析中進行處理,因為這是必要的。但是,如果你想更進一步,你可能希望對提取的文字進行比下面函式演示的更多的預處理。

對於我的get_contents函式,我堅持最基本的原則——我在黑名單中列出了一些不想被提取的文字。然後從頁面中提取所有文字並將其附加到一個臨時字串中,該字串又被附加到列表content_text中。

# 提取釋出資料

def get_date(soup):
  try:
    data=soup.find('span',{'class':'submitted'})
    content=data.find('span')
    date=content.get('content')
    dates.append(date)
  except Exception:
    dates.append('')
    pass

# 提取產品標題
def get_title(soup):
  try:
    title=soup.find('h1',{'class':'title'}).contents
    titles.append(title[0])
  except Exception:
    titles.append('')
    pass

# 提取產品的文字內容
def get_contents(soup):
  try:
    parents_blacklist=['[document]','html','head',
                       'style','script','body',
                       'div','a','section','tr',
                       'td','label','ul','header',
                       'aside',]
    content=''
    text=soup.find_all(text=True)

    for t in text:
      if t.parent.name not in parents_blacklist and len(t) > 10:
        content=content+t+' '

    content_text.append(content)
  except Exception:
    content_text.append('')
    pass

自然語言處理

接下來,我們將找出產品中引用了哪些國家。有很多API可以用於檢查國家的文字內容,但這裡我們將使用一個簡單的方法:列出世界上所有國家的列表。這個列表來自維基百科:https://en.wikipedia.org/wiki/Lists_of_countries_and_territories

在函式得到all_mentioned_countries 後,它使用基本統計分析來確定哪些國家最突出——這些國家最有可能成為檔案敘述的焦點。為此,該函式計算整個文件中提到一個國家的次數,然後查詢比平均值提到次數多的國家。然後將這些國家追加到key_countries列表中。

# 在正文中引用所有國家的列表。
# 如果文字中的一個單詞與列表中的一個國家匹配,那麼它將被新增到國家列表中。 

def get_countries(content_list):
  iteration=1
  for i in range(len(content_list)):
    print('Getting countries',iteration,'/',len(content_list))
    temp_list=[]
    for word in word_tokenize(content_list[i]):
      for country in country_list:
        if word.lower().strip() == country.lower().strip():
          temp_list.append(country)

    counted_countries=dict(Counter(temp_list))
    temp_dict=dict.fromkeys(temp_list,0)
    temp_list=list(temp_dict)
    if len(temp_list)==0:
      temp_list.append('Worldwide')
    mentioned_countries.append(temp_list)

    # 計算每個國家被提及的次數,然後對照平均值檢查每次計數。 
    # 如果一個國家被提及的次數超過了平均次數,它就會作為一個關鍵字被記錄。

    keywords=[]
    for key in counted_countries.keys():
      if counted_countries[key] > np.mean(list(counted_countries.values())):
        keywords.append(key)
    if len(keywords) != 0:
      key_countries.append(keywords)
    else:
      key_countries.append(temp_list)
    iteration+=1

命名實體識別:地點

接下來,我們要豐富我們的資料。最終,結構化資料的目標通常是執行某種分析或視覺化——在這種國際衝突資訊的情況下,將資訊按地理位置繪製出來是很有價值的。為此,我們需要與文件對應的座標。

找到地名

首先,我們將使用自然語言處理(NLP)和命名實體識別(NER)從文字中提取地名。

NLP是機器學習的一種形式,計算機演算法使用語法和語法規則來學習文字中單詞之間的關係。通過這種學習,NER能夠理解某些單詞在句子或段落中所起的作用。本教程並不打算全面介紹NLP—對於這樣的資源,請檢視:https://medium.com/@ODSC/an-introduction-to-natural-language-processing-nlp-8e476d9f5f59

從外部API獲取座標

為了找到地名的座標,我們將使用 Open Cage API查詢座標;你可以在這裡建立一個免費帳戶並接收API金鑰。還有許多其他流行的地理api可供選擇,但通過反覆試驗,我發現Open-Cage在中東地區有著最好的效能。

首先,我們迭代從文件中檢索到的每個地名,並在Open Cage中查詢它。一旦完成這項工作,我們將對比Open Cage與先前建立的mentioned_countries 列表。這將確保我們檢索的查詢結果位於正確的位置。

# 使用NLP提取地名,然後查詢open-cage API以獲得繪圖所需的座標

# 插入你自己的OpenCage API key:
geo_api_key='Insert Your API Key Here'

def get_coords(content_list):
  iteration=1
  for i in range(len(content_list)):
    print('Getting coordinates',iteration,'/',len(content_list))
    temp_list=[]
    text=content_list[i]

    # 應用一個NER演算法,從python庫'ner-d'中查詢地名。

    doc=nlp(text)
    location=[X.text for X in doc.ents if X.label_ == 'GPE']
    location_dict=dict.fromkeys(location,0)
    location=list(location_dict)


    # 查詢位置。

    for l in location:
      try: 
        request_url='https://api.opencagedata.com/geocode/v1/json?q={}&key={}'.format(l,geo_api_key)
        page=requests.get(request_url)
        data=page.json()
        for n in range(len(data)):

          # 這行程式碼檢查查詢結果中的國家是否與mentioned_countries之一相匹配。如果不是,那麼查詢結果很可能是假正例。

          if data['results'][n]['components']['country'] in mentioned_countries[i]:
            lat=data['results'][n]['geometry']['lat']
            lng=data['results'][n]['geometry']['lng']
            coordinates={'Location': l,
                          'Lat': lat,
                          'Lon': lng}
            temp_list.append(coordinates)
            break
          else:
            continue    
      except Exception: 
        continue
    coord_list.append(temp_list)
    iteration+=1

命名實體識別:人

接下來,我們將提取文件中提到的人的姓名。為此,我們將再次使用NER-d python庫中的NER演算法。

獲取全名

在最終的結構化資料中,我只想要全名。只找到“Jack”或“John”的資料,會不會令人困惑?為此,我們將再次使用一些基本的統計資料。當提到全名時,函式將跟蹤全名,通常是在文字的開頭。

當後面提到部分名稱時,它將引用全名列表,以標識部分名稱引用的是誰。例如,如果一篇新聞文章是這樣寫的:“喬·拜登正在競選總統。喬是前總統奧巴馬的副總統,我們知道喬指的是拜登,因為他的全名在文中早些時候已經給出。此函式將以相同的方式執行。

重複的名字

如果出現了重複的情況,該函式將使用前面用於國家/地區函式的相同統計資料。它將測量一個名字被提及的次數,並將其作為最有可能的名字。例如:喬·拜登和他的兒子亨特·拜登都是受歡迎的美國政治家。喬·拜登是前副總統。拜登現在正在與現任總統唐納德·川普競選總統”。根據文字的統計重點,這篇文章顯然是關於喬·拜登,而不是亨特·拜登。

驗證名字

一旦函式計算出所有提到的全名,它將把它們新增到一個列表中。然後,它將查詢維基百科中的每個名字,以驗證它是否是值得包含在結構化資料中的有影響力的人的名字。

def get_people(content_list):
  iteration=1

  # 使用NER在文字中查詢人名。

  for i in range(len(content_list)):
    print('Getting people',iteration,'/',len(content_list))
    temp_list=[]
    text=content_list[i]
    doc=nlp(text)
    persons=[X.text for X in doc.ents if X.label_ == 'PERSON']
    persons_dict=dict.fromkeys(persons,0)
    persons=list(persons_dict)

    full_names=[]
    for person in persons: 
      if len(word_tokenize(person)) >= 2:
        string_name=re.sub(r"[^a-zA-Z0-9]+", ' ', person).strip()
        full_names.append(string_name)

    final_names=[]
    for person in persons:
      for name in full_names:
        tokens=word_tokenize(name)
        for n in range(len(tokens)):
          if person==tokens[n]:
            final_names.append(name)

    for name in full_names:
      final_names.append(name)

    name_dict=dict.fromkeys(final_names,0)
    final_names=list(name_dict)
    valid_names=[]

    for name in final_names:
      page=requests.get('https://en.wikipedia.org/wiki/'+name)
      if page.status_code==200:
        valid_names.append(name)

    people.append(valid_names)
    iteration+=1

關鍵詞提取:TF-IDF

我們的下一個任務是從文字中提取關鍵字。最常見的方法是使用一種稱為TF-IDF的方法。TF-IDF模型測量單個文件中單詞的使用頻率,然後將其與整個文件語料庫中的平均使用率進行比較。

如果一個術語在單個文件中頻繁使用,並且很少在整個文件語料庫中使用,那麼該術語很可能表示該特定文件特有的關鍵字。這篇文章並不是一篇關於TF-IDF模型的全面概述。要了解更多資訊,請檢視這篇關於Medium的文章:https://medium.com/datadriveninvestor/tf-idf-in-natural-language-processing-8db8ef4a7736

首先,我們的函式將建立通常所說的“詞袋”。這將跟蹤每個文件中使用的每個單詞。然後,它將計算每個文件中每個單詞的每次使用次數—單詞頻率(TF)。然後,它計算逆文件頻率(IDF)。然後將這些值寫入矩陣中的座標,然後對矩陣進行排序,以幫助我們找到最有可能表示文件的單詞。

# 第一個函式通過降低字元大小寫和刪除特殊字元對文字進行預處理。

def pre_process(text):
    text=text.lower()
    text=re.sub("</?.*?>"," <> ",text)
    text=re.sub("(\\d|\\W)+"," ",text)
    return text

# 這個函式將矩陣對映到座標。TF-IDF函式將頻率分數對映到矩陣,然後需要對這些矩陣進行排序,以幫助我們找到關鍵字。 

def sort_coo(coo_matrix):
    tuples = zip(coo_matrix.col, coo_matrix.data)
    return sorted(tuples, key=lambda x: (x[1], x[0]), reverse=True)

# 與上面一樣,這是一個幫助函式,一旦頻率對映到矩陣,它將幫助排序和選擇關鍵字。 
# 這個函式專門幫助我們根據TF-IDF統計資料選擇最相關的關鍵字

def extract_topn_from_vector(feature_names, sorted_items, topn=10):
    sorted_items = sorted_items[:topn]
    score_vals = []
    feature_vals = []

    for idx, score in sorted_items:
        fname = feature_names[idx]
        score_vals.append(round(score, 3))
        feature_vals.append(feature_names[idx])

    results= {}
    for idx in range(len(feature_vals)):
        results[feature_vals[idx]]=score_vals[idx]
    return results

#最後一個函式包含了上述helper函式,它對正文應用TF-IDF演算法,根據使用頻率查詢關鍵字。

def get_keywords(content_list):
  iteration=1
  processed_text=[pre_process(text) for text in content_list]
  stop_words=set(stopwords.words('english'))
  cv=CountVectorizer(max_df=0.85,stop_words=stop_words)
  word_count_vector=cv.fit_transform(processed_text)

  tfidf_transformer=TfidfTransformer(smooth_idf=True,use_idf=True)
  tfidf_transformer.fit(word_count_vector)

  feature_names=cv.get_feature_names()

  for i in range(len(processed_text)):
    print('Getting Keywords',iteration,'/',len(content_list))
    doc=processed_text[i]
    tf_idf_vector=tfidf_transformer.transform(cv.transform([doc]))
    sorted_items=sort_coo(tf_idf_vector.tocoo())
    keys=extract_topn_from_vector(feature_names,sorted_items,10)
    keywords.append(list(keys.keys()))
    iteration+=1

主題模型

NLP中最常見的任務之一就是主題模型。這是一種聚類形式,它嘗試根據文件的文字內容自動對文件進行分類。在這個具體的例子中,我想一眼就知道ISW涉及哪些主題。通過根據文字內容對文件進行分類,我可以輕鬆地對文件的主要思想有一個大致的瞭解。

向量化

對於這個例子,我將使用k-means聚類演算法來進行主題建模。首先,我將再次使用TF-IDF演算法對每個文件進行向量化。向量化是一個機器學習術語,指的是將非數字資料轉換成計算機可以用來執行機器學習任務的數字空間資料。

優化

一旦文件被向量化,helper函式就會檢查簇的最佳數量。(k表示k-means的k)。在本例中,最佳數目是50。一旦我找到了最佳的數字,在這個例子中,我註釋掉了這行程式碼,並手動將引數調整為等於50。這是因為我正在分析的資料集不會經常更改,所以我可以期望隨著時間的推移,最佳簇的數量會保持不變。對於變化更頻繁的資料,你應該返回最佳的簇數量作為變數-這將幫助你的聚類演算法自動設定其最佳引數。我在我的時間序列分析文章中展示了一個例子。

聚類

每個簇完成後,我將每個簇的編號(1–50)儲存到簇編號的列表中,而組成每個簇的關鍵字儲存到cluster_keywords的列表中。這些簇關鍵字稍後將用於向每個主題簇新增標題。

# 該函式根據各種“k”引數檢查聚類演算法,以找到“k”的最優值。

def find_optimal_clusters(data, max_k):
    iters = range(2, max_k+1, 2)

    sse = []
    for k in iters:
        sse.append(MiniBatchKMeans(n_clusters=k, 
                                   init_size=1024, 
                                   batch_size=2048,
                                   random_state=20).fit(data).inertia_)

        print('Fit {} clusters'.format(k))

    f, ax = plt.subplots(1, 1)
    ax.plot(iters, sse, marker='o')
    ax.set_xlabel('Cluster Centers')
    ax.set_xticks(iters)
    ax.set_xticklabels(iters)
    ax.set_ylabel('SSE')
    ax.set_title('SSE by Cluster Center Plot')

# 從內容列表中獲取關鍵詞來幫助對主題模型的分類

def get_top_keywords(data, clusters, labels, n_terms):
  df = pd.DataFrame(data.todense()).groupby(clusters).mean()
  for i,r in df.iterrows():
    cluster_keywords.append(','.join([labels[t] for t in np.argsort(r)[-n_terms:]]))

# 應用於主題建模的內容列表

def get_topics(content_list):
  processed_text=[pre_process(text) for text in content_list]
  stop_words=set(stopwords.words('english'))
  cv=CountVectorizer(max_df=0.85,stop_words=stop_words)
  word_count_vector=cv.fit_transform(processed_text)

  tfidf_transformer=TfidfTransformer(smooth_idf=True,use_idf=True)
  tfidf_transformer.fit(word_count_vector)

  feature_names=cv.get_feature_names()
  vector=tfidf_transformer.transform(cv.transform(processed_text))

  #find_optimal_clusters(vector,50)

  clusters = MiniBatchKMeans(n_clusters=50, init_size=1024, batch_size=2048, random_state=20).fit_predict(vector)
  for cluster in clusters:
    cluster_number.append(int(cluster))

  get_top_keywords(vector, clusters, cv.get_feature_names(), 20)

放在一起

最後,我們將提取我們的資料。使用我們之前得到的href列表,現在是將所有提取函式應用於web內容的時候了。

# 遍歷從“browse”中提取的href,提取相關內容

iteration=1

# 前幾個函式依賴於原始提取的web內容作為引數。這些都是基本的web抓取技術。

for href in hrefs:
  print('Web scraping: iteration',iteration,'/',len(hrefs))
  page=requests.get(href)
  soup=BeautifulSoup(page.text,'html.parser')
  links.append(href)
  get_date(soup)
  get_title(soup)
  get_contents(soup)
  iteration+=1

# 下面這些函式依賴於文字主體作為引數。
# 這些是基於nlp的函式。

# 注意:由於查詢外部API,
# 需要一個超時來阻止伺服器過載。
# 這部分程式碼的執行時間很長。

get_countries(content_text)
get_coords(content_text)
get_people(content_text)
get_keywords(content_text)
get_topics(content_text)

豐富主題模型

我們的下一個問題是:我們的簇為我們提供了一個與每個簇相關聯的單詞列表,但是簇的名稱僅僅是數字。這使我們有機會繪製一個詞雲或其他有趣的視覺化圖,可以幫助我們理解每個簇,但對於結構化資料集中的一目瞭然的理解來說,它並沒有那麼有用。另外,我認為有些文件可能屬於多個主題類別。k-means不支援多重聚類,因此我必須手動識別這些文件。首先,我將列印前幾行關鍵字,以瞭解我正在處理的資料。

與每個主題相關聯的一些關鍵字。我們將使用這些關鍵字將簇分類到預定義的類別中。

在對各種技術進行了大量實驗之後,我決定採用一種非常簡單的方法。我掃描了與每個簇相關的每個關鍵字列表,並在每個與特定主題相關的關鍵字中記錄了重要的關鍵字。在這個階段,領域知識是關鍵。例如,我知道,ISW檔案中的阿勒頗幾乎肯定提到了敘利亞內戰。對於你的資料,如果你缺乏適當的領域知識,你可能需要做進一步的研究,諮詢你團隊中的其他人,或者定義一個更高階的程式設計方法來命名簇。

然而,對於這個例子,簡單的方法很有效。在記錄了簇列表中存在的幾個重要關鍵字之後,我自己製作了幾個列表,其中包含了與結構化資料中我想要的最終主題類別相關聯的關鍵字。該函式簡單地將每個簇的關鍵字列表與我建立的列表進行比較,然後根據列表中的匹配項分配主題名稱。然後將這些最後的主題附加到主題類別列表中。

#搜尋與主題對應的關鍵詞列表,與聚類詞庫交叉引用,為每篇文章分配一個主題類別。

oir=['OIR Iraq','yezidis','mosul','peshmerga','isis','iraq','sinjar','baghdad','maliki',
     'daquq','anbar','isf','abadi','malaki','ramadi','iraqi','fallujah','dabiq']

terrorism=['Terrorism','jihadi','islamic','salafi','qaeda',
           'caliphate','isis','terrorist','terrorism']

syrian_conflict=['Syrian Conflict','sana','syria','assad',
                 'idlib','afrin','aleppo']

russia=['Russia','russia','belarus','slavic','kremlin','russian',
        'minsk','ukraine','putin']

iran=['Iran','iran','iranian','proxy','militias','militia','marjah']

turkey=['Turkey','erdogan','turkish','turkey']

ors=['ORS','kabul','ghani','pakistan','afghan','afghanistan',
     'taliban','ansf','karzai','helmand']

africa=['Africa','libya','libyan','egypt','egyptian','africa','african']

cat_list=[oir,terrorism,syrian_conflict,russia,iran,turkey,ors,africa]

topic_dict={}

for i in range(len(cluster_keywords)):
  temp_list=[]
  for n in nltk.word_tokenize(cluster_keywords[i]):
    for item in cat_list:
      if n in item:
        temp_list.append(item[0])

  temp_dict=dict.fromkeys(temp_list,0)
  temp_list=list(temp_dict)

  topic_dict[i] = temp_list

for num in cluster_number:
  topic_categories.append(topic_dict[num])

資料庫建立

最後一步是將我們提取的所有資料集中起來。對於這些資料,我更喜歡JSON格式。這是因為我想以不同的方式組織某些型別的資料—例如,locations欄位將包含地名、緯度和經度的字典列表。在我看來,JSON格式是將這種格式化的資料儲存到本地磁碟的最有效的方法。我還在文件資料庫MongoDB中備份了這個資料庫的副本,但這不是本文的重點。

#將一個空列表初始化

db=[]

for i in range(len(hrefs)):
  countries={
      'focus area': key_countries[i],
      'all mentioned countries': mentioned_countries[i]
  }

  # 將函式中定義的所有列表新增到新的儲存列表中

  doc={ 
      '_id': len(hrefs) - i,
       'title': titles[i],
       'date': dates[i],
       'places': coord_list[i],
       'people': people[i],
       'keywords': keywords[i],
       'countries': countries,
       'full text': content_text[i],
       'url': links[i],
       'topic cluster': cluster_number[i],
       'categories': topic_categories[i]
  }

  db.append(doc)

# 將列表儲存為谷歌驅動器內的.JSON資料儲存檔案(用於演示目的)

with open ('/content/drive/My Drive/Colab Notebooks/isw_products.json', 'w') as fout:
  json.dump(db, fout)

摘要

現在我們結束了!我們從網頁中提取連結,然後使用這些連結從網站中提取更多內容。我們使用這些內容,然後使用外部api、ML簇演算法和NLP來提取和增強這些資訊。TF-IDF向量化、關鍵字提取和主題模型,這些是NLP的基石。如果你有更多問題或需要資訊,請聯絡我們,祝你在未來的NLP中好運!

原文連結:https://towardsdatascience.com/something-from-nothing-use-nlp-and-ml-to-extract-and-structure-web-data-3f49b2f72b13

歡迎關注磐創AI部落格站: http://panchuang.net/

sklearn機器學習中文官方文件: http://sklearn123.com/

歡迎關注磐創部落格資源彙總站: http://docs.panchuang.net/

相關文章