生成AIを用いたDatabricksリリースノートの翻訳、拡張

アドベントカレンダーということで、日曜大工的に。

こちらのページは、[Databricksリリースノート](https://docs.databricks.com/en/release-notes/index.html)のページをパーシングして自動で更新されるようになっています。

https://qiita.com/taka_yayoi/items/57923e2d159a65a22118

:::note info
**注意**
- こちらのページの更新は予告なく停止される可能性があります。
- こちらのページをストックしても更新の通知は来ません。[更新API](https://qiita.com/api/v2/docs#patch-apiv2itemsitem_id)にそのオプションがありませんでした。
:::

# モチベーション

- Databricksの機能更新の頻度は高く、頻繁にリリースノートが更新されるのですが英語のみです。
- 通知の仕組みの一つとして、更新情報を翻訳してQiitaのページに反映させようと思いました。本当はRSSとかにしたいのですが、今後の宿題ということで。

以降で実装を説明します。スクレイピングを行ないますので用法用量にはご注意ください。

# データベースの準備

```py
db_name = "takaaki_yayoi"

# specify catalog name
spark.sql(f"USE CATALOG users")

spark.sql(f"CREATE DATABASE IF NOT EXISTS {db_name}")
spark.sql(f"USE {db_name}")

print(f"database_name: {db_name}")
```

```sql
%sql
CREATE TABLE IF NOT EXISTS databricks_release_notes_jpn (
  year INT,
  month INT,
  date DATE,
  title STRING,
  id STRING,
  url STRING,
  inserted TIMESTAMP,
  title_japanese STRING,
  summary STRING,
  summary_japanese STRING,
  user_impact STRING
);
```

```py
from pyspark.sql.types import *

def is_exists_in_db(year, month, id):
    """テーブルに格納済みかどうかをチェック
    """
    sql = f"SELECT * FROM databricks_release_notes_jpn WHERE year={year} AND month={month} AND id='{id}'"
    # print(sql)
    result = spark.sql(sql)

    if result.count() == 0:
        return False
    else:
        return True


def insert_into_db(
    year, month, date, title, id, url, title_japanese, summary, summary_japanese, user_impact
):
    """テーブルに格納
    """
    sql = "INSERT INTO databricks_release_notes_jpn(year, month, date, title, id, url, inserted, title_japanese, summary, summary_japanese, user_impact) VALUES (:year, :month, to_date(:date), :title, :id, :url, current_timestamp(), :title_japanese, :summary, :summary_japanese, :user_impact)"
    #print(sql)
    result = spark.sql(
        sql,
        args={
            "year": year,
            "month": month,
            "date": date,
            "title": title,
            "id": id,
            "url": url,
            "title_japanese": title_japanese,
            "summary": summary,
            "summary_japanese": summary_japanese,
            "user_impact": user_impact
        },
    )
```

# ヘルパー関数

```py
import calendar

def month_index(month_name):
  """月の英語表記を数字に変換
  """
  # https://stackoverflow.com/questions/3418050/how-to-map-month-name-to-month-number-and-vice-versa
  return list(calendar.month_name).index(month_name.capitalize())
```

# 基盤モデルAPIによる翻訳、文の生成

[基盤モデルAPI](https://docs.databricks.com/ja/machine-learning/foundation-models/index.html)を使ってサクッと翻訳します。せっかくなので、新機能がユーザーに与えるインパクトの文章も生成してもらいます。

```py
from databricks.sdk import WorkspaceClient
from databricks.sdk.service.serving import ChatMessage, ChatMessageRole

w = WorkspaceClient()

def translate(text):
  """翻訳
  """
  response = w.serving_endpoints.query(
    name="databricks-meta-llama-3-3-70b-instruct",
    messages=[
        ChatMessage(
            role=ChatMessageRole.SYSTEM, content="あなたは有能な翻訳家です。与えられた英語を日本語に翻訳します。HTML文字列には変更を加えません。"
        ),
        ChatMessage(
            role=ChatMessageRole.USER, content=text
        ),
    ],
    max_tokens=500,
  )
  return response.choices[0].message.content

def generate_user_impact(text):
  """ユーザーへのインパクトを記述
  """
  response = w.serving_endpoints.query(
    name="databricks-meta-llama-3-3-70b-instruct",
    messages=[
        ChatMessage(
            role=ChatMessageRole.SYSTEM, content="あなたはDatabicksの専門家です。与えられたリリースノートを読んで、Databricksユーザーに対する影響を簡潔に回答します。"
        ),
        ChatMessage(
            role=ChatMessageRole.USER, content=text
        ),
    ],
    max_tokens=500,
  )
  return response.choices[0].message.content
```

# BS4によるリリースノートのスクレイピング

ある意味、ここが処理の肝です。以下の記事を参考にさせて頂きながら実装しました。

- [図解！Python BeautifulSoupの使い方を徹底解説！\(select、find、find\_all、インストール、スクレイピングなど\) \- ビジPy](https://ai-inter1.com/beautifulsoup_1/)
- [［Python入門］Beautiful Soup 4によるスクレイピングの基礎：Python入門（2/2 ページ） \- ＠IT](https://atmarkit.itmedia.co.jp/ait/articles/1910/18/news015_2.html)
- [［解決！Python］re\.search／re\.match関数と正規表現を使って文字列から部分文字列を抽出するには：解決！Python \- ＠IT](https://atmarkit.itmedia.co.jp/ait/articles/2103/09/news022.html)

```py
import requests, bs4
import re
import urllib
import datetime

def parse_release_note(year, month):
  return_list = []
  target_url = f'https://docs.databricks.com/ja/release-notes/product/{year}/{month}.html'

  # ページの存在確認
  response = requests.get(target_url)
  if response.status_code == 404:
    print('Page not found')
  else:

    # 文字列の月を数値に変換
    month_no = month_index(month)

    res = requests.get(target_url)
    res.raise_for_status()
    soup = bs4.BeautifulSoup(res.text, "html.parser")
    sections = soup.select("div.section")

    for section in sections:
      # skip the first div
      if section.get("id") != f"{month}-{year}":

        #print("##################")
        #print(section.getText())
        #print("##################")
    
        # 要素の抽出
        id = section.get("id")
        url = target_url + "#" + id
        title = section.select("h2")[0].getText()
        title_japanese = translate(title)
        date = section.select('p > strong')[0].getText().replace("th", "").strip()
        date = datetime.datetime.strptime(date, "%B %d, %Y").strftime('%Y-%m-%d') # Y-M-Dに変換
        summary = str(section.select('p')[1])

        # 新規リリースの場合
        if is_exists_in_db(year, month_no, id) == False:

          # 相対パスを絶対パスに変換
          results = re.finditer('href="([^"]+)"', summary)
          #print(result.group(1))

          if results is not None:
            for result in results:
              relative_url = result.group(1)
              absolute_url = urllib.parse.urljoin(target_url, relative_url)
              summary = summary.replace(relative_url, absolute_url)

          summary_japanese = translate(summary)
          user_impact = generate_user_impact(title_japanese + ":" + summary_japanese)
          #print(title_japanese, date, summary_japanese, user_impact)
          return_list.append({"title": title, "date": date, "summary": summary})
          # DBに登録
          insert_into_db(year, month_no, date, title, id, url, title_japanese, summary, summary_japanese, user_impact)

  return return_list
```

あとでジョブにするので、処理を行う日付の情報をパラメーターとして渡すようにしています。

```py
import datetime

dt_now = datetime.datetime.now()

current_year = dt_now.strftime('%Y')
current_month = dt_now.strftime('%B').lower()
print(current_year, current_month)
```
```
2024 december
```


こちらが処理のエントリーポイントになります。

```py
return_list = parse_release_note(current_year, current_month)
print("新着記事:", len(return_list))

# 新着記事がない場合には終了
if len(return_list) == 0:
  dbutils.notebook.exit("no new articles found")
```

# Qiitaへの投稿

**参考記事**
- [Qiita API v2を利用してcurlで投稿してみた \| iret\.media](https://iret.media/44143)
- [Qiita API v2ドキュメント \- Qiita Developer](https://qiita.com/api/v2/docs#%E6%8A%95%E7%A8%BF)

リリースノートを格納しているテーブルからデータを取り出します。

```py
release_notes = spark.sql("SELECT * FROM databricks_release_notes_jpn ORDER BY DATE DESC LIMIT 100").toPandas().to_dict("recoreds")
release_notes
```

あとは記事本文を組み立てて投稿するだけです。

```py
post_str = "Databricksのリリースノートをスクレイピングし、生成AIによる翻訳を行い、ユーザーへのインパクトを説明する文を生成しています。すべての処理は自動で行われています。\n\n:::note\n**注意**\n翻訳やインパクトの説明文は生成AIによるものですので、詳細は原文を確認ください。\n:::\n\n"

for release in release_notes:
  #print(release)
  url = ""

  post_str += f"# [{release['title_japanese']}]({release['url']})\n**{release['date']}**\n{release['summary_japanese']}\n\n**ユーザーへのインパクト**\n\n{release['user_impact']}\n\n"

post_str
```

```py
import requests
import pprint

url_items = 'https://qiita.com/api/v2/items/57923e2d159a65a22118'
headers = {'Authorization': 'Bearer <Qiita APIトークン>'}

item_data = {
    'title': 'Databricksリリースノート(毎日更新)',
    'body': post_str,
    'private': False,
    'tags': [{'name': 'Databricks'}],
    'coediting': False,
}

r_post = requests.patch(url_items, headers=headers, json=item_data)
pprint.pprint(r_post.json())
```

# ジョブの作成

これでロジックが固まったので、ノートブック右上の**Schedule**ボタンでジョブ化します。

![Screenshot 2024-12-15 at 9.43.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/03def1d6-f6fc-784d-01ad-1c08beb636bb.png)

これで定期的にリリースノートページを解析し、翻訳と拡張を行いQiitaに記事として反映されるようになりました。

![Screenshot 2024-12-15 at 9.44.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/f679a903-e86e-da09-a35d-189c264e6144.png)

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)