{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "2f007628",
   "metadata": {},
   "source": [
    "# 応用への道（20分）\n",
    "\n",
    "この章ではデータ分析の応用分野を紹介します。また、応用の実践例としてWebページから**スクレイピング**によって情報を取り出し、DataFrameにまとめる流れを講師が実演します。\n",
    "\n",
    "```{note}\n",
    "入力が遅れた場合などに、{nb-download}`この章のNotebook <7_extend.ipynb>`をダウンロードできます。ダウンロードしたファイルを作業フォルダに置き、JupyterLabのファイルブラウザでダブルクリックして開いてください。\n",
    "\n",
    "講義中は講師の実演を見るだけにし、自分で試すのは講義の後にしてください。対象サイトへのアクセスが一度に集中することを避けるためです。\n",
    "```\n",
    "\n",
    "## データ分析の応用分野\n",
    "\n",
    "講座ではデータの入力、加工、可視化を学びました。この先には、データを使って予測したり、さまざまな形式の情報を扱ったりする応用があります。ここでは4つの分野を紹介します。\n",
    "\n",
    "### 機械学習\n",
    "\n",
    "**機械学習**は、データから規則性を学び、予測や分類を行う分野です。売上や需要の予測、問い合わせ内容の分類などに使われます。代表的なライブラリに[scikit-learn](https://scikit-learn.org/stable/)があります。\n",
    "\n",
    "### スクレイピング\n",
    "\n",
    "Webページから必要な情報をプログラムで取り出すことを**スクレイピング**といいます。用途としては、商品の価格調査や公開情報の定期的な収集などが考えられます。[Requests](https://requests.readthedocs.io/)でWebページをダウンロードし、[Beautiful Soup 4](https://www.crummy.com/software/BeautifulSoup/)で必要な要素を抜き出せます。詳しくは次節以降で解説します。\n",
    "\n",
    "### 画像処理\n",
    "\n",
    "**画像処理**は、画像を加工したり、画像から特徴を取り出したりする分野です。製品の外観検査や書類画像の整理などに使われます。代表的なライブラリに[Pillow](https://python-pillow.org/)や[OpenCV](https://opencv.org/)があります。また、画像に写っている人や物を検出する[Ultralytics YOLO](https://docs.ultralytics.com/)も広く使われています。\n",
    "\n",
    "### 自然言語処理\n",
    "\n",
    "**自然言語処理**は、人が使う言葉をコンピューターで扱い、文章を単語に分けたり意味や傾向を調べたりする分野です。アンケートの自由記述の分析や問い合わせの自動振り分けなどに使われます。代表的なライブラリに[spaCy](https://spacy.io/)や[GiNZA](https://megagonlabs.github.io/ginza/)があります。\n",
    "\n",
    "ここで紹介したライブラリのうち、講座の環境に入っているのはRequestsとBeautiful Soup 4だけです。ほかのライブラリを試すときは、別途インストールが必要です。\n",
    "\n",
    "ここからはスクレイピングの概要と、スクレイピングを行う際の注意点を説明します。その後、python.orgのWebページをダウンロードし、Pythonのリリース情報を抜き出してDataFrameにまとめる流れを解説します。\n",
    "\n",
    "## スクレイピングとは\n",
    "\n",
    "スクレイピングは、Webページから必要な情報をプログラムで取り出すことです。Webブラウザに表示されるページの内容は、HTMLという形式のテキストで書かれています。スクレイピングでは、プログラムでHTMLテキストをダウンロードし、必要な部分を抜き出して、プログラムが扱いやすい形にまとめます。\n",
    "\n",
    "画面を見ながら手作業でコピー＆ペーストする場合と比べて、多くのページから情報を集めたり、同じ作業を定期的に繰り返したりする作業を自動化できます。\n",
    "\n",
    "## スクレイピングを行う際の注意点\n",
    "\n",
    "スクレイピングでは、相手のサイトにプログラムでアクセスするため、サイトに負担をかけたり、サイトのルールに反したりしないよう注意する必要があります。スクレイピングを行う際は、次の点に注意してください。\n",
    "\n",
    "* 対象サイトの利用規約と**robots.txt**に従う\n",
    "  * robots.txtはサイトの直下に置かれ、プログラムによる取得を許可・禁止するページを示すファイル\n",
    "* アクセスの頻度を抑え、同じページに繰り返しアクセスしない\n",
    "  * 取得したデータはファイルに保存して使い回す\n",
    "* CSVのダウンロードやAPIなどデータの提供があれば、そちらを優先する\n",
    "* ボット対策（CAPTCHAなど）でプログラムからのアクセスを拒否しているサイトは対象にしない\n",
    "\n",
    "## Webページをダウンロードする\n",
    "\n",
    "ここでは、[python.orgのダウンロードページ](https://www.python.org/downloads/)をスクレイピングの対象にします。このページの「Looking for a specific release?」には、Pythonの過去のリリースが新しい順に並んでいます。各行には、バージョン、リリース日、ダウンロードのリンク、リリースノートのリンクがあります。\n",
    "\n",
    "```{figure} images/7_python_downloads.png\n",
    ":alt: python.orgのダウンロードページのリリース一覧\n",
    "\n",
    "python.orgのリリース一覧（出典: Python Software Foundation、[python.org](https://www.python.org/downloads/)）\n",
    "```\n",
    "\n",
    "python.orgの[robots.txt](https://www.python.org/robots.txt)を確認すると、次のように書かれています（抜粋）。取得が禁止（`Disallow`）されているのは`/~guido/orlijn/`と`/webstats/`だけで、`/downloads/`の取得は許可されています。\n",
    "\n",
    "```text\n",
    "# Hide old versions of the documentation and various large sets of files.\n",
    "User-agent: *\n",
    "Disallow: /~guido/orlijn/\n",
    "Disallow: /webstats/\n",
    "```\n",
    "\n",
    "Webページのダウンロードには、Requestsを使います。requestsをインポートし、`get()`関数でページをダウンロードします。戻り値（`r`）の`status_code`が`200`なら、ダウンロードに成功しています。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "39a8fa52",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "200"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import requests\n",
    "\n",
    "url = \"https://www.python.org/downloads/\"\n",
    "r = requests.get(url)  # Webページをダウンロード\n",
    "r.status_code  # 200なら成功"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28af32dc",
   "metadata": {},
   "source": [
    "ダウンロードしたHTMLは、`r.text`に文字列として入っています。中身が正しいかを確認するため、HTMLを1行ずつ調べて、`<title>`という文字列を含む行を表示します。`splitlines()`メソッドは、文字列を行ごとに分けたリストを返します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "3e01f72c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<title>Download Python | Python.org</title>\n"
     ]
    }
   ],
   "source": [
    "# HTMLを1行ずつ調べ、<title>を含む行を前後の空白を除いて表示\n",
    "for line in r.text.splitlines():\n",
    "    if \"<title>\" in line:\n",
    "        print(line.strip())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff6258ee",
   "metadata": {},
   "source": [
    "ページのタイトルが表示され、目的のページをダウンロードできたことが分かります。\n",
    "\n",
    "## Webページから要素を抜き出す\n",
    "\n",
    "さきほどは文字列の処理で`<title>`を含む行を抜き出しました。しかし、HTMLから複雑な要素を文字列の処理で抜き出すのは大変です。そこで、HTMLを構文解析して、要素を探しやすい形にします。HTMLの文字列を読み取り、タグの構造をたどれる形にすることを**パース**（構文解析）といいます。\n",
    "\n",
    "**HTML**はWebページの内容と構造を**タグ**で囲んで表す書き方です。たとえば、`<title>`と`</title>`で囲んだ部分はページのタイトル、`<a>`と`</a>`で囲んだ部分はリンクを表します。タグには`class`や`href`などの**属性**を付けられます。\n",
    "\n",
    "Beautiful Soup 4を使うと、HTMLをパースして、タグを指定して要素を取り出せます。`BeautifulSoup()`にHTMLの文字列とパーサー（`\"html.parser\"`はPythonの標準ライブラリのパーサー）を渡すと、パースした結果が返ります。まずは`<title>`タグを抜き出します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "160f3950",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<title>Download Python | Python.org</title>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from bs4 import BeautifulSoup\n",
    "\n",
    "soup = BeautifulSoup(r.text, \"html.parser\")  # HTMLをパース\n",
    "soup.title  # <title>タグを取り出す"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "efd135e9",
   "metadata": {},
   "source": [
    "さきほど文字列の処理で抜き出したのと同じ`<title>`タグを取り出せたことが確認できます。\n",
    "\n",
    "`find_all()`メソッドは、条件に合うすべての要素をリストで返します。ページ内のすべての`<a>`タグ（リンク）を探し、全件数と、先頭5件のリンク文字列と`href`属性の中身（リンク先）を出力します。`.text`で要素の中の文字列を、`[\"href\"]`で属性の値を取り出せます。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "b2b54a8d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "aタグの数: 1032\n",
      "タイトル: Skip to content\n",
      "リンク: #content\n",
      "タイトル: ▼ Close\n",
      "リンク: #python-network\n",
      "タイトル: Python\n",
      "リンク: /\n",
      "タイトル: PSF\n",
      "リンク: https://www.python.org/psf/\n",
      "タイトル: Docs\n",
      "リンク: https://docs.python.org\n"
     ]
    }
   ],
   "source": [
    "links = soup.find_all(\"a\")  # すべての<a>タグを取得\n",
    "print(f\"aタグの数: {len(links)}\")\n",
    "for link in links[:5]:\n",
    "    print(f\"タイトル: {link.text.strip()}\")  # <a>タグの文字列を取得\n",
    "    print(f\"リンク: {link[\"href\"]}\")  # <a>タグのリンク先を取得"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d93dce3",
   "metadata": {},
   "source": [
    "## Pythonのリリース情報を抜き出す\n",
    "\n",
    "それでは、このページからリリース情報を抜き出します。各リリース情報から、バージョン、リリース日、ダウンロードのURL、リリースノートのURLを取得します。\n",
    "\n",
    "```{figure} images/7_python_downloads.png\n",
    ":alt: python.orgのダウンロードページのリリース一覧\n",
    "\n",
    "python.orgのリリース一覧（出典: Python Software Foundation、[python.org](https://www.python.org/downloads/)）\n",
    "```\n",
    "\n",
    "プログラムを作成する前に、HTMLの構成を確認します。リリース一覧のHTMLは次の内容です（この資料を作成したときの先頭2件分。見やすいようにインデントを整えています）。\n",
    "\n",
    "```html\n",
    "<div class=\"row download-list-widget\">\n",
    "    <h2 class=\"widget-title\">Looking for a specific release?</h2>\n",
    "    <p class=\"success-quote\">Python releases by version number:</p>\n",
    "    <div class=\"list-row-headings\">\n",
    "        <span class=\"release-number\">Release version</span>\n",
    "        <span class=\"release-date\">Release date</span>\n",
    "        <span class=\"release-download\">&nbsp;</span>\n",
    "        <span class=\"release-enhancements\">Click for more</span>\n",
    "    </div>\n",
    "    <ol class=\"list-row-container menu\">\n",
    "        <li>\n",
    "            <span class=\"release-number\"><a href=\"/downloads/release/python-3151/\">Python 3.15.1</a></span>\n",
    "            <span class=\"release-date\">Oct. 21, 2026</span>\n",
    "            <span class=\"release-download\"><a href=\"/downloads/release/python-3151/\"><span aria-hidden=\"true\" class=\"icon-download\"></span> Download</a></span>\n",
    "            <span class=\"release-enhancements\"><a href=\"https://docs.python.org/release/3.15.1/whatsnew/changelog.html\">Release notes</a></span>\n",
    "        </li>\n",
    "        <li>\n",
    "            <span class=\"release-number\"><a href=\"/downloads/release/python-3150/\">Python 3.15.0</a></span>\n",
    "            <span class=\"release-date\">Oct. 9, 2026</span>\n",
    "            <span class=\"release-download\"><a href=\"/downloads/release/python-3150/\"><span aria-hidden=\"true\" class=\"icon-download\"></span> Download</a></span>\n",
    "            <span class=\"release-enhancements\"><a href=\"https://docs.python.org/release/3.15.0/whatsnew/changelog.html\">Release notes</a></span>\n",
    "        </li>\n",
    "        ...\n",
    "    </ol>\n",
    "</div>\n",
    "```\n",
    "\n",
    "このHTMLは次のような構成になっています。\n",
    "\n",
    "* クラス名が`download-list-widget`の`<div>`タグが、リリース一覧の部分全体を囲んでいる\n",
    "  * 中には、見出し（`<h2>`）、説明文（`<p>`）、列の見出し行（`list-row-headings`の`<div>`）、リリース一覧（`<ol>`）がある\n",
    "* `<ol>`タグがリリース一覧、その中の`<li>`タグ1つがリリース1件を表す\n",
    "* `<li>`の中に4つの`<span>`タグがあり、`class`属性の値（クラス名）で項目の種類を区別している\n",
    "  * `release-number`: バージョン\n",
    "  * `release-date`: リリース日\n",
    "  * `release-download`: ダウンロードのリンク\n",
    "  * `release-enhancements`: リリースノートのリンク\n",
    "* リンクは`<a>`タグで、`href`属性の値がリンク先のURLを表す。ダウンロードのリンクは、サイト内のパス（`/downloads/release/...`）で書かれている\n",
    "\n",
    "列の見出し行にも`release-number`などのクラス名があり、ページには同じクラス名の`<ol>`がもう1つあります。そのため、`download-list-widget`の`<div>`を探し、その中の`<li>`をすべて取り出します。`find()`メソッドは、条件に合う最初の要素を返します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "a9398d10",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "269"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# リリース一覧を囲む<div>を探し、その中の<li>をすべて取得\n",
    "release_list = soup.find(\"div\", class_=\"download-list-widget\")\n",
    "items = release_list.find_all(\"li\")\n",
    "len(items)  # リリースの件数"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "570e5448",
   "metadata": {},
   "source": [
    "引数の`class_`に`_`が付くのは、`class`がPythonの予約語だからです。件数は取得した日によって変わります。\n",
    "\n",
    "リリースごとに、バージョン、リリース日、ダウンロードのURL、リリースノートのURLを取り出します。ダウンロードのリンクはサイト内のパスなので、先頭に`\"https://www.python.org\"`を付けてURLにします。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "fd2d5633",
   "metadata": {},
   "outputs": [],
   "source": [
    "versions = []\n",
    "dates = []\n",
    "downloads = []\n",
    "notes = []\n",
    "\n",
    "for item in items:\n",
    "    # クラス名で<span>を探し、文字列は.text、リンク先は<a>タグのhref属性で取り出す\n",
    "    versions.append(item.find(\"span\", class_=\"release-number\").text)\n",
    "    dates.append(item.find(\"span\", class_=\"release-date\").text)\n",
    "    path = item.find(\"span\", class_=\"release-download\").find(\"a\")[\"href\"]\n",
    "    downloads.append(\"https://www.python.org\" + path)\n",
    "    notes.append(item.find(\"span\", class_=\"release-enhancements\").find(\"a\")[\"href\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4f89c46",
   "metadata": {},
   "source": [
    "## DataFrameにする\n",
    "\n",
    "pandasをインポートし、第5章と同じく、列名と値のリストを辞書で渡してDataFrameを作ります。\n",
    "\n",
    "リリース日は「Oct. 21, 2026」のような英語の文字列で、月には「Oct.」「Sept.」のような省略形と「June」のような省略しない形が混在しています。`pd.to_datetime()`関数で、文字列を日付の型（`datetime`）に変換します。`format=\"mixed\"`を指定すると、書き方がそろっていない日付もそれぞれ読み取って変換します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "95c60c0e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>version</th>\n",
       "      <th>release_date</th>\n",
       "      <th>download_url</th>\n",
       "      <th>release_notes</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Python 3.15.1</td>\n",
       "      <td>2026-10-21</td>\n",
       "      <td>https://www.python.org/downloads/release/pytho...</td>\n",
       "      <td>https://docs.python.org/release/3.15.1/whatsne...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Python 3.15.0</td>\n",
       "      <td>2026-10-09</td>\n",
       "      <td>https://www.python.org/downloads/release/pytho...</td>\n",
       "      <td>https://docs.python.org/release/3.15.0/whatsne...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Python 3.14.9</td>\n",
       "      <td>2026-10-21</td>\n",
       "      <td>https://www.python.org/downloads/release/pytho...</td>\n",
       "      <td>https://docs.python.org/release/3.14.9/whatsne...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Python 3.14.8</td>\n",
       "      <td>2026-09-30</td>\n",
       "      <td>https://www.python.org/downloads/release/pytho...</td>\n",
       "      <td>https://docs.python.org/release/3.14.8/whatsne...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Python 3.14.7</td>\n",
       "      <td>2026-08-05</td>\n",
       "      <td>https://www.python.org/downloads/release/pytho...</td>\n",
       "      <td>https://docs.python.org/release/3.14.7/whatsne...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         version release_date  \\\n",
       "0  Python 3.15.1   2026-10-21   \n",
       "1  Python 3.15.0   2026-10-09   \n",
       "2  Python 3.14.9   2026-10-21   \n",
       "3  Python 3.14.8   2026-09-30   \n",
       "4  Python 3.14.7   2026-08-05   \n",
       "\n",
       "                                        download_url  \\\n",
       "0  https://www.python.org/downloads/release/pytho...   \n",
       "1  https://www.python.org/downloads/release/pytho...   \n",
       "2  https://www.python.org/downloads/release/pytho...   \n",
       "3  https://www.python.org/downloads/release/pytho...   \n",
       "4  https://www.python.org/downloads/release/pytho...   \n",
       "\n",
       "                                       release_notes  \n",
       "0  https://docs.python.org/release/3.15.1/whatsne...  \n",
       "1  https://docs.python.org/release/3.15.0/whatsne...  \n",
       "2  https://docs.python.org/release/3.14.9/whatsne...  \n",
       "3  https://docs.python.org/release/3.14.8/whatsne...  \n",
       "4  https://docs.python.org/release/3.14.7/whatsne...  "
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "\n",
    "# 列名と値のリストの辞書からDataFrameを作成\n",
    "df = pd.DataFrame({\n",
    "    \"version\": versions,\n",
    "    \"release_date\": pd.to_datetime(dates, format=\"mixed\"),  # 日付の型に変換\n",
    "    \"download_url\": downloads,\n",
    "    \"release_notes\": notes,\n",
    "})\n",
    "df.head()  # 先頭の5行を表示"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f54fcff3",
   "metadata": {},
   "source": [
    "表の形ではないWebページの情報を、第5章で学んだDataFrameにまとめられました。列は`version`、`release_date`、`download_url`、`release_notes`の4つで、`release_date`は日付の型、ほかは文字列です。\n",
    "\n",
    "この後は、第5章で学んだ方法で抽出や集計をしたり、第6章で学んだグラフで可視化したりできます。\n",
    "\n",
    "## この章のまとめ\n",
    "\n",
    "```{table} この章で行ったこと\n",
    "| 節 | 行ったこと | 使ったライブラリ・関数・メソッドなど |\n",
    "|---|---|---|\n",
    "| データ分析の応用分野 | 機械学習、スクレイピング、画像処理、自然言語処理の紹介 | scikit-learn、Requests、Beautiful Soup 4、Pillow、OpenCV、Ultralytics YOLO、spaCy、GiNZA |\n",
    "| スクレイピング | Webページのダウンロード、要素の抜き出し、DataFrameの作成 | `requests.get()`、`BeautifulSoup()`、`find()`、`find_all()`、`pd.DataFrame()`、`pd.to_datetime()` |\n",
    "```\n",
    "\n",
    "実演ではWebページの情報をDataFrameにし、講座で学んだ入力 ➔ 加工 ➔ 可視化の「入力」につなげました。次の第8章では、講座全体を振り返ります。"
   ]
  }
 ],
 "metadata": {
  "file_format": "mystnb",
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.3"
  },
  "source_map": [
   6,
   80,
   86,
   90,
   95,
   107,
   112,
   118,
   124,
   180,
   185,
   191,
   204,
   212,
   223
  ]
 },
 "nbformat": 4,
 "nbformat_minor": 5
}