はじめに
Atlassianのアプリケーションを利用している組織では、プロジェクト管理ツールのJiraと情報共有ツール(エンタープライズWiki)のConfluenceの組み合わせで良く利用されます。
特に、Confluenceは職種(エンジニア・非エンジニア)や役職、部門を問わず、様々なユーザーに利用されますので、組織内の多くのナレッジが蓄積されます(Confluence上に集約されます)。また、多くのナレッジが蓄積されることから、このデータを活用したいという思いや、他部門からのリクエストも出てくるかと思います。
システム管理者としては、構造化すべきデータは、JiraやDB、その他のシステムで構造化して管理してほしいというのが理想(本音)かと思いますが、ユーザーの使い方がシステム管理者の理想通りにならないことはよくあると思います。
また、ユーザーは、自分の要件を満たすことができれば、自分のスキルの範囲で扱えるツール、準備することなく今すぐ使える始められる既に設定済みのツール(手軽で簡単に使えるツール)で対応するものかと思います。
しかし、Confluence上の情報(データ)は基本的には非構造化データであり、構造化データ(例えばJiraのIssue)のように特定のフィールドに値がセットされている(キーバリューでキーを指定して目的のデータを取得できる)わけではないため、Confluenceと連携先のシステムの中間でデータの加工を行う仕組みが必要となります。
また、テーブル(表)のような見た目構造化されている情報であっても、Confluence上は非構造データとして管理されるため、同じくConfluenceと連携先のシステムの中間でデータの加工を行う仕組みが必要となります。
Workatoを利用すると、このような非構造データの加工と連携の仕組みをノーコードとローコードで実装することができます。
PythonコネクタによるHTML解析
Workatoには、ローコードコネクタとしてPythonコネクタ(Python snippets by Workato)が用意されています。
また、Pythonコネクタではlxmlモジュールが利用可能となっており、lxmlモジュールを利用することで、HTMLを簡単に解析し、目的の値にアクセスすることが可能です。
PythonとlxmlによるHTMLスクレイピング
読む方にとっては釈迦に説法的な話になりますが、PythonとlxmlモジュールによるHTML解析(HTMLスクレイピング)には、Python, lxml, DOM, XPath に関する理解が必要になります。
lxmlによるHTMLスクレイピングについては、以下の内容が参考になると思います。
HTMLスクレイピングを進めるにあたっては、DOM(Document Object Model)を理解しておく必要があります。もしHTMLスクレイピングに初めてチャレンジされる方は、まずはDOM操作について理解されることをお勧めします。以下のページが参考になると思います。
また、必須ではありませんが、lxmlによるHTMLスクレイピングでは、XPathが利用できると効率的にスクレイピングできます。とりあえずXPathとは何か、すぐに試してみたいという方は、以下のページが参考になると思います。
なお、上記ではHTMLスクレイピングをすぐに始められる情報を紹介しています。DOMやXPathについて、より深い情報を知りたい方は、MDNや、専門書などのリソースを参照いただければと思います。
次項からは、Workatoでどのように実装を進めていくか説明します。
手順
今回は、毎日1回Confluenceの特定ページ(以下)上にあるテーブルの1行目の情報(赤枠)を取得し、その結果をLookup Tablesにセット(更新)&Slackへ通知するレシピを作成していきます。
Confluenceではページのデータは、HTML(とConfluenceの独自タグ)で非構造データとして管理されます。非構造データではありますが、幸いフォーマット化されたデータ(HTML)であるため、前述の通りlxmlを使用すれば簡単に目的のデータを取り出すことが可能です。
最終的には次のようなレシピとなりますが、ステップごとに実装手順を解説していきます。
Step 1
「Scheduer by Workato」コネクタを選択します。
「New recurring event」を選択します。
毎日12:00(日本時間)に実行することとします。
次の通り設定します。
- Time unit: Days
- Trigger every: 1 (days)
- Trigger at: 12:00 AM
- Timezone: Asia/Tokyo
Step 2
「Confluence」コネクタを選択します。
「Custom action」を選択します。
Confluenceコネクタには、ページを取得するアクションが標準で存在しないため、Custom actionで対応します。
次の通り設定します。
- Action name: 任意の名称
- Method: GET
- Path:
content/<ConfluenceのページID>?expand=space,body.view,version,container
- Response type: JSON response
なお、Pathで指定したAPIについては、以下を参照ください。
Response bodyは、以下のJSONを利用してスキーマを生成します。
以下のJSONを利用したスキーマの設定にあたっては、以下のページを参考に対応してください。
[
{
"control_type": "text",
"label": "ID",
"type": "string",
"name": "id"
},
{
"control_type": "text",
"label": "Type",
"type": "string",
"name": "type"
},
{
"control_type": "text",
"label": "Status",
"type": "string",
"name": "status"
},
{
"control_type": "text",
"label": "Title",
"type": "string",
"name": "title"
},
{
"properties": [
{
"control_type": "number",
"label": "ID",
"parse_output": "float_conversion",
"type": "number",
"name": "id"
},
{
"control_type": "text",
"label": "Key",
"type": "string",
"name": "key"
},
{
"control_type": "text",
"label": "Name",
"type": "string",
"name": "name"
},
{
"control_type": "text",
"label": "Type",
"type": "string",
"name": "type"
},
{
"properties": [
{
"control_type": "text",
"label": "Webui",
"type": "string",
"name": "webui"
},
{
"control_type": "text",
"label": "Self",
"type": "string",
"name": "self"
}
],
"label": "Links",
"type": "object",
"name": "_links"
},
{
"properties": [
{
"control_type": "text",
"label": "Metadata",
"type": "string",
"name": "metadata"
},
{
"control_type": "text",
"label": "Icon",
"type": "string",
"name": "icon"
},
{
"control_type": "text",
"label": "Description",
"type": "string",
"name": "description"
},
{
"control_type": "text",
"label": "Homepage",
"type": "string",
"name": "homepage"
}
],
"label": "Expandable",
"type": "object",
"name": "_expandable"
}
],
"label": "Space",
"type": "object",
"name": "space"
},
{
"properties": [
{
"properties": [
{
"control_type": "text",
"label": "Type",
"type": "string",
"name": "type"
},
{
"control_type": "text",
"label": "Username",
"type": "string",
"name": "username"
},
{
"control_type": "text",
"label": "User key",
"type": "string",
"name": "userKey"
},
{
"properties": [
{
"control_type": "text",
"label": "Path",
"type": "string",
"name": "path"
},
{
"control_type": "number",
"label": "Width",
"parse_output": "float_conversion",
"type": "number",
"name": "width"
},
{
"control_type": "number",
"label": "Height",
"parse_output": "float_conversion",
"type": "number",
"name": "height"
},
{
"control_type": "text",
"label": "Is default",
"render_input": {},
"parse_output": {},
"toggle_hint": "Select from option list",
"toggle_field": {
"label": "Is default",
"control_type": "text",
"toggle_hint": "Use custom value",
"type": "boolean",
"name": "isDefault"
},
"type": "boolean",
"name": "isDefault"
}
],
"label": "Profile picture",
"type": "object",
"name": "profilePicture"
},
{
"control_type": "text",
"label": "Display name",
"type": "string",
"name": "displayName"
},
{
"properties": [
{
"control_type": "text",
"label": "Self",
"type": "string",
"name": "self"
}
],
"label": "Links",
"type": "object",
"name": "_links"
},
{
"properties": [
{
"control_type": "text",
"label": "Status",
"type": "string",
"name": "status"
}
],
"label": "Expandable",
"type": "object",
"name": "_expandable"
}
],
"label": "By",
"type": "object",
"name": "by"
},
{
"control_type": "text",
"label": "When",
"render_input": "date_time_conversion",
"parse_output": "date_time_conversion",
"type": "date_time",
"name": "when"
},
{
"control_type": "text",
"label": "Message",
"type": "string",
"name": "message"
},
{
"control_type": "number",
"label": "Number",
"parse_output": "float_conversion",
"type": "number",
"name": "number"
},
{
"control_type": "text",
"label": "Minor edit",
"render_input": {},
"parse_output": {},
"toggle_hint": "Select from option list",
"toggle_field": {
"label": "Minor edit",
"control_type": "text",
"toggle_hint": "Use custom value",
"type": "boolean",
"name": "minorEdit"
},
"type": "boolean",
"name": "minorEdit"
},
{
"control_type": "text",
"label": "Hidden",
"render_input": {},
"parse_output": {},
"toggle_hint": "Select from option list",
"toggle_field": {
"label": "Hidden",
"control_type": "text",
"toggle_hint": "Use custom value",
"type": "boolean",
"name": "hidden"
},
"type": "boolean",
"name": "hidden"
},
{
"properties": [
{
"control_type": "text",
"label": "Self",
"type": "string",
"name": "self"
}
],
"label": "Links",
"type": "object",
"name": "_links"
},
{
"properties": [
{
"control_type": "text",
"label": "Content",
"type": "string",
"name": "content"
}
],
"label": "Expandable",
"type": "object",
"name": "_expandable"
}
],
"label": "Version",
"type": "object",
"name": "version"
},
{
"properties": [
{
"control_type": "number",
"label": "ID",
"parse_output": "float_conversion",
"type": "number",
"name": "id"
},
{
"control_type": "text",
"label": "Key",
"type": "string",
"name": "key"
},
{
"control_type": "text",
"label": "Name",
"type": "string",
"name": "name"
},
{
"control_type": "text",
"label": "Type",
"type": "string",
"name": "type"
},
{
"properties": [
{
"control_type": "text",
"label": "Webui",
"type": "string",
"name": "webui"
},
{
"control_type": "text",
"label": "Self",
"type": "string",
"name": "self"
}
],
"label": "Links",
"type": "object",
"name": "_links"
},
{
"properties": [
{
"control_type": "text",
"label": "Metadata",
"type": "string",
"name": "metadata"
},
{
"control_type": "text",
"label": "Icon",
"type": "string",
"name": "icon"
},
{
"control_type": "text",
"label": "Description",
"type": "string",
"name": "description"
},
{
"control_type": "text",
"label": "Homepage",
"type": "string",
"name": "homepage"
}
],
"label": "Expandable",
"type": "object",
"name": "_expandable"
}
],
"label": "Container",
"type": "object",
"name": "container"
},
{
"properties": [
{
"properties": [
{
"control_type": "text",
"label": "Value",
"type": "string",
"name": "value"
},
{
"control_type": "text",
"label": "Representation",
"type": "string",
"name": "representation"
},
{
"properties": [
{
"control_type": "text",
"label": "Webresource",
"type": "string",
"name": "webresource"
},
{
"control_type": "text",
"label": "Content",
"type": "string",
"name": "content"
}
],
"label": "Expandable",
"type": "object",
"name": "_expandable"
}
],
"label": "View",
"type": "object",
"name": "view"
},
{
"properties": [
{
"control_type": "text",
"label": "Editor",
"type": "string",
"name": "editor"
},
{
"control_type": "text",
"label": "Export view",
"type": "string",
"name": "export_view"
},
{
"control_type": "text",
"label": "Styled view",
"type": "string",
"name": "styled_view"
},
{
"control_type": "text",
"label": "Storage",
"type": "string",
"name": "storage"
},
{
"control_type": "text",
"label": "Anonymous export view",
"type": "string",
"name": "anonymous_export_view"
}
],
"label": "Expandable",
"type": "object",
"name": "_expandable"
}
],
"label": "Body",
"type": "object",
"name": "body"
},
{
"properties": [
{
"control_type": "number",
"label": "Position",
"parse_output": "float_conversion",
"type": "number",
"name": "position"
}
],
"label": "Extensions",
"type": "object",
"name": "extensions"
},
{
"properties": [
{
"control_type": "text",
"label": "Webui",
"type": "string",
"name": "webui"
},
{
"control_type": "text",
"label": "Edit",
"type": "string",
"name": "edit"
},
{
"control_type": "text",
"label": "Tinyui",
"type": "string",
"name": "tinyui"
},
{
"control_type": "text",
"label": "Collection",
"type": "string",
"name": "collection"
},
{
"control_type": "text",
"label": "Base",
"type": "string",
"name": "base"
},
{
"control_type": "text",
"label": "Context",
"type": "string",
"name": "context"
},
{
"control_type": "text",
"label": "Self",
"type": "string",
"name": "self"
}
],
"label": "Links",
"type": "object",
"name": "_links"
},
{
"properties": [
{
"control_type": "text",
"label": "Metadata",
"type": "string",
"name": "metadata"
},
{
"control_type": "text",
"label": "Operations",
"type": "string",
"name": "operations"
},
{
"control_type": "text",
"label": "Children",
"type": "string",
"name": "children"
},
{
"control_type": "text",
"label": "Restrictions",
"type": "string",
"name": "restrictions"
},
{
"control_type": "text",
"label": "History",
"type": "string",
"name": "history"
},
{
"control_type": "text",
"label": "Ancestors",
"type": "string",
"name": "ancestors"
},
{
"control_type": "text",
"label": "Descendants",
"type": "string",
"name": "descendants"
}
],
"label": "Expandable",
"type": "object",
"name": "_expandable"
}
]
Step 3
「Python snippets by Workato」コネクタを選択します。
「Execute code」を選択します。
次の通り設定します。
Name
任意の名称
Input fields
「html_body」というString型のフィールドを追加します。
その後、フィールドに次の内容を入力します。
<html><head></head><body>
[Value]
</body></html>
[Value]
には、Step2の次のValueをセットします。
Output fields
String型で次のフィールドを追加します。
- date
- rate
- comment
Code
次のコードを入力します。
import lxml.html
def main(input):
root = lxml.html.fromstring(input['html_body'])
date = ''
rate = ''
comment = ''
date = root.xpath("//table/tbody/tr/td")[0].text
rate = root.xpath("//table/tbody/tr/td")[1].text
comment = root.xpath("//table/tbody/tr/td")[2].text
return { 'date': date, 'rate': rate, 'comment': comment}
コード解説
入力値にセットされた値(html_bodyの値)をHtmlElementオブジェクトにします
root = lxml.html.fromstring(input['html_body'])
XPathでテーブルの1行目の各列をXPathで取得します。
date = root.xpath("//table/tbody/tr/td")[0].text
rate = root.xpath("//table/tbody/tr/td")[1].text
comment = root.xpath("//table/tbody/tr/td")[2].text
XPathで取得した値を戻り値として返します
return { 'date': date, 'rate': rate, 'comment': comment}
Step 4
「Lookup tables by Workato」コネクタを選択します。
「Search entries」を選択します。
次の通り設定します。
- Lookup table:検索したいテーブルを選択
- Search by:検索条件をセット(ここでは
id
に1をセット)
Lookup Table
次のようなLookup tableを想定します。
Step 5
「Lookup tables by Workato」コネクタを選択します。
「Update entry」を選択します。
以下の通り値をセットします。
- Entry ID: Step4の
Entry ID
をセット - Entry fields: Step3, Step2の対応するフィールドをセット
Step 6
「Workbot for Slack」コネクタを選択します。
「Post message」を選択します。
次の項目を設定します。
- Channel name/DM: 通知先のチャンネルまたはユーザー
Message attachments
Title
, Title link
に通知メッセージを入力します。
Attachment fields
日付、TTMレート、備考欄を追加し、Step3の対応するフィールドをValueへセットします。
実行結果
Lookup tables
Confluenceの1行目の各列の値が、Lookup Tablesの各フィールドに追加されます。
Slack
Confluenceの1行目の各列の値が、Slackメッセージとして通知されます。
まとめ
Workatoでは、Pythonコネクタを利用することで、Confluenceのような非構造化データ(HTML)も構造化データのように扱えるようになります。Confluenceと何かのシステムを連携したい方の参考になりましたら幸いです。