🔥

Power Automate Desktop Webページからデータを抽出する

2024/01/23に公開

Webスクレイピング

はじめに

PADでスクレイピングの手順ですが、ちょっと癖があるというか使いにくくないですか?
最初、なんじゃこりゃ。となったのでその辺りスクリーンショット多めに記事にしてみました。

https://learn.microsoft.com/ja-jp/power-automate/desktop-flows/automation-web#extract-data-from-webpages


レシピ

Reader Storeの購入情報をExcelに出力して保存してみようと思います。
抽出する項目はだいたいこんな感じ。

  • 作品のタイトル
  • 著者
  • 定価
  • 割引価格
  • 割引率

スクレイピングする

まず初めに、スクレイピングの部分から作っていきます。
Reader Storeのサイトを開いて本を適当に何冊かカートに入れ、カートに進みます。
PADから新しいフローを追加します。フロー名は適当に。
以降、アクションを追加していきます。

https://learn.microsoft.com/ja-jp/power-automate/desktop-flows/actions-reference/webautomation

  1. 新しい Microsoft Edge を起動
    普段からEdgeを使っているのでEdgeを選択しました。ChromeでもFirefoxでも手順は一緒です。試していないけど。今回は、ブラウザーでカートを表示させた状態からフローを実行するので、先ず既に開いているタブに接続してインスタンスを変数に保存します。
    実行中のインスタンスに接続する

  2. Webページからデータを抽出する
    [Webページからデータを抽出する]アクションの編集画面を出した状態でスクレイピングしたいページをアクティブにすると抽出の操作が行えます。
    Webページからデータを抽出する
    ライブWebヘルパーが開きます。
    ライブWebヘルパー

  3. 抽出したい要素にカーソルを合わせます
    Anchor

  4. 右クリックをして、抽出したい属性を選択します
    要素の値を抽出

  5. 1件目のレコードにタイトルが入りました
    1つ目のタイトル

  6. 2件目のレコードのタイトルを選択します
    2つ目のタイトル

  1. 3件目以降のタイトルも選択されます
    残りのタイトルもハイライトされる

  2. 残りの項目を選択していきます
    著者

  3. 今度は、1件目の著者を選択しただけで残りも選択されました
    残りの著者もハイライトされる

  4. 価格
    価格

  5. うまくいきません
    セールになっていない作品とセール中の作品で価格の部分の構造が違うようなので、
    この場では諦めて後行程でなんとかしようと思います。
    1件目のレコードしか選択されない

  6. 2件目のレコードから定価を抽出します
    先ほどは価格部分のみ抽出できたのですが、この要素はできないので文字列全部抽出します。
    2件目の価格を選択

  7. 今度は3件目も選択されました
    3件目もハイライトされた

  8. 割引後の価格を選択します
    割引後の価格を選択

  9. これで良しとします
    割引率もテキストで抽出できるのですが、今回は計算で値を入れます。
    完成

  10. プレビューで抽出後のイメージを確認
    ライブWebヘルパーの画面をリサイズしてみました。
    このような感じで抽出されるはずです。
    プレビュー

  11. ここまでのコード

PAD
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
  1. 一度デバッグ実行してみる
    ここまでで一度デバッグして、期待通りに変数に入るか確認してみます。
    だいたいOKですね。
    DataFromWebPage
    ただ、著者の値がちょっとイケてないので、これも後工程でなんとかしようと思います。
    トリムしたくなる値

  2. ライブWebヘルパーの抽出プレビュー画面からヘッダー名を変更できます。日本語の入力もOK
    日本語のヘッダーに変えた

  1. 日本語にしても大丈夫そうですね
    確認

スクレイピングの作りこみは完成。


.NETスクリプトを使う

DataTableに格納できたので、ここから.NETスクリプトでデータを弄っていきます。

今はこの状態になっています。
価格と定価、同じ列にできなかったので分かれていますが、一緒にしたい。

タイトル 著者 価格 定価 割引価格

↓他にも項目を追加して以下のようにします。

No. タイトル 著者 定価 割引価格 割引率

.NET スクリプト実行

.NET (C#/VB.NET) スクリプト コードを実行し、出力を取得します

https://learn.microsoft.com/ja-jp/power-automate/desktop-flows/actions-reference/scripting#rundotnetscript

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%
.NET code to run
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;

著者の値を整えます。
金額の計算をするのに余計な文字列が入っていると邪魔なので正規表現で数値だけにします。

https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expressions

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の書式設定で%表記にするのでこのまま使います。
OK


Excelにデータを書き込む手順は省略


VBSを使う

Excelの仕上げはVBSでやっちゃいます。

VBScript の実行

今回は、Option Explicitを記述しました。
ただ、個人的にはPADで変数の宣言をしてもまるで役に立たないと感じているので、
なくても良いんじゃないかなあ。という気はします。

  • 自動構文チェックがない
  • 実際に動かしてみるまで間違いに気付けない

スクリプトの途中で止めれないからデバッグし辛いし、使いやすい部分が何一つ思い浮かばない……という。メリットがあるなら教えてほしいです。

もちろん、他で使う場合は変数の宣言を強制するのは必須です。

なくても良いと言えば最後のQuitも別になくても良いのですが、これに関してはあえて入れています。

以前、「Power Automate Desktop VBScriptでExcelを操作する」で書いた公式サイトから引用したコードでも書いていないのですけど、要するに
Workbookオブジェクトを適切に閉じていたらプロセスも適切に終了されるので書く必要はなし。

http://rucio.a.la9.jp/main/technique/teq_15.htm

Run VBScript
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スクリプトを使うと良いと思います。
……アクションだと大変そう。

脚注
  1. 山田 祥寛.独習C# 第5版.翔泳社,2022,5.2.1. ↩︎

Discussion