【项目完结】笑靥如春三冬暖,嫣语似晴沉霾散。西子湖畔梦犹然,情起缘尽余心安。

极其罕见的与人谈崩,有点难受,不知道以后怎么再去面对对方。

连着肝了两晚上,今晚基本赶完,写好说明文档,也不知再说些什么,如果有办法能抹去以前的一切就好了。

这个东西算我欠你的,事已至此,我亦无力纠缠,这一个多月来就像一场梦。

目之所及,心之所向,情之所往,皆为幻象。

我也该醒醒认清自己了。

RAEDME.md

# NewsCrawl

# 1 简介
1. 这是一个基于新闻网站的新闻文本爬虫, 向指定微信群中定时发布新闻消息的项目; 
2. 20191226商议时标注了四个网站, 但是发现第一电动网新闻质量参差不齐(我怀疑是不是个卖车平台), 所以没有去做, 目前只进行了编写三个网站的爬虫, 分别是:
  - SEMI大半导体产业网: http://www.semi.org.cn/
  - 新材料在线: http://www.XCL.com/
  - 高工锂电: https://www.gg-lb.com/


# 2 使用说明

## 2.1 第一次使用前
1. 根目录下应当存在的文件: 
  - ../               根目录
    + Crawl.py        爬虫总父类
	+ Crawl_SEMI.py   大半导体产业网爬虫类
	+ Crawl_XCL.py    新材料网爬虫类
    + Crawl_GGLB.py   高工锂电爬虫类
	+ utils.py        常用工具函数
	+ manage.py       主函数
	+ meta/           (文件夹)存放元数据
	  - websites.csv  存放网站信息的csv文件
	  - keywords.txt  存放关键词的txt文件
	+ README.md       (可选)即本文件

2. 本爬虫全部基于requests库, 没有使用selenium的方法, 如果是安装的conda, 则额外安装一个itchat库即可(该库为微信操作库)
  - 版本: python 3.x (我的是python 3.6)
  - 所有用到的库:
    + requests
	+ os 
	+ re
	+ time
	+ pandas
	+ itchat: 该库conda没有预装, 需手动安装: pip install itchat即可
	+ datetime
	+ bs4
	
3. 以下将分别对各个文件的结构做简要说明;

## 2.2 文件说明
1. websites.csv: csv文件, 存放需要爬取的网站(目前只放三个网站), 各条目间的间隔应为"\t", 即制表符Tab: 
  - 表头为: "index	site	url";
    + index: 1,2,3,...
    + site: 网站名称
    + url: 网站URL, 注意一定要是根域名URL, 如http://www.semi.org.cn/, 而非http://www.semi.org.cn/index, 虽然它们指向同一页面;
  - 如下所示:
  '''
  index	site	url	
  1	SEMI大半导体产业网	http://www.semi.org.cn/
  2	新材料在线	http://www.XCL.com/
  3	高工锂电	https://www.gg-lb.com/
  '''

2. keywords.txt: 一行写一个关键词, 如下所示:
  '''
  永磁
  半导体
  靶材
  石英
  氮化镓
  砷化镓
  液晶显示面板
  正极
  负极
  隔膜
  电解液
  高温合金
  碳纤维
  钛材
  '''

3. Crawl.py  爬虫总父类
  - 编写了Crawl类, 该类仅有一个构造函数__init__();
  - 在Crawl类的构造函数中定义了各个网站爬虫中常用的变量: 
    + 类构造变量(即实例化类时应当传入的参数)
      - self.user_agent: 用户代理(即伪装浏览器头);
  
    + 类常用变量:
      - self.wordspace: 当前工作目录
      - self.date: 类创建时间(如20191228)
      - self.dirs: 存放了一些需要新建的文件夹, 如news(新闻文件), log(记录文件), temp(临时文件)
      - 预先编译了一些可能用到的正则表达式: self.invalid_compiler, self.label_compiler, self.date_compiler 
  
    + 类初始化: 生成一些其他类变量及一些初始化操作
      - 检查根目录下是否存在meta文件夹(如不存在则抛出异常);
	  - 新建news, log, temp三个文件夹:
	    + 每个文件夹中新建以当日日期命名的文件夹; 
	    + 在news文件夹中还会在当日的文件夹里为每个网站新建一个文件夹;
	  - 以上新建操作都是在文件夹不存在的情况下才会发生, 如文件夹存在则自动跳过;
	  - 运行后根目录下应当多出如下文件(以2019年12月28日为例):
	    + ../
	      - log/                                    文件夹: 存放记录文件
		    + 20191228/                             文件夹: 存放20191228的记录文件(目前代码没有生成过记录文件)
	      - temp/                                   文件夹: 存放临时文件
		    + 20191228/                             文件夹: 存放20191228的记录文件(目前代码没有生成过记录文件)
		  - news/                                   文件夹: 存放新闻文件
		    + 20191228/                             文件夹: 存放20191228爬取到的新闻文件
		      - 1_SEMI大半导体产业网/               文件夹: 存放大半导体产业网20191228爬取到的新闻数据
	          - 2_新材料在线/						文件夹: 存放新材料网20191228爬取到的新闻数据
			  - 3_高工锂电/							文件夹: 存放高工锂电网20191228爬取到的新闻数据
    
	  - 创建一些其他类变量:
	    + self.urls: <列表>存放meta/websites.csv中的网址;
	    + self.keywords: <列表>存放meta/keywords.txt中的关键词;
	    + self.directorys: <列表>存放文件夹信息, 如['1_SEMI大半导体产业网', '2_新材料在线', '3_高工锂电'];

  - 使用方法: 
    + c = Crawl()  
	  - 注意可以写成 c = Crawl(user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0")
	  - user_agent我已经赋给默认值, 所以不传该参数也可以;
    + 效果: 生成各个文件夹;

4. Crawl_SEMI.py, Crawl_XCL.py, Crawl_GGLB.py 三个网站的爬虫类
  - 分别封装了Crawl类的三个子类: SEMI类, XCL类, GGLB类;
  - 这三个类的结构类似, 我一并说明:
    + 构造函数: __init__(day_before=0)  
	  - 注意SEMI类比其他两个要多一个参数plates, 默认值为["半导体新闻"], 因为SEMI网站上首页有很多板块, 目前只写了"半导体新闻"板块的, 其他与"半导体新闻"结构类似的板块可以加在这个列表中作为构造参数传入;
	  - day_before指获取多少天前的新闻, 如果是0则默认只获取今天的;
	  - 构造函数里先继承父类的构造函数(即生成很多类变量以及检查文件夹生成情况, 缺的文件夹会新建), 然后做了一些其他的常规操作;
	+ 爬取新闻列表的函数: parse_newslist(session,url)
	  - 获取新闻列表面上的数据: 如该页面上所有的新闻标题, 新闻发布时间, 每个新闻的链接URL, 以及可能有新闻来源;
    + 爬取新闻内容的函数: parse_newcontent(session,url)
      - parse_newslist(session,url)函数得到的新闻链接URL, 对每个URL中的新闻内容获取, 一般可以获取到新闻的文本, 新闻的来源等;
    + 主函数: main()
      - 利用以上两个函数获取到所有新闻数据, 存放在一个列表中, 每个新闻有5个属性(url,title,date,paragraphs,source), 结构如下:
	    + [
			{
				"url": "http://www.semi.org.cn/news/news_show.aspx?ID=58264&classid=117",
				"title": "耐威科技北京8英寸MEMS国际代工线首台设备搬入仪式圆满举行",
				"date": "20191227",
				"paragraphs": [
					"段落1","段落2",..."段落n",
				]
				"source": "出自:耐威科技",
				
			},
			{
				"url": "http://www.semi.org.cn/news/news_show.aspx?ID=58264&classid=117",
				"title": "耐威科技北京8英寸MEMS国际代工线首台设备搬入仪式圆满举行",
				"date": "20191227",
				"paragraphs": [
					"段落1","段落2",..."段落n",
				]
				"source": "出自:耐威科技",				
			},
			...
			{
				"url": "http://www.semi.org.cn/news/news_show.aspx?ID=58264&classid=117",
				"title": "耐威科技北京8英寸MEMS国际代工线首台设备搬入仪式圆满举行",
				"date": "20191227",
				"paragraphs": [
					"段落1","段落2",..."段落n",
				]
				"source": "出自:耐威科技",				
			}
		]
		+ 然后将该列表以字符串格式写入到指定文件夹中, 如news/20191228/1_SEMI大半导体产业网/SEMI_20191228.txt中, 便于后续处理

5. utils.py  一些常用的工具函数
  - sent_tokenize(x)  中文分句
  - generate_forward_content(news_info,	                                     # news_info参数即为上文中每条新闻的字典({url: ..., title: ..., date: ..., paragraphs: ..., source: ...,})
      sent_minlen=20,														 # 每句话的最少字符数(用于过滤废话和无关信息)
	  minlen=300,															 # 推送文字的最小长度(只要少于这个字数就会再加一句话)
	  maxlen=500,															 # 推送文字的最大长度(如果大于这个字数就不增加句子了)
	  keywords=None,														
	)  
	该函数返回可以直接发送到微信群中的字符串;

  - send_group(message,gname)  发送message到群名为gname的群中
    + 注意一下, 如果gname没有找到会抛出异常, 最好的方法是使用前先在群里发一条信息, 然后就可以找到了, 否则很多微信群是找不到的;
	
6. manage.py 主函数
  - 详见该文件, 一些参数都用大写字母命名的变量做好了;
  
## 2.3 使用方法及可能的问题
1. 根目录下打开cmd: python manage.py 即可
2. 三个爬虫很类似, 基本上不会有太大问题, 我将一些要点写在这里:
  - SEMI网站的新闻较少, 一天也就几条, 所以只拿了第一页的新闻, 后面几页就不管了, 因为三个爬虫中它响应最慢;
    + SEMI的新闻文本相对问题少, 有明确的文本区域, 各段落文本也应该都是在<p>标签下的;
  - 新材料网一天会有大约3~4页的新闻, 相对多一些, 它的问题在于各段落新闻文本并不都是在<p>标签中, 有可能有<br>半标签, 
    + 如http://www.xincailiao.com/news/news_detail.aspx?id=559043新闻文本都是在<p>标签下的;
	+ 如http://www.xincailiao.com/news/news_detail.aspx?id=559055则夹杂很多<br>半标签;
	+ 因此新材料网站我直接对新闻文本区域用去标签处理, 应该也问题不大
  - 高工锂电网不存在明确的新闻文本区域, 因此就找了所有的<p>标签, 问题也应该不大;
  - SEMI拿的是半导体新闻板块下的, 新材料网与高工锂电都是拿的最新资讯(而非约定的半导体订阅号之类的, 因为发现那些订阅号更新的很少)

3. 关于关键词的问题:
  - 如何查看文本各个关键词权重?
  '''
  # 安装jieba库: pip install jieba 该库较大需要较长时间, 我不清楚conda有没有预装, 应该是没有
  from jieba import analyse
  a = analyse.extract_tags(
	  "这是一个文本xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	  topK=20,
	  withWeight=True,
	  allowPOS=('n',"nr","ns")
  )
  print(a) # 则输出a可以看到前20个关键词的权重, 我试过测试关键词, 但是发现基本上都对应不上excel中的关键词
  '''
  - 如何解决该问题: 可以考虑计算关键词词向量的相似度, 设计一个简单的近似算法来评估新闻文本匹配excel中所有关键词的一个得分, 但这有点麻烦, 一方面词向量文件很大, 读取和使用都很耗时, 另外我觉得我选的这几个新闻类别与这些关键词大部分还是匹配得上的, 综上所以就不使用
  
4. 关于log和temp文件夹: 
  - log文件夹: 一般可以放一些异常抛出的报告, 或者爬虫的一个精确时间记录, 虽然我都没有写;
  - temp文件夹: 放一些临时文件, 可能也不会有什么临时文件, 万一会有呢?
  - 总之这两个文件夹目前代码里没有放东西在里面;

5. 关于定时启动:
  - 这个说实话很麻烦, 如果是用服务器, 我不清楚怎么登录微信, 因为在电脑上需要扫码登录微信, 如果用的是电脑, 我不清楚itchat的登录会持续多久, 可能需要每天手动运行然后登录, 如果itchat可以做到长时间保持微信号登录状态, 则可以定时使用(定时使用的代码在manage.py中被注释掉的一长串中)

Crawl.py

# -*- coding: UTF-8 -*-
# Author: 囚生
# 定义一个爬虫父类, 被其他各个网站的爬虫子类继承, 用于对各个爬虫中出现的共性问题做处理: 类项目下各个文件夹, 文件的初始化, 元数据的读取, 共用参数的设置等;

import os
import re
import time
import pandas

class Crawl():
	def __init__(self,
		user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0",
	):																	 # 类构造函数
		""" 类构造参数 """
		self.user_agent = user_agent									 # 用户代理
		
		""" 类常用参数 """
		self.workspace = os.getcwd()									 # 类工作目录
		self.date = time.strftime("%Y%m%d")								 # 类创建时间
		self.dirs = {													 # 常用的文件夹
			"元": "meta",												 # 存放元文件的文件夹: 该文件夹必须在第一次使用时预先设置, 里面的文件请根据README配置好
			"新闻": "news",												 # 存放新闻文件的文件夹
			"记录": "log",												 # 存放记录文件的文件夹
			"临时": "temp",												 # 存放临时文件的文件夹
		}
		# 一些常用的正则表达式
		self.invalid_pattern = r'[/|\\|?|*|\||"|<|>|:| |\r|\n|\t]'		 # 文件名中不合理的字符正则
		self.invalid_compiler = re.compile(self.invalid_pattern)		 # 编译正则
		self.label_pattern = r'<[^>]+>'									 # HTML中标签的正则: 去除HTML中的标签, 剩下的就是需要的信息
		self.label_compiler = re.compile(self.label_pattern)			 # 编译正则
		self.date_regular = r"[^\d]"									 # 用于去除日期中的非数字的正则: 去除日期中非数字字符, 这样日期就变成了yyyymmdd
		self.date_compiler = re.compile(self.date_regular)				 # 编译正则
		
		""" 类初始化 """
		for key,value in self.dirs.items():								 # 确认self.dirs中的目录是否都存在
			if not os.path.exists(value):								 # 不存在则新建或抛出异常
				if key=="元": raise Exception("找不到元文件夹!请阅读README!")	
				else:
					loginfo = "正在新建{}文件夹...".format(key)
					print(loginfo)										 # 输出日志信息
					os.mkdir("{}/{}".format(self.workspace,value))
					os.mkdir("{}/{}/{}".format(self.workspace,value,self.date))
			else:														 # 存在则进一步检查是否存在当日的文件夹
				if not os.path.exists("{}/{}".format(value,self.date)) and not key=="元":
					loginfo = "正在{}文件夹中新建{}文件夹...".format(key,self.date)
					print(loginfo)										 # 输出日志信息
					os.mkdir("{}/{}/{}".format(self.workspace,value,self.date))
		self.websites = pandas.read_table("{}/websites.csv".format(self.dirs["元"]),header=0,sep="\t",dtype=str,encoding="UTF-8")
		with open("{}/keywords.txt".format(self.dirs["元"]),"r",encoding="UTF-8") as f:
			self.keywords = list(map(lambda x: x.strip(),f.read().splitlines()))
		self.urls = self.websites["url"].tolist()						 # 所有的网址
		self.directorys = []											 # 对应self.urls的存放各个网址新闻的文件夹名
		for i in range(self.websites.shape[0]):
			site = self.websites.loc[i,"site"]
			directory = "{}_{}".format(i+1,site)						 # 命名文件夹
			self.directorys.append(directory)
			if not os.path.exists("{}/{}/{}".format(self.dirs["新闻"],self.date,directory)):
				loginfo = '正在为"{}"创建{}的文件夹...'.format(site,self.date)
				print(loginfo)											 # 输出日志信息
				os.mkdir("{}/{}/{}/{}".format(self.workspace,self.dirs["新闻"],self.date,directory))

if __name__ == "__main__":
	crawl = Crawl()

	

Crawl_SEMI.py

# -*- coding: UTF-8 -*-
# Author: 囚生
# 获取SEMI大半导体产业网站(半导体新闻板块的内容)

from Crawl import Crawl

import re
from requests import Session
from bs4 import BeautifulSoup
from datetime import datetime

class SEMI(Crawl):

	def __init__(self,
		plates=["半导体新闻"],											 # 获取哪些板块的新闻: 默认获取半导体新闻
		day_before=0,													 # 获取几天前的新闻: 默认0即获取今天的数据
	):	
		Crawl.__init__(self,)											 # 继承父类Crawl的成员变量及方法
		
		""" 类构造参数 """
		self.plates = plates											 # 获取哪些板块的新闻
		self.day_before = day_before									 # 获取几天前的新闻
		
		""" 类常用参数 """
		self.homeURL = self.urls[0].strip("/")							 # SEMI主页URL: 它应该位于websites.csv列表中的第1个
		self.session = Session()										 # 类会话对象
		
		""" 类初始化 """
		self.session.headers = {"User-Agent": self.user_agent}			 # 为类会话设置用户代理

	def parse_newslist(self,session,url):								 # 获取新闻列表: session(类会话对象), url(新闻列表的网址URL)
		html = session.get(url).text									 # 获取新闻列表页面的HTML
		soup = BeautifulSoup(html,"lxml")								 # 解析HTML
		aLabels = soup.find_all("a")									 # 获取所有的a标签
		news = list()													 # 用于存储列表中各个新闻的(新闻标题,新闻超链接URL,发布时间)
		for a in aLabels:												 # 遍历所有的a标签
			href = a.attrs.get("href")									 # 获取新闻超链接URL
			if href is None: continue									 # 如果没有超链接则跳过: 确实存在一些<a>标签没有超链接
			if "news_show.aspx" in href:								 # 判断是否为新闻链接: 这里以超链接URL中是否包含"news_show.aspx"子串为标准
				href = href.strip("/")									 # 去除超链接URL两端的"/"字符
				title = str(a.string)									 # 获取新闻标题
				date = a.find_parent("td").find_next_sibling("td").string# 这里使用了很硬的寻找日期的方式: 即超链接的父亲的下一个兄弟, 也可以用<td align="right">属性来定位, 但也很硬;
				date = str(date)										 # 转化为字符串
				date = self.date_compiler.sub("",date)					 # 删除date中不为数字的字符(即希望得到的日期格式是yyyymmdd, 便于转化为日期数据格式比较)

				""" 这片代码用于检查新闻日期是否符合day_before要求, 不合要求则退出 """
				datetime_date = datetime.strptime(date,"%Y%m%d")		
				datetime_now = datetime.strptime(self.date,"%Y%m%d")
				interval = datetime_now - datetime_date
				if interval.days>self.day_before: break					 # 退出
				
				newsURL = "{}/{}".format(								 # 事实上href并不是新闻页面的绝对URL, 是基于新闻列表的相对URL, 因此需要通过新闻列表页面网址的URL来生成绝对地址
					"/".join(url.split("/")[:-1]),						 # 前半部分是新闻列表页面URL的去除最后一个"/"之后的部分, 剩下的字符串
					href.strip("/")										 # 后半部分即为<a>标签中的超链接URL(去除两端的"/"字符)
				)
				news.append({											 # 将(新闻链接URL,新闻标题,新闻发布时间)作为三元组添加到news列表中
					"url": newsURL,
					"title": title,
					"date": date,
				})
		return news														 # 返回第一页上的所有新闻信息(第一页的应该基本上够用了)

	def parse_newscontent(self,session,url):							 # 获取新闻内容: session(新闻)
		html = session.get(url).text									 # 获取新闻内容页面的HTML
		soup = BeautifulSoup(html,"lxml")								 # 解析HTML
		table = soup.find("table",class_="gongzuo")						 # 新闻的主体内容在某个<table>标签中
		span1 = soup.find("span",attrs={"id":"lblAuthor"})				 # 文章来源在一个<span>标签中
		span2 = soup.find("span",attrs={"id":"lblTitle"})				 # 文章标题在一个<span>标签中
		if table is None: ps = soup.find_all("p")						 # 为了防止网站页面结构发生更新或者有例外情况: 那就直接找所有的<p>标签
		else: ps = table.find_all("p")									 # 有这张<table>则从这个<table>中寻找所有的<p>标签
		if span1 is not None: source = str(span1.string)				 # 找到这个<span>标签则获取来源
		else: source = "出自: SEMI大半导体产业网"							 # 否则默认来源是SEMI网
		if span2 is not None: title = str(span2.string)					 # 如果能找到文章标题则获取
		else: title = None												 # 否则就置None, 返回None表明不修改文章标题
		paragraphs = list()												 # 用来存储各个段落
		for p in ps:													 # 遍历每个段落
			content = self.label_compiler.sub("",str(p))				 # 去除标签
			paragraphs.append(content)									 # 非空段落添加到paragraphs中
		return paragraphs,source,title									 # 返回新闻段落列表与来源

	def main(self,):
		html = self.session.get(self.homeURL).text						 # 首先访问主页并获取HTML
		soup = BeautifulSoup(html,"lxml")								 # 解析页面
		aLabels = soup.find_all("a")									 # 找到所有<a>标签
		hrefs = list()													 # 用于存储需要爬取的板块的"更多>>"链接的URL
		for a in aLabels:												 # 遍历所有<a>标签: 为了找到"更多>>"
			string = str(a.string)										 # 获取<a>标签的字符串
			if string=="更多>>":											 # 如果该<a>标签的字符是"更多>>"则有可能是指向新闻列表
				try: plate = str(a.find_parent("td").find_previous_sibling("td").string)
				except: continue										 # 有可能压根找不到这些条件
				if plate in self.plates: hrefs.append("{}/{}".format(	 # 如果该板块是需要的, 则将该板块"更多>>"链接的URL添加到hrefs: 注意href的超链接是相对地址, 因此需要添加根域名http://www.semi.org.cn
					self.homeURL,
					a.attrs["href"].strip("/"),
				))
		print("共有{}个板块".format(len(hrefs)))
		for i in range(len(hrefs)):										 # 遍历所有的新闻列表页面URL: 默认只获取"半导体新闻"则hrefs中只有1个URL
			print("正在获取第{}个板块: {}".format(i+1,self.plates[i]))
			news = self.parse_newslist(self.session,hrefs[i])			 # 获取新闻列表页面的新闻信息: 
			print("  - 该板块下有{}条新闻".format(len(news)))
			for j in range(len(news)):									 # 遍历列表中所有新闻的基本信息: 链接URL, 标题, 发布时间
				print("正在获取第{}条新闻...".format(j+1))
				result = self.parse_newscontent(self.session,news[j]["url"])
				"""
				根据result信息, 更新news中每个字典, 如此字典有五个键值对,
				分别是新闻链接URL,新闻标题,新闻发布日期,新闻内容(段落列表),新闻来源
				"""
				news[j]["paragraphs"] = result[0]						 # 更新新闻内容
				news[j]["source"] = result[1]							 # 更新新闻来源
				if result[2] is not None: news[j]["title"] = result[2]	 # 更新新闻标题: 因为直接从新闻列表中拿的新闻标题有的太长就会有省略号...
		with open("{}/{}/{}/SEMI_{}.txt".format(self.dirs["新闻"],self.date,self.directorys[0],self.date),"w",encoding="UTF-8") as f:
			f.write(str(news))
		return news

if __name__ == "__main__":
	semi = SEMI(day_before=1)
	semi.main()

Crawl_XCL.py

# -*- coding: UTF-8 -*-
# Author: 囚生
# 获取新材料网新闻(最新新闻板块的内容)

from Crawl import Crawl

import re
from requests import Session
from bs4 import BeautifulSoup
from datetime import datetime

class XCL(Crawl):

	def __init__(self,
		day_before=0,													 # 获取几天前的新闻: 默认0即获取今天的数据
	):	
		Crawl.__init__(self,)											 # 继承父类Crawl的成员变量及方法
		
		""" 类构造参数 """
		self.day_before = day_before									 # 获取几天前的新闻
		
		""" 类常用参数 """
		self.homeURL = self.urls[1].strip("/")							 # 新材料网主页URL: 它应该位于websites.csv列表中的第2个

		# 最新新闻URL
		self.newestURL = self.homeURL+"/news/news_list.aspx?fenlei=latest&page={}"
		self.session = Session()										 # 类会话对象
		
		""" 类初始化 """
		self.session.headers = {"User-Agent": self.user_agent}			 # 为类会话设置用户代理

	def parse_newslist(self,session,url):								 # 获取新闻列表: session(类会话对象), url(新闻列表的网址URL)
		html = session.get(url).text
		soup = BeautifulSoup(html,"lxml")
		"""
		通过分析网页发现新闻列表区域在一个<div>标签中,
		但这个div标签的class属性太长, 如果某天发生改变就会很糟糕,
		我们选择先定位新闻列表底部的1,2,3,4翻页按钮, 因为它的class为page是很有特征的, 并且预计不会发生较大改变
		"""
		flag = True														 # 是否还需要爬取下一页: 见if interval.days>self.day_before:中的说明
		div1 = soup.find("div",class_="page")							 # 找到底部页码区域
		div2 = div1.find_previous_sibling("div")						 # 找到它的哥哥
		ul = div2.find("ul")											 # 找到哥哥里的无序列表
		lis = ul.find_all("li")											 # 找到无序列表的所有项目
		news = list()
		for li in lis:
			title_label = li.find("span",class_="title")
			newsURL = "{}/{}".format(self.homeURL,title_label.find("a").attrs["href"].strip("/"))
			title = self.label_compiler.sub("",str(title_label))		 # 用标签正则去除title_label中所有标签, 剩下的即为标题
			span = li.find("span",class_="lfoote")						 # 找到包含日期与来源的span标签
			date = str(span.find("p").string)							 # span下面第一个<p>标签包含日期
			date = self.date_compiler.sub("",date)						 # 使日期变为yyyymmdd格式

			datetime_date = datetime.strptime(date,"%Y%m%d")		
			datetime_now = datetime.strptime(self.date,"%Y%m%d")
			interval = datetime_now - datetime_date
			if interval.days>self.day_before:							 # 这里与SEMI中有所区别, 因为SEMI的新闻相对更新较少, 第一页基本上够用了, 但是新材料网的新闻更新很多, 以20191227为例, 新闻大概有3~4页, 因此该函数需要返回一个flag告诉外层调用是否还需要继续下一页
				flag = False
				break
			
			source = str(span.find("p",class_="author").string)			 # 第二个<p>标签或者class属性为author的p标签中包含来源
			news.append({
				"url": newsURL,
				"title": title,
				"date": date,
				"source": "出自:{}".format(source),
			})
		return news,flag

	def parse_newscontent(self,session,url):							 # 获取新闻内容: session(新闻)
		html = session.get(url).text
		soup = BeautifulSoup(html,"lxml")
		div = soup.find("div",class_="newsDetail")
		"""
		这边出现一个问题:
		并不是所有的新闻都是把文字放在<p>标签中的,
		意外的事情是可能有的新闻是用<br>半标签来分块的, 这就非常讨厌了,
		"""
	
		if div is None:													 # 为了防止这个div标签以后消失, 我们以全部的<p>标签作为备用: 这里
			ps = soup.find_all("p")										
			paragraphs = list()
			for p in ps:
				paragraph = self.label_compiler.sub("",str(p))
				paragraphs.append(paragraph)
			return paragraphs
		else:															 # 这个名为newsDetail的div若还在, 就直接拿到它的所有信息, 因为文本有的在<p>标签里, 有的在<br>标签中, <br>标签还是个很讨厌的半标签, 不是很好处理, 所以就暴力的把所有标签去除, 剩下的就是新闻内容了
			string = self.label_compiler.sub("",str(div))
			return string.split("\n")

	def main(self,):
		self.session.get(self.homeURL)									 # 首先访问主页: 
		page = 0
		all_news = []
		while True:
			page += 1
			print("正在获取第{}页的新闻...".format(page))
			news,flag = self.parse_newslist(self.session,self.newestURL.format(page))
			print("  - 共有{}条新闻".format(len(news)))
			for i in range(len(news)):
				print("  - 正在获取第{}条新闻...".format(i+1))
				url = news[i]["url"]
				paragraphs = self.parse_newscontent(self.session,url)
				news[i]["paragraphs"] = paragraphs[:]					 # 注意此处一定要浅复制[:], 否则会导致深复制错误
			all_news += news
			if not flag: break
		with open("{}/{}/{}/XCL_{}.txt".format(self.dirs["新闻"],self.date,self.directorys[1],self.date),"w",encoding="UTF-8") as f:
			f.write(str(all_news))
		return all_news

if __name__ == "__main__":
	xcl = XCL(day_before=1)
	xcl.main()

Crawl_GGLB.py

# -*- coding: UTF-8 -*-
# Author: 囚生
# 获取高工锂电网(最新资讯板块的内容)

from Crawl import Crawl

import re
from requests import Session
from bs4 import BeautifulSoup
from datetime import datetime

class GGLB(Crawl):

	def __init__(self,
		day_before=0,													 # 获取几天前的新闻: 默认0即获取今天的数据
	):	
		Crawl.__init__(self,)											 # 继承父类Crawl的成员变量及方法
		
		""" 类构造参数 """
		self.day_before = day_before									 # 获取几天前的新闻: 默认0为只获取今天的新闻, 一般建议还是设为1, 因为昨天的新闻可能昨天并没有能够全部获取到
		
		""" 类常用参数 """
		self.homeURL = self.urls[2].strip("/")							 # 高工锂电主页URL: 它应该位于websites.csv列表中的第3个
		self.newestURL = self.homeURL+"/list-0-{}.html"					 # 最新新闻列表URL: "{}"中是空给页码数的
		self.session = Session()										 # 类会话对象
		
		""" 类初始化 """
		self.session.headers = {"User-Agent": self.user_agent}			 # 为类会话设置用户代理

	def parse_newslist(self,session,url):								 # 获取新闻列表: session(类会话对象), url(新闻列表的网址URL)
		html = session.get(url).text
		soup = BeautifulSoup(html,"lxml")
		"""
		与新材料网一样:
		通过分析网页发现新闻列表区域在一个<div>标签中,
		但这个div标签的class属性太长, 如果某天发生改变就会很糟糕,
		我们同样选择先定位新闻列表底部的1,2,3,4翻页按钮, 因为它的id为pagination是很有特征的, 并且预计不会发生较大改变
		"""
		flag = True														 # 是否还需要爬取下一页: 见if interval.days>self.day_before:中的说明
		div1 = soup.find("div",attrs={"id":"pagination"})				 # 找到底部页码区域
		div2 = div1.find_previous_sibling("div")						 # 找到它的哥哥: 即新闻列表区域
		news_items = div2.find_all("div",class_="news-item")			 # 这个新闻列表区域里面会有若干<div class="news-item">, 遍历每个获取需要的信息即可
		news = list()													 # 存放新闻基本信息的列表
		for news_item in news_items:
			title_label = news_item.find("div",class_="title")
			aLabels = title_label.find_all("a")							 # 其实获取第一个就行了, 而且应该只有一个, 为什么我要获取所有的<a>标签呢, 因为担心之后在这里会多出一些<a>标签
			for a in aLabels:
				if "art" in a.attrs["href"]:							 # 这表明是我需要的超链接, 新闻内容页面URL结构是: "/art-<编号>"
					newsURL = "{}/{}".format(self.homeURL,a.attrs["href"])
			title = self.label_compiler.sub("",str(a))
			title = title.replace("【","[").replace("】","]")			 # 因为我最后发送到微信群里的时候会给标题两端加上"【】", 这个网站大部分新闻前面会有"【原创】", 影响美观, 所以把"【】"替换成"[]"

			date = str(title_label.find("span",class_="time").string)	 # 获取字符串的日期: 注意高工锂电的日期是年月日时分秒, 为了省事我们就获取前8位的年月日了
			date = self.date_compiler.sub("",date)[:8]					 # 使日期变为yyyymmdd格式
			datetime_date = datetime.strptime(date,"%Y%m%d")
			datetime_now = datetime.strptime(self.date,"%Y%m%d")
			interval = datetime_now - datetime_date
			if interval.days>self.day_before:							 # 同新材料网
				flag = False
				break

			news.append({
				"url": newsURL,
				"title": title,
				"date": date,
			})
		return news,flag

	def parse_newscontent(self,session,url):							 # 获取新闻内容: session(新闻)
		html = session.get(url).text
		soup = BeautifulSoup(html,"lxml")

		spans = soup.find_all("span")
		source_flag = False												 # 记录是否在页面上找到了新闻来源: 如果没有则默认来源于高工锂电
		for span in spans:
			if str(span.string)[:5]=="文章来源自":
				source_flag = True
				source = str(span.string)								 # 找到了文章来源, 则使用找到的来源
		if not source_flag: source = "文章来源自:高工锂电"					 # 找不到则默认
		div = soup.find("div",class_="content-article")					 # 即便如此, 为了相对精准一些, 我们还是先定位这个class为content-article的<div>标签
		"""
		高工锂电的就比较麻烦, 它没有一个<div>标签把所有的新闻文字<p>标签包裹起来, 虽然有一个class为content-article的<div>标签包含了新闻内容, 但它不止是包含新闻文字, 里面还有一些乱七八糟的东西
		<p>标签极其零散, 但是高工锂电似乎每篇新闻有个简短的摘要, 我觉得意义不大, 而且我不能确定每篇新闻都有这个摘要, 
		"""
		paragraphs = list()												 # 用于存储段落的列表
		if div is None:	ps = soup.find_all("p")							 # 为了防止这个div标签以后消失, 我们以全部的<p>标签作为备用
		else: ps = div.find_all("p")									 # 这个名为content-article的div若还在, 就直接拿到它所有的p标签
		for p in ps:
			paragraph = self.label_compiler.sub("",str(p))
			paragraphs.append(paragraph)
		return paragraphs,source

	def main(self,):
		self.session.get(self.homeURL)									 # 首先访问主页: 高工锂电
		page = 0
		all_news = []
		while True:
			page += 1
			print("正在获取第{}页的新闻...".format(page))
			news,flag = self.parse_newslist(self.session,self.newestURL.format(page))
			print("  - 共有{}条新闻".format(len(news)))
			for i in range(len(news)):
				print("  - 正在获取第{}条新闻...".format(i+1))
				url = news[i]["url"]
				paragraphs,source = self.parse_newscontent(self.session,url)
				news[i]["paragraphs"] = paragraphs[:]					 # 注意此处一定要浅复制[:], 否则会导致深复制错误
				news[i]["source"] = source
			all_news += news
			if not flag: break
		with open("{}/{}/{}/GGLB_{}.txt".format(self.dirs["新闻"],self.date,self.directorys[2],self.date),"w",encoding="UTF-8") as f:
			f.write(str(all_news))
		return all_news

if __name__ == "__main__":
	gglb = GGLB(day_before=1)
	gglb.main()

manage.py

# -*- coding: UTF-8 -*-
# Author: 囚生
# 主函数

import os
import time
import itchat
from utils import *
from Crawl_SEMI import SEMI
from Crawl_XCL import XCL
from Crawl_GGLB import GGLB
				
GNAME = "Wechat Debug"													 # 群名
TIME_INTERVAL = 60														 # 间隔多少秒发送一次新闻
DAY_BEFORE = 1															 # 获取几天前的数据

if __name__ == "__main__":
	itchat.auto_login(hotReload=True)									 # 微信登录
	
	semi = SEMI(day_before=DAY_BEFORE)
	xcl = XCL(day_before=DAY_BEFORE)
	gglb = GGLB(day_before=DAY_BEFORE)

	# 运行爬虫
	semi.main()
	xcl.main()
	gglb.main()

	date = semi.date													 # 这里用xcl.date, gglb.date也一样
	directorys = semi.directorys										 # 这里用xcl.directorys, gglb.directorys也一样	
	news = []															 # 存放所有新闻的列表

	# 读取爬取到的新闻: 这边写得有点硬
	with open("news/{}/{}/SEMI_{}.txt".format(date,directorys[0],date),"r",encoding="UTF-8") as f:
		news += eval(f.read())
	with open("news/{}/{}/XCL_{}.txt".format(date,directorys[1],date),"r",encoding="UTF-8") as f:
		news += eval(f.read())	
	with open("news/{}/{}/GGLB_{}.txt".format(date,directorys[2],date),"r",encoding="UTF-8") as f:
		news += eval(f.read())

	# 推送
	for new in news:													 # 遍历每条新闻
		forward = generate_forward_content(new)							 # 注意该函数还有其他4个默认参数: minlen, maxlen, sent_minlen, keywords
		flag = send_group(forward,GNAME)
		if not flag: raise Exception("没有找到微信群!")
		time.sleep(TIME_INTERVAL)

	# 如果需要定时启动: 
	'''
	while True:
		if flag and int(time.strftime("%H"))==7:						 # 每天早上七点多开始运行该代码
			itchat.auto_login(hotReload=True)							 # 微信登录
			semi = SEMI(day_before=DAY_BEFORE)
			xcl = XCL(day_before=DAY_BEFORE)
			gglb = GGLB(day_before=DAY_BEFORE)

			# 运行爬虫
			semi.main()
			xcl.main()
			gglb.main()


			date = semi.date											 # 这里用xcl.date, gglb.date也一样
			directorys = semi.directorys								 # 这里用xcl.directorys, gglb.directorys也一样		
			news = []													 # 存放所有新闻的列表

			# 读取爬取到的新闻: 这边写得有点硬
			with open("news/{}/{}/SEMI_{}.txt".format(date,directorys[0],date),"r",encoding="UTF-8") as f:
				news += eval(f.read())
			with open("news/{}/{}/XCL_{}.txt".format(date,directorys[1],date),"r",encoding="UTF-8") as f:
				news += eval(f.read())	
			with open("news/{}/{}/GGLB_{}.txt".format(date,directorys[2],date),"r",encoding="UTF-8") as f:
				news += eval(f.read())

			# 推送
			for new in news:											 # 遍历每条新闻
				forward = generate_forward_content(new)					 # 注意该函数还有其他4个默认参数: minlen, maxlen, sent_minlen, keywords
				flag = send_group(forward,GNAME)
				if not flag: raise Exception("没有找到微信群!")
				time.sleep(TIME_INTERVAL)
			time.sleep(50000)											 # 睡50000秒(这样就不会说7:00运行代码, 7:10发现int(time.strftime("%H"))还是7, 又运行一次)
		else:
			time.sleep(600)												 # 50000秒后: 十分钟检查一次是否到过7点了
	'''	


	pass

utils.py

# -*- coding: UTF-8 -*- 
# Author: 囚生
# 一些常用的工具函数

import re
import time
import itchat
#from jieba import analyse

CONTENT_REGULAR = re.compile(r'[ |\r|\n|\t|\xa0]')

def sent_tokenize(x):													 # 中文分句
    sents_temp = re.split('(。|!|\!|?|\?)',x)
    sents = []
    for i in range(len(sents_temp)//2):
        sent = sents_temp[2*i] + sents_temp[2*i+1]
        sents.append(sent)
    return sents

def generate_forward_content(news_info,									 # news_info为一个字典, 里面有五个键值对: url, title, date, paragraphs, source
	sent_minlen=20,														 # 每句话的最少字符数(用于过滤废话和无关信息)
	minlen=300,															 # 推送文字的最小长度
	maxlen=500,															 # 推送文字的最大长度
	keywords=None,
):																		 # 根据新闻信息字典生成推送内容: 若不传入keywords列表则默认不进行关键词匹配
	url = news_info["url"]
	title = news_info["title"]
	date = news_info["date"]
	paragraphs = news_info["paragraphs"]
	source = news_info["source"]
	source = CONTENT_REGULAR.sub("",source)

	content = str()
	for paragraph in paragraphs:
		# 这边是可以利用jieba进行关键词提取, 但是我测试结果发现基本上都跟需要的关键词对不上
		# a = analyse.extract_tags(paragraph,topK=20,withWeight=True,allowPOS=('n',"nr","ns"))
		# for i in a: print(i)
		string = CONTENT_REGULAR.sub("",paragraph)
		string = string.strip()
		sents = (sent_tokenize(string))
		sents = list(filter(lambda x: len(x)>=sent_minlen, sents))
		for sent in sents:
			temp = content+sent
			if len(temp)>maxlen:
				now = time.strftime("%H:%M")
				if len(content)>minlen: return "{}\n【{}】\n发布时间: {}\n{}\n({})\n{}".format(now,title,date,content,source,url)
				else:
					content = temp
					return "{}\n【{}】\n发布时间: {}\n{}\n({})\n{}".format(now,title,date,content,source,url)
			content = temp
	now = time.strftime("%H:%M")
	return "{}\n【{}】\n发布时间: {}\n{}\n({})\n{}".format(now,title,date,content,source,url)

def send_group(message,gname):											 # 发送message到群名为gname的群
	rooms = itchat.get_chatrooms(update=True)							 # 更新微信群: 好像没什么用, 还得是手动发一下消息把微信群调出来
	flag = False
	for room in rooms:
		if room["NickName"]==gname:
			flag = True
			room_id = room["UserName"]
			itchat.send(message,room_id)
			return flag
	return flag

if __name__ == "__main__":
	pass

(完结)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值