Pythonでつくる、HTMLソースを保存するツール

HTMLソースを保存するツールをPythonでつくった記録です。

このツールは、実行すると専用のブラウザが開き、ユーザーはHTMLソースを保存したいページを開いてから、ツール上のボタンをクリックするとHTMLソースが保存されます。

役に立つ場面は少なそうですが、「あるWebサイトのデータを保存して、解析したい。サイトへのアクセスは手動で」という要望の下、保存するために生まれました。

ツールの処理を間違えて、DoS攻撃みたいなことをしないよう、手動でアクセス操作をしたいのです。

使っている技術のキーワードは、Python、Slenium、Tkinterといったところです。

特別複雑なことはしていませんので、「アクセスは手動、後の処理は自動化したい」という場合のベースにはなるかと思います。

では、ソースコードを見ていきましょう。

# ベース

ベースは以下の通りです。

機能本体はWebScreenshotToolクラスに記述します。

```python
def main():
    tool = SaveThisHtmlSource()

    root = tk.Tk()
    root.title("SaveThisHtmlSource Tool")

    save_button = tk.Button(root, text="HTMLを保存", command=tool.save_html)
    save_button.pack(pady=10)

    root.mainloop()


if __name__ == "__main__":
    main()
```

https://docs.python.org/ja/3/library/__main__.html

一応、よくやるらしい`main()`関数の構造を導入しています。

この構造を採用することで、たとえば、他のツールにimportして、クラスだけを利用することができる、みたいな理解をしています。

# 機能本体

では、機能本体の全体像であるSaveThisHtmlSourceクラスを以下に示します。

```python
class SaveThisHtmlSource:
    def __init__(self):
        self.setup_driver()

    def setup_driver(self):
        self.driver = webdriver.Chrome()

    def save_html(self):
        try:
            html_source = self.driver.page_source

            if html_source:
                html_url = self.driver.current_url
                # https://example.com/aaa/bbb/db/item/123456/
                # https://example.com/aaa/ccc/db/recipe/789456/
                # 上記の形式を"item-123456", "recipe-789456"に変換する
                url_parts_name = "-".join(html_url.split("/")[-3:-1])
                url_parts_datetime = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
                filename = f"{url_parts_datetime}-{url_parts_name}.html"

                with open(filename, "w", encoding="utf-8") as file:
                    file.write(html_source)
                    # messagebox.showinfo("Information", "HTMLソースを保存しました。")
                    print(f"Information: HTMLソースを{filename}へ保存しました。")
            else:
                messagebox.showerror("Error", "HTMLソースが取得できませんでした。")
        except Exception as e:
            messagebox.showerror("Error", f"HTMLソースの保存に失敗しました: {e}")
```

## 初期化クラス
```python
def __init__(self):
    self.setup_driver()

def setup_driver(self):
    self.driver = webdriver.Chrome()
```

:::note
先に述べたとおり、Selenium（のWebDriver API）を利用するため、importも冒頭に追加します。
```python
from selenium import webdriver
```
:::

このへんの導入は公式ドキュメントを参照しました。

https://www.selenium.dev/ja/documentation/webdriver/getting_started/install_library/

https://www.selenium.dev/ja/documentation/webdriver/getting_started/first_script/

どうやら以前は、使用するブラウザのドライバを別途インストールする必要があったようですが、Selenium 4.6以上？あたりから、必要なくなったようです。

## 保存機能

保存機能は`save_html`へ実装してあります。

```python
def save_html(self):
    try:
        html_source = self.driver.page_source

        if html_source:
            html_url = self.driver.current_url
            # https://example.com/aaa/bbb/db/item/123456/
            # https://example.com/aaa/ccc/db/recipe/789456/
            # 上記の形式を"item-123456", "recipe-789456"に変換する
            url_parts_name = "-".join(html_url.split("/")[-3:-1])
            url_parts_datetime = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
            filename = f"{url_parts_datetime}-{url_parts_name}.html"

            with open(filename, "w", encoding="utf-8") as file:
                file.write(html_source)
                # messagebox.showinfo("Information", "HTMLソースを保存しました。")
                print(f"Information: HTMLソースを{filename}へ保存しました。")
        else:
            messagebox.showerror("Error", "HTMLソースが取得できませんでした。")
    except Exception as e:
        messagebox.showerror("Error", f"HTMLソースの保存に失敗しました: {e}")
```

### HTMLソースの取得

といっても、単純にHTMLソースを取得するだけならば、
```python
html_source = self.driver.page_source
```
で、済んでいます。

これを、`YYYYMMDD-HHMMSS-item-123456.html`のような様式のファイル名に保存します。

### ファイル名の作成

ファイル名は、プログラムのコメントにあるように、以下の通り決まります。

```
https://example.com/aaa/bbb/db/item/123456/
--> YYYYMMDD-HHMMSS-item-123456.html

https://example.com/aaa/ccc/db/recipe/789456/
--> YYYYMMDD-HHMMSS-recipe-789456.html
```

HTMLソース取得元のURLの一部を付与する形です。

ここらへんの処理は以下のように理解しています。

```python
html_url = self.driver.current_url
url_parts_name = "-".join(html_url.split("/")[-3:-1])
```
URLを`/`で分割して配列にし（`['https:', (略), 'db', 'item', '123456', '']`）
後ろから三番目から、後ろから一番目を含まないところまでの配列にして（`['item', '123456']`）
`"-"`で結合した文字列にする（`"item-123456"`）

日付は普通にdatetimeライブラリ？で取得して、フォーマットを指定して文字列化します。

最後に、がっちゃんとくっつけて、`.html`を付与すれば、ファイル名の完成です。

### ファイルへの保存

```python
with open(filename, "w", encoding="utf-8") as file:
    file.write(html_source)
    # messagebox.showinfo("Information", "HTMLソースを保存しました。")
    print(f"Information: HTMLソースを{filename}へ保存しました。")
```

保存には、未だに見慣れない`with文`を使っています。

https://docs.python.org/ja/3/reference/compound_stmts.html#with

ファイルの読み書きについての以下を読んでいて、使うといいよ！とあったので使っているだけで、理解できていません。

https://docs.python.org/ja/3/tutorial/inputoutput.html#reading-and-writing-files

これは、`open(filename, "w", encoding="utf-8")`が成功したら処理継続、失敗したら、openに対応する終了処理（close）を実行して、例外を投げてくれる仕組みかな？と、ふんわりと認識しています。

# 動作

これで、プログラムを実行すると、HTMLを保存ボタンがひとつ表示されたウィンドウとブラウザがひとつ立ちあがり、ブラウザに表示しているWebページのHTMLを保存する、という仕組みが実現できました。

# 終わりに

冒頭で書いたとおり、アクセスやページ遷移は手動で行い、HTMLを保存するだけのツールですので、ふつうに手動でWebブラウザのメニューなどから保存するのと手間は変わりません。

SPAや遅延読み込みなどのWebページの仕組みによっては、このように手動でアクセスする仕組みは役立つかも知れませんし、役立たないかもしれません。

「名前を付けてページを保存」などと比較した検証は行っていませんので、あくまで想像です。

とはいえ、保存したものをパースして、データを抽出するような仕組みを取り入れるのなら、一括して処理が行えて便利になるかも？

何か使える用途はないかと、時折考えていきたいと思います。

以上、ここまで目を通していただきありがとうございました。


# 付録

https://packaging.python.org/ja/latest/guides/installing-using-pip-and-virtual-environments/

```txt:requirements.txt
attrs==24.3.0
certifi==2024.12.14
cffi==1.17.1
h11==0.14.0
idna==3.10
outcome==1.3.0.post0
pycparser==2.22
PySocks==1.7.1
selenium==4.27.1
sniffio==1.3.1
sortedcontainers==2.4.0
trio==0.27.0
trio-websocket==0.11.1
typing_extensions==4.12.2
urllib3==2.2.3
websocket-client==1.8.0
wsproto==1.2.0
```

TkinterとdatetimeはPythonの標準モジュールなのかな？

https://tkdocs.com/tutorial/install.html#installwin