├── .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 | [![Source Code Check](https://github.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/actions/workflows/ci.yml/badge.svg)](https://github.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction/actions/workflows/ci.yml) 4 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | [![Code style: flake8](https://img.shields.io/badge/code%20style-flake8-black)](https://github.com/PyCQA/flake8) 7 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) 8 | [![Typing: mypy](https://img.shields.io/badge/typing-mypy-blue)](https://github.com/python/mypy) 9 | 10 | 11 | HTML 化された決算短信から、セグメント情報を抽出する方法が学べるハンズオンです。 12 | 13 | ![top.jpg](notebooks/images/top.jpg) 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 から情報を抽出する方法を学ぶ [![Open in SageMaker Studio Lab](https://studiolab.sagemaker.aws/studiolab.svg)](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 化された決算短信からセグメント情報を抽出する方法を学ぶ [![Open in SageMaker Studio Lab](https://studiolab.sagemaker.aws/studiolab.svg)](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の読み取り可否状況レポート [![Open in SageMaker Studio Lab](https://studiolab.sagemaker.aws/studiolab.svg)](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 | * ![referral_code.PNG](./images/usage/referral_code.PNG) 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 | ![browser_setting.png](images/usage/browser_setting.png) 34 | 35 | 1. 右上の "Sign in" ボタンを押す。 36 | * ![signin.PNG](images/usage/signin.PNG) 37 | 2. Eメールアドレス/ユーザー名、パスワードを入力する。 38 | 3. "Sign in" を押しプロジェクトのページを開く。 39 | * ![after-login.png](images/usage/after-login.png) 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 | * ![start_runtime.png](images/usage/start_runtime.png) 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 | * ![clone_git_repository.png](images/usage/clone_git_repository.png) 58 | 2. GitHubから教材のファイルをコピーする。 59 | * 「Git repository URL (.git)」に次のURLを入れて「Clone」を押してください。 60 | * `https://github.com/JapanExchangeGroup/FinancialResultsHTML-DataExtraction.git` 61 | * ![clone_git_repository_execute.png](images/usage/clone_git_repository_execute.png) 62 | 3. "Confirm you want to build..."が出たら「OK」を押す。 63 | * リポジトリに含まれている `environment.yml` から自動的環境を作成してくれます。 64 | * ![create_conda_environment.png](images/usage/create_conda_environment.png) 65 | * OKを押し忘れたら`environment.yml`を右クリックし「Build Conda Environment」を実行してください。 66 | * 起動したターミナルで実行されたコマンドが終了したら環境構築は完了です。「done」とコンソール上に表示されます。 67 | * ![create_environment_in_terminal.png](images/usage/create_environment_in_terminal.png) 68 | 4. 教材である `FinancialResultsHTML-DataExtraction/notebooks/01_how_to_extract_from_html.ipynb` を開く。 69 | * ![open_notebook.png](images/usage/open_notebook.png) 70 | 5. 教材のNotebookを動かすためのKernelを選択する。 71 | * 右上の 「No Kernel」を押し、 `jpx-frde`を選択。 72 | * ![run_notebook.png](images/usage/run_notebook.png) 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 | "![html_history.jpg](images/html_history.jpg)\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 | "![html_parser.png](images/html_parser.png)\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 | "五反田のセル (``) になります。 HTML のテーブルは、行 (``) の中にセル (`" 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 つしかないので隣はない気がしますが、セルの `` の隣は目黒の行という気がしますが、テキスト要素も含むため隣の改行文字が取得されます。" 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 |
大崎
五反田
目黒
恵比寿
渋谷
五反田五反田`) のさらに `parent` は、行 (`
`) が何個かあるという形式で定義されています。" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": 14, 426 | "metadata": {}, 427 | "outputs": [ 428 | { 429 | "data": { 430 | "text/plain": [ 431 | "
五反田
大崎
五反田
目黒
恵比寿
渋谷
` タグの隣にある 「五反田」 のテキストが取得されます。 " 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` を使います。 五反田の行 `
目黒
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 74 | 75 | 76 | 81 | 86 | 91 | 99 | 113 | 114 | 115 | 120 | 125 | 130 | 138 | 146 | 154 | 162 | 163 | 164 | 169 | 174 | 179 | 184 | 189 | 194 | 199 | 204 | 209 | 214 | 215 | 216 | 224 | 229 | 234 | 239 | 244 | 249 | 254 | 259 | 264 | 269 | 270 | 271 | 276 | 281 | 286 | 291 | 296 | 301 | 306 | 311 | 316 | 321 | 322 | 323 | 328 | 333 | 338 | 343 | 348 | 353 | 358 | 363 | 368 | 373 | 374 | 375 |
32 |

33 |   34 |

35 |
37 |

38 |   39 |

40 |
42 |

43 |   44 |

45 |
47 |

48 |   49 |

50 |
52 |

53 |   54 |

55 |
57 |

58 |   59 |

60 |
62 |

63 |   64 |

65 |
67 |

68 |   69 |

70 |
72 |

(単位:百万円)

73 |
77 |

78 |   79 |

80 |
82 |

83 | 報告セグメント 84 |

85 |
87 |

88 | 89 |

90 |
92 |

93 | 調整額 94 |

95 |

96 | (注)1 97 |

98 |
100 |

101 | 四半期連結 102 |

103 |

104 | 損益計算書 105 |

106 |

107 | 計上額 108 |

109 |

110 | (注)2 111 |

112 |
116 |

117 |   118 |

119 |
121 |

122 | 国内コンビニエンスストア事業 123 |

124 |
126 |

127 | 海外コンビニエンスストア事業 128 |

129 |
131 |

132 | スーパー 133 |

134 |

135 | ストア事業 136 |

137 |
139 |

140 | 百貨店・ 141 |

142 |

143 | 専門店事業 144 |

145 |
147 |

148 | 金融関連 149 |

150 |

151 | 事業 152 |

153 |
155 |

156 | その他の 157 |

158 |

159 | 事業 160 |

161 |
165 |

166 | 営業収益 167 |

168 |
170 |

171 |   172 |

173 |
175 |

176 |   177 |

178 |
180 |

181 |   182 |

183 |
185 |

186 |   187 |

188 |
190 |

191 |   192 |

193 |
195 |

196 |   197 |

198 |
200 |

201 |   202 |

203 |
205 |

206 |   207 |

208 |
210 |

211 |   212 |

213 |
217 |

218 | 外部顧客への 219 |

220 |

221 | 営業収益 222 |

223 |
225 |

226 | 217,107 227 |

228 |
230 |

231 | 678,802 232 |

233 |
235 |

236 | 450,012 237 |

238 |
240 |

241 | 165,934 242 |

243 |
245 |

246 | 41,925 247 |

248 |
250 |

251 | 1,589 252 |

253 |
255 |

256 | 1,555,371 257 |

258 |
260 |

261 | 262 |

263 |
265 |

266 | 1,555,371 267 |

268 |
272 |

273 | セグメント間の内部営業収益又は振替高 274 |

275 |
277 |

278 | 429 279 |

280 |
282 |

283 | 494 284 |

285 |
287 |

288 | 1,672 289 |

290 |
292 |

293 | 701 294 |

295 |
297 |

298 | 7,176 299 |

300 |
302 |

303 | 3,181 304 |

305 |
307 |

308 | 13,655 309 |

310 |
312 |

313 | △13,655 314 |

315 |
317 |

318 | 319 |

320 |
324 |

325 |  計 326 |

327 |
329 |

330 | 217,536 331 |

332 |
334 |

335 | 679,296 336 |

337 |
339 |

340 | 451,684 341 |

342 |
344 |

345 | 166,636 346 |

347 |
349 |

350 | 49,101 351 |

352 |
354 |

355 | 4,771 356 |

357 |
359 |

360 | 1,569,027 361 |

362 |
364 |

365 | △13,655 366 |

367 |
369 |

370 | 1,555,371 371 |

372 |
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 | 401 | 406 | 411 | 416 | 421 | 426 | 431 | 436 | 439 | 440 | 441 | 446 | 451 | 456 | 464 | 478 | 479 | 480 | 485 | 490 | 495 | 503 | 511 | 519 | 527 | 528 | 529 | 534 | 539 | 544 | 549 | 554 | 559 | 564 | 569 | 574 | 579 | 580 | 581 | 589 | 594 | 599 | 604 | 609 | 614 | 619 | 624 | 629 | 634 | 635 | 636 | 641 | 646 | 651 | 656 | 661 | 666 | 671 | 676 | 681 | 686 | 687 | 688 | 693 | 698 | 703 | 708 | 713 | 718 | 723 | 728 | 733 | 738 | 739 | 740 |
397 |

398 |   399 |

400 |
402 |

403 |   404 |

405 |
407 |

408 |   409 |

410 |
412 |

413 |   414 |

415 |
417 |

418 |   419 |

420 |
422 |

423 |   424 |

425 |
427 |

428 |   429 |

430 |
432 |

433 |   434 |

435 |
437 |

(単位:百万円)

438 |
442 |

443 |   444 |

445 |
447 |

448 | 報告セグメント 449 |

450 |
452 |

453 | 454 |

455 |
457 |

458 | 調整額 459 |

460 |

461 | (注)1 462 |

463 |
465 |

466 | 四半期連結 467 |

468 |

469 | 損益計算書 470 |

471 |

472 | 計上額 473 |

474 |

475 | (注)2 476 |

477 |
481 |

482 |   483 |

484 |
486 |

487 | 国内コンビニエンスストア事業 488 |

489 |
491 |

492 | 海外コンビニエンスストア事業 493 |

494 |
496 |

497 | スーパー 498 |

499 |

500 | ストア事業 501 |

502 |
504 |

505 | 百貨店・ 506 |

507 |

508 | 専門店事業 509 |

510 |
512 |

513 | 金融関連 514 |

515 |

516 | 事業 517 |

518 |
520 |

521 | その他の 522 |

523 |

524 | 事業 525 |

526 |
530 |

531 | 営業収益 532 |

533 |
535 |

536 |   537 |

538 |
540 |

541 |   542 |

543 |
545 |

546 |   547 |

548 |
550 |

551 |   552 |

553 |
555 |

556 |   557 |

558 |
560 |

561 |   562 |

563 |
565 |

566 |   567 |

568 |
570 |

571 |   572 |

573 |
575 |

576 |   577 |

578 |
582 |

583 | 外部顧客への 584 |

585 |

586 | 営業収益 587 |

588 |
590 |

591 | 217,107 592 |

593 |
595 |

596 | 678,802 597 |

598 |
600 |

601 | 450,012 602 |

603 |
605 |

606 | 165,934 607 |

608 |
610 |

611 | 41,925 612 |

613 |
615 |

616 | 1,589 617 |

618 |
620 |

621 | 1,555,371 622 |

623 |
625 |

626 | 627 |

628 |
630 |

631 | 1,555,371 632 |

633 |
637 |

638 | セグメント間の内部営業収益又は振替高 639 |

640 |
642 |

643 | 429 644 |

645 |
647 |

648 | 494 649 |

650 |
652 |

653 | 1,672 654 |

655 |
657 |

658 | 701 659 |

660 |
662 |

663 | 7,176 664 |

665 |
667 |

668 | 3,181 669 |

670 |
672 |

673 | 13,655 674 |

675 |
677 |

678 | △13,655 679 |

680 |
682 |

683 | 684 |

685 |
689 |

690 |  計 691 |

692 |
694 |

695 | 217,536 696 |

697 |
699 |

700 | 679,296 701 |

702 |
704 |

705 | 451,684 706 |

707 |
709 |

710 | 166,636 711 |

712 |
714 |

715 | 49,101 716 |

717 |
719 |

720 | 4,771 721 |

722 |
724 |

725 | 1,569,027 726 |

727 |
729 |

730 | △13,655 731 |

732 |
734 |

735 | 1,555,371 736 |

737 |
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 | " \n", 551 | " \n", 552 | " \n", 553 | " \n", 554 | " \n", 555 | " \n", 556 | " \n", 557 | " \n", 558 | " \n", 559 | " \n", 560 | " \n", 561 | " \n", 562 | " \n", 563 | " \n", 564 | " \n", 565 | " \n", 566 | " \n", 567 | " \n", 568 | " \n", 569 | " \n", 570 | " \n", 571 | " \n", 572 | " \n", 573 | " \n", 574 | " \n", 575 | " \n", 576 | " \n", 577 | " \n", 578 | " \n", 579 | " \n", 580 | " \n", 581 | " \n", 582 | " \n", 583 | " \n", 584 | " \n", 585 | " \n", 586 | " \n", 587 | " \n", 588 | " \n", 589 | " \n", 590 | " \n", 591 | " \n", 592 | " \n", 593 | " \n", 594 | " \n", 595 | " \n", 596 | " \n", 597 | " \n", 598 | " \n", 599 | " \n", 600 | " \n", 601 | " \n", 602 | " \n", 603 | " \n", 604 | " \n", 605 | " \n", 606 | " \n", 607 | " \n", 608 | " \n", 609 | " \n", 610 | " \n", 611 | " \n", 612 | " \n", 613 | " \n", 614 | " \n", 615 | " \n", 616 | " \n", 617 | " \n", 618 | " \n", 619 | " \n", 620 | " \n", 621 | " \n", 622 | " \n", 623 | " \n", 624 | " \n", 625 | " \n", 626 | " \n", 627 | " \n", 628 | " \n", 629 | " \n", 630 | " \n", 631 | " \n", 632 | " \n", 633 | " \n", 634 | " \n", 635 | " \n", 636 | " \n", 637 | " \n", 638 | " \n", 639 | " \n", 640 | " \n", 641 | " \n", 642 | " \n", 643 | " \n", 644 | " \n", 645 | " \n", 646 | " \n", 647 | " \n", 648 | " \n", 649 | " \n", 650 | " \n", 651 | " \n", 652 | " \n", 653 | " \n", 654 | " \n", 655 | " \n", 656 | " \n", 657 | " \n", 658 | " \n", 659 | " \n", 660 | " \n", 661 | " \n", 662 | " \n", 663 | " \n", 664 | " \n", 665 | " \n", 666 | " \n", 667 | " \n", 668 | " \n", 669 | " \n", 670 | " \n", 671 | " \n", 672 | " \n", 673 | " \n", 674 | " \n", 675 | " \n", 676 | " \n", 677 | " \n", 678 | " \n", 679 | " \n", 680 | " \n", 681 | " \n", 682 | " \n", 683 | " \n", 684 | " \n", 685 | " \n", 686 | " \n", 687 | " \n", 688 | " \n", 689 | " \n", 690 | " \n", 691 | " \n", 692 | " \n", 693 | " \n", 694 | " \n", 695 | " \n", 696 | " \n", 697 | " \n", 698 | " \n", 699 | " \n", 700 | " \n", 701 | " \n", 702 | " \n", 703 | " \n", 704 | " \n", 705 | " \n", 706 | " \n", 707 | "
segment_indexcolumnsegment_namevalue_indexrowvalue_kindvalue_nameunitvalue
001国内コンビニエンスストア事業06Sales営業収益1000000215243.0
101国内コンビニエンスストア事業17Profitセグメント利益又は損失(△)100000059282.0
212海外コンビニエンスストア事業06Sales営業収益10000001723889.0
312海外コンビニエンスストア事業17Profitセグメント利益又は損失(△)100000043981.0
423スーパーストア事業06Sales営業収益1000000355772.0
523スーパーストア事業17Profitセグメント利益又は損失(△)10000003517.0
634百貨店・専門店事業06Sales営業収益1000000112904.0
734百貨店・専門店事業17Profitセグメント利益又は損失(△)10000001086.0
845金融関連事業06Sales営業収益100000047560.0
945金融関連事業17Profitセグメント利益又は損失(△)10000009205.0
1056その他の事業06Sales営業収益10000005829.0
1156その他の事業17Profitセグメント利益又は損失(△)1000000-90.0
\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 | " \n", 777 | " \n", 778 | " \n", 779 | " \n", 780 | " \n", 781 | " \n", 782 | " \n", 783 | " \n", 784 | " \n", 785 | " \n", 786 | " \n", 787 | " \n", 788 | " \n", 789 | " \n", 790 | " \n", 791 | " \n", 792 | " \n", 793 | " \n", 794 | " \n", 795 | " \n", 796 | " \n", 797 | " \n", 798 | " \n", 799 | " \n", 800 | " \n", 801 | " \n", 802 | " \n", 803 | " \n", 804 | " \n", 805 | " \n", 806 | " \n", 807 | " \n", 808 | " \n", 809 | " \n", 810 | " \n", 811 | " \n", 812 | " \n", 813 | " \n", 814 | " \n", 815 | " \n", 816 | " \n", 817 | " \n", 818 | " \n", 819 | " \n", 820 | " \n", 821 | " \n", 822 | " \n", 823 | " \n", 824 | " \n", 825 | " \n", 826 | " \n", 827 | " \n", 828 | " \n", 829 | " \n", 830 | " \n", 831 | " \n", 832 | " \n", 833 | " \n", 834 | " \n", 835 | " \n", 836 | " \n", 837 | " \n", 838 | " \n", 839 | " \n", 840 | " \n", 841 | " \n", 842 | " \n", 843 | " \n", 844 | " \n", 845 | " \n", 846 | " \n", 847 | " \n", 848 | " \n", 849 | " \n", 850 | " \n", 851 | " \n", 852 | " \n", 853 | " \n", 854 | " \n", 855 | " \n", 856 | " \n", 857 | " \n", 858 | " \n", 859 | " \n", 860 | " \n", 861 | " \n", 862 | " \n", 863 | " \n", 864 | " \n", 865 | " \n", 866 | " \n", 867 | " \n", 868 | " \n", 869 | " \n", 870 | " \n", 871 | " \n", 872 | " \n", 873 | " \n", 874 | " \n", 875 | " \n", 876 | " \n", 877 | " \n", 878 | " \n", 879 | " \n", 880 | " \n", 881 | " \n", 882 | " \n", 883 | " \n", 884 | " \n", 885 | "
segment_indexcolumnsegment_namevalue_indexrowvalue_kindvalue_nameunitvalue
001国内ユニクロ事業03Sales売上収益1000000640972.0
101国内ユニクロ事業14Profit営業利益又は損失(△)1000000119067.0
212海外ユニクロ事業03Sales売上収益1000000841274.0
312海外ユニクロ事業14Profit営業利益又は損失(△)1000000132793.0
423ジーユー事業03Sales売上収益1000000190545.0
523ジーユー事業14Profit営業利益又は損失(△)100000017852.0
634グローバルブランド事業03Sales売上収益100000090084.0
734グローバルブランド事業14Profit営業利益又は損失(△)1000000720.0
\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 | " \n", 947 | " \n", 948 | " \n", 949 | " \n", 950 | " \n", 951 | " \n", 952 | " \n", 953 | " \n", 954 | " \n", 955 | " \n", 956 | " \n", 957 | " \n", 958 | " \n", 959 | " \n", 960 | " \n", 961 | " \n", 962 | " \n", 963 | " \n", 964 | " \n", 965 | " \n", 966 | " \n", 967 | " \n", 968 | " \n", 969 | " \n", 970 | " \n", 971 | " \n", 972 | " \n", 973 | " \n", 974 | " \n", 975 | " \n", 976 | " \n", 977 | " \n", 978 | " \n", 979 | " \n", 980 | " \n", 981 | " \n", 982 | " \n", 983 | " \n", 984 | " \n", 985 | " \n", 986 | " \n", 987 | " \n", 988 | " \n", 989 | " \n", 990 | " \n", 991 | " \n", 992 | " \n", 993 | " \n", 994 | " \n", 995 | " \n", 996 | " \n", 997 | " \n", 998 | " \n", 999 | " \n", 1000 | " \n", 1001 | " \n", 1002 | " \n", 1003 | " \n", 1004 | " \n", 1005 | " \n", 1006 | " \n", 1007 | " \n", 1008 | " \n", 1009 | " \n", 1010 | " \n", 1011 | " \n", 1012 | " \n", 1013 | " \n", 1014 | " \n", 1015 | " \n", 1016 | " \n", 1017 | " \n", 1018 | " \n", 1019 | " \n", 1020 | " \n", 1021 | " \n", 1022 | " \n", 1023 | " \n", 1024 | " \n", 1025 | " \n", 1026 | " \n", 1027 | " \n", 1028 | " \n", 1029 | " \n", 1030 | " \n", 1031 | " \n", 1032 | " \n", 1033 | " \n", 1034 | " \n", 1035 | " \n", 1036 | " \n", 1037 | " \n", 1038 | " \n", 1039 | " \n", 1040 | " \n", 1041 | " \n", 1042 | " \n", 1043 | " \n", 1044 | " \n", 1045 | " \n", 1046 | " \n", 1047 | " \n", 1048 | " \n", 1049 | " \n", 1050 | " \n", 1051 | " \n", 1052 | " \n", 1053 | " \n", 1054 | " \n", 1055 | "
segment_indexcolumnsegment_namevalue_indexrowvalue_kindvalue_nameunitvalue
001コンサルティング06Sales売上収益10000009711.0
101コンサルティング17Profit営業利益10000001859.0
213金融ITソリューション06Sales売上収益100000081490.0
313金融ITソリューション17Profit営業利益100000011678.0
425産業ITソリューション06Sales売上収益100000068841.0
525産業ITソリューション17Profit営業利益10000006679.0
637IT基盤サービス06Sales売上収益100000040403.0
737IT基盤サービス17Profit営業利益10000005652.0
\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 | --------------------------------------------------------------------------------