在《后疫情年代,数据科学赋能旅游职业服务质量提升》这篇博文中,咱们介绍了猫途鹰文本剖析项意图布景和解决方案,并展现了终究的剖析成果。接下来,关于中英文 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