【项目总结】近期爬虫详解(MBA智库百科词条爬虫&同花顺财经数据爬虫)

确实鸽了好一阵子了。上学期初心血来潮想要写写博客,写完第一篇博客我才发现写一篇充实的博客是多么费时费力,但是我还是坚持每周一篇博客地写了两个多月,毕竟期初事情也不是很多。直到期中过后我才发现每周余出一篇博客的时间是多么奢侈——我能坚持每天写千字日记,也做不到每周出一篇有质量的博客。实然有些心灰意冷,也许以后工作了再也没有年轻时的热情了。世事难有始有终,世俗聒噪,初心难追。

最近这几个月确实是项目缠身,无力旁顾他事。前期做了一些算法部分的工作,后来因为代码需要python 2.x的环境,我又不愿意卸载python 3.6去安装Anaconda(都怪早些时候没有随大流,坚持用了这么久python 3.6,太多的难装的库、难配的环境过来了,再从头开始实在是于心不忍),所以我后来主要负责数据爬虫以及与负责前端的朋友合作一起搭后端。

事情做的比较杂我也不便于一一赘述,我主要挑比较精华的部分写这篇博客,就MBA智库百科词条爬虫与同花顺财经数据爬虫两部分分别编写。

(一)MBA智库百科词条爬虫

MBA智库,一个专注于经济管理领域的知识平台。对于财经专业的学生应该是一部挺有用的百科全书了?

首先明确爬虫的目标,我希望能够将MBA智库中的四十多万词条包括其具体百科内容全部存储到本地,数据的宗旨是取精去糟,层次清晰,不要拿到无关信息(就像上面这个页面,像广告与推荐各种无关信息)。

自顶而下的看这个爬虫,主要分为两个主要部分——宏观上如何确保拿到全部词条条目数据?微观上如何精确获取词条百科中的全部信息?下面我就这两个问题分别阐释:

1.1 宏观问题——如何确保拿到全部词条条目数据

一个非常直观的思路是使用病毒式BFS搜索的方式来获取这种百科全书式的数据。说句老实话,这个方法我至今没有成功在爬虫中运用过。主要有两个原因:

  1. 一个问题是BFS会造成极大的重复搜索,体现在爬虫中就是需要使用更多的访问次数,这将大大增加你为了避免被目标服务器封锁而产生的时间代价,而如果为了减少访问次数而选择比对已获取的信息,则会随着爬虫的推进查重成本越来越沉重;
  2. 另一个问题是网络框架的连通性未知,BFS无法确保遍历整个网络,一些边缘上的“幽灵”结点往往无法获取,而很多时候这种“幽灵”结点的数量是庞大的,即使他们的价值很低;

所以做事之前先观察,磨刀不误砍柴功。事实上MBA智库提供了一个索引目录?

显然所有的词条不可能只有索引页面上的这些数量。点击“企业管理”类别进去可以发现,每个类别索引分类下包含两种信息,第一种是该类别下的亚类,另一个是隶属该类的词条(从索引树状结构来说每个结点下有非叶结点与叶子结点)。经过我的观察发现,每一块索引词条(如上页面中的企业管理~责任中心,财务管理~职业理财师)被包含在一个<p>...</p>标签内,事实上每对<p>标签下的第一个子标签中的分类词条将包含后面所有词条(如企业管理与财务管理分别包含了它们后面的所有词条),例外有两个:

  1. 如果出现<p>标签的第一个子“其他”或者“其它”,则跳过这个<p>标签;

  2. 此时下一个<p>标签里所有的词条包含的集合都是互斥的(即其他条目中的所有词条包含的亚类互相交集为空);

写到这里请容许我吐槽一下MBA智库这糟糕的网站设计。事实上索引页面上所有的<p>标签都是平级的,用Beautifulsoup库的<tag>.next_sibling()方法可以一路next到底,也就是说最大的六大类——管理、经济、金融、法律、行业、综合六大百科之间都没有一个<div>标签把它们隔开,唯一能够分开这六大类的方法就是如果在next的过程中找到了<h2>标签,也许是新的一大类开始了罢。而这种性质延续到了词条百科的全部信息之中。其他一些糟糕的地方就是像“其他”和“其它”居然都存在于索引页面上,实在是设计前端的人太不走心了。

	def parse_categories(self,categories):								 # 以参数列表中的条目为根目录, 穿透根目录就完事了
		with open("{}/{}".format(self.date,self.categoryWait),"a") as f: # 将传入的所有类别记录到categoryWait.txt中
			for category in categories: f.write("{}\n".format(category)) 
		for category in categories:
			with open("{}/{}".format(self.date,self.categoryDone),"a") as f: f.write("{}\n".format(category)) 			
			subCategories = list()										 # 存储该category下的所有亚类
			entries = list()											 # 存储该category下的所有条目
			html = self.session.get(self.categoryURL.format(category)).text
			count = 0
			while True:
				flag = True												 # flag用于判断是否还存在下一页
				count += 1
				soup = BeautifulSoup(html,"lxml")
				divs1 = soup.find_all("div",class_="CategoryTreeSection")# 子类超链接
				divs2 = soup.find_all("div",class_="page_ul")			 # 条目超链接
				if divs1:
					for div in divs1:
						aLabel = div.find_all("a")
						for a in aLabel:
							if str(a.string)=="+": continue				 # "+"号是一个特殊的书签超链接
							subCategories.append(str(a.string))
				if divs2:
					for div in divs2:
						aLabel = div.find_all("a")
						for a in aLabel:
							if a.attrs["title"][:6]=="Image:": entries.append("Image:{}".format(a.string))
							else: entries.append(str(a.string))
				string = "正在处理{}类别第{}页的信息 - 共{}个子类{}个条目...".format(category,count,len(subCategories),len(entries))
				print(string)
				with open("{}/{}".format(self.date,self.log),"a") as f:
					f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))	
				buttons = soup.find_all("a",title="Category:{}".format(category))
				if buttons:												 # 可能并没有这个按钮
					for button in buttons:
						if str(button.string)[0]=="后":
							nextURL = self.wikiURL + button.attrs["href"][1:]
							html = self.session.get(nextURL).text		 # 进入下一页
							flag = False								 # flag高高挂起
							break										 # 退出循环
				if flag: break											 # flag为True无非两种情况: 没有button, 或者只要前200页的button
			string = "开始获取{}类别下所有条目信息...".format(category)
			print(string)
			with open("{}/{}".format(self.date,self.log),"a") as f:
				f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			self.parse_entries(entries,False)							 # 爬取当前类别的条目
			string = "开始获取{}类别下所有亚类信息...".format(category)
			print(string)
			with open("{}/{}".format(self.date,self.log),"a") as f:
				f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			self.parse_categories(subCategories)						 # 爬取当前类别的亚类

	def parse_index_page(self,):										 # 爬取MBA词条分类索引页面
		print("正在获取MBA索引页面信息...")
		html = self.session.get(self.indexURL).text
		soup = BeautifulSoup(html,"lxml")
		h2s = soup.find_all("h2")										 # 由于<div>标签过于混乱, 采用<h2>标签来定位不同分类
		categories = dict()
		for h2 in h2s:
			tempList = list()
			div = h2.find_parent().next_sibling.next_sibling			 # 经验之谈, 没有为什么
			ps = div.find_all("p")										 # <p>标签索引每行第一个类别
			for p in ps:
				a = p.find("a")											 # 只要第一个<a>标签即可因为后面的都可以在第一个类别的亚类或者次亚类中寻找到
				if a: tempList.append(str(a.string))					 # 存在<a>标签则取第一个
				else:													 # 不存在<a>标签的情况比较复杂, 总的来说是MBAlib网页设计者实在是太垃圾了
					label = p.find_next_sibling("p")					 # 后续平行结点还有<p>标签定位
					label = label if label else p.find_next_sibling("dl")# 否则定位<dl>标签
					aLabel = label.find_all("a")						 # "其他/它"类别下的所有类别, 互相平行所以全都要
					for a in aLabel: tempList.append(str(a.string))
					break
			categories[str(h2.string)] = tempList						 # 考虑到未必需要全部词条,因此这里把根条目按照六类分别存入字典
		print("索引页面信息获取完毕!\n共有{}个大类,{}个根条目".format(len(categories.keys()),sum(len(value) for value in categories.values())))
		return categories

上面的代码分别是获取索引页面全部根条目的函数与递归获取全部词条条目数据的代码?

因为是从我写的类里面摘取出来的,单独肯定无法运行,全部代码我也许会放在这部分结尾罢。这边利用一个递归去遍历这棵索引树即可,实测一般来说不会有重复的词条出现。

1.2 微观问题——如何精确获取词条百科中的全部信息

这个问题可能是比较麻烦的一件事情了,因为百科词条页面的结构并不唯一,我这边整理出来的方法经过测试可以应对大部分的词条,但因为我暂时没有对全部词条完成测试,所以我并不确信我的方法是完全可行的。

简单谈一下我的思路,观察发现有两种词条——百科词条与图片词条,图片词条在宏观问题部分的代码里面回去到的字符串将以"Image:"开头,因此可以用这个特征区分出这两类词条。对于图片词条简单获取第一个<img>标签中的href属性链接,用"wb"写入图片文件即可。对于百科词条的思路还是一"p"到底,但是事实上这里面有很多细节性的问题,如果朋友您愿意自己先尝试一下的话可能会有所感触。很多事情都是看起来很容易,一做就到处都是问题。

下面这段代码即是我获取百科词条数据的方法?

	def parse_entries(self,entries,
		driver=False,
		cycle=100,
		minInterval=60,
		maxInterval=90,
	):																	 # 给定词条列表获取它们所有的信息		
		for entry in entries:
			if driver:
				self.webdriver.get(self.entryURL.format(entry))
				html = self.webdriver.page_source
			else:
				html = self.session.get(self.entryURL.format(entry)).text
				self.entryCount += 1
				if self.entryCount%cycle==0:
					interval = random.uniform(minInterval,maxInterval)
					time.sleep(interval)
					print("获取到第{}个条目 - 暂停{}秒...".format(self.entryCount,interval))
			string = "正在获取条目{}...".format(entry)
			print(string)
			with open("{}/{}".format(self.date,self.log),"a") as f:
				f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			soup = BeautifulSoup(html,"lxml")
			if entry[:6]=="Image:":										 # 图片词条保存为图片
				img = soup.find("img")
				imageURL = self.wikiURL + img.attrs["src"][1:]
				byte = self.session.get(imageURL).text
				with open("{}/{}/{}".format(self.date,self.imageFolder,entry[6:]),"wb") as f: f.write(byte)
			else:														 # 非图片词条处理相对麻烦
				for script in soup.find_all("script"): script.extract()	 # 删除脚本
				p = soup.find("p")										 # 从第一个<p>标签的前一个标签开始搞
				label = p if isinstance(p.previous_sibling,element.NavigableString) else p.previous_sibling  
				text = str()											 # 最终text被写入文本
				while True:
					if isinstance(label,element.NavigableString):		 # 下一个平行节点有可能是字符串, 那就扔了
						label = label.next_sibling
						continue
					if not label: break									 # 没有下一个平行节点, 那就再见
					if "class" in label.attrs.keys() and \
					   label.attrs["class"][0]=="printfooter": break	 # 页脚处与空标签处暂停
					string = self.labelCompiler.sub("",str(label))
					text += "{}\n".format(string.strip())
					label = label.next_sibling
				_nCompiler = re.compile(r"\n+",re.S)
				text = _nCompiler.sub("\n",text)
				with open("{}/{}/{}.txt".format(self.date,self.entryFolder,entry),"a",encoding="UTF-8") as f:
					f.write(text)

最后贴上MBAlib类的代码?

#-*- coding:UTF-8 -*-
"""
	作者:囚生CY
	平台:CSDN
	时间:201/04/03
	转载请注明原作者
	创作不易,仅供分享
"""
import os
import re
import sys
import time
import numpy
import random
import pandas
import requests

from bs4 import BeautifulSoup,element
from selenium import webdriver
from multiprocessing import Process


class MBAlib():
	def __init__(self,
		userAgent="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
		phantomPath="E:/Python/phantomjs-2.1.1-windows/bin/phantomjs.exe",
		driver=True,	):
		""" 设置类构造参数 """
		self.userAgent = userAgent										 # 设置浏览器用户代理
		self.phantomPath = phantomPath
		""" 设置类常用参数 """
		self.workspace = os.getcwd()									 # 获取当前工作目录
		self.date = time.strftime("%Y%m%d")								 # 获取类初始化的时间
		self.labelCompiler = re.compile(r"<[^>]+>",re.S)				 # 标签正则编译
		self.entryCount = 0												 # 记录已经爬取的条目数量
		self.mainURL = "https://www.mbalib.com/"						 # MBA首页URL
		self.wikiURL = "https://wiki.mbalib.com/"						 # MBA百科URL
		self.searchURL = self.mainURL + "s?q={}"						 # MBA搜索URL
		self.wikisearchURL = self.wikiURL + "wiki/Special:Search?search={}&fulltext=搜索"
		self.entryURL = self.wikiURL + "wiki/{}"						 # MBA条目URL
		self.indexURL = self.wikiURL + "wiki/MBA智库百科:分类索引"			 # MBA词条分类索引页面URL
		self.categoryURL = self.wikiURL + "wiki/Category:{}"			 # MBA词条类别页面URL
		self.session = requests.Session()								 # 类专用session
		self.myHeaders = {"User-Agent":self.userAgent}					 # 爬虫头信息
		self.imageFolder = "image"										 # 存放图片词条的子文件夹
		self.entryFolder = "entry"										 # 存放词条文本的子文件夹
		self.logFolder = "log"											 # 存放程序运行的子文件夹
		self.log = "{}/log.txt".format(self.logFolder)					 # 记录文件
		self.categoryWait = "{}/categoriyWait.txt".format(self.logFolder)# 记录队列中所有的类别文件
		self.categoryDone = "{}/categoriyDone.txt".format(self.logFolder)# 记录已经获取完毕的类别文件
		if driver: self.webdriver = webdriver.Firefox()					 # 初始化火狐浏览器

		""" 初始化操作 """
		self.session.headers = self.myHeaders							 # 设置session
		self.session.get(self.mainURL)									 # 定位主页
		if not os.path.exists("{}\\{}".format(self.workspace,self.date)):# 每个交易日使用一个单独的文件夹(用日期命名)存储金融数据
			print("正在新建文件夹以存储{}MBA智库的数据...".format(self.date))
			os.mkdir("{}\\{}".format(self.workspace,self.date))
			os.mkdir("{}\\{}\\{}".format(self.workspace,self.date,self.imageFolder))
			os.mkdir("{}\\{}\\{}".format(self.workspace,self.date,self.entryFolder))
			os.mkdir("{}\\{}\\{}".format(self.workspace,self.date,self.logFolder))

	def parse_entries(self,entries,
		driver=False,
		cycle=100,
		minInterval=60,
		maxInterval=90,
	):																	 # 给定词条列表获取它们所有的信息		
		for entry in entries:
			if driver:
				self.webdriver.get(self.entryURL.format(entry))
				html = self.webdriver.page_source
			else:
				html = self.session.get(self.entryURL.format(entry)).text
				self.entryCount += 1
				if self.entryCount%cycle==0:
					interval = random.uniform(minInterval,maxInterval)
					time.sleep(interval)
					print("获取到第{}个条目 - 暂停{}秒...".format(self.entryCount,interval))
			string = "正在获取条目{}...".format(entry)
			print(string)
			with open("{}/{}".format(self.date,self.log),"a") as f:
				f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			soup = BeautifulSoup(html,"lxml")
			if entry[:6]=="Image:":										 # 图片词条保存为图片
				img = soup.find("img")
				imageURL = self.wikiURL + img.attrs["src"][1:]
				byte = self.session.get(imageURL).text
				with open("{}/{}/{}".format(self.date,self.imageFolder,entry[6:]),"wb") as f: f.write(byte)
			else:														 # 非图片词条处理相对麻烦
				for script in soup.find_all("script"): script.extract()	 # 删除脚本
				p = soup.find("p")										 # 从第一个<p>标签的前一个标签开始搞
				label = p if isinstance(p.previous_sibling,element.NavigableString) else p.previous_sibling  
				text = str()											 # 最终text被写入文本
				while True:
					if isinstance(label,element.NavigableString):		 # 下一个平行节点有可能是字符串, 那就扔了
						label = label.next_sibling
						continue
					if not label: break									 # 没有下一个平行节点, 那就再见
					if "class" in label.attrs.keys() and \
					   label.attrs["class"][0]=="printfooter": break	 # 页脚处与空标签处暂停
					string = self.labelCompiler.sub("",str(label))
					text += "{}\n".format(string.strip())
					label = label.next_sibling
				_nCompiler = re.compile(r"\n+",re.S)
				text = _nCompiler.sub("\n",text)
				with open("{}/{}/{}.txt".format(self.date,self.entryFolder,entry),"a",encoding="UTF-8") as f:
					f.write(text)
				
	def parse_categories(self,categories):								 # 以参数列表中的条目为根目录, 穿透根目录就完事了
		with open("{}/{}".format(self.date,self.categoryWait),"a") as f: # 将传入的所有类别记录到categoryWait.txt中
			for category in categories: f.write("{}\n".format(category)) 
		for category in categories:
			with open("{}/{}".format(self.date,self.categoryDone),"a") as f: f.write("{}\n".format(category)) 			
			subCategories = list()										 # 存储该category下的所有亚类
			entries = list()											 # 存储该category下的所有条目
			html = self.session.get(self.categoryURL.format(category)).text
			count = 0
			while True:
				flag = True												 # flag用于判断是否还存在下一页
				count += 1
				soup = BeautifulSoup(html,"lxml")
				divs1 = soup.find_all("div",class_="CategoryTreeSection")# 子类超链接
				divs2 = soup.find_all("div",class_="page_ul")			 # 条目超链接
				if divs1:
					for div in divs1:
						aLabel = div.find_all("a")
						for a in aLabel:
							if str(a.string)=="+": continue				 # "+"号是一个特殊的书签超链接
							subCategories.append(str(a.string))
				if divs2:
					for div in divs2:
						aLabel = div.find_all("a")
						for a in aLabel:
							if a.attrs["title"][:6]=="Image:": entries.append("Image:{}".format(a.string))
							else: entries.append(str(a.string))
				string = "正在处理{}类别第{}页的信息 - 共{}个子类{}个条目...".format(category,count,len(subCategories),len(entries))
				print(string)
				with open("{}/{}".format(self.date,self.log),"a") as f:
					f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))	
				buttons = soup.find_all("a",title="Category:{}".format(category))
				if buttons:												 # 可能并没有这个按钮
					for button in buttons:
						if str(button.string)[0]=="后":
							nextURL = self.wikiURL + button.attrs["href"][1:]
							html = self.session.get(nextURL).text		 # 进入下一页
							flag = False								 # flag高高挂起
							break										 # 退出循环
				if flag: break											 # flag为True无非两种情况: 没有button, 或者只要前200页的button
			string = "开始获取{}类别下所有条目信息...".format(category)
			print(string)
			with open("{}/{}".format(self.date,self.log),"a") as f:
				f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			self.parse_entries(entries,False)							 # 爬取当前类别的条目
			string = "开始获取{}类别下所有亚类信息...".format(category)
			print(string)
			with open("{}/{}".format(self.date,self.log),"a") as f:
				f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			self.parse_categories(subCategories)						 # 爬取当前类别的亚类

	def parse_index_page(self,):										 # 爬取MBA词条分类索引页面
		print("正在获取MBA索引页面信息...")
		html = self.session.get(self.indexURL).text
		soup = BeautifulSoup(html,"lxml")
		h2s = soup.find_all("h2")										 # 由于<div>标签过于混乱, 采用<h2>标签来定位不同分类
		categories = dict()
		for h2 in h2s:
			tempList = list()
			div = h2.find_parent().next_sibling.next_sibling			 # 经验之谈, 没有为什么
			ps = div.find_all("p")										 # <p>标签索引每行第一个类别
			for p in ps:
				a = p.find("a")											 # 只要第一个<a>标签即可因为后面的都可以在第一个类别的亚类或者次亚类中寻找到
				if a: tempList.append(str(a.string))					 # 存在<a>标签则取第一个
				else:													 # 不存在<a>标签的情况比较复杂, 总的来说是MBAlib网页设计者实在是太垃圾了
					label = p.find_next_sibling("p")					 # 后续平行结点还有<p>标签定位
					label = label if label else p.find_next_sibling("dl")# 否则定位<dl>标签
					aLabel = label.find_all("a")						 # "其他/它"类别下的所有类别, 互相平行所以全都要
					for a in aLabel: tempList.append(str(a.string))
					break
			categories[str(h2.string)] = tempList						 # 考虑到未必需要全部词条,因此这里把根条目按照六类分别存入字典
		print("索引页面信息获取完毕!\n共有{}个大类,{}个根条目".format(len(categories.keys()),sum(len(value) for value in categories.values())))
		return categories

1.3 小结

这部分的最后我说一下爬虫被封的问题:

  • 不设定间断,每秒大约2~3个条目,连续120个条目后被封;
  • 每50个条目间断uniform(15,45)秒,大约700个条目后被封;
  • 使用selenium的问题在于加载页面很慢,我起初以为是图片加载很耗时,但是在设置了selenium的config后似乎并没有改善多少,这个问题我暂时还没有解决;
  • 对于四十多万的词条来说即便平均以1个/秒的速度也要花上整整五天的时间,所以如何加快速度确实是个很大的问题;

这些问题可能还需要后续的思考与解决。

(二)同花顺财经数据爬虫

同花顺财经数据爬虫的起因是我寒假实习中的体悟,在私募投研实习的过程中我深刻的感知到财经数据的稀贵性。除了那些基础的财经数据外,还有很多特殊的数据无从获取。像WIND,CHOICE这类付费的数据库有时候也无法满足机构投资者的个性化数据需求。因此我认为数据积累是一件极其重要的事情。

其中一个途径就是从同花顺财经网上获取数据?

我目前编写了每日获取上面第一张图中“资金流向”与“技术选股”两个模块下全部数据,“资金流向”部分可以参考我的博客https://blog.csdn.net/CY19980216/article/details/86647597中的内容,因为其实还是有不少细节性的问题,我都在之前那篇博客里面一一点出了。“技术选股”模块基本上是类同的,我不再赘述。

然后我重点谈一下上面第二、三张图i问财搜索引擎的使用问题。i问财搜索引擎可以说是目前全网最智能的一个财经领域具有问答系统性质的搜索引擎了。一般来说搜索结果将会以一张或若干张表格的形式呈现。但是如果简单使用requests方法去访问你将会发现得到的html中一张表格都没有。我的解决方案仍然是使用selenium,就可以成功获取到第三张图上的所有表格。

这边我不再提供代码,原因有两个,一方面确实花了我一段时间写StraightFlush类,这个类写了千行以上的代码,我有点不舍得来分享;另一方面也是团队的隐私,我个人不能透露太多。但是我还是觉得爬虫尽量自己写,我认为一百个人可以写出一百个不同的爬虫,而且自己写的爬虫以后维护起来也方便得多。

(三)总结

忙里偷闲写了这篇博客,事情多少善始善终罢,愿看到这篇博客的你也能在前进的路上不忘初心,别让过去的自己看不起将来的你。

发布了40 篇原创文章 · 获赞 133 · 访问量 44万+

猜你喜欢

转载自blog.csdn.net/CY19980216/article/details/88617597
今日推荐