在金融机构工作,每天早晨的晨会不可避免,晨会主要的环节是了解当天的上市公司公告。本文提供一个简易实现,自动抓取公告集锦,并邮件及时推送。
爬虫主要功能结构
notice_montage 可以做到拆箱即用。主要包括如下功能:
- 代码实现
- 初始化配置文件及日志
- 使用requests抓取网页页面
- 解析入口页,解析公告页内容
- 分析公告页内容并进行一些业务处理
- 邮件通知推送
- 代码部署
- windows机器使用定时计划自动执行
- 阿里云服务自动执行
代码实现
1.1 初始化配置及日志
项目含有邮件地址等敏感信息,将其独立出来形成配置文件,更利于项目部署。对配置文件利用ConfigParser模块进行解析,代码如下:
<pre>
def get_config_parser():
config_file_path = "notice_montage.ini"
cf = ConfigParser.ConfigParser()
cf.read(config_file_path)
return cf
解析配置
def init_config():
cf = get_config_parser()
global DEBUG, INTERVAL, WEBSITE
INTERVAL = int(cf.get("timeconf", "interval"))
DEBUG = cf.get("urlconf", "debug") == 'True'
WEBSITE = cf.get("urlconf", "website")
</pre>
配置文件notice_montage.ini,内容如下:
<pre>
[urlconf]
website = http://www.ccstock.cn/meiribidu/jiaoyitishi/
debug = True
[timeconf]
interval = 10
[mailconf]
接收通知的邮箱,多个邮箱使用,分割
to_list = xxx@foxmail.com ,xxx@qq.com
设置服务器
mail_host = smtp.exmail.qq.com
替换为发件邮箱用户名
mail_username = xxx
发件邮箱用户名
mail_user = xxx
发件邮箱口令密码
mail_pass = xxx
发件箱的后缀
mail_postfix = xxx
</pre>
爬虫部署后,自动运行,适当的记录一些文件日志,有利于追踪爬虫的运作运行状况,做到有迹可查。日志记录利用logging模块。
<pre>
日志记录器
logger = logging.getLogger()
def init_log():
if DEBUG:
handler = logging.StreamHandler()
else:
handler = logging.FileHandler("notice_montage.log")
formatter = logging.Formatter(
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
</pre>
测试情况下,日志输出到控制台,会更方便调试;正式部署后,输出到文件,要进行切换,直接修改notice_montage.ini中的debug值。
1.2 使用requests抓取网页页面
<pre>
def download_get_html(url, charset="utf-8", timeout=10, num_retries=3):
UA = random.choice(user_agent_list)
headers = {
'User-Agent': UA,
'Content-Type': 'text/html; charset=' + charset
}
try:
response = requests.get(url, headers=headers,
timeout=timeout)
response.encoding = charset
if response.status_code == 404:
logger.debug('get 404: %s ', url)
return None
else:
logger.debug('get : %s ', url)
return response.text
except:
if num_retries > 0:
time.sleep(10)
logger.debug('正在尝试,10S后将重新获取倒数第 %d 次', num_retries)
return download_get_html(url, charset, timeout, num_retries - 1)
else:
logger.debug('尝试也不好使了!取消访问')
return None
</pre>
download_get_html 是一个和业务无关的工具函数,主要使用request的get方法下载指定URL的内容。为了让下载更友好,模拟了User-Agent,并自动尝试3次。
1.3 使用BeautifulSoup解析页面内容
使用download_get_html获取页面内容后,使用BeautifulSoup对其进行解析,获取需要的信息。
解析入口列表页内容
入口页,是所有交易公告的列表清单,使用chrome的开发者工具,查看页面代码如下图:
列表按照时间进行倒序排列,晨报只需要最新的一条公告集锦页面,所以只需要找到class为listMain的div的第一个li类型子元素,代码如下:
<pre>
获取当期集锦的url
def parser_list_page(html_doc, now):
soup = BeautifulSoup(html_doc, 'lxml', from_encoding='utf-8')
# 只找第一个标签
tag = soup.find("div", class_="listMain").find("li")
link_tag = tag.find("a")
span_tag = tag.find("span")
page_url = link_tag['href']
# 截取日期
date_string = span_tag.string[0:10]
if date_string == now:
return page_url
else:
return None
</pre>
now参数为当期的时间,用于核对当期公告是否更新。已更新则返回次日集锦链接,未更新则返回空。
解析内容页内容
获取公告集锦页地址后,继续使用download_get_html获取内容页内容,如下图:
只需要找到id为newscontent的div即可,代码如下:
<pre>
def parser_item_page(html_doc, now):
soup = BeautifulSoup(html_doc, 'lxml', from_encoding='utf-8')
# title = soup.find("h1").string
newscontent = soup.find("div", id="newscontent")
html = newscontent.prettify()
return html
</pre>
1.4 分析页面内容
公告中数字都采用统计技术,并不利用阅读,我们利用正则将其进行处理。
例如:
<pre>
杰克股份(603337)7月23日晚间公告,截至2017年7月21日,公司2017年员工持股计划通过二级市场买入方式增持公司股票3,468,534股,占公司已发行股本1.68%。成交合计金额133,786,450.45元,成交均价38.57元。
</pre>
将其中的股数和成交金额进行转换,转换后如下:
<pre>
杰克股份(603337)7月23日晚间公告,截至2017年7月21日,公司2017年员工持股计划通过二级市场买入方式增持公司股票3,468,534股(346.8534万股),占公司已发行股本1.68%。成交合计金额133,786,450.45元(1.3378645045亿元),成交均价38.57元。
</pre>
这样阅读起来更符合习惯。格式化股份数代码如下:
<pre>
格式化股份计数
def transform_gu(lineText):
p = re.compile(u"[\d,]\d+股")
searchObj = re.findall(p, lineText)
if searchObj:
for x in xrange(0, len(searchObj)):
s1 = searchObj[x]
ns = filter(lambda ch: ch in '0123456789', s1)
nb = float(ns)
if nb >= 100000000:
s2 = str(nb / 100000000) + "亿股"
lineText = lineText.replace(s1, s1 + "(" + s2 + ")")
elif nb >= 10000:
s2 = str(nb / 10000) + "万股"
lineText = lineText.replace(s1, s1 + "(" + s2 + ")")
return lineText
</pre>
核心正则代码<code>u"[\d,]\d+股"</code>,表示至少含有任意个数字+,,然后连接至少一个数字,以“股”字结尾,匹配上3,468,534股这样的字符
串。
还可以对公告集锦匹配自选股池,做重点提醒;对大股东增持的利好消息,进行特殊提示。篇幅有限,涉及具体业务,处理方法也比较一致,本文就不进行展示。
1.5 邮件通知推送
公告获取后,立即推送给目标用户,做到快人一步。
<pre>
def send_notice_mail(html, now):
cf = get_config_parser()
to_list = cf.get("mailconf", "to_list").split(",")
mail_host = cf.get("mailconf", "mail_host")
mail_username = cf.get("mailconf", "mail_username")
mail_user = cf.get("mailconf", "mail_user")
mail_pass = cf.get("mailconf", "mail_pass")
mail_postfix = cf.get("mailconf", "mail_postfix")
me = "AStockMarketNoticeWatcher" + "<" +
mail_username + "@" + mail_postfix + ">"
msg = MIMEMultipart()
subject = now + ' 日 - 二级市场重要公告集锦'
msg['Subject'] = Header(subject, 'utf-8')
msg['From'] = me
msg['To'] = ";".join(to_list)
mail_msg = html
# 邮件正文内容
msg.attach(MIMEText(mail_msg, 'html', 'utf-8'))
try:
server = smtplib.SMTP()
server.connect(mail_host)
server.ehlo()
server.starttls()
server.login(mail_user, mail_pass)
server.sendmail(me, to_list, msg.as_string())
server.close()
logger.debug('sent mail successfully')
except smtplib.SMTPException, e:
logger.debug('Error: 无法发送邮件 %s ', repr(e))
</pre>
代码部署
2.1 windows 系统下定时任务自动执行
windows的定时任务,需要先编写bat脚本,代码非常简单,类似<code>python notice_montage.py</code>,autorun.bat 代码如下:
<pre>
:: 自动运行脚本
:: 需更换为本机路径
c:\python27\python.exe D:\xampp\htdocs\ding\morning\notice_montage.py %*
</pre>
在自动执行前,记得切换notice_montage.ini的debug值为False,留下日志文件,方便跟踪结果。
打开【计划任务程序】,选择右侧【操作】中的【创建任务】
需要注意的是,勾选【不管用户是否登录都要运行】,这样系统待机状态下也可以执行。【触发器】页签中,新建触发器:
根据需要,设置成每天定点到10点即可。(实际代码中,进行了自动尝试,如果公告10点没更新,会10分钟后尝试一次,自动尝试3次)
【操作】页签中,新建操作:
程序和脚本,选择前面创建的autorun.bat
确定后,就可以自动运行了,每天收到公告。
2.2 阿里云服务器自动执行
服务器部署好python/virtualenv后,编写crontab命令
<pre>
[root@iZ253amoxhcZ morning]# crontab -l
00 22 * * 0-5 /uy/flask-venv/bin/python notice_montage.py
</pre>
其中<code>/uy/flask-venv/bin/python</code>是virtualenv的路径,<code>00 22 * * 0-5</code> 表示周日-周五每天22点调度。
最后,爬虫源代码在(叮)[https://github.com/thebe2/ding] ,喜欢的请给个星。