NIKKEI TECHNOLOGY AND CAREER

正規表現を使った記事からの株価指数の抽出

この記事は Nikkei Advent Calendar 2020 7日目の記事です。

日本経済新聞社ではNIKKEI Wave という新しい技術や見せ方を検証するスマートフォンアプリを提供しています。 既存の電子版アプリに搭載される前の機能を先行して提供しており、ニュース記事から生成された動画を再生する機能もその一つです。

Waveのホーム画面

動画専用モードや記事の右上にある「動画ビュー」をタッチすることで自動で動画が生成されます。 基本的にはスライドショーのようにテキストを表示しますが、一部のデータをアニメーションで見せる機能もあります。

株価指数動画

株価指数の表示機能では動画の最初にアニメーションで指数を表示して以降、動画画面の上端・下端の帯にも情報を表示します。

株価指数画面

動画で表示される日経平均株価は、再生されるニュース記事の文章から抽出した情報を使っています。 この記事では、どのようにニュース記事から株価情報を抽出しているのかを紹介します。

必要な情報

株価指数の表示に必要な情報は大きく5つです。

情報説明
日付 (date)X月X日
指数 (index)日経平均株価, ダウ工業株30種平均, etc.
単位 (currency)円, ドル, ポイント, etc.
値 (price)メインの数値・値段
差 (diff)前日の値との比 (単位は値と同じ)

世界の市況 :マーケット :日経電子版 の情報をイメージしてもらえれば分かりやすいと思います。

株価指数は時時刻刻と変化するもので、「寄付」「前引け (午前の終値)」の指数も記事として出ますが、今回は「終値」のみ対象としています。

正規表現で抽出する

情報抽出には正規表現を採用しました。 正規表現は、「その表現にマッチ (match) する文字列の集合を指定」[1]できる表現方法です。 (株式|為替) のように 株式為替 のどちらも指定できる表現や、N.+ei のような N ではじまって ei で終わる文字を指定する表現のように、複数のパターンを 一つ の文字列で表現できるのが特徴です。

株価指数の情報の抽出に正規表現が適していると考えた理由として、ほぼ毎日公開される株価指数の記事は、文章が定型化・テンプレート化されている点が挙げられます。 特定のキーワードであれば単純な一致を調べればいいですが、数値のようにパターンが多い表現の検索には正規表現が有効だという側面もあります。

また、記事全体から情報を探すのではなく、冒頭の1~2文で株価指数について記述している記事を対象とするため、順序関係が固定されている正規表現でも事足ります。

なによりも、正規表現というホワイトボックスな手法は修正や追加が容易です。 機械学習とは異なり、学習データやモデルデータを必要としないため、実装・管理コストも低く済みます。

実装してみた

以降、Pythonで正規表現を使った実装について簡単に紹介します。

データ・パターン集め

日経電子版で「日経平均株価の終値」がメインになっている記事を例にパターンを探します。

一番単純なものは、

24日の東京株式市場で日経平均株価は4営業日ぶりに反発し、前週末比638円22銭(2.5%)高の2万6165円59銭で終えた。

https://www.nikkei.com/article/DGXMZO66550320U0A121C2000000/

のような、「〜で終えた」で終わるパターンです。 テンプレートとしては以下のようになるでしょう。

(date)日の……(index)は……(diff)円……[高安]の(price)円で終えた。

似たパターンとして

31日の東京株式市場で日経平均株価が6日続落し、終値は前日比629円安の2万1710円となった。

https://www.nikkei.com/article/DGXMZO62154190R30C20A7EA4000/

のように、数値の前に「終値は前日比〜」と書いてあるパターンもあります。

(date)日の……(index)……終値は前日比(diff)円……[高安]の(price)円……

その他、多くはないですが以下のような2文に跨っているパターンもあります。

25日の米株式市場でダウ工業株30種平均は3営業日ぶりに反落した。前日比173ドル77セント(0.6%)安の2万9872ドル47セントで終えた。

https://www.nikkei.com/article/DGXMZO66651790W0A121C2000000

文に対する正規表現

3つほどパターンを見つけたので、これらに対応するように正規表現を考えていきます。

流石に複数のパターンについて1文をまるごと判定する正規表現を書くと複雑になるため、 文始まりの表現、数値の正規表現、終値の表現、と分け、それぞれ判定してマッチした場合だけ抽出するようにしました。

前の節で紹介したパターンから、日付と指数は必ず文の前半部分に来ていることがわかります。 また、終値の表現も必ず数値の前後に位置しています。

そのことから、日付・指数パターン → 終値パターン → 数値パターンの順番で判定していくことにしました。 順に判定していくことによって、次の探索範囲を小さくできるというメリットもあります。

グルーピング

日付・指数パターンは一緒に表現し、グルーピングを使って必要な情報だけを抽出することにしました。

index_pattern = re.compile(
    "(?P<day>[0-9]+日)の.*"
    "(?P<index>日経平均株価|ダウ工業株30種平均)"
    "[はがの]"
    )

text = "25日の米株式市場でダウ工業株30種平均は3営業日ぶりに反落した。"
m = index_pattern.search(text)

m.group("index")
>> 'ダウ工業株30種平均'
m.group("day")
>> '25日'

index_pattern では dayindex を名前付きグループとして設定しています。 ?P<name> なしでも数字でグループを指定することもできますが、可読性が上がり、複数の正規表現パターンの場合でもグループの名前を同じにしておくことで管理がしやすくなります。

数値の抽出

通常、数値を取得するだけであれば正規表現は [0-9]+ で事足りるのですが、 万・億といった大数がついているため、[0-9千万億]+ と変更します。

また、円・銭のような複数の単位も同時に取得することを考慮し、以下のような正規表現 price_pattern を作成しました。

price_pattern = re.compile(
    "[0-9千万億]+"
    "(?P<currency>(ドル|円|ポイント)?)"
    "([0-9]+(セント|銭))?"
    )

# 2つの数値があるので、本来なら差の数値の判定後、値段を判定するようにします。
text = "前週末比638円22銭(2.5%)高の2万6165円59銭で終えた。"
price_pattern.search(text)
>> <re.Match object; span=(4, 11), match='638円22銭'>

数値と単位を抽出した後、単位・大数を取り除き、単純な数値へ変換する後処理が必要です。 2万6165 であれば漢字を取り除くだけで 26165 になるのですが、 2万5 だと 25 のように0の情報が抜けてしまうためです。

# 大数一覧
money_unit = ["億", "万", "千"]
money_unit_dict = {"千": 1000, "万": 10000, "億": 100000000}


def remove_numeral(num: str):
    num_value = 0
    for unit in money_unit:
        idx = num.find(unit)
        if idx != -1:
            num_value += int(num[:idx]) * money_unit_dict[unit]
            idx += 1
            num = num[idx:]
    if len(num) > 0:
        num_value += int(num)
    return num_value

remove_numeral("2万6165")
>> 26165

remove_numeral("2万5")
>> 20005

小数部分にあたる ([0-9]+(セント|銭)) は正規表現の上では分離したため、大数を取り除いた後、結合します。 この部分はアラビア数字以外含まないため [0-9]+ で問題ないです。

2万6165円59銭26165.59 に変換するフローをまとめると以下のようになります。

text = "2万6165円59銭で終えた。"
m = price_pattern.search(text)
money = m.group()

# 整数部・小数部にわける
currency = m.group("currency")
idx = text.find(currency)
idx_end = idx + len(currency)
money_int, money_dec = (
    money[:idx],
    money[idx_end:],
)

# 算用数字以外を取り除く
money_value = remove_numeral(money_int)
dec_value = re.search("[0-9]+", money_dec)
if dec_value:
    money_value = str(money_value) + "." + dec_value.group()

money_value
>> '26165.59'

終わりに

正規表現を組み合わせて株価指数を抽出する方法を簡単に紹介しました。

実際は記事には日付しか情報がないため何月かを推定したり、単位がない指数に対応したり、1文で判定後2文で判定するなど細々とした工夫がありますが、詳細は割愛しています。

今回はパターンが決まっており、短い文だったため、係り受けなどを考えずに正規表現だけで実装できましたが、種類が増えると厳しくなることがあると思います。 とはいえ、実装する最初の一手として正規表現はとても有用だということを感じていただければ幸いです。

[1]: re --- 正規表現操作 — Python 3.9.1rc1 ドキュメント, https://docs.python.org/ja/3/library/re.html .

白井穂乃
ENGINEER白井穂乃

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
カジュアル面談
Entry