Python3 標準モジュールだけで feed 取得

## 目標

Qiita の Feed から、直近の投稿記事のタイトル、URL、日付を取得する。それを、Python 標準モジュールだけで (つまり pip を使うことなく) 実現する。

URL 例: `https://qiita.com/aKuad/feed`

## 要素技術

### Feed 取得

HTTP GET リクエストして、body を受け取りたいです。そこで `urllib` が使えます。

```py
from urllib.request import urlopen

feed = urlopen("https://qiita.com/aKuad/feed")
feed_text = feed.read().decode("utf-8")

print(feed_text)
```

:::note warn
ちなみに[公式リファレンスでは](https://docs.python.org/ja/3/library/urllib.request.html#module-urllib.request)、より高水準なインターフェースとして、pip から入れられる requests が推奨されています。ただ、ここではシンプルな HTTP GET しかしないので、これで十分です。
:::

### Feed パース & 要素取得

Feed は XML 形式で記述されています。XML ライブラリも標準にあります。

```py
from xml.dom.minidom import parseString

feed_text = """
<feed>
  <entry>
    <title>Title1</title>
  </entry>
  <entry>
    <title>Title2</title>
  </entry>
</feed>"""

dom = parseString(feed_text)
items = dom.getElementsByTagName("entry")

for item in items:
  title = item.getElementsByTagName("title")[0].firstChild.data
  print(title)
  # Title1
  # Title2
```

最後の title タグの中を取得する部分がどうも不格好になってしまいましたが、これ以上キレイな手は自分には見つかりませんでした。orz

初めは `xml.etree.ElementTree` を試したのですが、どうも上手くいかなかったので minidom を使う方法でやりました。

:::note warn
[公式リファレンス](https://docs.python.org/ja/3/library/xml.html)で、この XML パーサには脆弱性があるとされています。ただ、ここでは決まった XML しか扱わないので問題ないとしています。もし、ユーザ任意の XML を入れる形を検討している場合は注意が必要です。
:::

### ついでに `dataclass`

C で言う構造体的なもの。

```py
from dataclasses import dataclass

@dataclass
class ArticleInfo:
  title: str
  url: str
  date: str

article = ArticleInfo("title text", "https://qiita.com", "2024-01-10T23:00:19+09:00")

print(article)
# ArticleInfo(title='title text', url='https://qiita.com', date='2024-01-10T23:00:19+09:00')
```

普通の Class と違うのは、`print` すると値が表示されること、初期化関数で値を代入するコードがいらないことです。

<details><summary>ちなみに普通の Class だと</summary>

```py
class ArticleInfo:
  def __init__(self, title: str, url: str, date: str):
    self.title = title
    self.url   = url
    self.date  = date

article = ArticleInfo("title text", "https://qiita.com", "2024-01-10T23:00:19+09:00")

print(article)
# <__main__.ArticleInfo object at 0x7fa72fe37970>
```

</details>

## 完成品

以上を組み合わせ、関数化とかしたものがこちら。

```py
# coding: UTF-8

from urllib.request import urlopen
from xml.dom.minidom import parseString
from dataclasses import dataclass

@dataclass
class ArticleInfo:
  title: str
  url: str
  date: str


def fetch_qiita_feed(user_id: str):
  feed = urlopen(f"https://qiita.com/{user_id}/feed")
  feed_text = feed.read().decode("utf-8")

  dom = parseString(feed_text)
  items = dom.getElementsByTagName("entry")

  articles = []
  for item in items:
    title = item.getElementsByTagName("title"    )[0].firstChild.data
    url   = item.getElementsByTagName("url"      )[0].firstChild.data
    date  = item.getElementsByTagName("published")[0].firstChild.data
    articles.append(ArticleInfo(title, url, date))

  return articles


if __name__ == "__main__":
  articles = fetch_qiita_feed("aKuad")

  for article in articles:
    print(article)
```

```txt
ArticleInfo(title='ユニットテストするならフレームワーク使おう (Python unittest)', url='https://qiita.com/aKuad/items/c01ef064603ac30e03fb', date='2024-01-10T23:00:19+09:00')
ArticleInfo(title='.service 自作して Python プログラムを systemd でデーモン化', url='https://qiita.com/aKuad/items/c702b508375a5d9c476b', date='2023-12-03T00:07:29+09:00')
ArticleInfo(title='Arduino Uno / atmega328p の SPI Slave ライブラリを自作する', url='https://qiita.com/aKuad/items/3d06998efc59daedd4c5', date='2023-11-12T16:02:11+09:00')
ArticleInfo(title='ESP32 マイコン を PlatformIO で動かす (on Linux Mint)', url='https://qiita.com/aKuad/items/eca015f2dcfd8bd57791', date='2023-11-08T22:47:41+09:00')
```

## ところで何がしたかった？

本当に急な思いつきで、こんなことを考えました。

「GitHub Pages に自分の Web ページ作ったとして、そこに Qiita とか Zenn の直近の投稿を表示させられたりするかな？せや、Python で Feed 収集するのを GitHub Actions で定期実行したらできそうやん。」

まぁ、まだ Web ページ作成計画すら無いんですけど･･･。

あと Actions 上で pip 呼ぶのは何ら難しくないので、普通に拡張ライブラリ使い放題。標準モジュールにこだわる必要はほぼほぼありません。せいぜい実行時間が数秒ほど短くなるだけ。

## おわり

えーっと、とりあえず、Python は標準モジュールだけでも割と色々できそうって話。

あと、Feed は古い技術ではありますが、単純で扱いやすいおかげで、こういう用途にとても適していると思います。