关于数据科学:数据科学在文本分析中的应用-中英文-NLP上

63次阅读

共计 12803 个字符,预计需要花费 33 分钟才能阅读完成。

在《后疫情时代,数据迷信赋能游览行业服务质量晋升》这篇博文中,咱们介绍了猫途鹰文本剖析我的项目的背景和解决方案,并展现了最终的剖析后果。接下来,对于中英文 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

本文中局部数据来自互联网,如若侵权,请分割删除

正文完
 0