Power Automate Desktop Webページからデータを抽出する
Webスクレイピング
はじめに
PADでスクレイピングの手順ですが、ちょっと癖があるというか使いにくくないですか?
最初、なんじゃこりゃ。となったのでその辺りスクリーンショット多めに記事にしてみました。
レシピ
Reader Storeの購入情報をExcelに出力して保存してみようと思います。
抽出する項目はだいたいこんな感じ。
- 作品のタイトル
- 著者
- 定価
- 割引価格
- 割引率
スクレイピングする
まず初めに、スクレイピングの部分から作っていきます。
Reader Storeのサイトを開いて本を適当に何冊かカートに入れ、カートに進みます。
PADから新しいフローを追加します。フロー名は適当に。
以降、アクションを追加していきます。
-
新しい Microsoft Edge を起動
普段からEdgeを使っているのでEdgeを選択しました。ChromeでもFirefoxでも手順は一緒です。試していないけど。今回は、ブラウザーでカートを表示させた状態からフローを実行するので、先ず既に開いているタブに接続してインスタンスを変数に保存します。
-
Webページからデータを抽出する
[Webページからデータを抽出する]アクションの編集画面を出した状態でスクレイピングしたいページをアクティブにすると抽出の操作が行えます。
ライブWebヘルパーが開きます。
-
抽出したい要素にカーソルを合わせます
-
右クリックをして、抽出したい属性を選択します
-
1件目のレコードにタイトルが入りました
-
2件目のレコードのタイトルを選択します
-
3件目以降のタイトルも選択されます
-
残りの項目を選択していきます
-
今度は、1件目の著者を選択しただけで残りも選択されました
-
価格
-
うまくいきません
セールになっていない作品とセール中の作品で価格の部分の構造が違うようなので、
この場では諦めて後行程でなんとかしようと思います。
-
2件目のレコードから定価を抽出します
先ほどは価格部分のみ抽出できたのですが、この要素はできないので文字列全部抽出します。
-
今度は3件目も選択されました
-
割引後の価格を選択します
-
これで良しとします
割引率もテキストで抽出できるのですが、今回は計算で値を入れます。
-
プレビューで抽出後のイメージを確認
ライブWebヘルパーの画面をリサイズしてみました。
このような感じで抽出されるはずです。
-
ここまでのコード
FUNCTION Main_copy GLOBAL
WebAutomation.LaunchEdge.AttachToEdgeByTitle TabTitle: $'''カート | ソニーの電子書籍ストア -Reader Store''' AttachTimeout: 5 BrowserInstance=> Browser
WebAutomation.ExtractData.ExtractTable BrowserInstance: Browser Control: $'''html > body > main > div > div:eq(22) > div:eq(0) > section:eq(0) > ul > li''' ExtractionParameters: {[$'''div:eq(0) > div > div:eq(0) > p > a''', $'''Own Text''', $'''''', $'''Value #1'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(0)''', $'''Own Text''', $'''''', $'''Value #2'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(1) > p:eq(1)''', $'''Own Text''', $'''''', $'''Value #3'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(1) > s > div''', $'''Own Text''', $'''''', $'''Value #4'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(2) > p:eq(1)''', $'''Own Text''', $'''''', $'''Value #5'''] } PostProcessData: False TimeoutInSeconds: 60 ExtractedData=> DataFromWebPage
END FUNCTION
-
一度デバッグ実行してみる
ここまでで一度デバッグして、期待通りに変数に入るか確認してみます。
だいたいOKですね。
ただ、著者の値がちょっとイケてないので、これも後工程でなんとかしようと思います。
-
ライブWebヘルパーの抽出プレビュー画面からヘッダー名を変更できます。日本語の入力もOK
- 日本語にしても大丈夫そうですね
スクレイピングの作りこみは完成。
.NETスクリプトを使う
DataTableに格納できたので、ここから.NETスクリプトでデータを弄っていきます。
今はこの状態になっています。
価格と定価、同じ列にできなかったので分かれていますが、一緒にしたい。
タイトル | 著者 | 価格 | 定価 | 割引価格 |
---|---|---|---|---|
↓他にも項目を追加して以下のようにします。
No. | タイトル | 著者 | 定価 | 割引価格 | 割引率 |
---|---|---|---|---|---|
.NET スクリプト実行
.NET (C#/VB.NET) スクリプト コードを実行し、出力を取得します
Language: C#
.NET script imports: System.Text.RegularExpressions
Script Parameters:
.NET parameter name | Type | Direction | Input value | Output value |
---|---|---|---|---|
dt | Datatable | In-Out | %DataFromWebPage% | %DataFromWebPage% |
var table = new DataTable();
table.Columns.Add("No.");
table.Columns.Add("タイトル");
table.Columns.Add("著者");
table.Columns.Add("定価");
table.Columns.Add("割引価格");
table.Columns.Add("割引率");
var no = 1;
foreach (DataRow row in dt.Rows)
{
var rate = 0.0;
var author = Regex.Replace(row["著者"].ToString(), @"\s", "");
if (row["定価"] == "")
{
row["定価"] = row["価格"];
}
var price = Regex.Replace(row["定価"].ToString(), @"\D", "");
var discount = Regex.Replace(row["割引価格"].ToString(), @"\D", "");
if (discount == "")
{
discount = price;
}
else
{
rate = 1 - Double.Parse(discount) / Double.Parse(price);
}
table.Rows.Add(no, row["タイトル"], author, price, discount, rate);
no += 1;
}
dt = table;
著者の値を整えます。
金額の計算をするのに余計な文字列が入っていると邪魔なので正規表現で数値だけにします。
Regex.Replace(String, @"\s", "")
: 空白文字を空文字に置換
Regex.Replace(String, @"\D", "")
: 数値以外を空文字に置換
特定の文字セット[1]
パターン | 概要 |
---|---|
\t | タブ文字 |
\n | 改行(ラインフィード) |
\r | 復帰(キャリッジリターン) |
\d | 数値にマッチ([0-9]と同じ) |
\D | 数値以外にマッチ([^0-9]と同じ) |
\s | 空白文字にマッチ([\t\n\f\r]と同じ) |
\S | 空白以外の文字にマッチ[^\s]と同じ |
\w | 大文字/小文字のアルファベット、数字、アンダースコア、ひらがな、カタカナ、漢字 |
\W | 文字以外にマッチ([^\w]と同じ) |
\b | 英数字とその他の文字との境界 |
実行して確認します。
大丈夫ですね。割引率は、Excelの書式設定で%表記にするのでこのまま使います。
Excelにデータを書き込む手順は省略
VBSを使う
Excelの仕上げはVBSでやっちゃいます。
VBScript の実行
今回は、Option Explicit
を記述しました。
ただ、個人的にはPADで変数の宣言をしてもまるで役に立たないと感じているので、
なくても良いんじゃないかなあ。という気はします。
- 自動構文チェックがない
- 実際に動かしてみるまで間違いに気付けない
スクリプトの途中で止めれないからデバッグし辛いし、使いやすい部分が何一つ思い浮かばない……という。メリットがあるなら教えてほしいです。
もちろん、他で使う場合は変数の宣言を強制するのは必須です。
なくても良いと言えば最後のQuit
も別になくても良いのですが、これに関してはあえて入れています。
以前、「Power Automate Desktop VBScriptでExcelを操作する」で書いた公式サイトから引用したコードでも書いていないのですけど、要するに
Workbookオブジェクトを適切に閉じていたらプロセスも適切に終了されるので書く必要はなし。
Option Explicit
Dim objExcel, objWorkbook, objSheet
Set objExcel = CreateObject("Excel.Application")
Set objWorkbook = objExcel.Workbooks.Open("%ExcelFile%")
Set objSheet = objWorkbook.Sheets(1)
objExcel.Application.Visible = False
Const xlSrcRange = 1
With objSheet
.Columns("D:E").Style = "Currency [0]"
.Columns("F").Style = "Percent"
.ListObjects.Add xlSrcRange, .Range("A1").CurrentRegion
End With
Const xlTotalsCalculationSum = 1
With objSheet.Range("A1").ListObject
.TableStyle = ""
.ShowTotals = True
.ListColumns("定価").TotalsCalculation = xlTotalsCalculationSum
.ListColumns("割引価格").TotalsCalculation = xlTotalsCalculationSum
End With
Dim dataAll
Set dataAll = objSheet.UsedRange
dataAll.Borders.LineStyle = True
Dim rows
rows = dataAll.Rows.Count
With objSheet
.Cells(rows + 2, "C").Value = "商品小計(" & rows - 2 & "点)"
.Cells(rows + 3, "C").Value = "ポイント利用"
.Cells(rows + 4, "C").Value = "クーポン利用"
.Cells(rows + 5, "C").Value = "お支払金額"
.Cells(rows + 6, "C").Value = "合計獲得ポイント"
.Cells(rows, "F").Value = "=1-E" & rows & "/D" & rows
.Cells(rows + 2, "D").Value = "=E" & rows
.Cells(rows + 5, "D").Value = "=D" & rows + 2 & "-D" & rows + 3 & "-D" & rows + 4
.Cells(rows + 5, "F").Value = "=1-(E" & rows & "-D" & rows + 4 & "-D" & rows + 6 & ")/D" & rows
.Columns("A:F").EntireColumn.Autofit
End With
Const xlCenter = -4108
Const xlThemeColorAccent6 = 10
Dim header
Set header = dataAll.Resize(1)
With header
.HorizontalAlignment = xlCenter
.Interior.ThemeColor = xlThemeColorAccent6
.Interior.TintAndShade = 0.8
End With
objWorkbook.Save
objWorkbook.Close True
objExcel.Quit
完成
ポイント利用、クーポン利用、合計獲得ポイントは、カートからお会計に進んだ先で出てくる項目なのであとで自分で埋めます。
→割引率もそれに合わせて変わるようにしています。
ポイントは獲得時に含めているので利用時は計算に含めていません。
完成フロー
FUNCTION Main_copy GLOBAL
**REGION 設定
DateTime.GetCurrentDateTime.Local DateTimeFormat: DateTime.DateTimeFormat.DateAndTime CurrentDateTime=> CurrentDateTime
Text.ConvertDateTimeToText.FromCustomDateTime DateTime: CurrentDateTime CustomFormat: $'''yyyyMMddTHHmmss''' Result=> FormattedDateTime
Folder.GetSpecialFolder SpecialFolder: Folder.SpecialFolder.Personal SpecialFolderPath=> Documents
SET ExcelFile TO $'''%Documents%\\ReaderStore-purchase-%FormattedDateTime%.xlsx'''
**ENDREGION
**REGION スクレイピング
WebAutomation.LaunchEdge.AttachToEdgeByTitle TabTitle: $'''カート | ソニーの電子書籍ストア -Reader Store''' AttachTimeout: 5 BrowserInstance=> Browser
WebAutomation.ExtractData.ExtractTable BrowserInstance: Browser Control: $'''html > body > main > div > div:eq(22) > div:eq(0) > section:eq(0) > ul > li''' ExtractionParameters: {[$'''div:eq(0) > div > div:eq(0) > p > a''', $'''Own Text''', $'''''', $'''タイトル'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(0)''', $'''Own Text''', $'''''', $'''著者'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(1) > p:eq(1)''', $'''Own Text''', $'''''', $'''価格'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(1) > s > div''', $'''Own Text''', $'''''', $'''定価'''], [$'''div:eq(0) > div > div:eq(0) > div:eq(2) > p:eq(1)''', $'''Own Text''', $'''''', $'''割引価格'''] } PostProcessData: False TimeoutInSeconds: 60 ExtractedData=> DataFromWebPage
**ENDREGION
Scripting.RunDotNetScript Imports: $'''System.Text.RegularExpressions''' Language: System.DotNetActionLanguageType.CSharp Script: $'''var table = new DataTable();
table.Columns.Add(\"No.\");
table.Columns.Add(\"タイトル\");
table.Columns.Add(\"著者\");
table.Columns.Add(\"定価\");
table.Columns.Add(\"割引価格\");
table.Columns.Add(\"割引率\");
var no = 1;
foreach (DataRow row in dt.Rows)
{
var rate = 0.0;
var author = Regex.Replace(row[\"著者\"].ToString(), @\"\\s\", \"\");
if (row[\"定価\"] == \"\")
{
row[\"定価\"] = row[\"価格\"];
}
var price = Regex.Replace(row[\"定価\"].ToString(), @\"\\D\", \"\");
var discount = Regex.Replace(row[\"割引価格\"].ToString(), @\"\\D\", \"\");
if (discount == \"\")
{
discount = price;
}
else
{
rate = 1 - Double.Parse(discount) / Double.Parse(price);
}
table.Rows.Add(no, row[\"タイトル\"], author, price, discount, rate);
no += 1;
}
dt = table;''' @'name:dt': DataFromWebPage @'type:dt': $'''Datatable''' @'direction:dt': $'''InOut''' @dt=> DataFromWebPage
**REGION 出力
Excel.LaunchExcel.LaunchUnderExistingProcess Visible: True Instance=> ExcelInstance
Excel.WriteToExcel.WriteCell Instance: ExcelInstance Value: DataFromWebPage.ColumnHeadersRow Column: $'''A''' Row: 1
Excel.WriteToExcel.WriteCell Instance: ExcelInstance Value: DataFromWebPage Column: $'''A''' Row: 2
Excel.RenameWorksheet.RenameWorksheetWithIndex Instance: ExcelInstance Index: 1 NewName: $'''購入履歴'''
Excel.CloseExcel.CloseAndSaveAs Instance: ExcelInstance DocumentFormat: Excel.ExcelFormat.FromExtension DocumentPath: ExcelFile
WAIT (File.WaitForFile.Created File: ExcelFile)
**ENDREGION
@@copilotGeneratedAction: 'False'
Scripting.RunVBScript.RunVBScript VBScriptCode: $'''Option Explicit
Dim objExcel, objWorkbook, objSheet
Set objExcel = CreateObject(\"Excel.Application\")
Set objWorkbook = objExcel.Workbooks.Open(\"%ExcelFile%\")
Set objSheet = objWorkbook.Sheets(1)
objExcel.Application.Visible = False
Const xlSrcRange = 1
With objSheet
.Columns(\"D:E\").Style = \"Currency [0]\"
.Columns(\"F\").Style = \"Percent\"
.ListObjects.Add xlSrcRange, .Range(\"A1\").CurrentRegion
End With
Const xlTotalsCalculationSum = 1
With objSheet.Range(\"A1\").ListObject
.TableStyle = \"\"
.ShowTotals = True
.ListColumns(\"定価\").TotalsCalculation = xlTotalsCalculationSum
.ListColumns(\"割引価格\").TotalsCalculation = xlTotalsCalculationSum
End With
Dim dataAll
Set dataAll = objSheet.UsedRange
dataAll.Borders.LineStyle = True
Dim rows
rows = dataAll.Rows.Count
With objSheet
.Cells(rows + 2, \"C\").Value = \"商品小計(\" & rows - 2 & \"点)\"
.Cells(rows + 3, \"C\").Value = \"ポイント利用\"
.Cells(rows + 4, \"C\").Value = \"クーポン利用\"
.Cells(rows + 5, \"C\").Value = \"お支払金額\"
.Cells(rows + 6, \"C\").Value = \"合計獲得ポイント\"
.Cells(rows, \"F\").Value = \"=1-E\" & rows & \"/D\" & rows
.Cells(rows + 2, \"D\").Value = \"=E\" & rows
.Cells(rows + 5, \"D\").Value = \"=D\" & rows + 2 & \"-D\" & rows + 3 & \"-D\" & rows + 4
.Cells(rows + 5, \"F\").Value = \"=1-(E\" & rows & \"-D\" & rows + 4 & \"-D\" & rows + 6 & \")/D\" & rows
.Columns(\"A:F\").EntireColumn.Autofit
End With
Const xlCenter = -4108
Const xlThemeColorAccent6 = 10
Dim header
Set header = dataAll.Resize(1)
With header
.HorizontalAlignment = xlCenter
.Interior.ThemeColor = xlThemeColorAccent6
.Interior.TintAndShade = 0.8
End With
objWorkbook.Save
objWorkbook.Close True
objExcel.Quit'''
END FUNCTION
あとがき
スクレイピングで取得したデータは使いにくいので.NETスクリプトを使うと良いと思います。
……アクションだと大変そう。
-
山田 祥寛.独習C# 第5版.翔泳社,2022,5.2.1. ↩︎
Discussion