數據科學在文本分析中的應用 :中英文 NLP(上)
在《後疫情時代,數據科學賦能旅遊行業服務質量提升》這篇博文中,我們介紹了貓途鷹文本分析項目的背景和解決方案,並展示了最終的分析結果。接下來,對於中英文 NLP 感興趣的讀者,我們會為大家詳細講解數據採集、數據入庫、數據清理和數據建模步驟中涉及的原理和代碼實現。由於篇幅的限制,上篇會重點講解數據採集、數據入庫和數據清理這三個步驟,下篇則會講解數據建模的完整流程。
數據採集
1. 抓取工具分析
網頁內容抓取是從互聯網上獲取數據的方式之一。對於使用 Python 進行網頁抓取的開發者,比較主流的工具有以下幾種:
Beautiful Soup
Beautiful Soup 是幾種工具中最容易上手的網頁抓取庫,它可以快速幫助開發者從 HTML 或 XML 格式的文件中獲取數據。在這個過程中,Beautiful Soup 會一定程度上讀取這類文件的數據結構,並在此基礎上提供許多與查找和獲取數據內容相關的方程。除此之外,Beautiful Soup 完善、易於理解的文檔和活躍的社區使得開發者不僅可以快速上手,也能快速精通,並靈活運用於開發者自己的應用當中。
不過正因為這些工作特性,相較於其他庫而言,Beautiful Soup也有比較明顯的缺陷。首先,Beautiful Soup 需要依賴其他 Python庫(如 Requests)才能向對象服務器發送請求,實現網頁內容的抓取;也需要依賴其他 Python 解析器(如 html.parser)來解析抓取的內容。其次,由於Beautiful Soup需要提前讀取和理解整個文件的數據框架以便之後內容的查找,從文件讀取速度的角度來看,Beautiful Soup 相對較慢。在許多網頁信息抓取的過程中,需要的信息可能只佔一小部分,這樣的讀取步驟並不是必需的。
Scrapy
Scrapy 是非常受歡迎的開源網頁抓取庫之一,它最突出的特性是抓取速度快,又因為它基於 Twisted 異步網絡框架,用户發送的請求是以無阻塞機制發送給服務器的,比阻塞機制更靈活,也更節省資源。因此,Scrapy 擁有了以下這些特性:
- 對於 HTML 類型網頁,使用XPath或者CSS表述獲取數據的支持
- 可運行於多種環境,不僅僅侷限於 Python。Linux、Windows、Mac 等系統都可以使用 Scrapy 庫。
- 擴展性強
- 速度和效率較高
- 需要的內存、CPU 資源較少
縱然 Scrapy 是功能強大的網頁抓取庫,也有相關的社區支持,但生澀難懂的文檔使許多開發者望而卻步,上手比較難。
Selenium
Selenium 的起源是為了測試網頁應用程序而開發的,它獲取網頁內容的方式與其他庫截然不同。Selenium 在結構設計上是通過自動化網頁操作來獲取網頁返回的結果,和 Java 的兼容性很好,也可以輕鬆應對 AJAX 和 PJAX 請求。和 Beautiful Soup 相似,Selenium 的上手相對簡單,但與其他庫相比,它最大的優勢是可以處理在網頁抓取過程中出現的需要文本輸入才能獲取信息、或者是彈出頁面等這種需要用户在瀏覽器中有介入動作的情況。這樣的特性使得開發者對網頁抓取的步驟更加靈活,Selenium 也因此成為了最流行的網頁抓取庫之一。
由於在獲取景點評論的過程中需要應對搜索欄輸入、彈出頁面和翻頁等情況,在本項目中,我們會使用 Selenium 進行網頁文本數據的抓取。
2. 網頁數據和結構的初步瞭解
各個網站在開發的過程中都有自己獨特的結構和邏輯。同樣是基於 HTML 的網頁,即使 UI 相同,背後的層級關係都可能大相徑庭。這意味着理清網頁抓取的邏輯不僅要了解目標網頁的特性,也要對未來同一個網址的更新換代、同類型其他平台的網頁特性有所瞭解,通過比較相似的部分整理出一個相對靈活的抓取邏輯。
貓途鷹國際版網站的網頁抓取步驟與中文版網站的步驟相似,這裏我們以 www.tripadvisor.cn 為例,先觀察一下從首頁到景點評論的大致步驟。
步驟一:進入首頁,在搜索欄中輸入想要搜索的景點名稱並回車
步驟二:頁面更新,出現景點列表,選擇目標景點。
在搜索景點名稱後,我們需要在圖中所示的列表裏鎖定目標景點。這裏可以有兩層邏輯疊加幫助我們達到這個目的:
- 貓途鷹的搜索引擎本身會對景點名稱和搜索輸入進行比較,通過自己內部的邏輯將符合條件的景點排名靠前
- 我們可以在結果出現後使用省份、城市等信息篩選得到目標景點
步驟三:點擊目標景點,彈出新頁面,切換至該頁面並尋找相關評論
根據評論格式的特點,我們可以抓取的信息如下:
- 用户
- 用户所在地
- 評分
- 點評標題
- 到訪日期
- 旅行類型
- 詳細點評
- 撰寫日期
步驟四:翻頁獲取更多評論
可以看到,在獲取相關網頁的過程中有許多需要瀏覽器去完成的動作,這也是我們選擇 Selenium 的原因。因此,我們的網頁抓取程序會在數據抓取之前,進行相同的步驟。
開發網頁抓取程序時一個非常便利的定位所需內容在 HTML 代碼中位置的方法是,在瀏覽器中將鼠標移至內容所在的區域,右鍵選擇 “Inspect”,瀏覽器會彈出網頁 HTML 元素並定位到和內容相關的代碼。基於這種方法,我們可以使用 Selenium 進行自動化操作和數據抓取。
以上述評論為例,它在 HTML 結構中的位置如下:
在使用 Selenium 時,元素類別和 class 名稱可以幫助我們定位到相關內容,進行進一步操作,抓取相關文本數據。我們可以使用這兩種定位方法:CSS 或 XPATH,開發者可以根據自身需求進行選擇。最終,我們執行的網頁抓取程序大致可以分成兩個步驟:
- 第一步:發送請求,使用 Selenium 操作瀏覽器找到指定景點的評論頁面
- 第二步:進入評論頁面,抓取評論數據
3. 獲取評論數據
這部分的功能實現需要先安裝和導入以下 Python 庫:
from selenium import webdriver import chromedriver_binary from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys import time import datetime import re import pandas as pd from utility import print_log_message, read_from_config
其中,utility 是一個輔助模塊,包含打印會話和發生時間的方程,以及從 ini 設置文件中讀取程序信息的方程。utility 中的輔助方程可以反覆出現在需要的模塊中。
#utility.py import time import configparser def print_log_message(app_name, procedure, message): ts = time.localtime() print(time.strftime("%Y-%m-%d %H:%M:%S", ts) + " **" + app_name + "** " + procedure + ":", message) return def read_from_config(file_name, section, var): config = configparser.ConfigParser() config.read(file_name) var_value = config.get(section, var) return var_value
在開始網頁抓取之前,我們需要先啟動一個網頁會話進程。
# Initiate web session chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--window-size=1920,1080') chrome_options.add_argument('--headless') chrome_options.add_argument('--disable-gpu') chrome_options.add_argument('--disable-dev-shm-usage') wd = webdriver.Chrome(ChromeDriverManager().install(),chrome_options=chrome_options) wd.get(self.web_url) wd.implicitly_wait(5) review_results = {}
考慮到運行環境不是 PC 或資源充足的實例,我們需要在代碼中説明程序沒有顯示方面的需求。ChromeDriverManager() 可以幫助程序在沒有 Chrome 驅動的環境中下載需要的驅動文件,並傳遞給 Selenium 的會話進程。
注意,許多網頁內容與 Chrome 版本、資源和系統環境、時間有關。本項目中使用的網頁並不受這類信息或環境的影響,但會受瀏覽器顯示設置的限制,進而影響被抓取的內容。請大家在開發此類抓取程序時,注意核對網頁顯示信息與實際抓取數據是否吻合。
進入貓途鷹主頁(https://www.tripadvisor.cn/)後,在搜索欄輸入目標景點名稱並回車,進入新頁面後,在景點列表里根據搜索引擎排序、省份和城市,尋找並點擊進入正確的景點頁面。這裏,我們以“外灘”為例:
location_name = '外灘' city = '上海' state = '上海'
# Find search box wd.find_element(By.CSS_SELECTOR, '.weiIG.Z0.Wh.fRhqZ>div>form>input').click()
# Enter location name wd.find_element(By.XPATH, '//input[@placeholder="去哪裏?"]').send_keys(f'{location_name}') wd.find_element(By.XPATH, '//input[@placeholder="去哪裏?"]').send_keys(Keys.ENTER)
# Find the right location with city + province info element = wd.find_element(By.XPATH, f'//*[@class="address-text" and contains(text(), "{city}") and contains(text(), "{state}")]') element.click()
在點擊目標景點後,切換至跳轉出的新頁面。進入景點評論頁面之後,我們就可以根據頁面 HTML 的結構和評論在其代碼層級中的位置將所需信息抓取下來。Selenium 在尋找某一個元素時,會在整個網頁框架中尋找相關信息,並不能像其他一些網頁抓取庫一樣鎖定某一個部分並只在該部分中尋找想要的元素。因此,我們需要將一類信息統一抓取出來,然後剔除一些不需要的信息。這一過程需要反覆核對真實網頁上顯示的信息,以防將不需要的內容抓取出來,影響數據質量。
抓取使用的代碼如下:
comment_section = wd.find_element(By.XPATH, '//*[@data-automation="WebPresentation_PoiReviewsAndQAWeb"]')
# user id user_elements = comment_section.find_elements(By.XPATH, '//div[@class="ffbzW _c"]/div/div/div/span[@class="WlYyy cPsXC dTqpp"]') user_list = [x.text for x in user_elements]
對於英文評論數據的抓取,除了網頁框架有一些區別以外,關於地點的數據要更復雜一些,需要進一步的處理。我們在抓取的過程中,默認逗號為分隔符,逗號前的值為城市,逗號後的值為國家地區。
# location loca_elements = comment_section.find_elements(By.XPATH, '//div[@class="ffbzW _c"]/div/div/div/div/div[@class="WlYyy diXIH bQCoY"]') loca_list = [x.text[5:] for x in loca_elements]
# trip type trips_element = comment_section.find_elements(By.XPATH, '//*[@class="eRduX"]') trip_types = [self.separate_trip_type(x.text) for x in trips_element]
注意,由於評價時間的定位相對困難,文本 class 類別會包含網頁景點介紹的信息,我們需要把這部分不需要的數據剔除。
# comment date comments_date_element = comment_section.find_elements(By.CSS_SELECTOR, '.WlYyy.diXIH.cspKb.bQCoY')
# drop out the first element comments_date_element.pop(0) comments_date = [x.text[5:] for x in comments_date_element]
由於用户評分並非文本,我們需要從 HTML 的結構中找到代表它的元素,以此來計算星級多少。在貓途鷹的網頁 HTML 中,代表星級的元素是 “bubble”,我們需要在 HTML 結構中找到相關的代碼,將代碼中的星級數據提取出來。
# rating rating_element = comment_section.find_elements(By.XPATH, '//div[@class="dHjBB"]/div/span/div/div[@style="display: block;"]') rating_list = [] for rating_code in rating_element: code_string = rating_code.get_attribute('innerHTML') s_ind = code_string.find(" bubble_") rating_score = code_string[s_ind + len(" bubble_"):s_ind + len(" bubble_") + 1] rating_list.append(rating_score)
# comments title comments_title_elements = comment_section.find_elements(By.XPATH, '//*[@class="WlYyy cPsXC bLFSo cspKb dTqpp"]') comments_title = [x.text for x in comments_title_elements]
# comments content comments_content_elements = wd.find_element(By.XPATH, '//*[@data-automation="WebPresentation_PoiReviewsAndQAWeb"]' ).find_elements(By.XPATH, '//*[@class="duhwe _T bOlcm dMbup "]') comments_content = [x.text for x in comments_content_elements]
在評論中查找圖片和尋找星級的邏輯一樣,先要在 HTML 結構中找到代表圖片的部分,然後在代碼中確認評論中是否包含圖片信息。
# if review contains pictures pic_sections = comment_section.find_elements(By.XPATH, '//div[@class="ffbzW _c"]/div[@class="hotels-community-tab-common-Card__card--ihfZB hotels-community-tab-common-Card__section--4r93H comment-item"]') pic_list = [] for r in pic_sections: if 'background-image' in r.get_attribute('innerHTML'): pic_list.append(1) else: pic_list.append(0)
綜上所述,我們可以將評論數據按照輸入景點名和所需評論頁數從貓途鷹網站抓取下來並進行整合,最終保存為一個 Pandas DataFrame。
整個過程可以實現自動化,打包成一個名為 data_processor 的 .py 格式文件。如需獲取評論數據,我們只需運行以下方程,即可獲得 Pandas DataFrame 格式的景點評論信息。
#引入之前定義的Python Class: from data_processor import WebScrapper scrapper = WebScrapper()
#運行網頁抓取方程抓取中文語料:
trip_review_data = scrapper.trip_advisor_zh_scrapper_runner(location, location_city, location_state, page_n=int(n_pages))
其中 location 代表景點名稱,location_city 和 location_state 代表景點所在的城市和省份,page_n 代表需要抓取的頁數。
數據入庫
在得到抓取的評論數據後,我們可以將數據存進數據庫,以便數據分享,進行下一步的分析和建模。以 PieCloudDB Database 為例,我們可以使用 Python 的 Postgres SQL 驅動與 PieCloudDB 進行連接。
本項目實現數據入庫的方式是,在獲取了評論數據並整合為 Pandas DataFrame 後,我們將藉助 SQLAlchemy 引擎將 Pandas 數據通過 psycopg2 上傳至數據庫。首先,我們需要定義連接數據庫的引擎:
from sqlalchemy import create_engine
import psycopg2
engine = create_engine('postgresql+psycopg2://user_name:password@db_ip:port /database')
其中 postgresql + psycopg2 是我們在連接數據庫時需要使用的驅動,user_name 是數據庫用户名,password 是對應的登陸密碼,db_ip 為數據庫 ip 或 endpoint,port 為數據庫外部連接接口,database 是數據庫名稱。
將引擎傳遞給 Pandas 後,我們就可以輕鬆地將 Pandas DataFrame 上傳至數據庫,完成入庫操作。
data.to_sql(table_name, engine, if_exists=‘replace’, index=False)
data 是我們需要入庫的 Pandas DataFrame 數據,table_name 是表名,engine 是我們之前定義的 SQLAlchemy 引擎, if_exists=‘replace’ 和 index=False 則是 Pandas to_sql() 方程的選項。這裏選項的含義是,如果表已存在則用現有數據替代已有數據,並且在入庫過程中,我們不需要考慮索引。
數據清洗
在這個步驟中,我們會根據原數據的特性對評論數據進行清理,為後續的建模做準備。抓取下來的評論數據包含以下三種類別的信息:
- 用户信息(如所在地等)
- 評論信息(如是否包含圖片信息等)
- 評論語料
在正式進入這個步驟前,我們需要導入以下代碼庫,其中部分代碼庫會在數據建模步驟使用:
import numpy as np import pandas as pd import psycopg2 from sqlalchemy import create_engine import langid import re import emoji from sklearn.preprocessing import MultiLabelBinarizer import demoji import random from random import sample import itertools from collections import Counter import matplotlib.pyplot as plt
用户信息與評論信息的運用主要在 BI 部分體現,建模部分主要依靠評論語料數據。我們需要根據評論語言採取合適的清理、分詞和建模方法。首先,我們從數據庫中調取數據,通過以下代碼可以實現。
中文評論數據:
df = pd.read_sql('SELECT * FROM "上海_上海_外灘_source_review"', engine) df.shape
英文評論數據:
df = pd.read_sql('SELECT * FROM "Shanghai_Shanghai_The Bund (Wai Tan)_source_review_EN"', engine) df.shape
我們在中文版網站抓取了171頁評論,每頁有10個評論,合計1710條評論;在國際版網站抓取了200頁評論,合計2000條評論。
1. 數據類型處理
由於寫入數據庫的數據都是字符串類型,我們需要先對每一列數據的數據類型進行校對和轉換。在中文評論數據中,需要轉換的變量是評論時間和評分。
df['comment_date'] = pd.to_datetime(df['comment_date']) df['rating'] = df['rating'].astype(str) df['comment_year'] = df['comment_date'].dt.year df['comment_month'] = df['comment_date'].dt.month
2. 瞭解數據狀況
在處理空值和轉換數據之前,我們可以大致瀏覽一下數據,對空值狀況有一個初步的瞭解。
df.isnull().sum()
中文評論數據的空值大致情況如下:
與中文評論數據不同的是,英文評論數據中需要處理的空白數據要多一些,主要集中在用户所在地和旅行類型兩個變量當中。
3. 處理旅行類型空值
對於存在空值的變量,我們可以通過對變量各類別的統計來大致瞭解其特性。以旅行類型(trip_type)為例,該變量有6種類型,其中一種是用户未表明的旅行類型,這類數據都以空值形式存在:
df.groupby(['trip_type']).size()
因為旅行類型是分類變量,在本項目的情況下,我們用類別“未知”或“NA”填充空值。
中文評論數據:
df['trip_type'] = df['trip_type'].fillna('未知')
英文評論數據:
df['trip_type'] = df['trip_type'].fillna('NA')
在中文評論的文本分析中,旅行類型分為以下六種,與英文是對應的關係:全家遊、商務行、情侶遊、獨自旅行、結伴旅行、未知。為了方便之後的分析,我們需要建立一個查詢表,將兩種語言的旅行類型對應起來。
zh_trip_type = ['全家遊', '商務行', '情侶遊', '獨自旅行', '結伴旅行', '未知'] en_trip_type = ['Family', 'Business', 'Couples', 'Solo', 'Friends', 'NA'] trip_type_df = pd.DataFrame({'zh_type':zh_trip_type, 'en_type':en_trip_type})
然後將該表寫進數據庫,以便後續的可視化分析。
trip_type_df.to_sql("tripadvisor_TripType_lookup", engine, if_exists="replace", index=False)
4. 處理英文評論數據中用户所在地信息
在英文評論數據中,由於用户所在地為用户自行填充的信息,地區數據非常混亂,並非按照某一個順序或者邏輯來填充。城市和國家字段不僅需要處理空值,還需要校正。在抓取數據時,我們抓取地區信息的邏輯為:
- 如果地區信息用逗號隔開,前一個詞為城市,後一個詞為國家/省份
- 如果沒有逗號,則默認該信息為國家信息
對於國際版網站的評論分析,我們選擇細分用户所在地到國家層級。注意,由於很多用户有拼寫錯誤或填寫虛假地名的問題,我們的目標是儘可能地在力所能及的範圍內修正信息,如校正大小寫、縮寫、對應城市信息等。這裏,我們的具體解決方法是:
- 將縮寫的國家/省份提取出來並單獨處理(以美國為主,用户在填寫地區信息時只填寫州名)
- 查看除縮寫以外的國家信息,如國家名稱未出現在國家列表裏,則認為是城市信息
- 國家字段中出現的城市名錯填(如大型城市)和拼寫錯誤問題,則手動修改處理
注意,本項目中使用的國家、地區名參考自 國家名稱信息來源 和 美國各州及其縮寫來源。
首先,我們從文件系統中讀取國家信息:
country_file = open("countries.txt", "r") country_data = country_file.read() country_list = country_data.split("\n") countries_lower = [x.lower() for x in country_list] 讀取美國州名及其縮寫信息: state_code = pd.read_csv("state_code_lookup.csv")
下列方程可以讀取一個國家名字符串,並判斷是否需要清理和修改:
def formating_country_info(s_input): if s_input is None: #若字符串輸入為空值,返回空值 return None if s_input.strip().lower() in countries_lower: #若字符串輸入在國家列表中,返回國家名 c_index = countries_lower.index(s_input.strip().lower()) return country_list[c_index] else: if len(s_input) == 2: #若輸入為縮寫,在美國州名、墨西哥省名和英國縮寫中查找,若可以找到,返回對應國家名稱 if s_input.strip().upper() in state_code["code"].to_list(): return "United States" elif s_input.strip().upper() == "UK": return "United Kingdom" elif s_input.strip().upper() in ("RJ", "GO", "CE"): return "Mexico" elif s_input.strip().upper() in ("SP", "SG"): return "Singapore" else: # could not detect country info return None else: #其他情況,需要手動修改國家名稱 if s_input.strip().lower() == "caior": return "Egypt" else: return None
擁有了清理單個值的方程後,我們可以通過 .apply() 函數將該方程應用至 Pandas DataFrame 中代表國家信息的列中。
df["location_country"] = df["location_country"].apply(formating_country_info)
然後,檢查一下清理後的結果:
df["location_country"].isnull().sum()
我們注意到空值的數量有所增加,除了修正部分數據以外,對於一些不存在的地名,以上方程會將其轉換為空值。接下來,我們來處理城市信息,並將可能被分類為城市的國家信息補充至國家變量中。我們可以根據國家的名稱篩選可能錯位的信息,將這類信息作為國家信息的填充,剩下的默認為城市名稱。
def check_if_country_info(city_list): clean_list = [] country_fill_list = [] for city in city_list: if city is None: clean_list.append(None) country_fill_list.append(None) elif city.strip().lower() in countries_lower: #如城市變量中出現的是國家名,記錄國家名稱 c_index = countries_lower.index(city.strip().lower()) country_name = country_list[c_index] if country_name == "Singapore": #如城市名為新加坡,保留城市名,如不是則將原先的城市名轉換為空值 clean_list.append(country_name) else: clean_list.append(None) country_fill_list.append(country_name) else: # format city string city_name = city.strip().lower().capitalize() clean_list.append(city_name) country_fill_list.append(None) return clean_list, country_fill_list
運行上述方程,我們會得到兩個數列,一個為清理後的城市數據,一個為填充國家信息的數據。
city_list, country_fillin = check_if_country_info(df["location_city"].to_list())
在數據中新建一個列,存儲填充國家信息的數列。
df["country_fill_temp"] = country_fillin
替換英文評論數據中的城市信息,並將新建的列填充進國家信息的空值中,再將用來填充的列刪除。
df["location_city"] = city_list df["location_country"] = df["location_country"].fillna(df["country_fill_temp"]) df = df.drop(columns=["country_fill_temp"])
至此,我們就講解完成了本項目中數據採集、數據入庫和數據清理步驟的原理和代碼實現。雖然處理數據的過程艱辛且漫長,但因此能將大量原始數據轉換成有用的數據是非常有價值的。如果大家對於更高階的數據建模步驟感興趣,想知道如何實現文本數據的 emoji 分析、分詞關鍵詞、文本情感分析、詞性詞頻分析和主題模型文本分類,請持續關注 Data Science Lab 的後續博文。
參考資料:
- 戴斌 | 春節旅遊市場高開 全年旅遊經濟穩增
- 西湖景區春節接待遊客292.86萬人次
- Scrapy Vs Selenium Vs Beautiful Soup for Web Scraping
- Extract Emojis from Python Strings and Chart Frequency using Spacy, Pandas, and Plotly
- Topic Modeling with LSA, PLSA, LDA & lda2Vec
本文中部分數據來自互聯網,如若侵權,請聯繫刪除
- 數據科學在文本分析中的應用 :中英文 NLP(上)
- 『堅如磐石的 PieCloudDB』:透明加密模塊的設計與實現
- 後疫情時代,數據科學賦能旅遊行業服務質量提升
- OpenPie 和 ChatGPT 聊聊雲上數據計算的那些事兒
- 正式上市丨拓數派發布eMPP存算分離軟硬件一體機
- 『Postgres.Live 技術沙龍回顧』揭祕 PieCloudDB Database eMPP 架構設計
- PieCloudDB Database 雲上商業智能的最佳實踐
- 數據科學在量化金融中的應用:指數預測(下)
- 數據科學在量化金融中的應用:指數預測(上)
- 【DTCC 2022】雲原生數據庫PieCloudDB全新eMPP架構是如何煉成的
- 數據科學,為企業創造更大的數據價值
- 擁抱開放|OpenPie引領PostgreSQL中國代碼貢獻力