在金融機(jī)構(gòu)工作,每天早晨的晨會(huì)不可避免,晨會(huì)主要的環(huán)節(jié)是了解當(dāng)天的上市公司公告。本文提供一個(gè)簡(jiǎn)易實(shí)現(xiàn),自動(dòng)抓取公告集錦,并郵件及時(shí)推送。
爬蟲主要功能結(jié)構(gòu)
notice_montage 可以做到拆箱即用。主要包括如下功能:
- 代碼實(shí)現(xiàn)
- 初始化配置文件及日志
- 使用requests抓取網(wǎng)頁頁面
- 解析入口頁,解析公告頁內(nèi)容
- 分析公告頁內(nèi)容并進(jìn)行一些業(yè)務(wù)處理
- 郵件通知推送
- 代碼部署
- windows機(jī)器使用定時(shí)計(jì)劃自動(dòng)執(zhí)行
- 阿里云服務(wù)自動(dòng)執(zhí)行
代碼實(shí)現(xiàn)
1.1 初始化配置及日志
項(xiàng)目含有郵件地址等敏感信息,將其獨(dú)立出來形成配置文件,更利于項(xiàng)目部署。對(duì)配置文件利用ConfigParser模塊進(jìn)行解析,代碼如下:
<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,內(nèi)容如下:
<pre>
[urlconf]
website = http://www.ccstock.cn/meiribidu/jiaoyitishi/
debug = True
[timeconf]
interval = 10
[mailconf]
接收通知的郵箱,多個(gè)郵箱使用,分割
to_list = xxx@foxmail.com ,xxx@qq.com
設(shè)置服務(wù)器
mail_host = smtp.exmail.qq.com
替換為發(fā)件郵箱用戶名
mail_username = xxx
發(fā)件郵箱用戶名
mail_user = xxx
發(fā)件郵箱口令密碼
mail_pass = xxx
發(fā)件箱的后綴
mail_postfix = xxx
</pre>
爬蟲部署后,自動(dòng)運(yùn)行,適當(dāng)?shù)挠涗浺恍┪募罩荆欣谧粉櫯老x的運(yùn)作運(yùn)行狀況,做到有跡可查。日志記錄利用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>
測(cè)試情況下,日志輸出到控制臺(tái),會(huì)更方便調(diào)試;正式部署后,輸出到文件,要進(jìn)行切換,直接修改notice_montage.ini中的debug值。
1.2 使用requests抓取網(wǎng)頁頁面
<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后將重新獲取倒數(shù)第 %d 次', num_retries)
return download_get_html(url, charset, timeout, num_retries - 1)
else:
logger.debug('嘗試也不好使了!取消訪問')
return None
</pre>
download_get_html 是一個(gè)和業(yè)務(wù)無關(guān)的工具函數(shù),主要使用request的get方法下載指定URL的內(nèi)容。為了讓下載更友好,模擬了User-Agent,并自動(dòng)嘗試3次。
1.3 使用BeautifulSoup解析頁面內(nèi)容
使用download_get_html獲取頁面內(nèi)容后,使用BeautifulSoup對(duì)其進(jìn)行解析,獲取需要的信息。
解析入口列表頁內(nèi)容
入口頁,是所有交易公告的列表清單,使用chrome的開發(fā)者工具,查看頁面代碼如下圖:

列表按照時(shí)間進(jìn)行倒序排列,晨報(bào)只需要最新的一條公告集錦頁面,所以只需要找到class為listMain的div的第一個(gè)li類型子元素,代碼如下:
<pre>
獲取當(dāng)期集錦的url
def parser_list_page(html_doc, now):
soup = BeautifulSoup(html_doc, 'lxml', from_encoding='utf-8')
# 只找第一個(gè)標(biāo)簽
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參數(shù)為當(dāng)期的時(shí)間,用于核對(duì)當(dāng)期公告是否更新。已更新則返回次日集錦鏈接,未更新則返回空。
解析內(nèi)容頁內(nèi)容
獲取公告集錦頁地址后,繼續(xù)使用download_get_html獲取內(nèi)容頁內(nèi)容,如下圖:

只需要找到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 分析頁面內(nèi)容
公告中數(shù)字都采用統(tǒng)計(jì)技術(shù),并不利用閱讀,我們利用正則將其進(jìn)行處理。
例如:
<pre>
杰克股份(603337)7月23日晚間公告,截至2017年7月21日,公司2017年員工持股計(jì)劃通過二級(jí)市場(chǎng)買入方式增持公司股票3,468,534股,占公司已發(fā)行股本1.68%。成交合計(jì)金額133,786,450.45元,成交均價(jià)38.57元。
</pre>
將其中的股數(shù)和成交金額進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換后如下:
<pre>
杰克股份(603337)7月23日晚間公告,截至2017年7月21日,公司2017年員工持股計(jì)劃通過二級(jí)市場(chǎng)買入方式增持公司股票3,468,534股(346.8534萬股),占公司已發(fā)行股本1.68%。成交合計(jì)金額133,786,450.45元(1.3378645045億元),成交均價(jià)38.57元。
</pre>
這樣閱讀起來更符合習(xí)慣。格式化股份數(shù)代碼如下:
<pre>
格式化股份計(jì)數(shù)
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>,表示至少含有任意個(gè)數(shù)字+,,然后連接至少一個(gè)數(shù)字,以“股”字結(jié)尾,匹配上3,468,534股這樣的字符
串。
還可以對(duì)公告集錦匹配自選股池,做重點(diǎn)提醒;對(duì)大股東增持的利好消息,進(jìn)行特殊提示。篇幅有限,涉及具體業(yè)務(wù),處理方法也比較一致,本文就不進(jìn)行展示。
1.5 郵件通知推送
公告獲取后,立即推送給目標(biāo)用戶,做到快人一步。
<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 + ' 日 - 二級(jí)市場(chǎng)重要公告集錦'
msg['Subject'] = Header(subject, 'utf-8')
msg['From'] = me
msg['To'] = ";".join(to_list)
mail_msg = html
# 郵件正文內(nèi)容
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: 無法發(fā)送郵件 %s ', repr(e))
</pre>
代碼部署
2.1 windows 系統(tǒng)下定時(shí)任務(wù)自動(dòng)執(zhí)行
windows的定時(shí)任務(wù),需要先編寫bat腳本,代碼非常簡(jiǎn)單,類似<code>python notice_montage.py</code>,autorun.bat 代碼如下:
<pre>
:: 自動(dòng)運(yùn)行腳本
:: 需更換為本機(jī)路徑
c:\python27\python.exe D:\xampp\htdocs\ding\morning\notice_montage.py %*
</pre>
在自動(dòng)執(zhí)行前,記得切換notice_montage.ini的debug值為False,留下日志文件,方便跟蹤結(jié)果。
打開【計(jì)劃任務(wù)程序】,選擇右側(cè)【操作】中的【創(chuàng)建任務(wù)】

需要注意的是,勾選【不管用戶是否登錄都要運(yùn)行】,這樣系統(tǒng)待機(jī)狀態(tài)下也可以執(zhí)行?!居|發(fā)器】頁簽中,新建觸發(fā)器:

根據(jù)需要,設(shè)置成每天定點(diǎn)到10點(diǎn)即可。(實(shí)際代碼中,進(jìn)行了自動(dòng)嘗試,如果公告10點(diǎn)沒更新,會(huì)10分鐘后嘗試一次,自動(dòng)嘗試3次)
【操作】頁簽中,新建操作:

程序和腳本,選擇前面創(chuàng)建的autorun.bat
確定后,就可以自動(dòng)運(yùn)行了,每天收到公告。
2.2 阿里云服務(wù)器自動(dòng)執(zhí)行
服務(wù)器部署好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點(diǎn)調(diào)度。
最后,爬蟲源代碼在(叮)[https://github.com/thebe2/ding] ,喜歡的請(qǐng)給個(gè)星。