BigQueryのJavaScript UDFでcheerioを使ってHTMLを解析する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

最近BigQueryを使い始めまして、かなり使い勝手がよく色々なことができて気に入っています。中でもJavaScript UDF(ユーザ定義関数)が便利そうと思い、試しに使ってみました。

JavaScript UDFとは、ユーザが定義したJavaScriptの関数を、BigQueryのSQLの中で実行できる機能です。

ユースケース

WordPressのようなCMSサービスを想像してください。このサービスでは、データベースのカラムでHTMLを管理しています。このHTMLから、特定の要素を抽出してみたいと思います。

やってみる

JavaScript UDFの準備

HTMLの解析には、cheerioを使用することにします。しかし、JavaScript UDFでは、requireが使えないため、直接ライブラリを読み込むことができません。そこでWebpackなどのビルドツールを利用し、ライブラリを単一のファイルに変換する必要があります。私はこのあたりのツールに詳しくないため、こちらの記事を参考にさせてもらいました。

まずnpmプロジェクトを作成して、必要なパッケージをインストールします。

mkdir bq-udf-cheerio
cd bq-udf-cheerio
npm init --yes
npm install cheerio
npm install -D webpack-cli
touch webpack.config.js

webpack.config.js を以下のように設定します。

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './node_modules/cheerio/lib/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'cheerio.js',
    library: {
      name: 'cheerio',
      type: 'var',
    },
  },
}

output.library.name に設定した名前で、ライブラリの関数を呼び出すことができるようになります。

ビルドを実行します。

$ npx webpack --config webpack.config.js
asset cheerio.js 336 KiB [compared for emit] [minimized] [big] (name: main) 1 related asset
runtime modules 670 bytes 3 modules
javascript modules 645 KiB 78 modules
json modules 30 KiB
  ./node_modules/entities/lib/maps/entities.json 28.4 KiB [built] 
  ./node_modules/entities/lib/maps/legacy.json 1.24 KiB [built] 
  ./node_modules/entities/lib/maps/xml.json 62 bytes [built] 
  ./node_modules/entities/lib/maps/decode.json 308 bytes [built] 

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  cheerio.js (336 KiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  main (336 KiB)
      cheerio.js


WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/

webpack 5.43.0 compiled with 3 warnings in 3823 ms

ビルドが成功すると、dist/cheerio.js が出力されます。これをGCSのバケットにアップロードしておきます。

アセットのサイズに関する警告が出ていますがとりあえず無視します。ちなみにUDFの外部ソースコードの最大サイズは1MBです。

クエリを実行する

今回はデータソースとしてBigQueryのマーケットプレイスにあるパブリックなデータの中からGitHub Activity Dataのsample_contentsテーブルを利用します。(HTMLのデータがありそうなデータソースを適当に探しました。)

CREATE TEMP FUNCTION構文で一時的なUDFを定義し、クエリを実行します。

CREATE TEMP FUNCTION get_title(html STRING)
RETURNS STRING
LANGUAGE js
OPTIONS (
library=["gs://bq_udf_library_igarashi_test/cheerio.js"]
)
AS r"""
const $ = cheerio.load(html);
return $('title').text();
""";

SELECT get_title(content) as title, content FROM `bigquery-public-data.github_repos.sample_contents` 
where sample_path like '%.html' and content like '%<title>%'
LIMIT 100;

実行結果(画像が細かくて申し訳ないです...)

狙い通り、titleタグの中身を出力することができました。実行速度は23.7GB処理して2.8秒、かなり早いです。ちょっとした調査には十分利用できそうです。

参考