gitee内容下载
文章主要运用了Request和Selenium两个爬虫库;LDA模型库gensim;主方法都封装到了对应的run方法中,模型可以是训练,也可以用我提供的已经训练好的一个简易模型。
介绍
1.文章背景介绍:
本文是我自己毕业设计的开头部分内容的一个修改版,整个毕业设计的目的是设计一个基于财经新闻的实时热门话题生成系统,类似于微博热搜这样一个可以提供实时热度的系统;论文行文结构为爬虫阶段 --> 数据清洗阶段 --> 模型训练和聚类阶段 --> 数据传输 --> 实时流式数据处理和分析 --> 网页设计展示,这篇文章就是爬虫阶段 --> 数据清洗阶段 --> 模型训练和聚类阶段等前三个阶段的一个简单展示版;
这篇文章的主要目的是构建一个窗口时限爬虫,即实现一个模拟固定窗口时间的爬虫机制,从而获得一段时间内的实时新闻数据(滚动窗口,后续设计中的滑窗设计和流式处理代码不再由Python完成,所以这里不做过多描述)
2.文章结构分析:
本文主要分为爬虫阶段 --> 数据清洗阶段 --> 模型训练和聚类阶段,在后续的内容中,会针对每个阶段做单独的讲解分析:所谓窗口时限爬虫机制,就是指让爬虫持续不断的运行,并在经过一个大致相等的事件后,对输出做一次整体输出,这里是对新闻数据做滚动窗口输出,默认时限是1小时;由于本文只爬取了东方财富和科创版日报两个网站的内容,所以新闻内容较少,所以模型训练由提前收集到的数据完成,这里只对后续的新闻做一个主题归纳;
3.爬虫阶段:
这里是对东方财富和科创版日报两个网站做新闻内容爬取,后续可以添加更多的网站,并可以做多线程优化;以下对两个网站做一个分析,来帮助判断如何编写爬虫程序:
3.1东方财富网页分析:
地址为:https://kuaixun.eastmoney.com/,大致内容展示如下所示:
由于这个页面新闻的url变化无规律可行,且可以自动更新,所以我采用selenium的方式爬取内容,通过网页观察,这些新闻最短也是1-2分钟刷新一次,所以我将爬虫程序的休眠时间设置在60秒,也即每60秒,整个爬虫程序会运行一次(两个网页),算上整体的运行时间(大致为30秒,因为科创版日报有访问限制,所以也做了休眠处理),大致为90秒一次;爬到内容后,对数据进行一个预处理,获取到title、time和content三部分内容,其中正文部分是用来做后续篇章主题分析的,本文不做讲述,time是后续输入到Flink框架中作为EventTime事件时间戳使用的,这里也不做讲述,title是本文用来做LDA主题模型的训练和后续聚类分析的,是本文的重点分析对象;
3.2科创版日报网页分析:
地址为:https://www.chinastarmarket.cn/telegraph,大致内容展示如下所示:
科创版日报的这一网页采用的是滚动刷新的方式进行,观察url,发现url最后6位数字虽然不是以1为步长进行更新,但是每两条新闻之间的步长基本不会超过5,所以想到采用requests和beautifulSoup模块来编写爬虫程序;为保险起见,我将每次运行的步长扩展到20,分20次运行,即每次增加1,由于网站新闻更新的速度较慢,所以基本不会出现短时间内出现20以上的步长增长(每次爬虫运行周期90秒,也即90秒内爬取20次网站内容来搜索新闻),当搜索到新闻后,更新url的后6位数字,保存好后做后续的爬取,后续爬取过程与之前完全一致;
3.3爬虫整体设计:
设置窗口次数为40,即爬虫运行40次后,会对数据做一次整体输出,也即1个小时对两个网站的数据做一个合并输出,这一个小时也就是我们的滚动窗口时限,这里由于没有后续的实时处理部分,所以这里不对数据做IO,只是将数据进行保存,然后重新读取处理分析,展示分析结果;
大致效果如下所示:(这里是几个小时生成的数据放在了一起)
最后要声明一点,这不是最终代码的样式,只是一个雏形,更多的是完成这个框架,后续还会补充更多的数据获取途径,并且会转化为多线程运行,提高实时性;友情提示,还可以吧数据保存和爬虫、模型部分全部封装在一起,作为一个数据接口来使用;
3.4.爬虫阶段代码:
from selenium import webdriver
import requests
import time
import datetime
import re
import pandas as pd
from bs4 import BeautifulSoup
import warnings
warnings.filterwarnings('ignore')
# 爬虫
class Scrapy:
"""
:parameter
flagDfcf : 是否开启东方财富爬虫
flagKcbrb : 是否开启科创版日报爬虫
windowCount : 默认一个窗口为30s, 经历windowCount次后输出数据;
biginningIndex :科创版日报爬取网站的起始位置,可通过查看url自由选取;
:arg
outputCount : 输出文件的后缀名
flag : 开启webDriver的开关,默认True
errorCount : 爬虫出错的次数
hashContent : 文章标题的哈希值集合
data : 每次输出的内容
"""
# 初始化
def __init__(self, flagDfcf=False, flagKcbrb=False, windowCount=4, beginningIndex=766707):
self.flagDfcf = flagDfcf
self.flagKcbrb = flagKcbrb
self.windowCount = windowCount
self.windowCount_ = windowCount
self.beginningIndex = beginningIndex
self.outputCount = 0
self.flag = True
self.errorCount = 0
self.hashDfcfContent = []
self.hashKcbrbContent = []
self.data = pd.DataFrame({"title": [""], "time": [""], "content": [""]})
# 爬虫执行程序
def run(self):
self.clear()
while (True):
self.windowCount_ -= 1
self.runDfcf()
self.runKcbrb()
# 每隔60秒启动一次
time.sleep(60)
# 进行数据输出
if self.windowCount_ <= 0:
self.data.to_csv("scrapy{}.csv".format(self.outputCount), index=False)
self.outputCount += 1
self.windowCount_ = self.windowCount
self.data = pd.DataFrame({"title": [""], "time": [""], "content": [""]})
print("----------------------------")
print("DATA数据为" + self.data)
# 清除缓存中的值
def clear(self):
self.data = pd.DataFrame({"title": [""], "time": [""], "content": [""]})
self.hashKcbrbContent = []
self.hashDfcfContent = []
self.errorCount = 0
# 东方财富爬虫执行
def runDfcf(self):
flag1 = self.flagDfcf
while (flag1):
self.scrapyDfcf()
flag1 = False
# 科创版日报爬虫执行
def runKcbrb(self):
flag2 = self.flagKcbrb
while (flag2):
self.scrapyKcbrb()
flag2 = False
# 东方财富爬虫主逻辑
def scrapyDfcf(self):
# 声明全局变量
global DRIVER
while (self.flag):
DRIVER = webdriver.Chrome('D:\pythonProject\chromedriver.exe')
html = DRIVER.get('https://kuaixun.eastmoney.com/')
# 只运行一次web,因为这个网站会自动更新
self.flag = False
try:
# 东方财富新闻爬虫主逻辑
element = DRIVER.find_element_by_class_name("media-content")
# 得到最新的爬取内容
text = element.text
# 获取标题
pt1 = re.compile(r'【.*】')
res1 = re.findall(pt1, str(text))
kcbrbTitle = str(res1).replace("【", "").replace("】", "").replace("['", "").replace("']", "")
# 获取时间
element = DRIVER.find_element_by_class_name("time")
kcbrbTime = element.text
nowTime = datetime.datetime.now()
nowTimeMonth = nowTime.month
# 做时间规范化处理
if ( len(str(nowTime.month)) == 1 ):
nowTimeMonth = "0" + str(nowTime.month)
kcbrbTime = str(nowTime.year) + "-" + nowTimeMonth + "-" + str(nowTime.day) + " " + kcbrbTime
# 获取正文
pt2 = re.compile(r'】.*(')
res2 = re.findall(pt2, str(text))
kcbrbcontent = str(res2).replace("'】", "").replace("。('", "").replace("[", "").replace("]", "")
# 数据处理
data1 = pd.DataFrame(kcbrbTitle, columns=["title"], index=range(1))
data2 = pd.DataFrame(kcbrbTime, columns=["time"], index=range(1))
data3 = pd.DataFrame(kcbrbcontent, columns=["content"], index=range(1))
data4 = pd.concat([data1, data2, data3], axis=1)
# 判断文章是否已经获取过
# 对每个文章标题做哈希计算,将值保存起来
hashCode = hash(kcbrbTitle)
# 保存好第一条数据
if len(self.hashDfcfContent) == 0:
self.hashDfcfContent.append(hashCode)
self.data = data4
# 针对后续数据做判断
else:
# 与前一条获取到的数据的哈希值做判断,若不同,说明这是一条新数据,加入集合中;
if (hashCode != self.hashDfcfContent[-1]):
self.hashDfcfContent.append(hashCode)
self.data = pd.concat([self.data, data4])
self.data.index = range(len(self.data))
# 爬虫出错后逻辑
except:
self.errorCount += 1
print("失败{}次".format(self.errorCount))
# 科创版日报爬虫主逻辑
def scrapyKcbrb(self):
# 科创板日版爬虫主逻辑
# 计算url后缀增长次数
count = 0
# 统计每次成功爬取网页数据时所增长的次数
step = 0
# 表示成功抽取到网页数据
flag = False
# 每一轮测试20条数据,如果
for i in range(1, 20):
count += 1
# 爬取文章主体内容
r = requests.get("https://www.chinastarmarket.cn/detail/{}".format(self.beginningIndex + count)).content.decode("utf-8")
bs1 = BeautifulSoup(r, "lxml")
try:
# 爬取正文
pt1 = re.compile(r'detail-telegraph-content content">(.*?)</div>')
res1 = re.findall(pt1, str(bs1))
content = str(res1).replace("【", "").replace("】", "").replace("[", "").replace("]", "").replace("'", "")
# 爬取标题
pt2 = re.compile(r'<title class="next-head">(.*?)</title>')
res2 = re.findall(pt2, str(bs1))
title = str(res2).replace("[", "").replace("]", "").replace("'", "")
# 爬取时间
pt3 = re.compile(r'c-222">(.*?)</span></div>')
res3 = re.findall(pt3, str(bs1))
time = str(res3).replace("[", "").replace("]", "").replace("'", "").replace("年","-").replace("月","-")
time = time[:-3]
if res1 != []:
# 抽取到了数据
flag = True
# 更新爬取到数据时的后缀值增长次数
step = count
# 数据处理
hashCode = hash(str(content))
Data1 = pd.DataFrame(title, columns=["title"], index=range(1))
Data2 = pd.DataFrame(time, columns=["time"], index=range(1))
Data3 = pd.DataFrame(content, columns=["content"], index=range(1))
Data4 = pd.concat([Data1, Data2, Data3], axis=1)
if len(self.hashKcbrbContent) == 0:
self.hashKcbrbContent.append(hashCode)
self.data = pd.concat([self.data, Data4])
self.data.index = range(len(self.data))
else:
# 与前一条获取到的数据的哈希值做判断,若不同,说明这是一条新数据,加入集合中;
if (hashCode != self.hashKcbrbContent[-1]):
self.hashKcbrbContent.append(hashCode)
self.data = pd.concat([self.data, Data4])
self.data.index = range(len(self.data))
except:
self.errorCount += 1
print("失败{}次".format(self.errorCount))
#爬取到了内容,把最后一条爬取到的内容的url的后6位数作为新起点;
if flag:
self.beginningIndex += step
4.数据清洗阶段:
import jieba
import numpy as np
import warnings
from collections import Counter
warnings.filterwarnings('ignore')
class dataClean:
'''
为了方便后续修改,这里对数据清洗的四个步骤都进行了函数编写;
:parameter
data : 要处理的数据
highFrequencyFlag : 是否开启高频词获取,False表示进行清洗,True表示训练;
'''
# 初始化
def __init__(self, data, highFrequencyFlag=False):
self.data = data
self.highFrequencyFlag = highFrequencyFlag
# 运行数据清理
def run(self):
if self.highFrequencyFlag == True:
# 高频词获取
self.highFrequencyTrain()
return
else:
# 数据清洗
wordListClean = self.stopWordsClean()
return wordListClean
#分词
def wordsSplit(self):
table = []
# 如果启用了高频词获取,这里用的应该是content列数据,即文本主体数据;如果未启用,则用标题数据做分析即可,即获取标题数据即可;
if self.highFrequencyFlag == True:
table = self.data
else:
table = self.data["title"]
wordList = []
for sentence in table:
wordList.append(jieba.lcut(sentence, cut_all=False, HMM=True))
return wordList
#高频词训练
def highFrequencyTrain(self):
totalCount = 0
totalWords = []
threshold = 0.8
t = 1e-5
for i in range(len(self.data)):
totalCount = totalCount + len(self.data[i])
for j in self.data[i]:
totalWords.append(j)
wordsCounter = Counter(totalWords)
wordFreqs = {w: c / totalCount for w, c in wordsCounter.items()}
# 计算被删除的概率
probDrop = {w: 1 - np.sqrt(t / f) for w, f in wordFreqs.items()}
# 对单词进行采样
trainWords = [w for w in totalWords if probDrop[w] < threshold]
train_words = np.array(trainWords)
np.save(r'train_words.npy', train_words)
print("完成高频词提取")
# 剔除高频词
def highFrequencyClean(self):
wordsAfterClean = []
wordList = self.wordsSplit()
# 加载训练好的高频词集合
train_words = np.load(r"train_words.npy")
# 取唯一值
trainWords = set(train_words)
#遍历数据集,剔除高频词;
for wordList_ in wordList:
listTemp = []
for word in wordList_:
if word not in trainWords:
listTemp.append(word)
wordsAfterClean.append(listTemp)
return wordsAfterClean
#去除停用词
def stopWordsClean(self):
wordListClean = []
# 加载停用词表
with open("D:\JupyterProject\编程作业2\中文停用词表.txt", "r") as f: # 打开文件
stopWords = f.read() # 读取文件
wordsClean = self.highFrequencyClean()
#停用词处理
for wordList in wordsClean:
listTemp = []
for word in wordList:
if (word not in stopWords) & (word != ' ') & (word.isdigit() == False):
listTemp.append(word)
wordListClean.append(listTemp)
return wordListClean
5.模型训练和聚类阶段:
import gensim
from gensim import corpora, models
import numpy as np
class ladAnalysis:
'''
:parameter
input : 输入数据
modelTrainingFlag : 是否开启LDA模型训练
'''
def __init__(self, input, modelTrainingFlag = False):
self.modelTrainingFlag = modelTrainingFlag
self.input = input
self.dictionaryWords = np.load(r"D:\pythonProject\endOfTerm\word_list.npy", allow_pickle=True)
def run(self):
if self.modelTrainingFlag:
self.modelTraining()
else:
self.modelAnalyze()
#模型训练
def modelTraining(self):
#词典
dictionary = corpora.Dictionary(self.input)
#保存词典
#dictionary.save('dictionary.dictionary')
#生成对应词袋向量
corpus = [dictionary.doc2bow(doc) for doc in self.input]
#保存词袋向量
corpora.MmCorpus.serialize('corpus.mm', corpus)
#训练模型
lda = gensim.models.ldamodel.LdaModel(corpus, num_topics=20, id2word=dictionary,passes=100, random_state=1) # 调用LDA模型,20个主题;训练语料库50次
#保存模型
lda.save(r"C:\Users\chenxu\Desktop\组会\LDA主题模型\LDA.model")
#模型分析
def modelAnalyze(self):
#加载已经训练好的lda模型
lda = gensim.models.ldamodel.LdaModel.load(r"LDA.model")
#词典
dictionary = corpora.Dictionary(self.input)
#词袋向量
corpus = [dictionary.doc2bow(doc) for doc in self.input]
#对文本进行主题聚类
lda.get_document_topics(corpus, minimum_probability=1e-8)
# 输出对应文档的最大可能性的主题归类
maxProbability = []
maxTopic = []
for zip in [*lda.get_document_topics(corpus, minimum_probability=1e-8)]:
index = []
probability = []
for k in zip:
index.append(k[0])
probability.append(k[1])
maxProbability.append(max(probability))
maxTopic.append(probability.index(max(probability)))
print(maxProbability)
print(maxTopic)
for i in maxTopic:
print("LDA模型主题分布:", lda.print_topics(num_topics=30, num_words=5)[i])
6.结果展示:
#主逻辑代码:
#爬虫:
if __name__ == "__main__":
Scrapy(flagDfcf=True, flagKcbrb=True, beginningIndex=766697).run()
#数据清洗和模型部分:
import pandas as pd
from endOfTerm.dataClean import dataClean
from endOfTerm.ldaAnalysis import ladAnalysis
if __name__ == "__main__":
data = pd.read_csv(r"scrapy0.csv")
print(data)
words = dataClean(data=data).run()
ladAnalysis(words).run()
上面部分是一个时段内的爬取内容进行聚类后的结果,分别对应输出文本数据、最大概率聚类值、最大概率聚类类型划分、所聚类型关键词展示;
由于数据集的部分问题,这里的展示效果可能不是很好,但第5和第6个新闻,模型仍成功判断这两个新闻属于利好一类新闻,被归在了6类主题中,其中该类主题关键词中有上涨、拉升等词语;第二个新闻,模型将其归纳到了药物一类中,说明这一个时段内,这几个话题的热度最高;随着后续新闻爬取网站和内容的增加,以及模型的优化,后续效果应该会更好,这里只做一个初步效果展示;