上次村长介绍了如何快速在新闻中搜索特定词条的方法。这个问题在经济和金融学研究中非常常见:给定一组新闻标题和股票名称,我们想知道每个股票在这些新闻标题中分别出现多少次。村长的解决办法使用的是 R 和 JiebaR,这里大猫给出用 Python 的解法。
先来看一下数据集。首先我们有news
数据集,它保存着新闻 ID 和新闻标题:
news[:3,:]
NewsID | Title | |
---|---|---|
▪▪▪▪ | ▪▪▪▪ | |
0 | 57285603 | 富机达能正式挂牌 |
1 | 57463469 | 新能源过剩事出有因 多晶硅风电获准松绑 |
2 | 57463473 | 近9年元旦后首个交易日沪指走势一览 虎年怎开盘 |
3 rows × 2 columns
其中,NewsID
是新闻 ID,而Title
就是我们要搜索的新闻标题。为了方便后面处理,我们把NewsID
和Title
分别新建为两个独立的 list,命名为news_id
和 news_title
:
print(f'news_id: {news_id[:3]}')
print(f'news_title: {news_title[:3]}')
news_id: [57285603, 57463469, 57463473]
news_title: ['富机达能正式挂牌', '新能源过剩事出有因 多晶硅风电获准松绑', '近9年元旦后首个交易日沪指走势一览 虎年怎开盘']
第二个需要用到的股票列表stock_name
,它保存着所有需要检索的股票名称:
print(f'stock_name: {stock_name[:3]}')
stock_name: ['平安银行', '万科A', '国农科技']
最终生成的结果如下。其中stock
是股票名, news
是对应的股票名所在的新闻标题,news_id
是对应的新闻 ID。需要注意,对于同一个股票名,我们可能会成功匹配多条新闻。
stock | news_id | news | |
---|---|---|---|
▪▪▪▪ | ▪▪▪▪ | ▪▪▪▪ | |
0 | 三力士 | 57464083 | 浙江三力士橡胶股份有限公司关于产品提价的公告 |
1 | 东方园林 | 57464009 | 北京东方园林股份有限公司关于使用超募资金偿还银行贷款及补充流动资金的公告 |
2 | 中国人寿 | 57464089 | 中国人寿保险股份有限公司关于执行《保险合同相关会计处理规定》的提示性公告 |
3 rows × 3 columns
我们使用正则表达式来进行字符串匹配。在第一种解法中,我们只寻找新闻标题中出现的第一个股票。举个例子,假如我们的新闻标题是
平安银行和中国人寿公布第三季度财报 ”
那么这种方法只能找到标题中出现的第一个股票,也就是平安银行。在解法 2 中,我们会给出如何标题中出现所有股票的方法。
在进行所有操作之前,我们需要对股票名称进行清洗。我们知道,有些股票名前可能会带有“*”,比如*st 康达
。在正则表达式中,_
是一个特殊字符,表示是“匹配0次或任意次”。因此我们需要把_
从股票名中删去。最终生成清洗过的股票名列表stock_name_clean
:
stock_name_clean = [s.replace('*','') for s in stock_name]
print(f'stock_name_clean: {stock_name_clean[:3]}')
stock_name_clean: ['平安银行', '万科A', '国农科技']
接下来我们正式进行正则匹配。解法的核心是把候选的股票名称变成一个正则表达式能够接受的 pattern。在正则表达式中,竖杠 “|
” 用来表达“或”。所以我们只需要用竖杠把所有的候选股票名连起来:
import re
pattern = re.compile('|'.join(stock_name_clean))
print(f'pattern: {pattern}')
pattern: re.compile('平安银行|万科A|国农科技|世纪星源|深振业A|全新好|神州高铁|中国宝安|ST美丽|深物业A|南玻A|沙河股份|深康佳A|深中华A|ST神城|深粮控股|深华发A|深科技|深天地A|特力A|飞亚达A|深圳能源|国药一致|深深房A|富奥股份|大悦城|深桑达A|神州数码|中国天楹|华联控股|深南电A|深大通|中集集团|东旭蓝天|中洲控股|中航善达|深纺织A|泛海控股|ST康达|德赛电池|深天马A|方)
其中,pattern
就是正则表达式要匹配的模式。我们这里使用了re.compile
预先编译模式,这样在接下来的循环中就不需要一次次重复编译模式了,大大增加了效率。
接下来我们使用一个循环来从每一条新闻标题中寻找股票名。我们使用search
这个函数:
import datatable as dt
import pandas as pd
# results 用来保存最终结果
results = []
for id, title in tqdm(zip(news_id[:8], news_title[:8])):
# stock 是匹配的结果,如果匹配失败为None,如果匹配成功则为一个"re.Match"对象
stock = pattern.search(title)
# 如果结果不为None,那么保存结果
if stock:
results.append((stock.group(), id, title))
# 尝试打印结果的前三行
# 这里大猫使用了datatable包。如果读者使用pandas,那么就注销此条语句,并执行下一条
dt.Frame(results, names=['stock', 'news_id', 'news'])[:,:,dt.sort(f.stock)][:3,:]
# 如果使用pandas,则执行下面语句:
# pd.DataFrame(results, columns=['stock', 'news_id', 'news'])[:3]
8it [00:00, 3859.94it/s]
stock | news_id | news | |
---|---|---|---|
▪▪▪▪ | ▪▪▪▪ | ▪▪▪▪ | |
0 | 酒鬼酒 | 57463969 | 酒鬼酒股份有限公司更正公告 |
1 row × 3 columns
正则表达式的匹配是非常快的,尤其是在预先使用re.compile
编译了表达式以后。
我们的数据集包括25 万条新闻标题,需要在每条标题中搜索 3600 个可能的股票名称。在大猫的 Intel 十代 i7 移动版 CPU 上,只花费了 17 秒。 ”
在解法二中,我们使用re.findall
函数,它能够找到标题中出现的所有股票名。比如“平安银行和中国人寿公布第三季度财报”这则标题,代码能够识别出“平安银行”和“中国人寿”两个股票。
# 预先编译表达式
pattern = re.compile('|'.join(stock_name_clean))
# results 用来保存最终结果
results = []
for id, title in tqdm(zip(news_id, news_title)):
# matched_stock 是一个list,它保存了所有匹配到的股票名。如果列表为空,则意味着
# 标题中不包含任何股票
matched_stocks = pattern.findall(title)
# 如果结果不为空,保存结果
if len(matched_stocks) > 0:
# 这里使用了list comprehension, 对每个匹配的stock都新建一个tuple
out = [(s, id, title) for s in matched_stocks]
results.extend(out)
# 尝试打印结果的前三行
# 这里大猫使用了datatable包。如果读者使用pandas,那么就注销此条语句,并执行下一条
dt.Frame(results, names=['stock', 'news_id', 'news'])[:,:,dt.sort(f.news)][f.news_id==57467518,:]
# 如果使用pandas,则执行下面语句:
# pd.DataFrame(results, columns=['stock', 'news_id', 'news'])[:3]
249474it [00:19, 12576.39it/s]
stock | news_id | news | |
---|---|---|---|
▪▪▪▪ | ▪▪▪▪ | ▪▪▪▪ | |
0 | 赛象科技 | 57467518 | 金色阳光新股快车—1月5日申购版(赛象科技、奥普光电、皖新传媒) |
1 | 奥普光电 | 57467518 | 金色阳光新股快车—1月5日申购版(赛象科技、奥普光电、皖新传媒) |
2 | 皖新传媒 | 57467518 | 金色阳光新股快车—1月5日申购版(赛象科技、奥普光电、皖新传媒) |
3 rows × 3 columns
从上面的结果可以看到,对于news_id=57467518
这则新闻标题,代码成功识别了它里面包含的三个股票:赛象科技、奥普光电、皖新传媒。
正则表达式的匹配是非常快的,即使我们这次匹配的是“所有”股票而不是“出现的第一个股票”,代码也只多跑了 3 秒。
我们的数据集包括25 万条新闻标题,需要在每条标题中搜索 3600 个可能的股票名称。在大猫的 Intel 十代 i7 移动版 CPU 上,只花费了 20 秒。 ”
希望大家觉得这期推送有用!