├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── data
├── external
│ └── companies.csv
├── interim
│ └── .gitkeep
├── processed
│ └── .gitkeep
└── raw
│ ├── .gitkeep
│ └── HTML
│ ├── あおぞら銀行
│ └── qualitative.htm
│ ├── アサヒグループホールディングス
│ └── qualitative.htm
│ ├── アスクル
│ └── qualitative.htm
│ ├── アドバンスト
│ └── qualitative.htm
│ ├── イオン北海道
│ └── qualitative.htm
│ ├── ウェザーニューズ
│ └── qualitative.htm
│ ├── エーザイ
│ └── qualitative.htm
│ ├── オリンパス
│ └── qualitative.htm
│ ├── コジマ
│ └── qualitative.htm
│ ├── サッポロホールディングス
│ └── qualitative.htm
│ ├── シャープ
│ └── qualitative.htm
│ ├── セブン&アイ・ホールディングス
│ └── qualitative.htm
│ ├── ソニー
│ └── qualitative.htm
│ ├── ソフトバンク
│ └── qualitative.htm
│ ├── トレイダーズホールディングス
│ └── qualitative.htm
│ ├── パナソニック
│ └── qualitative.htm
│ ├── ビックカメラ
│ └── qualitative.htm
│ ├── ブリヂストン
│ └── qualitative.htm
│ ├── プロネクサス
│ └── qualitative.htm
│ ├── マネーパートナーズグループ
│ └── qualitative.htm
│ ├── マルハニチロ
│ └── qualitative.htm
│ ├── ヨネックス
│ └── qualitative.htm
│ ├── ライフネット生命保険
│ └── qualitative.htm
│ ├── リンナイ
│ └── qualitative.htm
│ ├── 三井不動産
│ └── qualitative.htm
│ ├── 三井物産
│ └── qualitative.htm
│ ├── 三和ホールディングス
│ └── qualitative.htm
│ ├── 三菱商事
│ └── qualitative.htm
│ ├── 三菱地所
│ └── qualitative.htm
│ ├── 丸紅
│ └── qualitative.htm
│ ├── 京セラ
│ └── qualitative.htm
│ ├── 住友ゴム工業
│ └── qualitative.htm
│ ├── 味の素
│ └── qualitative.htm
│ ├── 大和証券グループ本社
│ └── qualitative.htm
│ ├── 大東銀行
│ └── qualitative.htm
│ ├── 川崎重工業
│ └── qualitative.htm
│ ├── 帝人
│ └── qualitative.htm
│ ├── 愛知銀行
│ └── qualitative.htm
│ ├── 日本マクドナルドホールディングス
│ └── qualitative.htm
│ ├── 日本取引所グループ
│ └── qualitative.htm
│ ├── 日野自動車
│ └── qualitative.htm
│ ├── 東京海上ホールディングス
│ └── qualitative.htm
│ ├── 松屋フーズホールディングス
│ └── qualitative.htm
│ ├── 横河ブリッジホールディングス
│ └── qualitative.htm
│ ├── 武田薬品工業
│ └── qualitative.htm
│ ├── 江崎グリコ
│ └── qualitative.htm
│ ├── 窪田製薬ホールディングス
│ └── qualitative.htm
│ ├── 花王
│ └── qualitative.htm
│ ├── 象印マホービン
│ └── qualitative.htm
│ ├── 近鉄グループホールディングス
│ └── qualitative.htm
│ ├── 高知銀行
│ └── qualitative.htm
│ ├── ENEOSホールディングス
│ └── qualitative.htm
│ ├── SGホールディングス
│ └── qualitative.htm
│ └── TAKARA & COMPANY
│ └── qualitative.htm
├── docs
├── README_usage.md
└── images
│ └── usage
│ ├── after-login.png
│ ├── browser_setting.png
│ ├── clone_git_repository.png
│ ├── clone_git_repository_execute.png
│ ├── clone_git_repository_set_url.png
│ ├── copy_from_github.png
│ ├── copy_to_project.png
│ ├── create_conda_environment.png
│ ├── create_environment_in_terminal.png
│ ├── open_in_studio_lab.png
│ ├── open_notebook.png
│ ├── referral_code.PNG
│ ├── run_notebook.png
│ ├── signin.PNG
│ └── start_runtime.png
├── environment.yml
├── notebooks
├── .gitkeep
├── 01_how_to_extract_from_html.ipynb
├── 02_how_to_extract_segment_data_from_html.ipynb
├── HomeworkTemplete.pptx
├── a1_financial_result_to_dataframe.ipynb
├── a2_financial_result_to_dataframe_all.ipynb
├── images
│ ├── error_example_01.png
│ ├── error_example_02.png
│ ├── error_example_03.png
│ ├── error_example_04.png
│ ├── error_example_05.png
│ ├── error_example_06.png
│ ├── error_example_11.png
│ ├── error_example_12.png
│ ├── error_example_13.png
│ ├── error_example_14.png
│ ├── financial_html.png
│ ├── homework.png
│ ├── how_to_download_001.png
│ ├── how_to_download_002.png
│ ├── html_history.jpg
│ ├── html_parser.png
│ ├── segment_information.png
│ ├── step_001.png
│ ├── step_002.png
│ ├── step_003.png
│ ├── step_004.png
│ ├── step_005.png
│ └── top.jpg
└── sample.csv
├── pyproject.toml
├── requirements.txt
├── scripts
├── __init__.py
├── financial_result_reader.py
└── financial_result_to_dataframe.py
└── tests
├── __init__.py
├── data
└── raw
│ ├── exceptions
│ ├── account_not_found.htm
│ ├── period_not_exist.htm
│ ├── too_little_segment_table.htm
│ ├── too_much_segment_tables.htm
│ └── value_read_failed.htm
│ ├── sample1.htm
│ ├── sample2.htm
│ ├── sample3.htm
│ ├── sample4.htm
│ ├── sample5.htm
│ ├── sample6.htm
│ ├── sample7.htm
│ └── sample_list.txt
├── notebooks
└── test_financial_result_read.ipynb
├── test_financial_result_reader.py
└── test_financial_result_to_dataframe.py
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Source Code Check
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | ci:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Setup Python
15 | uses: conda-incubator/setup-miniconda@v2
16 | with:
17 | auto-update-conda: true
18 | python-version: "3.10"
19 | environment-file: environment.yml
20 | activate-environment: jpx-frde
21 | - name: Test
22 | shell: bash -l {0}
23 | run: pre-commit run --all-files
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | /data
163 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | exclude: "^tests/data"
4 | repos:
5 | - repo: https://github.com/pre-commit/pre-commit-hooks
6 | rev: v4.2.0
7 | hooks:
8 | - id: trailing-whitespace
9 | - id: end-of-file-fixer
10 | - id: check-yaml
11 | - id: check-added-large-files
12 |
13 | - repo: https://github.com/pycqa/flake8
14 | rev: 5.0.4
15 | hooks:
16 | - id: flake8
17 | entry: pflake8
18 | additional_dependencies: [pyproject-flake8]
19 |
20 | - repo: https://github.com/psf/black
21 | rev: 22.6.0
22 | hooks:
23 | - id: black
24 |
25 | - repo: https://github.com/pycqa/isort
26 | rev: 5.10.1
27 | hooks:
28 | - id: isort
29 |
30 | - repo: https://github.com/pre-commit/mirrors-mypy
31 | rev: v0.971
32 | hooks:
33 | - id: mypy
34 |
35 | - repo: local
36 | hooks:
37 | - id: pytest
38 | name: pytest
39 | stages: [commit]
40 | language: system
41 | entry: pytest -v tests/
42 | types: [python]
43 | pass_filenames: false
44 | always_run: true
45 |
46 | - id: pytest-cov
47 | name: pytest-cov
48 | stages: [push]
49 | language: system
50 | entry: pytest -vv --cov=scripts --cov-report=term-missing --cov-report=xml tests/
51 | types: [python]
52 | pass_filenames: false
53 | always_run: true
54 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Our Pledge
2 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
3 |
4 | ## Our Standards
5 | Examples of behavior that contributes to creating a positive environment include:
6 |
7 | - Using welcoming and inclusive language
8 | - Being respectful of differing viewpoints and experiences
9 | - Gracefully accepting constructive criticism
10 | - Focusing on what is best for the community
11 | - Showing empathy towards other community members
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | - The use of sexualized language or imagery and unwelcome sexual attention or advances
16 | - Trolling, insulting/derogatory comments, and personal or political attacks
17 | - Public or private harassment
18 | - Publishing others’ private information, such as a physical or electronic address, without explicit permission
19 | - Other conduct which could reasonably be considered inappropriate in a professional setting
20 |
21 | ## Our Responsibilities
22 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
23 |
24 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
25 |
26 | ## Scope
27 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
28 |
29 | ## Enforcement
30 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at inf_dev@jpx.co.jp. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
31 |
32 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership.
33 |
34 | ## Attribution
35 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
36 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Guidelines for repository contributors
2 |
3 | 私たちのプロジェクトへのコントリビュートに関心を示していただきありがとうございます。
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Our standard
12 |
13 | 本リポジトリに関して、問題の報告、建設的な批評、ドキュメントの更新、プルリクエストの要求やパッチの提供、その他の活動を通じて貢献してくれる方に深く感謝いたします。
14 |
15 | 本リポジトリに参加する全ての人に対してのハラスメントを禁止します。また、個人攻撃、煽り、公的私的な嫌がらせ、侮辱、その他コントリビュータとして恥ずべき行動を禁止します。
16 |
17 | 我々は、行動規範に従っていないissue、プルリクエスト等に対して、削除、編集、拒否の権利と責任があります。
18 |
19 | ## With issues
20 | 我々は、GitHub issuesを参考にコンテンツの改善やバグ修正を行いますので、本リポジトリに関してのご要望は、issuesにご意見をいただけますと幸いです。(なお、投稿の際には、過去に同じ要望がないかご確認ください。)
21 |
22 | ## With pull requests
23 | 本リポジトリに含まれるものへの改善、加筆等をプルリクエストにて受け付けております。
24 | また、branchは、main(release用), dev, feature(加筆修正用)で管理をしております。
25 |
26 | なお、プルリクエストを出していただく際には、以下の観点にご注意ください。
27 |
28 | - 幅広い読者層に読まれることを想定してください。
29 | - 複数のブランチがありますが、プルリクエストはForkの上、**devブランチ** に対して行ってください。
30 | - 基本的には二つ以上のコミットに分けず、コミットをまとめて行ってください。
31 | - 文献等を参考にした場合は、下記を参考に参考元を必ず明記してください。
32 |
33 | (ex.)被引用箇所を「 」で囲い、引用元のタイトルとURLを(タイトル,URL)記載する。
34 | (具体例)ファンダメンタルズ
35 | 「「経済の基礎的条件」のことで、経済のマクロ面あるいは個別企業の財務状況などのミクロ面についての指標を意味します」
36 | (JPX用語集,https://www.jpx.co.jp/glossary/ha/530.html)
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 JPX Market Innovation & Research, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 決算短信セグメント情報のデータ抽出ハンズオン
2 |
3 | [](https://github.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/actions/workflows/ci.yml)
4 | [](https://github.com/pre-commit/pre-commit)
5 | [](https://github.com/psf/black)
6 | [](https://github.com/PyCQA/flake8)
7 | [](https://pycqa.github.io/isort/)
8 | [](https://github.com/python/mypy)
9 |
10 |
11 | HTML 化された決算短信から、セグメント情報を抽出する方法が学べるハンズオンです。
12 |
13 | 
14 |
15 | HTML 化された決算短信は、[適時開示情報閲覧サービス](https://www.release.tdnet.info/inbs/I_main_00.html)か、[東証上場会社情報サービス](https://www.jpx.co.jp/listing/co-search/index.html)から取得できます。データを取得し、セグメント情報を抽出する方法はハンズオン資料を参照してください。
16 |
17 | ## ハンズオンコンテンツ
18 |
19 | 1. HTML から情報を抽出する方法を学ぶ [](https://studiolab.sagemaker.aws/import/github/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/blob/main/notebooks/01_how_to_extract_from_html.ipynb)
20 | * HTML とは
21 | * Python による HTML からの情報抽出
22 | * Exercise1: 目的の HTML 要素を検索する
23 | * Exercise2: 目的の HTML 要素へ移動する
24 | 2. HTML 化された決算短信からセグメント情報を抽出する方法を学ぶ [](https://studiolab.sagemaker.aws/import/github/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/blob/main/notebooks/02_how_to_extract_segment_data_from_html.ipynb)
25 | * HTML 化された決算短信とは
26 | * Exercies1: 決算短信 HTML ファイルからセグメント情報を抽出する
27 | * Exercies2: セグメント情報の抽出が失敗する理由を分析する
28 |
29 | ※本ハンズオンはあらゆる企業の HTML からセグメント情報が抽出できるプログラムを提供するものではありません。抽出が失敗する理由を理解し、修正箇所を特定できる技能を身に着けることを目的としています。
30 |
31 | ## ハンズオンの進め方
32 |
33 | Amazon SageMaker Studio Lab を使用し簡単に始めることができます。ハンズオンのはじめ方は、 [ハンズオンの進め方](docs/README_usage.md)を参照してください。
34 |
35 | ハンズオンは2部構成を想定して作られています。
36 |
37 | * Day1: ハンズオンコンテンツを実施し、HTMLから情報を抽出する方法を身に着ける。宿題として興味ある企業からセグメント情報の抽出を試み、HomeworkTemplateに記載する。
38 | * Day2: Homeworkの共有を行う。読み取り結果の統計を参照しながら、発行体に促すべき記載の方式についてディスカッションする。
39 | * 決算短信HTMLの読み取り可否状況レポート [](https://studiolab.sagemaker.aws/import/github/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/blob/main/notebooks/a1_financial_result_to_dataframe.ipynb)
40 |
--------------------------------------------------------------------------------
/data/interim/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/data/interim/.gitkeep
--------------------------------------------------------------------------------
/data/processed/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/data/processed/.gitkeep
--------------------------------------------------------------------------------
/data/raw/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/data/raw/.gitkeep
--------------------------------------------------------------------------------
/docs/README_usage.md:
--------------------------------------------------------------------------------
1 | # 決算短信セグメント情報のデータ抽出ハンズオンの進め方
2 |
3 | ハンズオンの進め方を解説します。
4 |
5 | 1. アカウントを作成する
6 | 2. ログインする
7 | 3. Jupyter Labを起動する
8 | 4. 教材を開く
9 |
10 | 手順の質問をするときは、手順の番号を伝えてください。(例: 1番のアカウント作成の手順3番目のapprovedのメールが届かない・・・など)。
11 |
12 | ## 1. アカウントを作成する
13 |
14 | SageMaker Studio Labのアカウントを持っていない場合は、アカウントを作成してください。
15 |
16 | 1. [アカウント作成フォーム](https://bit.ly/3kIjuZL)からアカウントの申し込みを行う。
17 | * **リファラルコードが提供されている場合、アカウント作成フォームで忘れずに入力ください。**
18 | * 
19 | 2. `Account request confirmed ...`のメールを受信する。
20 | * アカウントの申し込みが受け付けられた連絡です。リクエストの受付はすぐにメールが届きます。
21 | 3. `Account request approved ...`のメールを受信し、メール内のリンクからアカウントを作成する。
22 | * 申し込みが承認された連絡です。メール内のリンクからアカウント作成を行ってください。
23 | * 承認は 5 営業日以内に結果が通知されます。リファラルコードを利用している場合は 2~3 分以内に結果が届きます。
24 | 4. `Verify your email ...`のメールを受信し、メール内のリンクからメールアドレスを認証する。
25 | * アカウント作成後にメールアドレスの認証を行います。メール内のリンクからメールアドレスを認証してください。
26 | 5. `Your account is ready ...`のメールを受信する。
27 | * お待たせしました!利用開始いただけます。
28 |
29 | ## 2. ログインする
30 |
31 | Studio Labへのログインは、[Studio Lab のランディングページ](https://studiolab.sagemaker.aws/)から行います。ブラウザは、次のように本手順書とSageMaker Studio Labを並べて見られるようにしてください。
32 |
33 | 
34 |
35 | 1. 右上の "Sign in" ボタンを押す。
36 | * 
37 | 2. Eメールアドレス/ユーザー名、パスワードを入力する。
38 | 3. "Sign in" を押しプロジェクトのページを開く。
39 | * 
40 |
41 | ## 3. Jupyter Labを起動する
42 |
43 | Studio LabではCPU/GPUのいずれかでJupyter Notebookを実行することができます。CPUは12時間/セッション、GPUは4時間/セッションです。GPUは24時間以内8時間までという制約があります。
44 |
45 | 1. 「My Project」の「Select compute type」から CPUを選択する。
46 | 2. 「Start runtime」を押す。
47 | * 
48 | * 起動時に“There is no runtime available right now.”と表示された場合は何回かボタンを押してみてください。
49 | 3. ランタイムが開始したら「Open project」を押す。
50 | * JupyterLab 環境が起動します。
51 |
52 | ## 4. 教材を開く
53 |
54 | ハンズオンの教材を、Studio Labにコピーします。
55 |
56 | 1. 左サイドバーのメニューを選択し、「Clone a Repository」を選択する。
57 | * 
58 | 2. GitHubから教材のファイルをコピーする。
59 | * 「Git repository URL (.git)」に次のURLを入れて「Clone」を押してください。
60 | * `https://github.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction.git`
61 | * 
62 | 3. "Confirm you want to build..."が出たら「OK」を押す。
63 | * リポジトリに含まれている `environment.yml` から自動的環境を作成してくれます。
64 | * 
65 | * OKを押し忘れたら`environment.yml`を右クリックし「Build Conda Environment」を実行してください。
66 | * 起動したターミナルで実行されたコマンドが終了したら環境構築は完了です。「done」とコンソール上に表示されます。
67 | * 
68 | 4. 教材である `FinancialResultsHTML-DataExtraction/notebooks/01_how_to_extract_from_html.ipynb` を開く。
69 | * 
70 | 5. 教材のNotebookを動かすためのKernelを選択する。
71 | * 右上の 「No Kernel」を押し、 `jpx-frde`を選択。
72 | * 
73 | 6. お疲れさまでした!以後はNotebookに書かれている手順に沿って進めてください。
74 |
75 | ## 参考資料
76 |
77 | * [公式ドキュメント](https://docs.aws.amazon.com/sagemaker/latest/dg/studio-lab.html)
78 | * [公式FAQ](https://studiolab.sagemaker.aws/faq)
79 | * [Studio Lab日本コミュニティQA](https://github.com/aws-sagemaker-jp/awesome-studio-lab-jp/discussions)
80 | * 使い方に関する質問があればこちらのDiscussionに投稿をお願いします!
81 |
--------------------------------------------------------------------------------
/docs/images/usage/after-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/after-login.png
--------------------------------------------------------------------------------
/docs/images/usage/browser_setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/browser_setting.png
--------------------------------------------------------------------------------
/docs/images/usage/clone_git_repository.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/clone_git_repository.png
--------------------------------------------------------------------------------
/docs/images/usage/clone_git_repository_execute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/clone_git_repository_execute.png
--------------------------------------------------------------------------------
/docs/images/usage/clone_git_repository_set_url.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/clone_git_repository_set_url.png
--------------------------------------------------------------------------------
/docs/images/usage/copy_from_github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/copy_from_github.png
--------------------------------------------------------------------------------
/docs/images/usage/copy_to_project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/copy_to_project.png
--------------------------------------------------------------------------------
/docs/images/usage/create_conda_environment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/create_conda_environment.png
--------------------------------------------------------------------------------
/docs/images/usage/create_environment_in_terminal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/create_environment_in_terminal.png
--------------------------------------------------------------------------------
/docs/images/usage/open_in_studio_lab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/open_in_studio_lab.png
--------------------------------------------------------------------------------
/docs/images/usage/open_notebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/open_notebook.png
--------------------------------------------------------------------------------
/docs/images/usage/referral_code.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/referral_code.PNG
--------------------------------------------------------------------------------
/docs/images/usage/run_notebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/run_notebook.png
--------------------------------------------------------------------------------
/docs/images/usage/signin.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/signin.PNG
--------------------------------------------------------------------------------
/docs/images/usage/start_runtime.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/docs/images/usage/start_runtime.png
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | name: jpx-frde
2 |
3 | channels:
4 | - conda-forge
5 |
6 | dependencies:
7 | - ipython
8 | - ipykernel
9 | - python
10 | - numpy
11 | - matplotlib
12 | - pandas
13 | - altair
14 | - beautifulsoup4
15 | - pre-commit
16 | - pip
17 | - pip:
18 | - -r requirements.txt
19 |
--------------------------------------------------------------------------------
/notebooks/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/.gitkeep
--------------------------------------------------------------------------------
/notebooks/01_how_to_extract_from_html.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# HTML から情報を抽出する方法を学ぶ\n",
8 | "\n",
9 | "このNotebookで、Pythonを用いてHTMLから必要な情報を抽出するための手法を学ぶことができます。\n",
10 | "\n",
11 | "## HTML とは\n",
12 | "\n",
13 | "**HTML(HyperText Markup Language) は、「 Web ページを記述するためのマークアップ言語」です**。最新の HTML5 は、 HTML 形式でも XML 形式でも記述できます(※1)。 HTML を定義が厳密な XML に寄せ、 Web ページのデータ化を推進する XHTML (※2)がかつて検討されていましたが、現在この流れはなくなりました。つまり、 HTML からのデータ抽出は今後も完全に機械的に行うことは困難です。\n",
14 | "\n",
15 | "\n",
16 | "\n",
17 | "[「HTMLの方向性とXMLの位置付け~HTML5の概要と注目機能~」より引用](http://x-plus.utj.co.jp/xml-exp/32-tokushu.html)\n",
18 | "\n",
19 | "※1: HTML5の標準化はHTML/XMLをパースした後のDOMのレベルで行われています。\n",
20 | "※2: XHTMLはXBRLの表示に利用されています。\n",
21 | "\n",
22 | "HTML は、パーサーと呼ばれるツールで解析をします。パーサーは HTML (もしくは XML )の文字列を、プログラムから扱えるオブジェクトのツリーに変換します。オブジェクトのツリーを Document Object Model 、DOM と呼びます。シリアライザは、逆にDOMをHTML/XMLに変換します。\n",
23 | "\n",
24 | "\n",
25 | "\n",
26 | "[「Constructing the Object Model」より引用](https://web.dev/critical-rendering-path-constructing-the-object-model/)\n",
27 | "\n",
28 | "## Python による HTML からの情報抽出\n",
29 | "\n",
30 | "[BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) は Python からパーサーを操作するための代表的なライブラリです。 BeautifulSoup からパーサーを操作することで、 HTML / XML を Python オブジェクトのツリーに変換できます。 \n",
31 | "BeautifulSoup からは、 `html.parser` 、 `lxml` 、 `html5lib` などのパーサーが利用できます。パーサーによって実行速度やパースの方法に違いがあります(詳細は[ドキュメント](https://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser)をご参照ください)。 Python 標準の `html.parser` 以外は、別途インストールが必要です。\n",
32 | "\n",
33 | "なお、 BeautifulSoup は MIT ライセンスのソフトウェアです。会社で使用する場合は [Tidelift のサポート](https://tidelift.com/subscription/pkg/pypi-beautifulsoup4)を受けることもできます。\n",
34 | "\n",
35 | "HTML からの情報抽出は基本的に次の 2 ステップです。\n",
36 | "\n",
37 | "1. 目的の情報がある HTML 要素を取得する\n",
38 | "2. HTML 要素からデータを抽出する\n",
39 | "\n",
40 | "1 ができてしまえば、 2 は比較的簡単です。本 Notebook では、 1 の方法として「検索」と「移動」を学びます。\n",
41 | "\n",
42 | "## Exercise1: 目的の HTML 要素を検索する\n",
43 | "\n",
44 | "目的の情報があるHTML要素を取得する一番簡単な方法は、検索することです。 [`find`](https://www.crummy.com/software/BeautifulSoup/bs4/doc/#find) を利用することで検索ができます。\n",
45 | "\n",
46 | "唐突ですが、 AWS Japan のオフィスは目黒にあります。山手線の目黒駅周辺の駅をいくつかピックアップし、 HTML で表現してみました。"
47 | ]
48 | },
49 | {
50 | "cell_type": "code",
51 | "execution_count": 1,
52 | "metadata": {},
53 | "outputs": [],
54 | "source": [
55 | "html_content = \"\"\"\n",
56 | "\n",
57 | "\n",
58 | "
\n",
59 | " \n",
63 | " \n",
64 | " \n",
65 | " \n",
66 | " 大崎 |
\n",
67 | " 五反田 |
\n",
68 | " 目黒 |
\n",
69 | " 恵比寿 |
\n",
70 | " 渋谷 |
\n",
71 | "
\n",
72 | " \n",
73 | "\n",
74 | "\"\"\""
75 | ]
76 | },
77 | {
78 | "cell_type": "markdown",
79 | "metadata": {},
80 | "source": [
81 | "このHTMLは、表示すると次のようになります。"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": 2,
87 | "metadata": {},
88 | "outputs": [
89 | {
90 | "data": {
91 | "text/html": [
92 | "\n",
93 | "\n",
94 | "\n",
95 | " \n",
96 | " \n",
100 | " \n",
101 | " \n",
102 | " \n",
103 | " 大崎 |
\n",
104 | " 五反田 |
\n",
105 | " 目黒 |
\n",
106 | " 恵比寿 |
\n",
107 | " 渋谷 |
\n",
108 | "
\n",
109 | " \n",
110 | "\n"
111 | ],
112 | "text/plain": [
113 | ""
114 | ]
115 | },
116 | "execution_count": 2,
117 | "metadata": {},
118 | "output_type": "execute_result"
119 | }
120 | ],
121 | "source": [
122 | "from IPython.display import HTML\n",
123 | "\n",
124 | "\n",
125 | "HTML(html_content)"
126 | ]
127 | },
128 | {
129 | "cell_type": "markdown",
130 | "metadata": {},
131 | "source": [
132 | "これから、 BeautifulSoup を使用しこの HTML から「目黒」の HTML 要素を取得します。はじめに、 BeautifulSoup で HTML を読み込みます。"
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": 3,
138 | "metadata": {},
139 | "outputs": [],
140 | "source": [
141 | "from bs4 import BeautifulSoup\n",
142 | "\n",
143 | "\n",
144 | "html = BeautifulSoup(html_content.strip())"
145 | ]
146 | },
147 | {
148 | "cell_type": "markdown",
149 | "metadata": {},
150 | "source": [
151 | "次に、目黒の HTML 要素を取得してみましょう。ヒントとして、恵比寿を取得するコードを掲載します。"
152 | ]
153 | },
154 | {
155 | "cell_type": "code",
156 | "execution_count": 4,
157 | "metadata": {},
158 | "outputs": [
159 | {
160 | "data": {
161 | "text/plain": [
162 | "恵比寿 | "
163 | ]
164 | },
165 | "execution_count": 4,
166 | "metadata": {},
167 | "output_type": "execute_result"
168 | }
169 | ],
170 | "source": [
171 | "# 恵比寿の HTML 要素の id を指定して検索する\n",
172 | "ebisu = html.find(id=\"ebisu\")\n",
173 | "ebisu"
174 | ]
175 | },
176 | {
177 | "cell_type": "markdown",
178 | "metadata": {},
179 | "source": [
180 | "目黒の HTML 要素を取得するコードを次のセルに実装してみてください。取得した HTML 要素は `meguro` という変数に入れてください。"
181 | ]
182 | },
183 | {
184 | "cell_type": "code",
185 | "execution_count": 5,
186 | "metadata": {},
187 | "outputs": [],
188 | "source": [
189 | "# 目黒の HTML 要素を取得するコードを実装する"
190 | ]
191 | },
192 | {
193 | "cell_type": "markdown",
194 | "metadata": {},
195 | "source": [
196 | "上手く取得できているか、次のセルを実行すると確認できます。\n",
197 | "\n",
198 | "最初はエラーが表示されていますが、 `meguro` の変数に目黒の HTML 要素を入れたうえで実行すればエラーが消えるはずです。\n",
199 | "\n",
200 | " `.string` で取得した HTML 要素の中にあるテキストを取得しています。これが冒頭の「 2. HTML 要素からデータを抽出する」に相当します。"
201 | ]
202 | },
203 | {
204 | "cell_type": "code",
205 | "execution_count": 6,
206 | "metadata": {},
207 | "outputs": [
208 | {
209 | "ename": "NameError",
210 | "evalue": "name 'meguro' is not defined",
211 | "output_type": "error",
212 | "traceback": [
213 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
214 | "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)",
215 | "Input \u001b[1;32mIn [6]\u001b[0m, in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[43mmeguro\u001b[49m\u001b[38;5;241m.\u001b[39mstring \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m目黒\u001b[39m\u001b[38;5;124m\"\u001b[39m\n",
216 | "\u001b[1;31mNameError\u001b[0m: name 'meguro' is not defined"
217 | ]
218 | }
219 | ],
220 | "source": [
221 | "assert meguro.string == \"目黒\""
222 | ]
223 | },
224 | {
225 | "cell_type": "markdown",
226 | "metadata": {},
227 | "source": [
228 | "[`find`](https://www.crummy.com/software/BeautifulSoup/bs4/doc/#find) は、検索条件に一致する単一の要素を取得します。検索条件に当てはまる HTML 要素が複数ある場合は`find_all`を使います。"
229 | ]
230 | },
231 | {
232 | "cell_type": "code",
233 | "execution_count": 7,
234 | "metadata": {},
235 | "outputs": [
236 | {
237 | "data": {
238 | "text/plain": [
239 | "[大崎 | ,\n",
240 | " 五反田 | ,\n",
241 | " 目黒 | ,\n",
242 | " 恵比寿 | ,\n",
243 | " 渋谷 | ]"
244 | ]
245 | },
246 | "execution_count": 7,
247 | "metadata": {},
248 | "output_type": "execute_result"
249 | }
250 | ],
251 | "source": [
252 | "html.find_all(\"td\")"
253 | ]
254 | },
255 | {
256 | "cell_type": "markdown",
257 | "metadata": {},
258 | "source": [
259 | "CSSの指定に慣れている方はCSSセレクタを使用した検索を`select`で行うことができます。"
260 | ]
261 | },
262 | {
263 | "cell_type": "code",
264 | "execution_count": 8,
265 | "metadata": {},
266 | "outputs": [
267 | {
268 | "data": {
269 | "text/plain": [
270 | "[目黒 | ]"
271 | ]
272 | },
273 | "execution_count": 8,
274 | "metadata": {},
275 | "output_type": "execute_result"
276 | }
277 | ],
278 | "source": [
279 | "html.select(\".aws\")"
280 | ]
281 | },
282 | {
283 | "cell_type": "markdown",
284 | "metadata": {},
285 | "source": [
286 | "## Exercise2: 目的の HTML 要素へ移動する\n",
287 | "\n",
288 | "先程は確実な目印として `meguro` という `id` の属性がありましたがそれがない場合はどうすればよいでしょうか? 実際の HTML ではそうしたことが良くあります。"
289 | ]
290 | },
291 | {
292 | "cell_type": "code",
293 | "execution_count": 9,
294 | "metadata": {},
295 | "outputs": [],
296 | "source": [
297 | "html_content_without_id = \"\"\"\n",
298 | "\n",
299 | "\n",
300 | " \n",
301 | " \n",
305 | " \n",
306 | " \n",
307 | " \n",
308 | " 大崎 | \n",
309 | " 五反田 | \n",
310 | " 目黒 | \n",
311 | " 恵比寿 | \n",
312 | " 渋谷 | \n",
313 | " \n",
314 | " \n",
315 | "\n",
316 | "\"\"\""
317 | ]
318 | },
319 | {
320 | "cell_type": "markdown",
321 | "metadata": {},
322 | "source": [
323 | "この場合、近くの確実な目印まで一旦到達し、そこから移動して到達する方法が考えられます。\n",
324 | "\n",
325 | "1. 全ての駅の HTML 要素 ( `td` ) のうち、3 番目の要素を目黒として取得する。\n",
326 | "2. 「目黒」というてテキストを検索して、テキストが目黒である HTML 要素を取得する。\n",
327 | "\n",
328 | "実際に行ってみましょう。まず、 BeautifulSoup で HTML を読み込みます。"
329 | ]
330 | },
331 | {
332 | "cell_type": "code",
333 | "execution_count": 10,
334 | "metadata": {},
335 | "outputs": [],
336 | "source": [
337 | "html_without_id = BeautifulSoup(html_content_without_id.strip())"
338 | ]
339 | },
340 | {
341 | "cell_type": "markdown",
342 | "metadata": {},
343 | "source": [
344 | "目黒の手前の五反田に到達してみます。五反田は 2 つめですが、プログラムで指定する時は 0 番目を含むので 1 を指定します。"
345 | ]
346 | },
347 | {
348 | "cell_type": "code",
349 | "execution_count": 11,
350 | "metadata": {},
351 | "outputs": [
352 | {
353 | "data": {
354 | "text/plain": [
355 | "五反田 | "
356 | ]
357 | },
358 | "execution_count": 11,
359 | "metadata": {},
360 | "output_type": "execute_result"
361 | }
362 | ],
363 | "source": [
364 | "gotanda = html_without_id.find_all(\"td\")[1]\n",
365 | "gotanda"
366 | ]
367 | },
368 | {
369 | "cell_type": "markdown",
370 | "metadata": {},
371 | "source": [
372 | "`find_all` ですべての駅を検索し、 2 つめを五反田として取得しました。\n",
373 | "\n",
374 | "次に、五反田のテキストを検索し、五反田のテキストを含む HTML 要素を取得してみましょう。"
375 | ]
376 | },
377 | {
378 | "cell_type": "code",
379 | "execution_count": 12,
380 | "metadata": {},
381 | "outputs": [
382 | {
383 | "data": {
384 | "text/plain": [
385 | "五反田 | "
386 | ]
387 | },
388 | "execution_count": 12,
389 | "metadata": {},
390 | "output_type": "execute_result"
391 | }
392 | ],
393 | "source": [
394 | "gotanda = html_without_id.find(text=\"五反田\").parent\n",
395 | "gotanda"
396 | ]
397 | },
398 | {
399 | "cell_type": "markdown",
400 | "metadata": {},
401 | "source": [
402 | "`find` で五反田のテキストを検索し、 `parent` でテキストを含む(親となる) HTML 要素を取得しました。\n",
403 | "\n",
404 | "1 と 2 、お好きな方で目黒を取得してみてください。"
405 | ]
406 | },
407 | {
408 | "cell_type": "code",
409 | "execution_count": 13,
410 | "metadata": {},
411 | "outputs": [],
412 | "source": [
413 | "# 目黒の HTML 要素を取得するコードを実装する"
414 | ]
415 | },
416 | {
417 | "cell_type": "markdown",
418 | "metadata": {},
419 | "source": [
420 | "五反田のセル (``) のさらに `parent` は、行 (` | `) になります。 HTML のテーブルは、行 (` `) の中にセル (``) が何個かあるという形式で定義されています。"
421 | ]
422 | },
423 | {
424 | "cell_type": "code",
425 | "execution_count": 14,
426 | "metadata": {},
427 | "outputs": [
428 | {
429 | "data": {
430 | "text/plain": [
431 | " | 五反田 | "
432 | ]
433 | },
434 | "execution_count": 14,
435 | "metadata": {},
436 | "output_type": "execute_result"
437 | }
438 | ],
439 | "source": [
440 | "gotanda.parent"
441 | ]
442 | },
443 | {
444 | "cell_type": "markdown",
445 | "metadata": {},
446 | "source": [
447 | "`parent` とは逆に、子となる要素は `children` / `contents` で取得できます。"
448 | ]
449 | },
450 | {
451 | "cell_type": "code",
452 | "execution_count": 15,
453 | "metadata": {},
454 | "outputs": [
455 | {
456 | "data": {
457 | "text/plain": [
458 | "['\\n',\n",
459 | " 大崎 | ,\n",
460 | " '\\n',\n",
461 | " 五反田 | ,\n",
462 | " '\\n',\n",
463 | " 目黒 | ,\n",
464 | " '\\n',\n",
465 | " 恵比寿 | ,\n",
466 | " '\\n',\n",
467 | " 渋谷 | ,\n",
468 | " '\\n',\n",
469 | " '\\n']"
470 | ]
471 | },
472 | "execution_count": 15,
473 | "metadata": {},
474 | "output_type": "execute_result"
475 | }
476 | ],
477 | "source": [
478 | "html_without_id.find(\"table\").contents"
479 | ]
480 | },
481 | {
482 | "cell_type": "markdown",
483 | "metadata": {},
484 | "source": [
485 | "隣の要素へは `next_element` や `previous_element` で移動できます。五反田の行はセルが 1 つしかないので隣はない気がしますが、セルの `` タグの隣にある 「五反田」 のテキストが取得されます。 "
486 | ]
487 | },
488 | {
489 | "cell_type": "code",
490 | "execution_count": 16,
491 | "metadata": {},
492 | "outputs": [
493 | {
494 | "data": {
495 | "text/plain": [
496 | "'五反田'"
497 | ]
498 | },
499 | "execution_count": 16,
500 | "metadata": {},
501 | "output_type": "execute_result"
502 | }
503 | ],
504 | "source": [
505 | "gotanda.next_element"
506 | ]
507 | },
508 | {
509 | "cell_type": "markdown",
510 | "metadata": {},
511 | "source": [
512 | "同じ親を持つ兄弟要素を取得するには `next_sibling` 、 `previous_sibling` を使います。 五反田の行 ` | ` の隣は目黒の行という気がしますが、テキスト要素も含むため隣の改行文字が取得されます。"
513 | ]
514 | },
515 | {
516 | "cell_type": "code",
517 | "execution_count": 17,
518 | "metadata": {},
519 | "outputs": [
520 | {
521 | "data": {
522 | "text/plain": [
523 | "'\\n'"
524 | ]
525 | },
526 | "execution_count": 17,
527 | "metadata": {},
528 | "output_type": "execute_result"
529 | }
530 | ],
531 | "source": [
532 | "gotanda.parent.next_sibling"
533 | ]
534 | },
535 | {
536 | "cell_type": "markdown",
537 | "metadata": {},
538 | "source": [
539 | "あまりきれいでない HTML の場合、 `next` や `previous` でなにが取得されるか予想は困難です。そのため、 `find_next` や `find_previous` 、 `find_next_siblings` や `find_previous_siblings` で意図した要素を指定して検索することをお勧めします。 `find_next` と `find_previous` は内部的に `next_element` / `previous_element` を使っており、`find_next_siblings` と `find_previous_siblings` は `next_siblings` / `previous_siblings` を使っています。"
540 | ]
541 | },
542 | {
543 | "cell_type": "code",
544 | "execution_count": 18,
545 | "metadata": {},
546 | "outputs": [
547 | {
548 | "data": {
549 | "text/plain": [
550 | " 目黒 | "
551 | ]
552 | },
553 | "execution_count": 18,
554 | "metadata": {},
555 | "output_type": "execute_result"
556 | }
557 | ],
558 | "source": [
559 | "gotanda.parent.find_next(\"tr\")"
560 | ]
561 | }
562 | ],
563 | "metadata": {
564 | "interpreter": {
565 | "hash": "9e92602f210dc61ccc424c74f06e3d050a4530807035fa2e9f9fdb5f45ecdbb6"
566 | },
567 | "kernelspec": {
568 | "display_name": "Python 3.10.6 64-bit ('jpx-frde': conda)",
569 | "language": "python",
570 | "name": "python3"
571 | },
572 | "language_info": {
573 | "codemirror_mode": {
574 | "name": "ipython",
575 | "version": 3
576 | },
577 | "file_extension": ".py",
578 | "mimetype": "text/x-python",
579 | "name": "python",
580 | "nbconvert_exporter": "python",
581 | "pygments_lexer": "ipython3",
582 | "version": "3.10.6"
583 | },
584 | "orig_nbformat": 4
585 | },
586 | "nbformat": 4,
587 | "nbformat_minor": 2
588 | }
589 |
--------------------------------------------------------------------------------
/notebooks/HomeworkTemplete.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/HomeworkTemplete.pptx
--------------------------------------------------------------------------------
/notebooks/images/error_example_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_01.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_02.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_03.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_04.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_05.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_06.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_11.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_12.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_13.png
--------------------------------------------------------------------------------
/notebooks/images/error_example_14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/error_example_14.png
--------------------------------------------------------------------------------
/notebooks/images/financial_html.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/financial_html.png
--------------------------------------------------------------------------------
/notebooks/images/homework.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/homework.png
--------------------------------------------------------------------------------
/notebooks/images/how_to_download_001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/how_to_download_001.png
--------------------------------------------------------------------------------
/notebooks/images/how_to_download_002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/how_to_download_002.png
--------------------------------------------------------------------------------
/notebooks/images/html_history.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/html_history.jpg
--------------------------------------------------------------------------------
/notebooks/images/html_parser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/html_parser.png
--------------------------------------------------------------------------------
/notebooks/images/segment_information.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/segment_information.png
--------------------------------------------------------------------------------
/notebooks/images/step_001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/step_001.png
--------------------------------------------------------------------------------
/notebooks/images/step_002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/step_002.png
--------------------------------------------------------------------------------
/notebooks/images/step_003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/step_003.png
--------------------------------------------------------------------------------
/notebooks/images/step_004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/step_004.png
--------------------------------------------------------------------------------
/notebooks/images/step_005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/step_005.png
--------------------------------------------------------------------------------
/notebooks/images/top.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/notebooks/images/top.jpg
--------------------------------------------------------------------------------
/notebooks/sample.csv:
--------------------------------------------------------------------------------
1 | period_kind,period_description,period_begin,period_end,segment_order,segment_position,segment_name,account_order,account_position,account_kind,account_name,account_unit,value
2 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,0,1,国内コンビニエンスストア事業,0,6,Sales,営業収益,1000000,217536.0
3 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,0,1,国内コンビニエンスストア事業,1,7,Profit,セグメント利益又は損失(△),1000000,60573.0
4 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,1,2,海外コンビニエンスストア事業,0,6,Sales,営業収益,1000000,679296.0
5 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,1,2,海外コンビニエンスストア事業,1,7,Profit,セグメント利益又は損失(△),1000000,12136.0
6 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,2,3,スーパーストア事業,0,6,Sales,営業収益,1000000,451684.0
7 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,2,3,スーパーストア事業,1,7,Profit,セグメント利益又は損失(△),1000000,5843.0
8 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,3,4,百貨店・専門店事業,0,6,Sales,営業収益,1000000,166636.0
9 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,3,4,百貨店・専門店事業,1,7,Profit,セグメント利益又は損失(△),1000000,-3442.0
10 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,4,5,金融関連事業,0,6,Sales,営業収益,1000000,49101.0
11 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,4,5,金融関連事業,1,7,Profit,セグメント利益又は損失(△),1000000,10431.0
12 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,5,6,その他の事業,0,6,Sales,営業収益,1000000,4771.0
13 | previous,I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日),2021-03-01,2021-05-31,5,6,その他の事業,1,7,Profit,セグメント利益又は損失(△),1000000,291.0
14 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,0,1,国内コンビニエンスストア事業,0,6,Sales,営業収益,1000000,215243.0
15 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,0,1,国内コンビニエンスストア事業,1,7,Profit,セグメント利益又は損失(△),1000000,59282.0
16 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,1,2,海外コンビニエンスストア事業,0,6,Sales,営業収益,1000000,1723889.0
17 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,1,2,海外コンビニエンスストア事業,1,7,Profit,セグメント利益又は損失(△),1000000,43981.0
18 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,2,3,スーパーストア事業,0,6,Sales,営業収益,1000000,355772.0
19 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,2,3,スーパーストア事業,1,7,Profit,セグメント利益又は損失(△),1000000,3517.0
20 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,3,4,百貨店・専門店事業,0,6,Sales,営業収益,1000000,112904.0
21 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,3,4,百貨店・専門店事業,1,7,Profit,セグメント利益又は損失(△),1000000,1086.0
22 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,4,5,金融関連事業,0,6,Sales,営業収益,1000000,47560.0
23 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,4,5,金融関連事業,1,7,Profit,セグメント利益又は損失(△),1000000,9205.0
24 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,5,6,その他の事業,0,6,Sales,営業収益,1000000,5829.0
25 | current,II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日),2022-03-01,2022-05-31,5,6,その他の事業,1,7,Profit,セグメント利益又は損失(△),1000000,-90.0
26 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.flake8]
2 | exclude = ".git, .tox, .venv, .eggs, build, dist, docs"
3 | extend-ignore = "E203, W503, W504"
4 | max-line-length = 99
5 |
6 | [tool.black]
7 | exclude = """
8 | /(
9 | .eggs
10 | | .git
11 | | .hg
12 | | .pytest_cache
13 | | .mypy_cache
14 | | .tox
15 | | .venv
16 | | build
17 | | dist
18 | )/
19 | """
20 |
21 | [tool.isort]
22 | profile = "black"
23 | include_trailing_comma = true
24 | multi_line_output = 3
25 |
26 | [tool.mypy]
27 | ignore_missing_imports = true
28 | disallow_untyped_defs = true
29 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-cov
3 |
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/scripts/__init__.py
--------------------------------------------------------------------------------
/scripts/financial_result_reader.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | import re
3 | import unicodedata
4 | from dataclasses import dataclass
5 | from datetime import datetime
6 | from pathlib import Path
7 | from typing import Optional
8 |
9 | import pandas as pd
10 | from bs4 import BeautifulSoup
11 | from bs4.element import Tag
12 |
13 |
14 | @dataclass
15 | class Period:
16 | kind: str = ""
17 | description: str = ""
18 | begin: Optional[datetime] = None
19 | end: Optional[datetime] = None
20 |
21 |
22 | @dataclass
23 | class Segment:
24 | order: int = 0
25 | position: int = 0
26 | name: str = ""
27 |
28 |
29 | @dataclass
30 | class Account:
31 | order: int = 0
32 | position: int = 0
33 | kind: str = ""
34 | name: str = ""
35 | unit: int = 1
36 |
37 |
38 | def read_financial_result_html(path: str | Path) -> Optional[BeautifulSoup]:
39 | """
40 | 決算短信のHTMLをBeautiful Soupに読み込む。
41 |
42 | Parameters
43 | ----------
44 | path : str | Path
45 | 決算短信のHTMLファイルのパス
46 |
47 | Returns
48 | -------
49 | Optional[BeautifulSoup]
50 | Beautiful Soupのオブジェクト
51 | """
52 |
53 | _path = path if isinstance(path, Path) else Path(path)
54 | text = ""
55 | if not _path.exists():
56 | return None
57 |
58 | with _path.open(encoding="utf-8") as r:
59 | lines = r.readlines()
60 | # 記載されたテキストの文字種を統一するため、Unicode正規化を行う
61 | lines = [unicodedata.normalize("NFKC", line) for line in lines]
62 | text = "".join(lines)
63 |
64 | html = BeautifulSoup(text, features="html.parser")
65 | return html
66 |
67 |
68 | def find_segment_tables(html: BeautifulSoup) -> list[Tag]:
69 | """
70 | 「報告セグメント」のテキストを含むテーブルを検索する
71 |
72 | Parameters
73 | ----------
74 | html: BeautifulSoup
75 | 決算短信のHTMLを読み込んだBeautiful Soupのオブジェクト
76 |
77 | Returns
78 | -------
79 | list[Tag]
80 | 「報告セグメント」のテキストを含むテーブルのリスト
81 | """
82 |
83 | tables = html.find_all("table")
84 | segment_tables = []
85 | for t in tables:
86 | text = t.find(text="報告セグメント")
87 | if text is not None:
88 | td = text.find_previous("td")
89 | if td is not None and "colspan" in td.attrs:
90 | segment_tables.append(t)
91 |
92 | return segment_tables
93 |
94 |
95 | def read_table_period(table: Tag) -> Period:
96 | """
97 | セグメント報告のテーブル上部にあるテキストから、報告年月日を取得する
98 |
99 | Parameters
100 | ----------
101 | table: Tag
102 | セグメント報告のテーブル要素
103 |
104 | Returns
105 | -------
106 | Period
107 | 報告年月日
108 | """
109 |
110 | FIND_LIMIT = 3
111 | PATTERN = re.compile(r".*(前|当).+(\d{4}年\d+月\d+日).+(\d{4}年\d+月\d+日)")
112 | DATE_PATTERN = re.compile(r"\d{4}年\d+月\d+日")
113 | period = Period()
114 |
115 | count = 0
116 | tag = table
117 | while count < FIND_LIMIT:
118 | p = tag.find_previous("p")
119 | if p is not None:
120 | text = p.get_text().strip()
121 | if re.search(PATTERN, text):
122 | period.description = text
123 | if "前" in text:
124 | period.kind = "previous"
125 | elif "当" in text:
126 | period.kind = "current"
127 |
128 | dates = DATE_PATTERN.findall(period.description)
129 | if len(dates) == 2:
130 | from_date = dates[0].strip()
131 | period.begin = datetime.strptime(from_date, "%Y年%m月%d日")
132 | to_date = dates[1].strip()
133 | period.end = datetime.strptime(to_date, "%Y年%m月%d日")
134 | break
135 | count += 1
136 | tag = p
137 |
138 | return period
139 |
140 |
141 | def read_table_segments(table: Tag) -> list[Segment]:
142 | """
143 | セグメント報告のテーブルから、セグメントの位置を取得する
144 |
145 | Parameters
146 | ----------
147 | table: Tag
148 | セグメント報告のテーブル要素
149 |
150 | Returns
151 | -------
152 | list[Segment]
153 | セグメントのリスト
154 | """
155 |
156 | merged_cells = table.find_all("td", colspan=True)
157 | SEGMENT_TEXT = "報告セグメント"
158 | EXCLUDES = re.compile(r".{0,2}計$")
159 | NORMALIZER = re.compile(r"\s|\r|\n")
160 | segments = [] # type: list[Segment]
161 |
162 | segment_title_cells = [cell for cell in merged_cells if SEGMENT_TEXT in cell.text]
163 | if len(segment_title_cells) == 0:
164 | return segments
165 | else:
166 | segment_title_cell = segment_title_cells[0]
167 | num_segments = int(segment_title_cell.attrs["colspan"])
168 |
169 | segment_begin = 0
170 | skipped = 0
171 | for cell in segment_title_cell.find_previous("tr").find_all("td"):
172 | if cell.text.strip() == SEGMENT_TEXT:
173 | break
174 | else:
175 | if "rowspan" not in cell.attrs:
176 | segment_begin += 1
177 | else:
178 | skipped += 1
179 |
180 | segment_row = segment_title_cell.find_next("tr")
181 | order = 0
182 | for i, segment_cell in enumerate(segment_row.find_all("td")):
183 | if i < segment_begin:
184 | continue
185 | elif i < (segment_begin + num_segments):
186 | segment_text = NORMALIZER.sub("", segment_cell.text.strip())
187 | if segment_text and not EXCLUDES.match(segment_text):
188 | segments.append(Segment(order, i + skipped, segment_text))
189 | order += 1
190 |
191 | return segments
192 |
193 |
194 | def read_table_sales_profit(table: Tag) -> list[Account]:
195 | """
196 | セグメント報告のテーブルから、売上・利益の勘定の位置を取得する
197 |
198 | Parameters
199 | ----------
200 | table: Tag
201 | セグメント報告のテーブル要素
202 |
203 | Returns
204 | -------
205 | list[Account]
206 | 勘定のリスト
207 | """
208 |
209 | SUM = re.compile(r".*計$")
210 | NORMALIZER = re.compile(r"\s|\r|\n")
211 | UNIT_TEXT = "単位"
212 | unit = 1000000
213 |
214 | merged_cell = table.find_all("td", colspan=True)
215 | unit_cells = [cell for cell in merged_cell if UNIT_TEXT in cell.text]
216 |
217 | if len(unit_cells) > 0:
218 | cell = unit_cells[0]
219 | if "千円" in cell.text:
220 | unit = 1000
221 | elif "十億" in cell.text:
222 | unit = 1000000000
223 |
224 | rows = table.find_all("tr")
225 |
226 | skip_rows = 0
227 | sales = None
228 | profit = None
229 | for i, row in enumerate(rows):
230 | if skip_rows > 0:
231 | skip_rows -= 1
232 | continue
233 |
234 | account_cell = row.find_next("td")
235 | if "rowspan" in account_cell.attrs:
236 | skip_rows = int(account_cell.attrs["rowspan"]) - 1
237 |
238 | account_text = NORMALIZER.sub("", account_cell.text)
239 | if account_text:
240 | if sales is None:
241 | sales = Account(0, i, "Sales", account_text, unit)
242 | profit = None
243 | elif sales and SUM.match(account_text):
244 | sales = Account(0, i, "Sales", sales.name, unit)
245 | profit = None
246 | elif sales and profit is None:
247 | profit = Account(1, i, "Profit", account_text, unit)
248 |
249 | accounts = []
250 | if sales is not None:
251 | accounts.append(sales)
252 | if profit is not None:
253 | accounts.append(profit)
254 |
255 | return accounts
256 |
257 |
258 | def read_segment_sales_profit(
259 | table: Tag, segment: Segment, account: Account
260 | ) -> pd.Series:
261 | """
262 | セグメント報告のテーブルから、セグメント、勘定を指定してデータを取得する
263 |
264 | Parameters
265 | ----------
266 | table: Tag
267 | セグメント報告のテーブル要素
268 | segment: Segment
269 | セグメントの位置
270 | account: Account
271 | 勘定の位置
272 |
273 | Returns
274 | -------
275 | pd.Series
276 | 指定されたセグメント、勘定のデータ
277 | """
278 |
279 | cell = table.find_all("tr")[account.position].find_all("td")[segment.position]
280 | data = {}
281 | data.update(
282 | {f"segment_{key}": value for key, value in dataclasses.asdict(segment).items()}
283 | )
284 | data.update(
285 | {f"account_{key}": value for key, value in dataclasses.asdict(account).items()}
286 | )
287 | value = cell.text.strip().replace("-", "").replace(",", "").replace("△", "-")
288 | try:
289 | data["value"] = float(value)
290 | except ValueError:
291 | data["value"] = None
292 |
293 | return pd.Series(data)
294 |
--------------------------------------------------------------------------------
/scripts/financial_result_to_dataframe.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from dataclasses import dataclass
3 | from pathlib import Path
4 | from typing import Optional
5 |
6 | import pandas as pd
7 | from bs4.element import Tag
8 |
9 | import scripts.financial_result_reader as frr
10 |
11 |
12 | @dataclass
13 | class ReadResult:
14 | read_html_failed: bool = False
15 | segment_table_not_exist: bool = False
16 | too_little_segment_table: bool = False
17 | too_much_segment_tables: bool = False
18 | period_not_found: bool = False
19 | segment_not_found: bool = False
20 | account_not_found: bool = False
21 | value_read_failed: bool = False
22 | completed: bool = False
23 |
24 |
25 | class ReadException(Exception):
26 | def __init__(self, index: int, read_result: str, html: Tag) -> None:
27 | super().__init__(read_result)
28 | self.index = index
29 | self.read_result = read_result
30 | self.html = html
31 |
32 |
33 | class ReadLog:
34 | def __init__(self, status: ReadResult, logs: list[ReadException]) -> None:
35 | self.status = status
36 | self.logs = logs
37 |
38 |
39 | def financial_result_to_dataframe(
40 | path: str | Path,
41 | ) -> tuple[Optional[pd.DataFrame], ReadLog]:
42 | result = ReadResult()
43 | logs = []
44 |
45 | html = frr.read_financial_result_html(path)
46 | if html is None:
47 | result.read_html_failed = True
48 | logs.append(ReadException(-1, "read_html_failed", None))
49 | return None, ReadLog(result, logs)
50 |
51 | segment_tables = frr.find_segment_tables(html)
52 | if len(segment_tables) == 0:
53 | result.segment_table_not_exist = True
54 | logs.append(ReadException(-1, "segment_table_not_exist", None))
55 | return None, ReadLog(result, logs)
56 | elif len(segment_tables) < 2:
57 | result.too_little_segment_table = True
58 | elif len(segment_tables) > 2:
59 | result.too_much_segment_tables = True
60 |
61 | data = []
62 | number_of_completed_table = 0
63 | read_current = False
64 | read_previous = False
65 | for i, table in enumerate(segment_tables):
66 | period = frr.read_table_period(table)
67 | if not period.kind:
68 | result.period_not_found = True
69 | logs.append(ReadException(i, "period_not_found", table))
70 |
71 | segments = frr.read_table_segments(table)
72 | if len(segments) == 0:
73 | result.segment_not_found = True
74 | logs.append(ReadException(i, "segment_not_found", table))
75 |
76 | accounts = frr.read_table_sales_profit(table)
77 | if len(accounts) != 2:
78 | result.account_not_found = True
79 | logs.append(ReadException(i, "account_not_found", table))
80 |
81 | if (
82 | not result.period_not_found
83 | and not result.segment_not_found
84 | and not result.account_not_found
85 | ):
86 | period_dict = {
87 | f"period_{key}": value
88 | for key, value in dataclasses.asdict(period).items()
89 | }
90 | _period = pd.Series(period_dict)
91 | for s in segments:
92 | for a in accounts:
93 | _data = frr.read_segment_sales_profit(table, s, a)
94 | if _data.value is not None:
95 | data.append(pd.concat([_period, _data]))
96 | else:
97 | result.value_read_failed = True
98 | logs.append(ReadException(i, "value_read_failed", table))
99 | break
100 |
101 | if not result.value_read_failed:
102 | if period.kind == "previous":
103 | read_previous = True
104 | elif period.kind == "current":
105 | read_current = True
106 | number_of_completed_table += 1
107 |
108 | if number_of_completed_table == 2 and read_previous and read_current:
109 | result.completed = True
110 | return pd.DataFrame(data), ReadLog(result, logs)
111 | else:
112 | return pd.DataFrame(data), ReadLog(result, logs)
113 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/0165b4049062009ba35d494d2a38afb1117ea9f8/tests/__init__.py
--------------------------------------------------------------------------------
/tests/data/raw/exceptions/account_not_found.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | qualitative
9 |
10 |
11 |
12 | (7)セグメント情報
13 | Ⅰ 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日)
14 | 1.報告セグメントごとの営業収益及び利益又は損失の金額に関する情報
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | |
36 |
37 |
38 |
39 |
40 | |
41 |
42 |
43 |
44 |
45 | |
46 |
47 |
48 |
49 |
50 | |
51 |
52 |
53 |
54 |
55 | |
56 |
57 |
58 |
59 |
60 | |
61 |
62 |
63 |
64 |
65 | |
66 |
67 |
68 |
69 |
70 | |
71 |
72 | (単位:百万円)
73 | |
74 |
75 |
76 |
77 |
78 |
79 |
80 | |
81 |
82 |
83 | 報告セグメント
84 |
85 | |
86 |
87 |
88 | 計
89 |
90 | |
91 |
92 |
93 | 調整額
94 |
95 |
96 | (注)1
97 |
98 | |
99 |
100 |
101 | 四半期連結
102 |
103 |
104 | 損益計算書
105 |
106 |
107 | 計上額
108 |
109 |
110 | (注)2
111 |
112 | |
113 |
114 |
115 |
116 |
117 |
118 |
119 | |
120 |
121 |
122 | 国内コンビニエンスストア事業
123 |
124 | |
125 |
126 |
127 | 海外コンビニエンスストア事業
128 |
129 | |
130 |
131 |
132 | スーパー
133 |
134 |
135 | ストア事業
136 |
137 | |
138 |
139 |
140 | 百貨店・
141 |
142 |
143 | 専門店事業
144 |
145 | |
146 |
147 |
148 | 金融関連
149 |
150 |
151 | 事業
152 |
153 | |
154 |
155 |
156 | その他の
157 |
158 |
159 | 事業
160 |
161 | |
162 |
163 |
164 |
165 |
166 | 営業収益
167 |
168 | |
169 |
170 |
171 |
172 |
173 | |
174 |
175 |
176 |
177 |
178 | |
179 |
180 |
181 |
182 |
183 | |
184 |
185 |
186 |
187 |
188 | |
189 |
190 |
191 |
192 |
193 | |
194 |
195 |
196 |
197 |
198 | |
199 |
200 |
201 |
202 |
203 | |
204 |
205 |
206 |
207 |
208 | |
209 |
210 |
211 |
212 |
213 | |
214 |
215 |
216 |
217 |
218 | 外部顧客への
219 |
220 |
221 | 営業収益
222 |
223 | |
224 |
225 |
226 | 217,107
227 |
228 | |
229 |
230 |
231 | 678,802
232 |
233 | |
234 |
235 |
236 | 450,012
237 |
238 | |
239 |
240 |
241 | 165,934
242 |
243 | |
244 |
245 |
246 | 41,925
247 |
248 | |
249 |
250 |
251 | 1,589
252 |
253 | |
254 |
255 |
256 | 1,555,371
257 |
258 | |
259 |
260 |
261 | -
262 |
263 | |
264 |
265 |
266 | 1,555,371
267 |
268 | |
269 |
270 |
271 |
272 |
273 | セグメント間の内部営業収益又は振替高
274 |
275 | |
276 |
277 |
278 | 429
279 |
280 | |
281 |
282 |
283 | 494
284 |
285 | |
286 |
287 |
288 | 1,672
289 |
290 | |
291 |
292 |
293 | 701
294 |
295 | |
296 |
297 |
298 | 7,176
299 |
300 | |
301 |
302 |
303 | 3,181
304 |
305 | |
306 |
307 |
308 | 13,655
309 |
310 | |
311 |
312 |
313 | △13,655
314 |
315 | |
316 |
317 |
318 | -
319 |
320 | |
321 |
322 |
323 |
324 |
325 | 計
326 |
327 | |
328 |
329 |
330 | 217,536
331 |
332 | |
333 |
334 |
335 | 679,296
336 |
337 | |
338 |
339 |
340 | 451,684
341 |
342 | |
343 |
344 |
345 | 166,636
346 |
347 | |
348 |
349 |
350 | 49,101
351 |
352 | |
353 |
354 |
355 | 4,771
356 |
357 | |
358 |
359 |
360 | 1,569,027
361 |
362 | |
363 |
364 |
365 | △13,655
366 |
367 | |
368 |
369 |
370 | 1,555,371
371 |
372 | |
373 |
374 |
375 |
376 |
377 |
378 | Ⅰ 当第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日)
379 | 1.報告セグメントごとの営業収益及び利益又は損失の金額に関する情報
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 | |
401 |
402 |
403 |
404 |
405 | |
406 |
407 |
408 |
409 |
410 | |
411 |
412 |
413 |
414 |
415 | |
416 |
417 |
418 |
419 |
420 | |
421 |
422 |
423 |
424 |
425 | |
426 |
427 |
428 |
429 |
430 | |
431 |
432 |
433 |
434 |
435 | |
436 |
437 | (単位:百万円)
438 | |
439 |
440 |
441 |
442 |
443 |
444 |
445 | |
446 |
447 |
448 | 報告セグメント
449 |
450 | |
451 |
452 |
453 | 計
454 |
455 | |
456 |
457 |
458 | 調整額
459 |
460 |
461 | (注)1
462 |
463 | |
464 |
465 |
466 | 四半期連結
467 |
468 |
469 | 損益計算書
470 |
471 |
472 | 計上額
473 |
474 |
475 | (注)2
476 |
477 | |
478 |
479 |
480 |
481 |
482 |
483 |
484 | |
485 |
486 |
487 | 国内コンビニエンスストア事業
488 |
489 | |
490 |
491 |
492 | 海外コンビニエンスストア事業
493 |
494 | |
495 |
496 |
497 | スーパー
498 |
499 |
500 | ストア事業
501 |
502 | |
503 |
504 |
505 | 百貨店・
506 |
507 |
508 | 専門店事業
509 |
510 | |
511 |
512 |
513 | 金融関連
514 |
515 |
516 | 事業
517 |
518 | |
519 |
520 |
521 | その他の
522 |
523 |
524 | 事業
525 |
526 | |
527 |
528 |
529 |
530 |
531 | 営業収益
532 |
533 | |
534 |
535 |
536 |
537 |
538 | |
539 |
540 |
541 |
542 |
543 | |
544 |
545 |
546 |
547 |
548 | |
549 |
550 |
551 |
552 |
553 | |
554 |
555 |
556 |
557 |
558 | |
559 |
560 |
561 |
562 |
563 | |
564 |
565 |
566 |
567 |
568 | |
569 |
570 |
571 |
572 |
573 | |
574 |
575 |
576 |
577 |
578 | |
579 |
580 |
581 |
582 |
583 | 外部顧客への
584 |
585 |
586 | 営業収益
587 |
588 | |
589 |
590 |
591 | 217,107
592 |
593 | |
594 |
595 |
596 | 678,802
597 |
598 | |
599 |
600 |
601 | 450,012
602 |
603 | |
604 |
605 |
606 | 165,934
607 |
608 | |
609 |
610 |
611 | 41,925
612 |
613 | |
614 |
615 |
616 | 1,589
617 |
618 | |
619 |
620 |
621 | 1,555,371
622 |
623 | |
624 |
625 |
626 | -
627 |
628 | |
629 |
630 |
631 | 1,555,371
632 |
633 | |
634 |
635 |
636 |
637 |
638 | セグメント間の内部営業収益又は振替高
639 |
640 | |
641 |
642 |
643 | 429
644 |
645 | |
646 |
647 |
648 | 494
649 |
650 | |
651 |
652 |
653 | 1,672
654 |
655 | |
656 |
657 |
658 | 701
659 |
660 | |
661 |
662 |
663 | 7,176
664 |
665 | |
666 |
667 |
668 | 3,181
669 |
670 | |
671 |
672 |
673 | 13,655
674 |
675 | |
676 |
677 |
678 | △13,655
679 |
680 | |
681 |
682 |
683 | -
684 |
685 | |
686 |
687 |
688 |
689 |
690 | 計
691 |
692 | |
693 |
694 |
695 | 217,536
696 |
697 | |
698 |
699 |
700 | 679,296
701 |
702 | |
703 |
704 |
705 | 451,684
706 |
707 | |
708 |
709 |
710 | 166,636
711 |
712 | |
713 |
714 |
715 | 49,101
716 |
717 | |
718 |
719 |
720 | 4,771
721 |
722 | |
723 |
724 |
725 | 1,569,027
726 |
727 | |
728 |
729 |
730 | △13,655
731 |
732 | |
733 |
734 |
735 | 1,555,371
736 |
737 | |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
--------------------------------------------------------------------------------
/tests/data/raw/sample_list.txt:
--------------------------------------------------------------------------------
1 | sample1: セブンアンドアイ_2023年2月期 第1四半期決算短信〔日本基準〕(連結)
2 | sample2: ファーストリテイリング_2022年8月期 第3四半期決算短信〔IFRS〕(連結)
3 | sample3: オービック_2022年3月期 第3四半期決算短信[日本基準](連結)
4 | sample4: ディスコ_2023年3月期第1四半期決算短信〔日本基準〕(連結)
5 | sample5: 野村総合研究所_2023年3月期 第1四半期決算短信〔IFRS〕(連結)
6 | sample6: オリンパス
7 | 報告セグメントを含む、決算以外のテーブルが存在する
8 | sample7: アスクル
9 | rowspanを使用している。
10 |
--------------------------------------------------------------------------------
/tests/notebooks/test_financial_result_read.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "aad2f762-a860-4632-8fde-b5df70d04190",
6 | "metadata": {
7 | "tags": []
8 | },
9 | "source": [
10 | "# セグメント情報の読み取り手順のテスト\n",
11 | "\n",
12 | "本ノートブックで、HTML化された決算短信からセグメント情報が読み取れるかテストします。\n",
13 | "\n",
14 | "## 手順\n",
15 | "\n",
16 | "1. 「報告セグメント」を含むHTMLテーブルを取得する\n",
17 | " * 処理: 「報告セグメント」の記載を内部に含むtableタグを取得する。\n",
18 | " * 解釈: 取得できない場合、会社独自様式で報告しているかセグメント情報の記載がない。\n",
19 | " * 事例: ディスコ(61460)は記載がそもそもなく、オービック(46840)はテーブルがない。\n",
20 | "3. 「報告セグメント」のHTMLテーブルから、報告時期を取得する\n",
21 | " * 処理: 「報告セグメント」のtableタグ周辺の記載から報告時期を取得する。直前3行以内にある「当第」、「前第」で始まるテキストから読み取る。\n",
22 | " * 解釈: 取得できない場合、報告時期について明記されていないか想定された記載で書いていない。\n",
23 | "4. 「報告セグメント」のHTMLテーブルから、セグメントの列を取得する\n",
24 | " * 処理: 「報告セグメント」のtableタグからセグメントの列を取得する。table内のcolspanでまとめられた列をセグメントとする。\n",
25 | " * 解釈: 取得できない場合、セグメントについて一般的ではない様式で記載していない。\n",
26 | "5. 「報告セグメント」のHTMLテーブルから、売上・利益の行を取得する\n",
27 | " * 処理: 「報告セグメント」のtableタグから売上・利益について書かれた行を取得する。table内の最も左の列のうち、最初の空白ではない行タイトルを売り上げの勘定とし次の行を利益とする。ただし、「計」を含む場合は計を売上とし次行を利益とする。\n",
28 | " * 解釈: 取得できない場合、売上・利益について一般的ではない様式で記載していない。\n",
29 | "6. 「報告セグメント」のHTMLテーブルから、各セグメントの売上・利益を取得する\n",
30 | " * 処理: 「報告セグメント」のtableタグからセグメントの列、売上・利益の行を指定し値を取得する。\n",
31 | " * 解釈: 4, 5が成功していれば該当のデータが取得できる。\n",
32 | "7. Optional: セグメントについての記載を取得する\n",
33 | " * 処理: 4で取得したセグメント名を含む記載を取得する。セグメント名もしくは記号で囲まれた見出しから、次の空行もしくは次のセグメントについて記載がある箇所までをセグメントの説明として取得する。\n",
34 | " * 解釈: 取得できない場合、セグメントについて本文に記載がない。\n",
35 | "\n",
36 | "## 検証対象\n",
37 | "\n",
38 | "ランダムに選択した5社の`qualitative.htm`で検証する。\n",
39 | "\n",
40 | "* sample1: セブンアンドアイ_2023年2月期 第1四半期決算短信〔日本基準〕(連結)\n",
41 | "* sample2: ファーストリテイリング_2022年8月期 第3四半期決算短信〔IFRS〕(連結)\n",
42 | "* sample3: オービック_2022年3月期 第3四半期決算短信[日本基準](連結)\n",
43 | "* sample4: ディスコ_2023年3月期第1四半期決算短信〔日本基準〕(連結)\n",
44 | "* sample5: 野村総合研究所_2023年3月期 第1四半期決算短信〔IFRS〕(連結)\n",
45 | "\n",
46 | "## 検証\n",
47 | "\n",
48 | "### 1. 「報告セグメント」を含むHTMLテーブルを取得する\n",
49 | "\n",
50 | "* 処理: 「報告セグメント」の記載を内部に含むtableタグを取得する。\n",
51 | "* 解釈: 取得できない場合、会社独自様式で報告しているかセグメント情報の記載がない。\n",
52 | "* 事例: ディスコ(61460)は記載がそもそもなく、オービック(46840)はテーブルがない。\n",
53 | "\n",
54 | "\n",
55 | "まず、htmファイルを読み込む"
56 | ]
57 | },
58 | {
59 | "cell_type": "code",
60 | "execution_count": 1,
61 | "id": "a9d8da42-c4ce-4383-88e0-d4d8532fda79",
62 | "metadata": {},
63 | "outputs": [],
64 | "source": [
65 | "from pathlib import Path\n",
66 | "import unicodedata\n",
67 | "import numpy as np\n",
68 | "from bs4 import BeautifulSoup\n",
69 | "\n",
70 | "\n",
71 | "def read_financial_result_html(path):\n",
72 | " _path = path if isinstance(path, Path) else Path(path)\n",
73 | " text = \"\"\n",
74 | " with _path.open() as r:\n",
75 | " lines = r.readlines()\n",
76 | " # 記載されたテキストの文字種を統一するため、Unicode正規化を行う\n",
77 | " lines = [unicodedata.normalize(\"NFKC\",line) for line in lines]\n",
78 | " text = \"\".join(lines)\n",
79 | " \n",
80 | " html = BeautifulSoup(text)\n",
81 | " return html"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": 2,
87 | "id": "9d97ec3d-9cd3-4b8f-bc2f-2c1b8d4d40a9",
88 | "metadata": {},
89 | "outputs": [],
90 | "source": [
91 | "htmls = []\n",
92 | "\n",
93 | "target_path = Path(\"./data/raw\")\n",
94 | "for path in sorted(target_path.glob(\"*.htm\")):\n",
95 | " htmls.append(read_financial_result_html(path))"
96 | ]
97 | },
98 | {
99 | "cell_type": "markdown",
100 | "id": "088d3335-42ae-4a4d-9964-beb230b0d03a",
101 | "metadata": {},
102 | "source": [
103 | "次に、報告セグメントを含むテーブルを読み込む"
104 | ]
105 | },
106 | {
107 | "cell_type": "code",
108 | "execution_count": 3,
109 | "id": "4ea76a18-f7a1-40f1-9a48-3167cc06c6bf",
110 | "metadata": {},
111 | "outputs": [],
112 | "source": [
113 | "def find_segment_tables(html):\n",
114 | " tables = html.find_all(\"table\")\n",
115 | " segment_tables = []\n",
116 | " for t in tables:\n",
117 | " if t.find(text=\"報告セグメント\"):\n",
118 | " segment_tables.append(t)\n",
119 | " \n",
120 | " return segment_tables"
121 | ]
122 | },
123 | {
124 | "cell_type": "code",
125 | "execution_count": 4,
126 | "id": "6eea1b6c-092d-4f20-b2cf-8734c048b0eb",
127 | "metadata": {},
128 | "outputs": [],
129 | "source": [
130 | "segment_tables_list = []\n",
131 | "\n",
132 | "for html in htmls:\n",
133 | " segment_tables_list.append(find_segment_tables(html))"
134 | ]
135 | },
136 | {
137 | "cell_type": "markdown",
138 | "id": "33592c8a-6219-4be2-842e-480a9aee4e81",
139 | "metadata": {},
140 | "source": [
141 | "3番目のオービック、4番目のディスコには報告セグメントが存在しない。"
142 | ]
143 | },
144 | {
145 | "cell_type": "code",
146 | "execution_count": 5,
147 | "id": "a9747c7e-b921-4c2d-84bb-ec173b3ffdd4",
148 | "metadata": {},
149 | "outputs": [
150 | {
151 | "ename": "AssertionError",
152 | "evalue": "\nArrays are not equal\n\n(shapes (0,), (5,) mismatch)\n x: array([], dtype=float64)\n y: array([ True, True, False, False, True])",
153 | "output_type": "error",
154 | "traceback": [
155 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
156 | "\u001b[1;31mAssertionError\u001b[0m Traceback (most recent call last)",
157 | "Input \u001b[1;32mIn [5]\u001b[0m, in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtesting\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43massert_array_equal\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mtables\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m>\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mtables\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43msegment_tables_list\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n",
158 | " \u001b[1;31m[... skipping hidden 1 frame]\u001b[0m\n",
159 | "File \u001b[1;32mC:\\tools\\miniconda3\\envs\\jpx-frde\\lib\\site-packages\\numpy\\testing\\_private\\utils.py:763\u001b[0m, in \u001b[0;36massert_array_compare\u001b[1;34m(comparison, x, y, err_msg, verbose, header, precision, equal_nan, equal_inf)\u001b[0m\n\u001b[0;32m 757\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m cond:\n\u001b[0;32m 758\u001b[0m msg \u001b[38;5;241m=\u001b[39m build_err_msg([x, y],\n\u001b[0;32m 759\u001b[0m err_msg\n\u001b[0;32m 760\u001b[0m \u001b[38;5;241m+\u001b[39m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m(shapes \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mx\u001b[38;5;241m.\u001b[39mshape\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, \u001b[39m\u001b[38;5;132;01m{\u001b[39;00my\u001b[38;5;241m.\u001b[39mshape\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m mismatch)\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[0;32m 761\u001b[0m verbose\u001b[38;5;241m=\u001b[39mverbose, header\u001b[38;5;241m=\u001b[39mheader,\n\u001b[0;32m 762\u001b[0m names\u001b[38;5;241m=\u001b[39m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mx\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124my\u001b[39m\u001b[38;5;124m'\u001b[39m), precision\u001b[38;5;241m=\u001b[39mprecision)\n\u001b[1;32m--> 763\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAssertionError\u001b[39;00m(msg)\n\u001b[0;32m 765\u001b[0m flagged \u001b[38;5;241m=\u001b[39m bool_(\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[0;32m 766\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m isnumber(x) \u001b[38;5;129;01mand\u001b[39;00m isnumber(y):\n",
160 | "\u001b[1;31mAssertionError\u001b[0m: \nArrays are not equal\n\n(shapes (0,), (5,) mismatch)\n x: array([], dtype=float64)\n y: array([ True, True, False, False, True])"
161 | ]
162 | }
163 | ],
164 | "source": [
165 | "np.testing.assert_array_equal([len(tables) > 0 for tables in segment_tables_list], [True, True, False, False, True])"
166 | ]
167 | },
168 | {
169 | "cell_type": "markdown",
170 | "id": "1fe04819-7318-44ad-a131-704de3c522e0",
171 | "metadata": {},
172 | "source": [
173 | "取得できたもののみ次の処理対象とする。"
174 | ]
175 | },
176 | {
177 | "cell_type": "code",
178 | "execution_count": null,
179 | "id": "30e3db00-2282-431c-8c5b-bd26342a775a",
180 | "metadata": {},
181 | "outputs": [],
182 | "source": [
183 | "segment_tables_list = [tables for tables in segment_tables_list if len(tables) > 0]"
184 | ]
185 | },
186 | {
187 | "cell_type": "markdown",
188 | "id": "ac4d0e5a-9463-495e-9a05-96ad5dbb2c8b",
189 | "metadata": {},
190 | "source": [
191 | "報告セグメントで報告している場合、前期・当期の2期のテーブルのみ存在する。"
192 | ]
193 | },
194 | {
195 | "cell_type": "code",
196 | "execution_count": null,
197 | "id": "298a4a10-66d1-4eb6-afbc-daf31d81d828",
198 | "metadata": {},
199 | "outputs": [],
200 | "source": [
201 | "np.testing.assert_array_equal([len(tables) for tables in segment_tables_list], [2, 2, 2])"
202 | ]
203 | },
204 | {
205 | "cell_type": "markdown",
206 | "id": "cefa486b-1e62-44a2-9901-8bfe0d46e357",
207 | "metadata": {},
208 | "source": [
209 | "### 2. 「報告セグメント」のHTMLテーブルから、報告時期を取得する\n",
210 | "\n",
211 | "* 処理: 「報告セグメント」のtableタグ周辺の記載から報告時期を取得する。直前3行以内にある「当第」、「前第」で始まるテキストから読み取る。\n",
212 | "* 解釈: 取得できない場合、報告時期について明記されていないか想定された記載で書いていない。"
213 | ]
214 | },
215 | {
216 | "cell_type": "code",
217 | "execution_count": null,
218 | "id": "f182d2c0-f873-449e-ab67-10e69f7cab36",
219 | "metadata": {},
220 | "outputs": [],
221 | "source": [
222 | "import re\n",
223 | "from datetime import datetime\n",
224 | "\n",
225 | "\n",
226 | "def read_table_period(table):\n",
227 | " FIND_LIMIT = 3\n",
228 | " PREVIOUS = \"前第\"\n",
229 | " CURRENT = \"当第\"\n",
230 | " FROM_PATTERN = re.compile(\"自.*?\\d+年\\d+月\\d+日\")\n",
231 | " TO_PATTERN = re.compile(\"至.*?\\d+年\\d+月\\d+日\")\n",
232 | " period = {\n",
233 | " \"kind\": \"\",\n",
234 | " \"description\": \"\",\n",
235 | " \"begin_date\": \"\",\n",
236 | " \"end_date\": \"\"\n",
237 | " }\n",
238 | "\n",
239 | " count = 0\n",
240 | " tag = table\n",
241 | " while count < FIND_LIMIT:\n",
242 | " p = tag.find_previous(\"p\")\n",
243 | " text = p.string.strip()\n",
244 | " if re.match(f\".*{PREVIOUS}.+\", text):\n",
245 | " period[\"kind\"] = \"previous\"\n",
246 | " period[\"description\"] = text\n",
247 | " if re.match(f\".*{CURRENT}.+\", text):\n",
248 | " period[\"kind\"] = \"current\"\n",
249 | " period[\"description\"] = text\n",
250 | " \n",
251 | " if period[\"kind\"]:\n",
252 | " if FROM_PATTERN.search(period[\"description\"]):\n",
253 | " date_text = FROM_PATTERN.search(text).group(0).replace(\"自\",\"\").strip()\n",
254 | " period[\"begin_date\"] = datetime.strptime(date_text, \"%Y年%m月%d日\")\n",
255 | " if TO_PATTERN.search(period[\"description\"]):\n",
256 | " date_text = TO_PATTERN.search(text).group(0).replace(\"至\",\"\").strip()\n",
257 | " period[\"end_date\"] = datetime.strptime(date_text, \"%Y年%m月%d日\")\n",
258 | "\n",
259 | " break\n",
260 | " count += 1\n",
261 | " tag = p\n",
262 | " \n",
263 | " return period"
264 | ]
265 | },
266 | {
267 | "cell_type": "markdown",
268 | "id": "c4f3a9b7-61bf-4e63-b90b-b8c556ca3766",
269 | "metadata": {},
270 | "source": [
271 | "全てのHTMLについて前期/当期が存在する。"
272 | ]
273 | },
274 | {
275 | "cell_type": "code",
276 | "execution_count": null,
277 | "id": "c4b58e82-3dfc-44d1-86fb-84b2d076715f",
278 | "metadata": {},
279 | "outputs": [
280 | {
281 | "name": "stdout",
282 | "output_type": "stream",
283 | "text": [
284 | "{'kind': 'previous', 'description': 'I 前第1四半期連結累計期間(自 2021年3月1日 至 2021年5月31日)', 'begin_date': datetime.datetime(2021, 3, 1, 0, 0), 'end_date': datetime.datetime(2021, 5, 31, 0, 0)}\n",
285 | "{'kind': 'current', 'description': 'II 当第1四半期連結累計期間(自 2022年3月1日 至 2022年5月31日)', 'begin_date': datetime.datetime(2022, 3, 1, 0, 0), 'end_date': datetime.datetime(2022, 5, 31, 0, 0)}\n",
286 | "{'kind': 'previous', 'description': '前第3四半期連結累計期間(自 2020年9月1日 至 2021年5月31日)', 'begin_date': datetime.datetime(2020, 9, 1, 0, 0), 'end_date': datetime.datetime(2021, 5, 31, 0, 0)}\n",
287 | "{'kind': 'current', 'description': '当第3四半期連結累計期間(自 2021年9月1日 至 2022年5月31日)', 'begin_date': datetime.datetime(2021, 9, 1, 0, 0), 'end_date': datetime.datetime(2022, 5, 31, 0, 0)}\n",
288 | "{'kind': 'previous', 'description': '前第1四半期連結累計期間(自 2021年4月1日 至 2021年6月30日)', 'begin_date': datetime.datetime(2021, 4, 1, 0, 0), 'end_date': datetime.datetime(2021, 6, 30, 0, 0)}\n",
289 | "{'kind': 'current', 'description': '当第1四半期連結累計期間(自 2022年4月1日 至 2022年6月30日)', 'begin_date': datetime.datetime(2022, 4, 1, 0, 0), 'end_date': datetime.datetime(2022, 6, 30, 0, 0)}\n"
290 | ]
291 | }
292 | ],
293 | "source": [
294 | "for tables in segment_tables_list:\n",
295 | " for i, t in enumerate(tables):\n",
296 | " period = read_table_period(t)\n",
297 | " print(period)\n",
298 | " if i == 0:\n",
299 | " assert (period[\"kind\"] == \"previous\")\n",
300 | " else:\n",
301 | " assert (period[\"kind\"] == \"current\")"
302 | ]
303 | },
304 | {
305 | "cell_type": "markdown",
306 | "id": "a9cb3c0e-decb-4d33-b212-42170156b2f5",
307 | "metadata": {},
308 | "source": [
309 | "以降は当期のテーブルを対象に検証する。"
310 | ]
311 | },
312 | {
313 | "cell_type": "code",
314 | "execution_count": null,
315 | "id": "198f9a28-bd92-43ae-813d-1bb0b094b740",
316 | "metadata": {},
317 | "outputs": [],
318 | "source": [
319 | "current_tables = [tables[1] for tables in segment_tables_list]\n",
320 | "assert len(current_tables) == 3"
321 | ]
322 | },
323 | {
324 | "cell_type": "markdown",
325 | "id": "85cff33d-5ef2-4e9d-892e-1e3eb981b3b6",
326 | "metadata": {},
327 | "source": [
328 | "### 4.「報告セグメント」のHTMLテーブルから、セグメントの列を取得する\n",
329 | "\n",
330 | "処理: 「報告セグメント」のtableタグからセグメントの列を取得する。table内のcolspanでまとめられた列をセグメントとする。\n",
331 | "解釈: 取得できない場合、セグメントについて一般的ではない様式で記載していない。"
332 | ]
333 | },
334 | {
335 | "cell_type": "code",
336 | "execution_count": null,
337 | "id": "856d55d2-6e2e-46af-9ca7-59ada926c51c",
338 | "metadata": {},
339 | "outputs": [],
340 | "source": [
341 | "def read_table_segments(table):\n",
342 | " merged_cell = table.find_all(\"td\", colspan=True)\n",
343 | " SEGMENT_TEXT = \"報告セグメント\"\n",
344 | " EXCLUDES = re.compile(\".{0,2}計$\")\n",
345 | " NORMALIZER = re.compile(\"\\s|\\r|\\n\")\n",
346 | " segments = []\n",
347 | " \n",
348 | " segment_cells = [cell for cell in merged_cell if SEGMENT_TEXT in cell.text]\n",
349 | " if len(segment_cells) == 0:\n",
350 | " return segments\n",
351 | " else:\n",
352 | " cell = segment_cells[0]\n",
353 | " num_segments = int(cell.attrs[\"colspan\"])\n",
354 | " descriptions = [d.text.strip() for d in cell.find_previous(\"tr\").find_all(\"td\")]\n",
355 | " segment_start = descriptions.index(SEGMENT_TEXT)\n",
356 | " title_row = cell.find_next(\"tr\")\n",
357 | " segment_index = 0\n",
358 | " for i, title in enumerate(title_row.find_all(\"td\")):\n",
359 | " if i < segment_start:\n",
360 | " continue\n",
361 | " elif i < (i + num_segments):\n",
362 | " segment_name = NORMALIZER.sub(\"\", title.text.strip())\n",
363 | " if segment_name and not EXCLUDES.match(segment_name):\n",
364 | " segments.append({\n",
365 | " \"segment_index\": segment_index,\n",
366 | " \"column\": i,\n",
367 | " \"segment_name\": segment_name\n",
368 | " })\n",
369 | " segment_index += 1\n",
370 | " \n",
371 | " return segments"
372 | ]
373 | },
374 | {
375 | "cell_type": "code",
376 | "execution_count": null,
377 | "id": "329c7f07-6f98-4677-8fd6-7851813e542d",
378 | "metadata": {},
379 | "outputs": [],
380 | "source": [
381 | "np.testing.assert_array_equal(\n",
382 | " [s[\"segment_name\"] for s in read_table_segments(current_tables[0])],\n",
383 | " [\"国内コンビニエンスストア事業\", \"海外コンビニエンスストア事業\", \"スーパーストア事業\",\n",
384 | " \"百貨店・専門店事業\", \"金融関連事業\", \"その他の事業\"]\n",
385 | ")\n",
386 | "np.testing.assert_array_equal(\n",
387 | " [s[\"segment_name\"] for s in read_table_segments(current_tables[1])],\n",
388 | " [\"国内ユニクロ事業\", \"海外ユニクロ事業\", \"ジーユー事業\", \"グローバルブランド事業\"]\n",
389 | ")\n",
390 | "np.testing.assert_array_equal(\n",
391 | " [s[\"segment_name\"] for s in read_table_segments(current_tables[2])],\n",
392 | " [\"コンサルティング\", \"金融ITソリューション\", \"産業ITソリューション\", \"IT基盤サービス\"]\n",
393 | ")"
394 | ]
395 | },
396 | {
397 | "cell_type": "markdown",
398 | "id": "6c22a0b6-8a2e-42af-b4ae-2717092877f1",
399 | "metadata": {},
400 | "source": [
401 | "### 5. 「報告セグメント」のHTMLテーブルから、売上・利益の行を取得する\n",
402 | " * 処理: 「報告セグメント」のtableタグから売上・利益について書かれた行を取得する。table内の最も左の列のうち、最初の空白ではない行タイトルを売り上げの勘定とし次の行を利益とする。ただし、「計」を含む場合は計を売上とし次行を利益とする。\n",
403 | " * 解釈: 取得できない場合、売上・利益について一般的ではない様式で記載していない。\n"
404 | ]
405 | },
406 | {
407 | "cell_type": "code",
408 | "execution_count": null,
409 | "id": "4ccc15f5-6cd4-4e5e-b130-c4bebe61936e",
410 | "metadata": {},
411 | "outputs": [],
412 | "source": [
413 | "def read_table_sales_profit(table):\n",
414 | " SUM = re.compile(\".{0,2}計$\")\n",
415 | " NORMALIZER = re.compile(\"\\s|\\r|\\n\")\n",
416 | " UNIT_TEXT = \"単位\"\n",
417 | " unit = 1000000\n",
418 | "\n",
419 | " merged_cell = table.find_all(\"td\", colspan=True)\n",
420 | " unit_cells = [cell for cell in merged_cell if UNIT_TEXT in cell.text]\n",
421 | " \n",
422 | " if len(unit_cells) > 0:\n",
423 | " cell = unit_cells[0]\n",
424 | " if \"千円\" in cell.text:\n",
425 | " unit = 1000\n",
426 | " elif \"十億\" in cell.text:\n",
427 | " unit = 1000000000\n",
428 | " \n",
429 | " rows = table.find_all(\"tr\")\n",
430 | " row_headers = [row.find_next(\"td\") for row in rows]\n",
431 | " header_texts = [NORMALIZER.sub(\"\", cell.text) for cell in row_headers]\n",
432 | " sales_index = 0\n",
433 | " sales_name = \"\"\n",
434 | " for i, text in enumerate(header_texts):\n",
435 | " if text and not sales_name:\n",
436 | " sales_name = text\n",
437 | " sales_index = i\n",
438 | " elif SUM.match(text):\n",
439 | " sales_index = i\n",
440 | " break\n",
441 | " \n",
442 | " values = []\n",
443 | " values.append({\n",
444 | " \"value_index\": 0,\n",
445 | " \"row\": sales_index,\n",
446 | " \"value_kind\": \"Sales\",\n",
447 | " \"value_name\": sales_name,\n",
448 | " \"unit\": unit\n",
449 | " })\n",
450 | " values.append({\n",
451 | " \"value_index\": 1,\n",
452 | " \"row\": sales_index + 1,\n",
453 | " \"value_kind\": \"Profit\",\n",
454 | " \"value_name\": header_texts[sales_index + 1],\n",
455 | " \"unit\": unit\n",
456 | " })\n",
457 | " \n",
458 | " return values"
459 | ]
460 | },
461 | {
462 | "cell_type": "code",
463 | "execution_count": null,
464 | "id": "96217a32-9ccd-4785-8d67-b6c14dcfa759",
465 | "metadata": {},
466 | "outputs": [],
467 | "source": [
468 | "np.testing.assert_array_equal(\n",
469 | " [s[\"value_name\"] for s in read_table_sales_profit(current_tables[0])],\n",
470 | " [\"営業収益\", \"セグメント利益又は損失(△)\"]\n",
471 | ")\n",
472 | "np.testing.assert_array_equal(\n",
473 | " [s[\"value_name\"] for s in read_table_sales_profit(current_tables[1])],\n",
474 | " [\"売上収益\", \"営業利益又は損失(△)\"]\n",
475 | ")\n",
476 | "np.testing.assert_array_equal(\n",
477 | " [s[\"value_name\"] for s in read_table_sales_profit(current_tables[2])],\n",
478 | " [\"売上収益\", \"営業利益\"]\n",
479 | ")"
480 | ]
481 | },
482 | {
483 | "cell_type": "markdown",
484 | "id": "f6f439e7-aaaa-4ee8-a1a9-76dc58f6dd4f",
485 | "metadata": {},
486 | "source": [
487 | "### 6. 「報告セグメント」のHTMLテーブルから、各セグメントの売上・利益を取得する\n",
488 | "\n",
489 | "* 処理: 「報告セグメント」のtableタグからセグメントの列、売上・利益の行を指定し値を取得する。\n",
490 | "* 解釈: 4, 5が成功していれば該当のデータが取得できる。\n"
491 | ]
492 | },
493 | {
494 | "cell_type": "code",
495 | "execution_count": null,
496 | "id": "02fa0956-a296-4e7a-afba-047615a3e93e",
497 | "metadata": {},
498 | "outputs": [],
499 | "source": [
500 | "import pandas as pd\n",
501 | "\n",
502 | "\n",
503 | "def read_segment_sales_profit(table):\n",
504 | " segment_data = []\n",
505 | " segments = read_table_segments(table)\n",
506 | " accounts = read_table_sales_profit(table)\n",
507 | " for s in segments:\n",
508 | " for a in accounts:\n",
509 | " cell = table.find_all(\"tr\")[a[\"row\"]].find_all(\"td\")[s[\"column\"]]\n",
510 | " result = {}\n",
511 | " result.update(s)\n",
512 | " result.update(a)\n",
513 | " value = cell.text.strip().replace(\"-\", \"\").replace(\",\", \"\").replace(\"△\", \"-\")\n",
514 | " try:\n",
515 | " result[\"value\"] = float(value)\n",
516 | " except Exception as ex:\n",
517 | " result[\"value\"] = None\n",
518 | " segment_data.append(result)\n",
519 | "\n",
520 | " return pd.DataFrame(segment_data)"
521 | ]
522 | },
523 | {
524 | "cell_type": "code",
525 | "execution_count": null,
526 | "id": "8ef1b5c3-345e-464b-9add-52811b45d915",
527 | "metadata": {},
528 | "outputs": [
529 | {
530 | "data": {
531 | "text/html": [
532 | "\n",
533 | "\n",
546 | " \n",
547 | " \n",
548 | " \n",
549 | " | \n",
550 | " segment_index | \n",
551 | " column | \n",
552 | " segment_name | \n",
553 | " value_index | \n",
554 | " row | \n",
555 | " value_kind | \n",
556 | " value_name | \n",
557 | " unit | \n",
558 | " value | \n",
559 | " \n",
560 | " \n",
561 | " \n",
562 | " \n",
563 | " 0 | \n",
564 | " 0 | \n",
565 | " 1 | \n",
566 | " 国内コンビニエンスストア事業 | \n",
567 | " 0 | \n",
568 | " 6 | \n",
569 | " Sales | \n",
570 | " 営業収益 | \n",
571 | " 1000000 | \n",
572 | " 215243.0 | \n",
573 | " \n",
574 | " \n",
575 | " 1 | \n",
576 | " 0 | \n",
577 | " 1 | \n",
578 | " 国内コンビニエンスストア事業 | \n",
579 | " 1 | \n",
580 | " 7 | \n",
581 | " Profit | \n",
582 | " セグメント利益又は損失(△) | \n",
583 | " 1000000 | \n",
584 | " 59282.0 | \n",
585 | " \n",
586 | " \n",
587 | " 2 | \n",
588 | " 1 | \n",
589 | " 2 | \n",
590 | " 海外コンビニエンスストア事業 | \n",
591 | " 0 | \n",
592 | " 6 | \n",
593 | " Sales | \n",
594 | " 営業収益 | \n",
595 | " 1000000 | \n",
596 | " 1723889.0 | \n",
597 | " \n",
598 | " \n",
599 | " 3 | \n",
600 | " 1 | \n",
601 | " 2 | \n",
602 | " 海外コンビニエンスストア事業 | \n",
603 | " 1 | \n",
604 | " 7 | \n",
605 | " Profit | \n",
606 | " セグメント利益又は損失(△) | \n",
607 | " 1000000 | \n",
608 | " 43981.0 | \n",
609 | " \n",
610 | " \n",
611 | " 4 | \n",
612 | " 2 | \n",
613 | " 3 | \n",
614 | " スーパーストア事業 | \n",
615 | " 0 | \n",
616 | " 6 | \n",
617 | " Sales | \n",
618 | " 営業収益 | \n",
619 | " 1000000 | \n",
620 | " 355772.0 | \n",
621 | " \n",
622 | " \n",
623 | " 5 | \n",
624 | " 2 | \n",
625 | " 3 | \n",
626 | " スーパーストア事業 | \n",
627 | " 1 | \n",
628 | " 7 | \n",
629 | " Profit | \n",
630 | " セグメント利益又は損失(△) | \n",
631 | " 1000000 | \n",
632 | " 3517.0 | \n",
633 | " \n",
634 | " \n",
635 | " 6 | \n",
636 | " 3 | \n",
637 | " 4 | \n",
638 | " 百貨店・専門店事業 | \n",
639 | " 0 | \n",
640 | " 6 | \n",
641 | " Sales | \n",
642 | " 営業収益 | \n",
643 | " 1000000 | \n",
644 | " 112904.0 | \n",
645 | " \n",
646 | " \n",
647 | " 7 | \n",
648 | " 3 | \n",
649 | " 4 | \n",
650 | " 百貨店・専門店事業 | \n",
651 | " 1 | \n",
652 | " 7 | \n",
653 | " Profit | \n",
654 | " セグメント利益又は損失(△) | \n",
655 | " 1000000 | \n",
656 | " 1086.0 | \n",
657 | " \n",
658 | " \n",
659 | " 8 | \n",
660 | " 4 | \n",
661 | " 5 | \n",
662 | " 金融関連事業 | \n",
663 | " 0 | \n",
664 | " 6 | \n",
665 | " Sales | \n",
666 | " 営業収益 | \n",
667 | " 1000000 | \n",
668 | " 47560.0 | \n",
669 | " \n",
670 | " \n",
671 | " 9 | \n",
672 | " 4 | \n",
673 | " 5 | \n",
674 | " 金融関連事業 | \n",
675 | " 1 | \n",
676 | " 7 | \n",
677 | " Profit | \n",
678 | " セグメント利益又は損失(△) | \n",
679 | " 1000000 | \n",
680 | " 9205.0 | \n",
681 | " \n",
682 | " \n",
683 | " 10 | \n",
684 | " 5 | \n",
685 | " 6 | \n",
686 | " その他の事業 | \n",
687 | " 0 | \n",
688 | " 6 | \n",
689 | " Sales | \n",
690 | " 営業収益 | \n",
691 | " 1000000 | \n",
692 | " 5829.0 | \n",
693 | " \n",
694 | " \n",
695 | " 11 | \n",
696 | " 5 | \n",
697 | " 6 | \n",
698 | " その他の事業 | \n",
699 | " 1 | \n",
700 | " 7 | \n",
701 | " Profit | \n",
702 | " セグメント利益又は損失(△) | \n",
703 | " 1000000 | \n",
704 | " -90.0 | \n",
705 | " \n",
706 | " \n",
707 | " \n",
708 | " "
709 | ],
710 | "text/plain": [
711 | " segment_index column segment_name value_index row value_kind \\\n",
712 | "0 0 1 国内コンビニエンスストア事業 0 6 Sales \n",
713 | "1 0 1 国内コンビニエンスストア事業 1 7 Profit \n",
714 | "2 1 2 海外コンビニエンスストア事業 0 6 Sales \n",
715 | "3 1 2 海外コンビニエンスストア事業 1 7 Profit \n",
716 | "4 2 3 スーパーストア事業 0 6 Sales \n",
717 | "5 2 3 スーパーストア事業 1 7 Profit \n",
718 | "6 3 4 百貨店・専門店事業 0 6 Sales \n",
719 | "7 3 4 百貨店・専門店事業 1 7 Profit \n",
720 | "8 4 5 金融関連事業 0 6 Sales \n",
721 | "9 4 5 金融関連事業 1 7 Profit \n",
722 | "10 5 6 その他の事業 0 6 Sales \n",
723 | "11 5 6 その他の事業 1 7 Profit \n",
724 | "\n",
725 | " value_name unit value \n",
726 | "0 営業収益 1000000 215243.0 \n",
727 | "1 セグメント利益又は損失(△) 1000000 59282.0 \n",
728 | "2 営業収益 1000000 1723889.0 \n",
729 | "3 セグメント利益又は損失(△) 1000000 43981.0 \n",
730 | "4 営業収益 1000000 355772.0 \n",
731 | "5 セグメント利益又は損失(△) 1000000 3517.0 \n",
732 | "6 営業収益 1000000 112904.0 \n",
733 | "7 セグメント利益又は損失(△) 1000000 1086.0 \n",
734 | "8 営業収益 1000000 47560.0 \n",
735 | "9 セグメント利益又は損失(△) 1000000 9205.0 \n",
736 | "10 営業収益 1000000 5829.0 \n",
737 | "11 セグメント利益又は損失(△) 1000000 -90.0 "
738 | ]
739 | },
740 | "execution_count": 16,
741 | "metadata": {},
742 | "output_type": "execute_result"
743 | }
744 | ],
745 | "source": [
746 | "read_segment_sales_profit(current_tables[0])"
747 | ]
748 | },
749 | {
750 | "cell_type": "code",
751 | "execution_count": null,
752 | "id": "3bab3fe6-dcbd-4815-82f2-3e8a276654e1",
753 | "metadata": {},
754 | "outputs": [
755 | {
756 | "data": {
757 | "text/html": [
758 | "\n",
759 | "\n",
772 | " \n",
773 | " \n",
774 | " \n",
775 | " | \n",
776 | " segment_index | \n",
777 | " column | \n",
778 | " segment_name | \n",
779 | " value_index | \n",
780 | " row | \n",
781 | " value_kind | \n",
782 | " value_name | \n",
783 | " unit | \n",
784 | " value | \n",
785 | " \n",
786 | " \n",
787 | " \n",
788 | " \n",
789 | " 0 | \n",
790 | " 0 | \n",
791 | " 1 | \n",
792 | " 国内ユニクロ事業 | \n",
793 | " 0 | \n",
794 | " 3 | \n",
795 | " Sales | \n",
796 | " 売上収益 | \n",
797 | " 1000000 | \n",
798 | " 640972.0 | \n",
799 | " \n",
800 | " \n",
801 | " 1 | \n",
802 | " 0 | \n",
803 | " 1 | \n",
804 | " 国内ユニクロ事業 | \n",
805 | " 1 | \n",
806 | " 4 | \n",
807 | " Profit | \n",
808 | " 営業利益又は損失(△) | \n",
809 | " 1000000 | \n",
810 | " 119067.0 | \n",
811 | " \n",
812 | " \n",
813 | " 2 | \n",
814 | " 1 | \n",
815 | " 2 | \n",
816 | " 海外ユニクロ事業 | \n",
817 | " 0 | \n",
818 | " 3 | \n",
819 | " Sales | \n",
820 | " 売上収益 | \n",
821 | " 1000000 | \n",
822 | " 841274.0 | \n",
823 | " \n",
824 | " \n",
825 | " 3 | \n",
826 | " 1 | \n",
827 | " 2 | \n",
828 | " 海外ユニクロ事業 | \n",
829 | " 1 | \n",
830 | " 4 | \n",
831 | " Profit | \n",
832 | " 営業利益又は損失(△) | \n",
833 | " 1000000 | \n",
834 | " 132793.0 | \n",
835 | " \n",
836 | " \n",
837 | " 4 | \n",
838 | " 2 | \n",
839 | " 3 | \n",
840 | " ジーユー事業 | \n",
841 | " 0 | \n",
842 | " 3 | \n",
843 | " Sales | \n",
844 | " 売上収益 | \n",
845 | " 1000000 | \n",
846 | " 190545.0 | \n",
847 | " \n",
848 | " \n",
849 | " 5 | \n",
850 | " 2 | \n",
851 | " 3 | \n",
852 | " ジーユー事業 | \n",
853 | " 1 | \n",
854 | " 4 | \n",
855 | " Profit | \n",
856 | " 営業利益又は損失(△) | \n",
857 | " 1000000 | \n",
858 | " 17852.0 | \n",
859 | " \n",
860 | " \n",
861 | " 6 | \n",
862 | " 3 | \n",
863 | " 4 | \n",
864 | " グローバルブランド事業 | \n",
865 | " 0 | \n",
866 | " 3 | \n",
867 | " Sales | \n",
868 | " 売上収益 | \n",
869 | " 1000000 | \n",
870 | " 90084.0 | \n",
871 | " \n",
872 | " \n",
873 | " 7 | \n",
874 | " 3 | \n",
875 | " 4 | \n",
876 | " グローバルブランド事業 | \n",
877 | " 1 | \n",
878 | " 4 | \n",
879 | " Profit | \n",
880 | " 営業利益又は損失(△) | \n",
881 | " 1000000 | \n",
882 | " 720.0 | \n",
883 | " \n",
884 | " \n",
885 | " \n",
886 | " "
887 | ],
888 | "text/plain": [
889 | " segment_index column segment_name value_index row value_kind \\\n",
890 | "0 0 1 国内ユニクロ事業 0 3 Sales \n",
891 | "1 0 1 国内ユニクロ事業 1 4 Profit \n",
892 | "2 1 2 海外ユニクロ事業 0 3 Sales \n",
893 | "3 1 2 海外ユニクロ事業 1 4 Profit \n",
894 | "4 2 3 ジーユー事業 0 3 Sales \n",
895 | "5 2 3 ジーユー事業 1 4 Profit \n",
896 | "6 3 4 グローバルブランド事業 0 3 Sales \n",
897 | "7 3 4 グローバルブランド事業 1 4 Profit \n",
898 | "\n",
899 | " value_name unit value \n",
900 | "0 売上収益 1000000 640972.0 \n",
901 | "1 営業利益又は損失(△) 1000000 119067.0 \n",
902 | "2 売上収益 1000000 841274.0 \n",
903 | "3 営業利益又は損失(△) 1000000 132793.0 \n",
904 | "4 売上収益 1000000 190545.0 \n",
905 | "5 営業利益又は損失(△) 1000000 17852.0 \n",
906 | "6 売上収益 1000000 90084.0 \n",
907 | "7 営業利益又は損失(△) 1000000 720.0 "
908 | ]
909 | },
910 | "execution_count": 17,
911 | "metadata": {},
912 | "output_type": "execute_result"
913 | }
914 | ],
915 | "source": [
916 | "read_segment_sales_profit(current_tables[1])"
917 | ]
918 | },
919 | {
920 | "cell_type": "code",
921 | "execution_count": null,
922 | "id": "bc364560-9e6a-45cf-b71e-5fe149208d99",
923 | "metadata": {},
924 | "outputs": [
925 | {
926 | "data": {
927 | "text/html": [
928 | "\n",
929 | "\n",
942 | " \n",
943 | " \n",
944 | " \n",
945 | " | \n",
946 | " segment_index | \n",
947 | " column | \n",
948 | " segment_name | \n",
949 | " value_index | \n",
950 | " row | \n",
951 | " value_kind | \n",
952 | " value_name | \n",
953 | " unit | \n",
954 | " value | \n",
955 | " \n",
956 | " \n",
957 | " \n",
958 | " \n",
959 | " 0 | \n",
960 | " 0 | \n",
961 | " 1 | \n",
962 | " コンサルティング | \n",
963 | " 0 | \n",
964 | " 6 | \n",
965 | " Sales | \n",
966 | " 売上収益 | \n",
967 | " 1000000 | \n",
968 | " 9711.0 | \n",
969 | " \n",
970 | " \n",
971 | " 1 | \n",
972 | " 0 | \n",
973 | " 1 | \n",
974 | " コンサルティング | \n",
975 | " 1 | \n",
976 | " 7 | \n",
977 | " Profit | \n",
978 | " 営業利益 | \n",
979 | " 1000000 | \n",
980 | " 1859.0 | \n",
981 | " \n",
982 | " \n",
983 | " 2 | \n",
984 | " 1 | \n",
985 | " 3 | \n",
986 | " 金融ITソリューション | \n",
987 | " 0 | \n",
988 | " 6 | \n",
989 | " Sales | \n",
990 | " 売上収益 | \n",
991 | " 1000000 | \n",
992 | " 81490.0 | \n",
993 | " \n",
994 | " \n",
995 | " 3 | \n",
996 | " 1 | \n",
997 | " 3 | \n",
998 | " 金融ITソリューション | \n",
999 | " 1 | \n",
1000 | " 7 | \n",
1001 | " Profit | \n",
1002 | " 営業利益 | \n",
1003 | " 1000000 | \n",
1004 | " 11678.0 | \n",
1005 | " \n",
1006 | " \n",
1007 | " 4 | \n",
1008 | " 2 | \n",
1009 | " 5 | \n",
1010 | " 産業ITソリューション | \n",
1011 | " 0 | \n",
1012 | " 6 | \n",
1013 | " Sales | \n",
1014 | " 売上収益 | \n",
1015 | " 1000000 | \n",
1016 | " 68841.0 | \n",
1017 | " \n",
1018 | " \n",
1019 | " 5 | \n",
1020 | " 2 | \n",
1021 | " 5 | \n",
1022 | " 産業ITソリューション | \n",
1023 | " 1 | \n",
1024 | " 7 | \n",
1025 | " Profit | \n",
1026 | " 営業利益 | \n",
1027 | " 1000000 | \n",
1028 | " 6679.0 | \n",
1029 | " \n",
1030 | " \n",
1031 | " 6 | \n",
1032 | " 3 | \n",
1033 | " 7 | \n",
1034 | " IT基盤サービス | \n",
1035 | " 0 | \n",
1036 | " 6 | \n",
1037 | " Sales | \n",
1038 | " 売上収益 | \n",
1039 | " 1000000 | \n",
1040 | " 40403.0 | \n",
1041 | " \n",
1042 | " \n",
1043 | " 7 | \n",
1044 | " 3 | \n",
1045 | " 7 | \n",
1046 | " IT基盤サービス | \n",
1047 | " 1 | \n",
1048 | " 7 | \n",
1049 | " Profit | \n",
1050 | " 営業利益 | \n",
1051 | " 1000000 | \n",
1052 | " 5652.0 | \n",
1053 | " \n",
1054 | " \n",
1055 | " \n",
1056 | " "
1057 | ],
1058 | "text/plain": [
1059 | " segment_index column segment_name value_index row value_kind value_name \\\n",
1060 | "0 0 1 コンサルティング 0 6 Sales 売上収益 \n",
1061 | "1 0 1 コンサルティング 1 7 Profit 営業利益 \n",
1062 | "2 1 3 金融ITソリューション 0 6 Sales 売上収益 \n",
1063 | "3 1 3 金融ITソリューション 1 7 Profit 営業利益 \n",
1064 | "4 2 5 産業ITソリューション 0 6 Sales 売上収益 \n",
1065 | "5 2 5 産業ITソリューション 1 7 Profit 営業利益 \n",
1066 | "6 3 7 IT基盤サービス 0 6 Sales 売上収益 \n",
1067 | "7 3 7 IT基盤サービス 1 7 Profit 営業利益 \n",
1068 | "\n",
1069 | " unit value \n",
1070 | "0 1000000 9711.0 \n",
1071 | "1 1000000 1859.0 \n",
1072 | "2 1000000 81490.0 \n",
1073 | "3 1000000 11678.0 \n",
1074 | "4 1000000 68841.0 \n",
1075 | "5 1000000 6679.0 \n",
1076 | "6 1000000 40403.0 \n",
1077 | "7 1000000 5652.0 "
1078 | ]
1079 | },
1080 | "execution_count": 18,
1081 | "metadata": {},
1082 | "output_type": "execute_result"
1083 | }
1084 | ],
1085 | "source": [
1086 | "read_segment_sales_profit(current_tables[2])"
1087 | ]
1088 | },
1089 | {
1090 | "cell_type": "code",
1091 | "execution_count": null,
1092 | "id": "e79b5011-84cd-4ed6-9263-de091e425abc",
1093 | "metadata": {},
1094 | "outputs": [],
1095 | "source": []
1096 | }
1097 | ],
1098 | "metadata": {
1099 | "interpreter": {
1100 | "hash": "9e92602f210dc61ccc424c74f06e3d050a4530807035fa2e9f9fdb5f45ecdbb6"
1101 | },
1102 | "kernelspec": {
1103 | "display_name": "jpx-frde:Python",
1104 | "language": "python",
1105 | "name": "python3"
1106 | },
1107 | "language_info": {
1108 | "codemirror_mode": {
1109 | "name": "ipython",
1110 | "version": 3
1111 | },
1112 | "file_extension": ".py",
1113 | "mimetype": "text/x-python",
1114 | "name": "python",
1115 | "nbconvert_exporter": "python",
1116 | "pygments_lexer": "ipython3",
1117 | "version": "3.10.6"
1118 | }
1119 | },
1120 | "nbformat": 4,
1121 | "nbformat_minor": 5
1122 | }
1123 |
--------------------------------------------------------------------------------
/tests/test_financial_result_reader.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | import numpy as np
5 | import pytest
6 | from bs4 import BeautifulSoup
7 | from bs4.element import Tag
8 |
9 | import scripts.financial_result_reader as frr
10 |
11 |
12 | @pytest.fixture
13 | def htmls() -> dict[str, BeautifulSoup]:
14 | data_dir = os.path.join(os.path.dirname(__file__), "data/raw")
15 | htmls = {}
16 | for path in Path(data_dir).glob("*.htm"):
17 | key = path.name
18 | htmls[key] = frr.read_financial_result_html(path)
19 | return htmls
20 |
21 |
22 | class TestFinancialResultReader:
23 | def test_read_financial_result_html_from_str(self) -> None:
24 | path = os.path.join(os.path.dirname(__file__), "data/raw/sample1.htm")
25 | html = frr.read_financial_result_html(path)
26 | assert html is not None
27 |
28 | def test_read_financial_result_html_from_path(self) -> None:
29 | path = Path(os.path.dirname(__file__)).joinpath("data/raw/sample1.htm")
30 | html = frr.read_financial_result_html(path)
31 | assert html is not None
32 |
33 | def test_read_financial_result_html_when_none(self) -> None:
34 | path = Path(os.path.dirname(__file__)).joinpath("xxxxx")
35 | html = frr.read_financial_result_html(path)
36 | assert html is None
37 |
38 | def test_find_segment_tables(self, htmls: dict[str, BeautifulSoup]) -> None:
39 | for name in htmls:
40 | segment_tables = frr.find_segment_tables(htmls[name])
41 | for table in segment_tables:
42 | # Found tag should include table tag ()
43 | assert len(table.find_all("tr")) > 0
44 | # Found table tag should include "報告セグメント"
45 | assert "報告セグメント" in table.get_text()
46 |
47 | if name in ["sample3.htm", "sample4.htm"]:
48 | assert len(segment_tables) == 0
49 | else:
50 | assert len(segment_tables) > 0
51 |
52 | def test_read_table_period(self, htmls: dict[str, BeautifulSoup]) -> None:
53 | for name in htmls:
54 | segment_tables = frr.find_segment_tables(htmls[name])
55 | for i, t in enumerate(segment_tables):
56 | period = frr.read_table_period(t)
57 | assert period.kind is not None
58 |
59 | if name == "sample1.htm":
60 | if i == 0:
61 | assert period.kind == "previous"
62 | else:
63 | assert period.kind == "current"
64 |
65 | def get_current_tables(self, htmls: dict[str, BeautifulSoup]) -> dict[str, Tag]:
66 | tables = {}
67 | for name in htmls:
68 | segment_tables = frr.find_segment_tables(htmls[name])
69 | valid_tables = []
70 | for table in segment_tables:
71 | period = frr.read_table_period(table)
72 | if not period.kind:
73 | continue
74 | segments = frr.read_table_segments(table)
75 | if len(segments) == 0:
76 | continue
77 | accounts = frr.read_table_sales_profit(table)
78 | if len(accounts) != 2:
79 | continue
80 |
81 | valid_tables.append(table)
82 |
83 | if len(valid_tables) > 0:
84 | tables[name] = valid_tables[1] # 0=前期、1=当期
85 | return tables
86 |
87 | def test_read_table_segments(self, htmls: dict[str, BeautifulSoup]) -> None:
88 | tables = self.get_current_tables(htmls)
89 |
90 | for name in tables:
91 | segments = frr.read_table_segments(tables[name])
92 | names = [s.name for s in segments]
93 | if name == "sample1.htm":
94 | np.testing.assert_array_equal(
95 | names,
96 | [
97 | "国内コンビニエンスストア事業",
98 | "海外コンビニエンスストア事業",
99 | "スーパーストア事業",
100 | "百貨店・専門店事業",
101 | "金融関連事業",
102 | "その他の事業",
103 | ],
104 | )
105 | elif name == "sample2.htm":
106 | np.testing.assert_array_equal(
107 | names, ["国内ユニクロ事業", "海外ユニクロ事業", "ジーユー事業", "グローバルブランド事業"]
108 | )
109 | elif name == "sample5.htm":
110 | np.testing.assert_array_equal(
111 | names, ["コンサルティング", "金融ITソリューション", "産業ITソリューション", "IT基盤サービス"]
112 | )
113 | elif name == "sample6.htm":
114 | np.testing.assert_array_equal(names, ["内視鏡", "治療機器", "科学", "その他"])
115 | elif name == "sample7.htm":
116 | np.testing.assert_array_equal(names, ["eコマース事業", "ロジスティクス事業"])
117 |
118 | def test_read_table_sales_profit(self, htmls: dict[str, BeautifulSoup]) -> None:
119 | tables = self.get_current_tables(htmls)
120 |
121 | for name in tables:
122 | accounts = frr.read_table_sales_profit(tables[name])
123 | names = [a.name for a in accounts]
124 | if name == "sample1.htm":
125 | np.testing.assert_array_equal(
126 | names,
127 | ["営業収益", "セグメント利益又は損失(△)"],
128 | )
129 | elif name == "sample2.htm":
130 | np.testing.assert_array_equal(names, ["売上収益", "営業利益又は損失(△)"])
131 | elif name == "sample5.htm":
132 | np.testing.assert_array_equal(names, ["売上収益", "営業利益"])
133 | elif name == "sample6.htm":
134 | np.testing.assert_array_equal(names, ["売上高", "営業利益又は損失"])
135 | elif name == "sample7.htm":
136 | np.testing.assert_array_equal(names, ["売上高", "セグメント利益又は損失(△)"])
137 |
138 | def test_read_segment_sales_profit(self, htmls: dict[str, BeautifulSoup]) -> None:
139 | tables = self.get_current_tables(htmls)
140 |
141 | for name in tables:
142 | segments = frr.read_table_segments(tables[name])
143 | accounts = frr.read_table_sales_profit(tables[name])
144 | if name == "sample1.htm":
145 | data = frr.read_segment_sales_profit(
146 | tables[name], segments[0], accounts[0]
147 | )
148 | assert data.segment_name == "国内コンビニエンスストア事業"
149 | assert data.account_name == "営業収益"
150 | assert data.value == 215243.0
151 |
152 | elif name == "sample2.htm":
153 | data = frr.read_segment_sales_profit(
154 | tables[name], segments[3], accounts[1]
155 | )
156 | assert data.segment_name == "グローバルブランド事業"
157 | assert data.account_name == "営業利益又は損失(△)"
158 | assert data.value == 720.0
159 |
160 | elif name == "sample5.htm":
161 | data = frr.read_segment_sales_profit(
162 | tables[name], segments[1], accounts[1]
163 | )
164 | assert data.segment_name == "金融ITソリューション"
165 | assert data.account_name == "営業利益"
166 | assert data.value == 11678.0
167 |
168 | elif name == "sample7.htm":
169 | data = frr.read_segment_sales_profit(
170 | tables[name], segments[0], accounts[0]
171 | )
172 | assert data.segment_name == "eコマース事業"
173 | assert data.account_name == "売上高"
174 | assert data.value == 418698.0
175 |
--------------------------------------------------------------------------------
/tests/test_financial_result_to_dataframe.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | from scripts.financial_result_to_dataframe import financial_result_to_dataframe
5 |
6 |
7 | class TestFinancialResultToDataFrame:
8 | def test_read_html_failed(self) -> None:
9 | df, log = financial_result_to_dataframe("xxxxx")
10 | assert df is None
11 | assert log.status.read_html_failed
12 |
13 | def test_segment_table_not_exist(self) -> None:
14 | path = Path(os.path.dirname(__file__)).joinpath("data/raw/sample3.htm")
15 | df, log = financial_result_to_dataframe(path)
16 | assert df is None
17 | assert not log.status.read_html_failed
18 | assert log.status.segment_table_not_exist
19 |
20 | def test_too_little_segment_table(self) -> None:
21 | path = Path(os.path.dirname(__file__)).joinpath(
22 | "data/raw/exceptions/too_little_segment_table.htm"
23 | )
24 | _, log = financial_result_to_dataframe(path)
25 | assert log.status.too_little_segment_table
26 |
27 | def test_too_much_segment_tables(self) -> None:
28 | path = Path(os.path.dirname(__file__)).joinpath(
29 | "data/raw/exceptions/too_much_segment_tables.htm"
30 | )
31 | _, log = financial_result_to_dataframe(path)
32 | assert log.status.too_much_segment_tables
33 |
34 | def test_period_not_found(self) -> None:
35 | path = Path(os.path.dirname(__file__)).joinpath(
36 | "data/raw/exceptions/period_not_exist.htm"
37 | )
38 | _, log = financial_result_to_dataframe(path)
39 | assert not log.status.read_html_failed
40 | assert not log.status.segment_table_not_exist
41 | assert log.status.period_not_found
42 |
43 | def test_account_not_found(self) -> None:
44 | path = Path(os.path.dirname(__file__)).joinpath(
45 | "data/raw/exceptions/account_not_found.htm"
46 | )
47 | _, log = financial_result_to_dataframe(path)
48 | assert not log.status.read_html_failed
49 | assert not log.status.segment_table_not_exist
50 | assert not log.status.period_not_found
51 | assert not log.status.segment_not_found
52 | assert log.status.account_not_found
53 |
54 | def test_value_read_failed(self) -> None:
55 | path = Path(os.path.dirname(__file__)).joinpath(
56 | "data/raw/exceptions/value_read_failed.htm"
57 | )
58 | _, log = financial_result_to_dataframe(path)
59 | assert not log.status.read_html_failed
60 | assert not log.status.segment_table_not_exist
61 | assert not log.status.period_not_found
62 | assert not log.status.segment_not_found
63 | assert not log.status.account_not_found
64 | assert log.status.value_read_failed
65 |
66 | def test_financial_result_to_dataframe(self) -> None:
67 | path = Path(os.path.dirname(__file__)).joinpath("data/raw/sample1.htm")
68 | _, log = financial_result_to_dataframe(path)
69 | assert not log.status.read_html_failed
70 | assert not log.status.segment_table_not_exist
71 | assert not log.status.period_not_found
72 | assert not log.status.segment_not_found
73 | assert not log.status.account_not_found
74 | assert not log.status.value_read_failed
75 | assert log.status.completed
76 |
--------------------------------------------------------------------------------
| |