- 数据清洗与优化主要涉及常规的数据清洗、文本内容过滤、数据乱码问题处理等。
5.1.1 常用的数据清洗手段及日期格式的统一
- 首先回顾一下常用的数据清洗手段,代码如下:
# 1.用strip()函数删除字符串首尾的空格、换行等空白字符
res = ' 华能信托2019年实现利润42.11亿元,行业排名第三 '
res = res.strip() #删除上面字符串首尾的空格和换行
res
# 2.用split函数()拆分字符串,以截取需要的内容
date = '2022-4-2 14:27:10'
date = date.split(' ')[0] #提取年月日信息
date
# 3.用re库中的sub()函数进行内容替换
import re
title = '阿里<em>巴巴<em>人工智能再发力'
title = re.sub('<.*?>','',title) #清除形式为"<xxx>"的内容
title
- 使用上述手段已能完成对标题、网址等信息的清洗和优化,下面在介绍一下如何对日期格式的统一。
- 之前从百度新闻爬取的日期没有一个统一的格式。假设以“2022-4-2”作为标准格式,那么对于格式类似“2022-4-2 14:32”的日期,可以用split()函数来处理。但是百度新闻里还有很多其他的日期格式,如“2022年4月2日”“4小时前”“56分钟前”。其处理办法也不复杂:对于类似“2022年4月2日”的情况,可以将“年”和“月”替换为“-”,将“日”替换为空白字符:对于类似“4小时前”和“56分钟前”的情况,可以将包含“小时”或“分钟”的日期都替换成当天的日期。具体代码如下:
import time
for i in range(len(title)):
date[i] = date[i].split(' ')[0]# 处理带时分秒的日期
date[i] = re.sub('年','-',date[i]) #将“年”替换为“-”
date[i] = re.sub('月','-',date[i]) #将“月”替换为“-”
date[i] = re.sub('日','',date[i]) #将“日”替换为空字符串
if ('小时' in date[i] ) or ('分钟' in date[i]):
date[i] = time.strftime('%Y-%m-%d') #替换为当天的日期
- 第7行代码用运算符in判断当前日期是否包含“小时”或“分钟”。第8行代码用time.strftime(’%Y-%m-%d’)获得标准格式的今天的日期,用于替换当前日期。如果想要更加简洁,可以把这几行代码写到最初的for i in range(len(title))循环语句里。
5.1.2 文本内容过滤——剔除噪声数据
- 来自网络的数据经常会包含一些噪声数据。例如,搜索“红豆集团”,搜索结果却包含“王菲经典歌曲红豆在登怀旧音乐榜首”,这显然不是我们想要的数据。那么如何将这些噪声数据过滤掉呢?这里介绍两种方式:一种是根据新闻标题进行简单过滤,另一种则是根据新闻正文内容进行深度过滤。此外,本节将介绍如何对新闻正文进行深度优化。
1.根据新闻标题简单过滤
- 这种方式的思路是观察所爬取的公司名称是否还在新闻标题里,如果没有,那么就删除该标题以及相关的网址、日期、来源。核心代码如下:
# 根据新闻标题简单过滤
for i in range(len(title)):
if company not in title[i]:
title[i] = ''
href[i] = ''
date[i] = ''
source[i] = ''
while '' in title:
title.remove('')
while '' in href:
href.remove('')
while '' in date:
date.remove('')
while '' in source:
source.remove('')
- 这里首先变量标题列表,如果公司名称不在标题里,则把相应的标题、网址、日期、来源都赋为空字符串,然后通过如下代码批量删除空字符串:
while '' in title:
title.remove('')
- 有的读者可能会问:为什么不直接在for循环语句中用del titile[i]的方式删除元素呢?这是因为for循环根据len(title)确定了循环次数后,循环次数就不会改变了,然而在for循环语句里删除列表元素后,比如原来有10个元素,删除后还有8个元素,那么此时的title[8]、title[9]就不存在,从而导致列表序号越界的错误(list index out of range)。因此,需要先赋值为空字符串,再用while循环来批量处理。
- 除了用上面的方法进行处理,还可以构建新列表来存储符合要求的内容,代码如下:
title_new = []
href_new = []
date_new = []
source_new = []
for i in range(len(title)):
if companys in title[i]:
title_new.append(title[i])
href_new.append(href[i])
date_new.append(date[i])
source_new.append(soure[i])
- 首先构造4个空列表,然后遍历每一个标题,如果公司名称在标题里,则把符合条件的信息用append()函数添加到行列表中。
2.根据新闻正文进行深度过滤(以爬取百度新闻为例)
- 根据新闻标题来进行筛选,有时过于粗放。某些情况下,虽然标题里没有公司名称,但是新闻正文里有公司名称,这种新闻也是我们需要的。下面就来讲解如何根据新闻正文进行深度过滤。
- 之前已经获取到每一条新闻的网址,那么通过下面这行代码就能爬取正文。其原理就是通过Requests库访问每一条新闻的网址,并获取网页源代码。
import re
import time
import requests
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36'}
def baidu(company):
# 1.获取网页源代码
url = 'https://www.baidu.com/s?tn=news&rtt=4&bsst=1&cl=2&wd=' + company # 其中设置rtt=4则为按时间排序,如果rtt=1则为按焦点排序
res = requests.get(url, headers=headers, timeout=10).text
# 2.编写正则提炼内容
#p_href = '<h3 class="news-title_1YtI1"><a href="(.*?)"'
p_href = '<h3 class="news-title_1YtI1 "><a href="(.*?)" '
href = re.findall(p_href, res) # 不存在换行,无须添加re.S
p_title = ' class="news-title-font_1xS-F" aria-label=.*? data-click=.*?<!--s-text-->(.*?)<!--/s-text--></a>'
title = re.findall(p_title, res, re.S)
p_date = '<span class="c-color-gray2 c-font-normal c-gap-right-xsmall" aria-label=.*?>(.*?)</span>'
date = re.findall(p_date, res)
p_source = '<span class="c-color-gray" aria-label=".*?">(.*?)</span>'
source = re.findall(p_source, res)
# 3.数据清洗
for i in range(len(title)):
title[i] = title[i].strip() # strip()函数用来取消字符串两端的换行或者空格,不过目前(2020-10)并没有换行或空格,所以其实不写这一行也没事
title[i] = re.sub('<.*?>', '', title[i]) # 核心,用re.sub()函数来替换不重要的内容
# 统一日期格式
date[i] = date[i].split(' ')[0] # 提取2020年11月15日 10:26:22前面的年月日信息
date[i] = re.sub('年', '-', date[i]) # 换成2020-11-15格式,注意只需要换年和月即可,日换成空值即可
date[i] = re.sub('月', '-', date[i])
date[i] = re.sub('日', '', date[i])
if ('小时' in date[i]) or ('分钟' in date[i]):
date[i] = time.strftime("%Y-%m-%d")
else:
date[i] = date[i]
# 4.正文爬取及数据深度清洗
for i in range(len(title)):
# 获取新闻正文
try:
article = requests.get(href[i], headers=headers, timeout=10).text
except:
article = '单个新闻爬取失败'
# 数据深度清洗
company_re = company[0] + '.{0,5}' + company[-1] # 匹配“公司名称第一个字 + 0到5个任意字符 + 公司最后一个字”这样的匹配规则
if (len(re.findall(company_re, title[i])) < 1) and (len(re.findall(company_re, article)) < 1):
title[i] = ''
source[i] = ''
href[i] = ''
date[i] = ''
while '' in title:
title.remove('')
while '' in href:
href.remove('')
while '' in date:
date.remove('')
while '' in source:
source.remove('')
# 5.打印清洗后的数据
for i in range(len(title)):
print(str(i + 1) + '.' + title[i] + '(' + date[i] + ' ' + source[i] + ')')
print(href[i])
# 6.批量爬取多家公司
companys = ['华能信托', '阿里巴巴', '百度集团']
for i in companys:
try:
baidu(i)
print(i + '数据爬取成功')
time.sleep(3)
except:
print(i + '数据爬取失败')
“.{0,5}”是一个新知识点,“.”表示任意一个字符,“.{0,5}”则表示0~5个任意字符(千万注意这里的逗号后不能有空格)。re.findall(company_re, aritcle)表示寻找满足匹配规则company_re的字符串。如果能找到相关内容,返回的列表长度大于等于1;如果不能找到相关内容,返回的列表长度小于1,此时就可以将该新闻复制为空并清除。
通过上述判断条件就能较精准地过滤和筛选正文。例如,将公司名称设置为“华能信托”,就能筛选出正文含有“华能贵诚信托”的新闻。
3.新闻正文深度优化
- 上面的正文捕捉还可以继续优化。通过article = requests.get(href[i]).text #获取新闻正文信息获得的是新闻详情页面的全部源代码,其中真正的新闻正文往往只占一小部分。例如,下图中的新闻(https://www.jiemian.com/article/2796459.html),正文其实很短,但是旁边的滚动新闻却不少。
- 如果这些滚动新闻中含有与目标公司无关的负面词,就会导致这条新闻被误判为负面新闻(其本身可能是正面新闻)。那么如何解决这个问题呢?2.3节讲过,段落内容通常是被
<p>
和</p>
包围,而大部分网页的正文内容也被<p>
和</p>
包围,如下图所示。
- 再看滚动新闻在网页源代码中的结构,发现它不是被
<p>
和</p>
包围,而是被<li>
和</li>
包围,如下图所示。
- 发现了这个规律后,便可以编写正则表达式提取所有被
<p>
和</p>
包围的内容,再把获得的列表用’连接符’.join(列表名)的方式转换成字符串,得到新的article,核心代码如下:
p_article = '<p>(.*?)</p>'
article_main = re.findall(p_article,article) #提取<p>和</p>之间的正文内容
article = ' '.join(article_main) #将列表换成字符串,用空格连接
- 完整代码如下:
import re
import time
import requests
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36'}
def baidu(company):
# 1.获取网页源代码
url = 'https://www.baidu.com/s?tn=news&rtt=4&bsst=1&cl=2&wd=' + company # 其中设置rtt=4则为按时间排序,如果rtt=1则为按焦点排序
res = requests.get(url, headers=headers, timeout=10).text
# 2.编写正则提炼内容
#p_href = '<h3 class="news-title_1YtI1"><a href="(.*?)"'
p_href = '<h3 class="news-title_1YtI1 "><a href="(.*?)" '
href = re.findall(p_href, res) # 不存在换行,无须添加re.S
p_title = ' class="news-title-font_1xS-F" aria-label=.*? data-click=.*?<!--s-text-->(.*?)<!--/s-text--></a>'
title = re.findall(p_title, res, re.S)
p_date = '<span class="c-color-gray2 c-font-normal c-gap-right-xsmall" aria-label=.*?>(.*?)</span>'
date = re.findall(p_date, res)
p_source = '<span class="c-color-gray" aria-label=".*?">(.*?)</span>'
source = re.findall(p_source, res)
# 3.数据清洗
for i in range(len(title)):
title[i] = title[i].strip() # strip()函数用来取消字符串两端的换行或者空格,不过目前(2020-10)并没有换行或空格,所以其实不写这一行也没事
title[i] = re.sub('<.*?>', '', title[i]) # 核心,用re.sub()函数来替换不重要的内容
# 统一日期格式
date[i] = date[i].split(' ')[0] # 提取2020年11月15日 10:26:22前面的年月日信息
date[i] = re.sub('年', '-', date[i]) # 换成2020-11-15格式,注意只需要换年和月即可,日换成空值即可
date[i] = re.sub('月', '-', date[i])
date[i] = re.sub('日', '', date[i])
if ('小时' in date[i]) or ('分钟' in date[i]):
date[i] = time.strftime("%Y-%m-%d")
else:
date[i] = date[i]
# 4.正文爬取及数据深度清洗
for i in range(len(title)):
# 获取新闻正文
try:
article = requests.get(href[i], headers=headers, timeout=10).text
except:
article = '单个新闻爬取失败'
# 正文信息再优化
p_article = '<p>(.*?)</p>'
article_main = re.findall(p_article,article) #提取<p>和</p>之间的正文内容
article = ' '.join(article_main) #将列表换成字符串,用空格连接
# 数据深度清洗
company_re = company[0] + '.{0,5}' + company[-1] # 匹配“公司名称第一个字 + 0到5个任意字符 + 公司最后一个字”这样的匹配规则
if (len(re.findall(company_re, title[i])) < 1) and (len(re.findall(company_re, article)) < 1):
title[i] = ''
source[i] = ''
href[i] = ''
date[i] = ''
while '' in title:
title.remove('')
while '' in href:
href.remove('')
while '' in date:
date.remove('')
while '' in source:
source.remove('')
# 5.打印清洗后的数据
for i in range(len(title)):
print(str(i + 1) + '.' + title[i] + '(' + date[i] + ' ' + source[i] + ')')
print(href[i])
# 6.批量爬取多家公司
companys = ['华能信托', '阿里巴巴', '百度集团']
for i in companys:
try:
baidu(i)
print(i + '数据爬取成功')
time.sleep(3)
except:
print(i + '数据爬取失败')
- 有时
<p>
标签中可能还有class属性,如<p class='main_content'>
,此时可以对上面的内容进行优化,用“.*?”代替可能变化的内容,代码如下:
p_article = '<p.*?>(.*?)</p>'
5.1.3 数据乱码问题处理
1.网页源代码乱码的原因
- 网页源代码乱码的原因主要是Python获取的网页源代码的编码格式和网页实际的编码格式不一致。网页中常用的编码格式有UTF-8、GBK、ISO-8859-1。
- 用开发者工具查看网页源代码,然后展开
<head>
标签,查看<meta>
标签里的charset属性,如下图所示,该属性的值就是网页的编码格式,这里为UTF-8。
- 用Requests库的encoding属性可以查看Python获取的网页源代码的编辑格式,代码如下:
code = requests.get(url,headers=headers).encoding
import requests
url = 'https://www.51job.com/'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36'}
code = requests.get(url,headers=headers).encoding
print(code)
- 将code打印输出,结果为ISO-8859-1,与网页实际的编码格式UTF-8不一致,这就是Python获取的内容乱码的原因。
2.网页源代码乱码的解决方法
- 在3.2.4节已经讲解了解决数据乱码的两种方法,这里总结一下,为之后讲解通用的解决方法做铺垫。
方法1:对获取的网页源代码文本进行处理
- 第1中方法是对获取的网页源代码文本进行重新编码及解码,代码如下:
res = requests.get(url).text
res = res.encode('ISO-8859-1').decode('utf-8')
方法2:对获取的网页响应进行编码处理,再提取文本
- 第2中方法是先对获取的网页响应进行编码处理,再通过text属性提取网页源代码的文本,代码如下:
res = requests.get(url,headers=headers)
res.encoding = 'utf-8'
res = res.text
- 之后讲解的通用解决方法主要是基于方法1的原理,即对获取的网页源代码文本进行重新编码及解码。
3.网页源代码乱码的通用解决方法
- 如果对于编码的知识还是不太理解,或者不愿意花精力去查看Python获取内容的编码格式及网页实际的编码格式,可以采用下面3种经验方法。通过逐个尝试着3种方法,或者直接将这3中方法整合,可以解决绝大部分的乱码问题。
- 方法1:对于大部分网页,可以先尝试如下所示的常规代码(为了演示编码知识,先不加headers和timeout参数)。
res = requests.get(url).text
- 方法2:如果方法1爬取到的内容有乱码,则尝试使用如下代码。
res = requests.get(url).text
res = res.encode('ISO-8859-1').decode('gbk')
- 方法3:如果方法2爬取到的内容还是乱码或者报错,继续尝试使用如下代码。
res = requests.get(url).text
res = res.encode('ISO-8859-1').decode('utf-8')
- 根据众多项目实战经验,导致乱码的大部分原因都是由于Python获取的内容是ISO-8859-1编码格式,而网页实际的编码格式时GBK或UTF-8。因此,通过逐个尝试上述3中方法,就可以尝试解决绝大多数的乱码问题。
- 如果不想逐个尝试,可以通过try/except语句把3种方法整合到一起,代码如下:
res = requests.get(url).text
try:
res = res.encode('ISO-8859-1').decode('utf-8') #方法3
except:
try:
res = res.encode('ISO-8859-1').decode('gbk') #方法2
except:
res = res #方法1
- 数据乱码通常出现在用Requests库获取的网页源代码中,用Selenium库获取的网页源代码不会出现乱码。不过因为Requests库有Selenium库无法取代的优势——爬取速度快,所以了解乱码的解决方法还是有意义的。
5.1.4 数据爬后处理值舆情评分
- 下面通过简单的案例来演示舆情评分的核心知识点,代码如下:
keywords = ['违约','诉讼','兑付','大跌','弄虚作假']
title = 'A公司大跌 且弄虚作假'
score = 100
for k in keywords:
if k in title:
score = score - 5
- 第一行代码定义负面词清单;第2行代码定义示例标题title;第3行代码为评分score赋初始值100;第4~6行代码为核心代码,通过for循环语句遍历负面词清单中的每一个负面词,如果该词出现在标题中,则扣5分。
# 舆情评分版本1
score = []
keywords = ['违约', '诉讼', '兑付', '阿里', '百度', '京东', '互联网'] # 这个关键词列表可以自己定义,这里只是为了演示
for i in range(len(title)):
num = 0
for k in keywords:
if k in title[i]:
num -= 5 # 也可以写成 num = num -5
score.append(num)
for i in range(len(title)):
title[i] = title[i].strip() # strip()函数用来取消字符串两端的换行或者空格,不过目前(2020-10)并没有换行或空格,所以其实不写这一行也没事
title[i] = re.sub('<.*?>', '', title[i]) # 核心,用re.sub()函数来替换不重要的内容
print(str(i + 1) + '.' + title[i])
print(company + '该条新闻舆情评分为' + str(score[i])) # 这边注意,不要写score[i],因为它是数字,需要通过str()函数进行字符串拼接
print(href[i])
companys = ['阿里巴巴', '万科集团', '百度']
for i in companys:
try:
baidu(i)
print(i + '该公司百度新闻爬取成功')
print('——————————————————————————————') # 这个是当分隔符使用
except:
print(i + '该公司百度新闻爬取失败')
# 舆情评分版本2
score = []
keywords = ['违约', '诉讼', '兑付', '阿里', '百度', '京东', '互联网'] # 这个关键词列表可以自己定义,这里只是为了演示
for i in range(len(title)):
num = 0
# 版本2:获取新闻正文
try:
article = requests.get(href[i], headers=headers, timeout=10).text
except:
article = '爬取失败'
for k in keywords:
if (k in article) or (k in title[i]):
num -= 5 # 也可以写成 num = num -5
score.append(num)
# 舆情评分版本3
score = []
keywords = ['违约', '诉讼', '兑付', '阿里', '百度', '京东', '互联网'] # 这个关键词列表可以自己定义,这里只是为了演示
for i in range(len(title)):
num = 0
# 版本2:获取新闻正文
try:
article = requests.get(href[i], headers=headers, timeout=10).text
except:
article = '爬取失败'
# 版本3:解决可能存在的乱码问题
try:
article = article.encode('ISO-8859-1').decode('utf-8') # 方法3
except:
try:
article = article.encode('ISO-8859-1').decode('gbk') # 方法2
except:
article = article # 方法1
for k in keywords:
if (k in article) or (k in title[i]):
num -= 5 # 也可以写成 num = num -5
score.append(num)
# 舆情评分版本4
score = []
keywords = ['违约', '诉讼', '兑付', '阿里', '百度', '京东', '互联网'] # 这个关键词列表可以自己定义,这里只是为了演示
for i in range(len(title)):
num = 0
# 获取新闻正文
try:
article = requests.get(href[i], headers=headers, timeout=10).text
except:
article = '爬取失败'
# 解决可能存在的乱码问题
try:
article = article.encode('ISO-8859-1').decode('utf-8') # 方法3
except:
try:
article = article.encode('ISO-8859-1').decode('gbk') # 方法2
except:
article = article # 方法1
# 只筛选真正的正文内容,旁边的滚动新闻之类的内容忽略
p_article = '<p.*?>(.*?)</p>' # 有的时候p标签里可能还有class属性,例如<p class='main_content'>,通过.*?代替<p>标签中可能变化的内容
article_main = re.findall(p_article, article) # 获取<p>标签里的正文信息
article = ''.join(article_main) # 将列表转换成为字符串
for k in keywords:
if (k in article) or (k in title[i]):
num -= 5
score.append(num)
# 舆情评分版本4
score = []
import pandas as pd
keywords = pd.read_excel('负面词清单.xlsx')['负面词'] #得先建立一个excel表
for i in range(len(title)):
num = 0
# 获取新闻正文
try:
article = requests.get(href[i], headers=headers, timeout=10).text
except:
article = '爬取失败'
# 解决可能存在的乱码问题
try:
article = article.encode('ISO-8859-1').decode('utf-8') # 方法3
except:
try:
article = article.encode('ISO-8859-1').decode('gbk') # 方法2
except:
article = article # 方法1
# 只筛选真正的正文内容,旁边的滚动新闻之类的内容忽略
p_article = '<p.*?>(.*?)</p>' # 有的时候p标签里可能还有class属性,例如<p class='main_content'>,通过.*?代替<p>标签中可能变化的内容
article_main = re.findall(p_article, article) # 获取<p>标签里的正文信息
article = ''.join(article_main) # 将列表转换成为字符串
for k in keywords:
if (k in article) or (k in title[i]):
num -= 5
score.append(num)