【Python】画像の表からテキストを取り出しCSVファイルにする

2023/02/07に公開

まえがき

先日、以下のようなツイートをしました。
https://twitter.com/arafipro/status/1621535571675086850?s=20&t=ubi_jQYvlpm6phfHdcSYjA
まとめると以下のような感じ

せっかく実現できたのを自分だけ満足してもったいない。
そこで記事にアウトプットしてみなさんと共有したいと思います。

目的

以下の画像の表からテキストを取り出しCSVファイルに編集します。
ファイル名はimage.jpgとします。

ちなみに今回扱う画像は米国株の高配当銘柄の一覧を表示しています。
1行目にタイトルがあります。
2行目から2行ごとの表になっています。
表の数値は適当です。
各項目は以下の通りです。

① 銘柄名
② ティッカー | 株式 | 普通株
③ 配当利回り
④ 年間配当額
⑤ 配当権利落日
⑥ 配当権利落年
⑦ 配当額
⑧ 前回配当額

CSVファイルでは1行にまとめる必要があります。
まずはテキストを取り出しCSVファイルに保存します。
CSVファイル上で2行を1行にまとめていきます。

環境構築

開発環境

VS Codeでコーディングします。
VS Codeの拡張機能Jupyterをインストールします。
そして、VS Code上のJupyter notebookの環境を使います。
開発環境の構築が済んでない方は以下の記事を参考にしてみてください。

https://zenn.dev/arafipro/books/python-plotly-chart/viewer/python-plotly-chart-00

仮想環境

標準ライブラリであるvenvを使ってPythonの仮想環境を構築します。
まずはプロジェクトのディレクトリを作成します。
ディレクトリ名はpyhon-image-ocr-ytとします。
ディレクトリ名は適当につけていただいて結構です。
次に作成したディレクトリ内に移動して以下のコマンドを実行します。

python3 -m venv .venv

実行すると、新たにディレクトリ.venvが作成されます。
仮想環境が構築できたら、仮想環境を有効化するために以下のコマンドを実行します。

source .venv/bin/activate

また、仮想環境を終わらせる時は、以下のコマンドを実行して終了します。

deactivate

https://www.python.jp/install/windows/venv.html

ライブラリなどをインストール

必要なライブラリを仮想環境内にインストールします。
忘れないように、仮想環境を有効化しておきます。

source .venv/bin/activate

ひとつづつインストールしても良いですが、複数ある場合にはファイルrequirements.txtを使う方法があります。
ファイルrequirements.txtにインストールするパッケージを記入します。
今回は画像処理ライブラリPillowとOCRのライブラリPyOCRとtesseractを使用します。

requirements.txt
# Jupyter Notebook
ipykernel

# 画像処理ライブラリ
Pillow

# OCRのライブラリ
PyOCR
tesseract

ファイルrequirements.txtが用意できたら、以下のコマンドを実行して一括してインストールします。

pip3 install -r requirements.txt

ファイルを作成

新しいJupyter notebookのファイルを作成します。
ファイル名はimage-ocr.ipynbとします。
このファイル名も適当につけていただいて結構です。

touch image-ocr.ipynb

画像を読み込む

画像を読み込むために、先ほどインストールした画像処理ライブラリPillowを使います。
まずはパッケージPILからImageをインポートします。

from PIL import Image

image.jpgImage.open()で取得して変数imgに渡します。

img = Image.open("image.jpg")

imgを実行すると読み込んだ画像が表示されます。

読み込んだ画像をOCRする

OCRのライブラリPyOCRとtesseractを使用します。

pyocrをインポートします。

import pyocr

pyocr.get_available_tools()でモジュールを取得して変数toolsに渡します。

tools = pyocr.get_available_tools()
tools

変数toolsに以下の2つのモジュールが入っています。

[<module 'pyocr.tesseract'>,
 <module 'pyocr.libtesseract'>]

今回はtoolsの中の1つ目のモジュールを使います。
変数toolに渡します。
プログラミングでは0から始まるので慣れないうちは気をつけましょう。

tool = tools[0]

tool.image_to_string()で画像データからテキストを読み込んで変数txtに渡します。
tool.image_to_string()の引数が3つあります。
1つ目の引数imgが先ほど取得した画像データimgです。
2つ目の引数langには言語を指定します。今回は英数字を読み込むので値を"eng"を渡します。
3つ目の引数builderにはpyocr.builders.TextBuilder(tesseract_layout=3)を渡します。
pyocr.builders.TextBuilder()の引数tesseract_layoutにはレイアウト解析のオプションを指定します。
オプションは13種類ありデフォルトの値が3になっています。
読み込んでみて求めた結果でない時は数値を変えてみるといいでしょう。

txt = tool.image_to_string(
  img,
  lang = "eng",
  builder = pyocr.builders.TextBuilder(tesseract_layout=3)
)

print()を使ってtxtの内容を確認します。

print(txt)

結果は以下のようになります。

name dividend-yield ex-date amount

CHEVRON CORP 3.56% 2/15 $1.51
CVX | stock | common $6.04 2023 Last $1.42
INTEL CORP. 4.82% 2/06 $0.37
INTC | stock | common $1.46 2023 Last $0.37
Pioneer Natural Resources Co. 0.60% 2/10 $0.23

PXD | stock | common $0.92 2023 Last $0.23

結果を見ると行の間に空の行があります。
そこでprint()を使わずにtxtの内容を確認します。

txt

確認すると1行目と6行目の行末に改行\nが2つ連続あります。

'name dividend-yield ex-date amount\n\n
CHEVRON CORP 3.56% 2/15 $1.51\n
CVX | stock | common $6.04 2023 Last $1.42\n
INTEL CORP. 4.82% 2/06 $0.37\n
INTC | stock | common $1.46 2023 Last $0.37\n
Pioneer Natural Resources Co. 0.60% 2/10 $0.23\n\n
PXD | stock | common $0.92 2023 Last $0.23'

2度改行されることで結果的に空の行ができています。
そこで2つの改行\nを1つにします。
replace()を使って'\n\n''\n'に書き換えます。

txt = txt.replace('\n\n', '\n')

再びprint()を使ってtxtの内容を確認します。

print(txt)

空の行がなくなりました。

name dividend-yield ex-date amount
CHEVRON CORP 3.56% 2/15 $1.51
CVX | stock | common $6.04 2023 Last $1.42
INTEL CORP. 4.82% 2/06 $0.37
INTC | stock | common $1.46 2023 Last $0.37
Pioneer Natural Resources Co. 0.60% 2/10 $0.23
PXD | stock | common $0.92 2023 Last $0.23

今回は単純な原因でしたが、複雑な原因になるかもしれません。
その時はまた原因を見極めて対処します。

OCRしたテキストデータをファイルに保存する

OCRしたテキストデータをtest.csvに保存します。
pathlibをインポートします。

import pathlib

空のファイルtest.csvのパスを指定します。
pathlib.Path()を使って引数test.csvを渡して、変数new_csv_fileに代入します。
ここではtest.csv自体は作成されていません。

new_csv_file = pathlib.Path("test.csv")

空のファイルtest.csvにOCRしたテキストをwrite_text()を使って保存します。
write_text()にOCRしたテキストを持つ引数txtを渡します。
ここでOCRしたテキストがtest.csvに保存されました。

new_csv_file.write_text(txt)

CSVファイルの中を処理する

ここからメインイベントです。
大変なのでじっくり読み進めてください。
多分、これが俗に言う「前処理」だと思います。

改めてtest.csvの内容を見てみます。

name dividend-yield ex-date amount
CHEVRON CORP 3.56% 2/15 $1.51
CVX | stock | common $6.04 2023 Last $1.42
INTEL CORP. 4.82% 2/06 $0.37
INTC | stock | common $1.46 2023 Last $0.37
Pioneer Natural Resources Co. 0.60% 2/10 $0.23
PXD | stock | common $0.92 2023 Last $0.23

単語はスペースで区切られています。
そこで区切り文字をスペースでtest.csvから文字列を取り出します。

csvをインポートします。

import csv

空の配列を持つ変数linesを準備します。
次にtest.csvを開いてスペースで区切った単語を格納して変数readerに取得します。
最後に変数readerから各要素を変数rowに取り出して変数linesに追加します。
それではprint()と使って変数linesの中を確認します。

lines = []
with open ("test.csv", 'r') as f :
  reader = csv.reader(f, delimiter=' ')
  for row in reader:
    lines.append(row)
print(lines)
f.close()

実行すると以下のような結果になります。
7個の配列がありますね。

['name', 'dividend-yield', 'ex-date', 'amount'],
['CHEVRON', 'CORP', '3.56%', '2/15', '$1.51'],
['CVX', '|', 'stock', '|', 'common', '$6.04', '2023', 'Last', '$1.42'],
['INTEL', 'CORP.', '4.82%', '2/06', '$0.37'],
['INTC', '|', 'stock', '|', 'common', '$1.46', '2023', 'Last', '$0.37'],
['Pioneer', 'Natural', 'Resources', 'Co.', '0.60%', '2/10', '$0.23'],
['PXD', '|', 'stock', '|', 'common', '$0.92', '2023', 'Last', '$0.23']

各要素を確認するとわかりにくいですが、4行目の2つ目の要素にドット.が含まれています。
せっかくなので確認してみます。

lines[3][1]

今回は4行目の2個目なので31で指定しています。
実行すると以下のような結果になります。

'CORP.'

CSVの区切り文字でよく使われるのはカンマ,ですが、今回はわかりやすいようにコロン:を使います。

出力した結果の各配列を見ると要素数がバラバラです。
そこで各配列の要素数を調べます。
len()linesを渡して実行すると配列の数がわかります。
ここではlinesの中の配列の数を調べることになります。

len(lines)

実行すると以下のような結果になります。

7

先ほど出力した結果を確認すると7個の配列があるので合っています。
7個の各配列の要素数を調べます。
まずは1行目の配列の要素数lines[0]を調べましょう。

len(lines[0])

実行すると結果が4と表示されます。
1行目の配列は['name', 'dividend-yield', 'ex-date', 'amount']なので合っています。

残りの配列も同様に調べてもいいんですが、配列の数が多くなればとても大変です。
for文を使って繰り返し調べるようにします。
先ほど配列の数を調べた方法を利用します。
inの後に繰り返す回数を指定します。
この場合は配列の数から繰り返す回数を指定できます。
forの後のiに0から順番に入れていきます。
結果はprint()と使って表示します。

ただ数字だけだとわかりにくいのでf文字列を使ってみます。
引数に渡す文字列""の前にfをつけます。
文字列の中で変数を{}で囲うと値を代入して表示してくれます。
言葉で説明してもわかりにくいので実行してみましょう。

for i in range(len(lines)):
  print(f"{i+1}行目の要素数は{len(lines[i])}個です")

実行すると以下のような結果になります。

1行目の要素数は4個です
2行目の要素数は5個です
3行目の要素数は9個です
4行目の要素数は5個です
5行目の要素数は9個です
6行目の要素数は7個です
7行目の要素数は9個です

今回は要素数がどういう状態なのかを調べるだけなのでf文字列は使わなくても大丈夫です。
この後f文字列を使うので頭の片隅に置いておいてください。

1行目は項目名なので無視します。
2行目から見ていくと偶数行は要素数が違いますが奇数行は要素数が9個と一定です。

偶数行

まずは要素数が違う偶数行に注目します。
以下のように偶数行だけ取り出します。

['CHEVRON', 'CORP', '3.56%', '2/15', '$1.51'],
['INTEL', 'CORP.', '4.82%', '2/06', '$0.37'],
['Pioneer', 'Natural', 'Resources', 'Co.', '0.60%', '2/10', '$0.23'],

銘柄名の単語数が異なることで要素数が違っています。
逆に後ろから3個の要素はいずれの配列でも同様な項目内容となっています。
よって後ろから要素を取り出します。
6行目を使って各要素を取り出して見ましょう。

lines[5]

実行すると以下のように表示されます。

['Pioneer', 'Natural', 'Resources', 'Co.', '0.60%', '2/10', '$0.23']

一番後ろの配当額を変数amountに代入、
後ろから2番目の配当権利落日を変数ex_dateに代入、
後ろから3番目の配当利回りを変数dividend_yieldに代入します。

amount = lines[5][-1]          # 配当額
ex_date = lines[5][-2]         # 配当権利落日
dividend_yield = lines[5][-3]  # 配当利回り

コードを見るとわかりやすいですが、配列の要素はマイナス値で指定すると後ろから取り出すことができます。
[-1]は1番後ろ、[-2]は後ろから2番目・・・のようにです。
想定通りか確認しておきます。

print(amount)
print(ex_date)
print(dividend_yield)

実行すると以下のような結果になります。
ここまでは問題ありません。

$0.23
2/10
0.60%

残りは単語単位で分かれている銘柄名のみとなります。
これは1つの要素にまとめます。
まずは最初の要素は確実に銘柄名の最初に単語なので変数companyを用意してlines[5][0]を代入します。

company = lines[5][0]

残りの必要な要素を取り出します。
再び要素を取り出す配列を見ます。

['Pioneer', 'Natural', 'Resources', 'Co.', '0.60%', '2/10', '$0.23'],

この場合は銘柄名に必要な残りの要素は3つです。
その要素はlines[5][1]lines[5][2]lines[5][3]で取り出せます。
3つの要素を取り出すためにここでもfor文を使います。
for文を使えば単語数が増えても簡単に対応できます。
要素数の値が必要なのでlen(lines[5])を参考に考えていきます。
銘柄名に必要な最後の要素は後ろから数えると4個目の要素となります。
これは銘柄名の単語数には影響されません。
と言うことはlen(lines[5])から4引けばfor文で使えそうです。
それでは百聞は一見に如かずなのでコードを書いていきます。

for i in range(len(lines[5])-4):
  print(i)

実行すると以下のような結果になります。

0
1
2

lines[5][1]lines[5][2]lines[5][3]と同様に値を得るためには結果の値に1を足せばいいですね。

for i in range(len(lines[5])-4):
  print(i+1)

実行すると以下のような結果になります。

1
2
3

print()を使ってlines[5][i]として実行します。
ここでは要素の値ではなくて要素の変数自体をf文字列を使って表示します。

for i in range(len(lines[5])-4):
  print(f"lines[5][{i+1}]")

実行すると以下のような結果になります。

lines[5][1]
lines[5][2]
lines[5][3]

後はf文字列を外せば変数に値自体を受け取れます。

for i in range(len(lines[5])-4):
  print(lines[5][i+1])

実行すると以下のような結果になります。

Natural     # lines[5][1]の値
Resources   # lines[5][2]の値
Co.         # lines[5][3]の値

後は繋げ合わせれば完成です。
銘柄名の最初の単語が入った変数companyに加えていきます。

company = lines[5][0]
for i in range(len(lines[5])-4):
  company = company + lines[5][i+1]
company

実行すると以下のような結果になります。

'PioneerNaturalResourcesCo.'

これでは単語が詰まって繋がっています。
単語の間にスペースを入れます。
ここで便利なのがf文字列です。
for文の中で変数companyf文字列を使って都度代入します。

company = lines[5][0]
for i in range(len(lines[5])-4):
  company = f"{company} {lines[5][i+1]}"
company

実行すると以下のような結果になります。

'Pioneer Natural Resources Co.'

今度こそ上手くいきました。
f文字列を使うことでうまい具合にスペースを入れることができました。
ここでいったんまとめると以下のコードになります。

company = lines[5][0]
for i in range(len(lines[5])-4):
  company = f"{company} {lines[5][i+1]}"
company                        # 銘柄名
amount = lines[5][-1]          # 配当額
ex_date = lines[5][-2]         # 配当権利落日
dividend_yield = lines[5][-3]  # 配当利回り

これで偶数行の「前処理」を終えました。

奇数行

次は奇数行の「前処理」です。
ここまで6行目に処理をしたので相方の7行目を使います。
まずは7行目の要素を表示します。

lines[6]

実行すると以下のように表示されます。

['PXD', '|', 'stock', '|', 'common', '$0.92', '2023', 'Last', '$0.23']

画像の表では1つの項目だった要素が分かれています。
具体的には②の値は5個の要素、⑧の値も2個の要素に分かれています。

ただ問題ないのでこのまま進めていきます。
'|''stock''common''Last'が必要ありません。
あとは適宜変数を決めて必要な要素を一気に代入します。
そしてprint()を使って確認します。

ticker = lines[6][0]        # ティッカー
year_dividend = lines[6][5] # 年間配当額
year_pay_date = lines[6][6] # 配当権利落年
last_amount = lines[6][8]   # 前回配当額

print(ticker)
print(year_dividend)
print(year_pay_date)
print(last_amount)

実行すると以下のように表示されます。

PXD
$0.92
2023
$0.23

奇数行と偶数行の要素をまとめる

奇数行と偶数行で取り出した要素を1つにまとめます。
再び要素を取り出したコードをまとめておきます。

# 偶数行
company = lines[5][0]
for i in range(len(lines[5])-4):
  company = f"{company} {lines[5][i+1]}"
company                        # 銘柄名
amount = lines[5][-1]          # 配当額
ex_date = lines[5][-2]         # 配当権利落日
dividend_yield = lines[5][-3]  # 配当利回り

# 奇数行
ticker = lines[6][0]           # ティッカー
year_dividend = lines[6][5]    # 年間配当額
year_ex_date = lines[6][6]     # 配当権利落年
last_amount = lines[6][8]      # 前回配当額

ここで1つ問題があります。
配当権利落日が月日と年で分かれています。
f文字列を使って結合します。

ex_date = f"{year_ex_date}/{ex_date}"

ex_dateに月日と年が結合して代入されました。
f文字列を使って1つにまとめていきます。
最初に区切り文字をコロン:にすることを忘れないようにします。

f"{company}:{ticker}:{amount}:{dividend_yield}:{ex_date}:{year_dividend}\n"

実行すると以下のように表示されます。

'Pioneer Natural Resources Co.:PXD:$0.23:0.60%:2023/2/10:$0.92\n'

少しわかりにくいですが、これで2行を1行にできました。
残りの複数の2行ごとも1行にしなければいけません。
そこで再びfor文で回して2行を1行にします。
コードを見ながら考えて見ましょう。

# 偶数行
company = lines[5][0]
for i in range(len(lines[5])-4):
  company = f"{company} {lines[5][i+1]}"
company                               # 銘柄名
amount = lines[5][-1]                 # 配当額
ex_date = lines[5][-2]                # 配当権利落月日
dividend_yield = lines[5][-3]         # 配当利回り

# 奇数行
ticker = lines[6][0]                  # ティッカー
year_dividend = lines[6][5]           # 年間配当額
year_ex_date = lines[6][6]            # 配当権利落年
last_amount = lines[6][8]             # 前回配当額

ex_date = f"{year_ex_date}/{ex_date}" # 配当権利落年月日

奇数と偶数を求める必要があります。
奇数と偶数を求めるには変数に2をかければ偶数、2をかけて1を足せば奇数になります。
それを踏まえて偶数行を求めたインデックス番号と奇数行を求めたインデックス番号を変数jを使って変更します。
変数iはもう使っているので気をつけましょう。
具体的には偶数行に[2*j-1]、奇数行は[2*j]に変更します。
今回は偶数と奇数が交差してわかりにくいですが混同しないように気をつけます。

# 偶数行
company = lines[2*j-1][0]
for i in range(len(lines[2*j-1])-4):
  company = f"{company} {lines[2*j-1][i+1]}"
company                               # 銘柄名
amount = lines[2*j-1][-1]             # 配当額
ex_date = lines[2*j-1][-2]            # 配当権利落月日
dividend_yield = lines[2*j-1][-3]     # 配当利回り

# 奇数行
ticker = lines[2*j][0]                # ティッカー
year_dividend = lines[2*j][5]         # 年間配当額
year_ex_date = lines[2*j][6]          # 配当権利落年
last_amount = lines[2*j][8]           # 前回配当額

ex_date = f"{year_ex_date}/{ex_date}" # 配当権利落年月日

これで変数jを回せるようになりました。
次は回す回数を考えます。
改めて「前処理」するデータを見ます。

['name', 'dividend-yield', 'ex-date', 'amount'],
['CHEVRON', 'CORP', '3.56%', '2/15', '$1.51'],
['CVX', '|', 'stock', '|', 'common', '$6.04', '2023', 'Last', '$1.42'],
['INTEL', 'CORP.', '4.82%', '2/06', '$0.37'],
['INTC', '|', 'stock', '|', 'common', '$1.46', '2023', 'Last', '$0.37'],
['Pioneer', 'Natural', 'Resources', 'Co.', '0.60%', '2/10', '$0.23'],
['PXD', '|', 'stock', '|', 'common', '$0.92', '2023', 'Last', '$0.23']

1行目lines[0]は項目名でした。
2行目lines[1]は1個目のデータの偶数行です。
3行目lines[2]は1個目のデータの奇数行です。
2行目と3行目を処理して1個の配列にまとめます。
後は同様に繰り返します。
そして配列数len(lines)は7になります。
ただここで単純にlen(lines)で変数jを回していくとエラーになります。
回数がオーバーしてしまうからです。
そこでオーバーしないように計算式を考えます。
今回の場合は3を求めることになります。
いろいろ求め方はありますが今回は2で割って小数点以下を切り捨てるようにします。
小数点以下を切り捨てるにはmathモジュールのfloor()を使います。

import math
math.floor(len(lines)/2)

実行すると以下のように表示されます。

3

これで準備ができたのでコードを書いていきます。

for j in range(math.floor(len(lines)/2)):
  # 偶数行
  company = lines[2*j-1][0]
  for i in range(len(lines[2*j-1])-4):
    company = f"{company} {lines[2*j-1][i+1]}"
  company                               # 銘柄名
  amount = lines[2*j-1][-1]             # 配当額
  ex_date = lines[2*j-1][-2]            # 配当権利落月日
  dividend_yield = lines[2*j-1][-3]     # 配当利回り

  # 奇数行
  ticker = lines[2*j][0]                # ティッカー
  year_dividend = lines[2*j][5]         # 年間配当額
  year_ex_date = lines[2*j][6]          # 配当権利落年
  last_amount = lines[2*j][8]           # 前回配当額

  ex_date = f"{year_ex_date}/{ex_date}" # 配当権利落年月日
  print(f"{company}:{ticker}:{amount}:{dividend_yield}:{ex_date}:{year_dividend}")

実行しましたがエラーになりました。
for文で回しているjの値を確認します。

for j in range(math.floor(len(lines)/2)):
  print(j)

実行すると以下のように表示されます。

0
1
2

うっかりしていました。
0から回してしまうと最初がlines[-1]とインデックスが-1になってしまいます。
lines[1]lines[2]から処理できるようにインデックスを指定する数式を変更します。

for j in range(math.floor(len(lines)/2)):
  # 偶数行
  company = lines[2*j+1][0]
  for i in range(len(lines[2*j+1])-4):
    company = f"{company} {lines[2*j+1][i+1]}"
  company                               # 銘柄名
  amount = lines[2*j+1][-1]             # 配当額
  ex_date = lines[2*j+1][-2]            # 配当権利落月日
  dividend_yield = lines[2*j+1][-3]     # 配当利回り

  # 奇数行
  ticker = lines[2*(j+1)][0]                # ティッカー
  year_dividend = lines[2*(j+1)][5]         # 年間配当額
  year_ex_date = lines[2*(j+1)][6]          # 配当権利落年
  last_amount = lines[2*(j+1)][8]           # 前回配当額

  ex_date = f"{year_ex_date}/{ex_date}" # 配当権利落年月日
  print(f"{company}:{ticker}:{amount}:{dividend_yield}:{ex_date}:{year_dividend}")

実行すると以下のように表示されます。

CHEVRON CORP:CVX:$1.51:3.56%:2023/2/15:$6.04
INTEL CORP.:INTC:$0.37:4.82%:2023/2/06:$1.46
Pioneer Natural Resources Co.:PXD:$0.23:0.60%:2023/2/10:$0.92

やっと想定通りに表示できました。
後はファイルに保存するだけです。
あと一息なのでがんばりましょう。
前のコードに少し手を加えます。
まずはfor文の前に保存するテキストを格納する空の変数outを設定します。
次にfor文の中の最後に空の変数outに出力した文字列を追加します。
最後にprint(out)で想定通りなのか確認します。

out = ""
for j in range(math.floor(len(lines)/2)):
  # 偶数行
  company = lines[2*j+1][0]
  for i in range(len(lines[2*j+1])-4):
    company = f"{company} {lines[2*j+1][i+1]}"
  company                               # 銘柄名
  amount = lines[2*j+1][-1]             # 配当額
  ex_date = lines[2*j+1][-2]            # 配当権利落月日
  dividend_yield = lines[2*j+1][-3]     # 配当利回り

  # 奇数行
  ticker = lines[2*(j+1)][0]                # ティッカー
  year_dividend = lines[2*(j+1)][5]         # 年間配当額
  year_ex_date = lines[2*(j+1)][6]          # 配当権利落年
  last_amount = lines[2*(j+1)][8]           # 前回配当額

  ex_date = f"{year_ex_date}/{ex_date}" # 配当権利落年月日
  out += f"{company}:{ticker}:{amount}:{dividend_yield}:{ex_date}:{year_dividend}\n"
print(out)

実行すると以下のような結果になります。
変数outの内容が想定通り表示されました。

CHEVRON CORP:CVX:$1.51:3.56%:2023/2/15:$6.04
INTEL CORP.:INTC:$0.37:4.82%:2023/2/06:$1.46
Pioneer Natural Resources Co.:PXD:$0.23:0.60%:2023/2/10:$0.92

変数outの内容をtest.csvに保存します。
open()を使って第1引数は開くファイル"test.csv"、第2引数には書き込み可能にする"w"を渡します。
test.csvを開いたらまずファイル内をtruncate()で空にします。
変数outを引数にしてwrite()で保存します。
最後はclose()してファイルを閉じます。

with open("test.csv", "w") as f:
  f.truncate()
  f.write(out)
  f.close()

実行してtest.csvを確認します。

CHEVRON CORP:CVX:$1.51:3.56%:2023/2/15:$6.04
INTEL CORP.:INTC:$0.37:4.82%:2023/2/06:$1.46
Pioneer Natural Resources Co.:PXD:$0.23:0.60%:2023/2/10:$0.92

やっとこれで完成しました。
長丁場でしたがおつかれさまでした。

あとがき

データを取り出して保存するところまでは面倒でありませんでした。
CSVファイルのデータを処理していくのはとても大変でした。
ただ、面倒でも一度だけプログラミングを頑張ってみてください。
プログラムで実現できれば、次からは代わりにやってくれます。

CSVファイルにすればいろんなことに展開できます。
CSVファイルからデータベースに登録するとか。
私の場合はツイートしたように、CSVファイルからHTMLファイルにしました。

アウトプットしたことで思っていた改善点を解決するヒントも得ることができました。

GitHubで編集を提案

Discussion