Help us understand the problem. What is going on with this article?

HARからファイルを抽出する

More than 3 years have passed since last update.

HARファイル(ブラウザの通信状況をダンプしたJSONファイル)からファイルを抽出します。

この記事では次の記事で作ったJSONパーサーを使用します。

調査

ある程度巨大なHARファイルは全容を把握しにくいので、項目を列挙して眺めます。

try
    use sr = new StreamReader("Test.har", Text.Encoding.UTF8)
    use jp = new JSONParser(sr)
    while jp.Read() do
    let v = jp.Value
    let v = if v.Length < 20 then v else v.[..19] + ".."
    printfn "%A %A %c %A" (List.rev jp.Stack) jp.Name jp.Type v
with e ->
    printf "%A" e
実行結果
[] "log" { ""
["log"] "version" : "1.1"
["log"] "creator" { ""
["log"; "creator"] "name" : "Firefox"
["log"; "creator"] "version" : "50.0"
["log"] "creator" } ""
["log"] "browser" { ""
["log"; "browser"] "name" : "Firefox"
["log"; "browser"] "version" : "50.0"
["log"] "browser" } ""
["log"] "pages" [ ""
["log"; "pages"] "" { ""
(略)

これを眺めてみると、主要部は以下の構造になっていることが分かりました。

log
+ creator
+ browser
+ pages
+ entries
  + (entry)
  | + request
  | | + url
  | + response
  |   + headers
  |   + cookies
  |   + content
  + (entry)
  + ...

contentの項目

ファイルの実体が入っているのは content です。すべてに同じ項目が存在するわけではないようなので、全部の項目をチェックして列挙します。

try
    use sr = new StreamReader("Test.har", Text.Encoding.UTF8)
    use jp = new JSONParser(sr)
    seq {
        while jp.Find "content" do
        for _ in jp.Each() -> jp.Name }
    |> Seq.distinct
    |> Seq.sort
    |> Seq.iter (printfn "%s")
with e ->
    printf "%A" e
実行結果
comment
encoding
mimeType
size
text

comment はオプショナルなようで、ほとんど存在しません。

パーサーの実装

エントリーを格納するレコード型を用意します。requesturlresponsecontent を読み込みます。

type Entry = { url:string; size:int; mimeType:string; encoding:string; text:string }

let entry (jp:JSONParser) =
    let mutable url, size, mimeType, encoding, text = "", 0, "", "", ""
    for _ in jp.Each() do
        match jp.Name with
        | "request" ->
            for _ in jp.Each() do
            if jp.Name = "url" then url <- jp.Value
        | "response" ->
            for _ in jp.Each() do
            if jp.Name = "content" then
                for _ in jp.Each() do
                match jp.Name with
                | "size"     -> size <- Convert.ToInt32 jp.Value
                | "mimeType" -> mimeType <- jp.Value
                | "encoding" -> encoding <- jp.Value
                | "text"     -> text     <- jp.Value
                | _          -> ()
        | _ -> ()
    { url = url; size = size; mimeType = mimeType; encoding = encoding; text = text }

JSONの構造と同じようにパーサーを書きます。階層ごとに jp.Each() でループを回しているのがポイントです。

値の調査

encodingmimeType の値を調べます。

try
    use sr = new StreamReader("Test.har", Text.Encoding.UTF8)
    use jp = new JSONParser(sr)
    seq {
        while jp.Find "content" do
        for _ in jp.Each() do
        match jp.Name with
        | "encoding" | "mimeType" -> yield jp.Name, jp.Value
        | _ -> () }
    |> Seq.groupBy fst
    |> Seq.iter (fun (n, vs) ->
        printfn "%s:" n
        vs
        |> Seq.map snd
        |> Seq.distinct
        |> Seq.sort
        |> Seq.iter (printfn "  %s"))
with e ->
    printf "%A" e
実行結果
mimeType:
  image/jpeg
  image/png
  text/css; charset=UTF-8
  text/html
  text/html; charset=UTF-8
  text/javascript
  text/javascript; charset=UTF-8
encoding:
  base64

サーバーによって charset が付いたり付かなかったりします。今回は簡単のため無視して UTF-8 で統一します。

拡張子の変換

MIMEタイプから拡張子に変換します。網羅しきれないため代表的なものだけです。

let mimeToExt (mime:string) =
    match (mime.Split ';').[0].Trim().ToLower() with
    | "application/javascript"   -> ".js"
    | "application/json"         -> ".json"
    | "application/octet-stream" -> ".bin"
    | "application/x-javascript" -> ".js"
    | "binary/octet-stream"      -> ".bin"
    | "image/gif"                -> ".gif"
    | "image/jpeg"               -> ".jpg"
    | "image/png"                -> ".png"
    | "image/x-win-bitmap"       -> ".bmp"
    | "text/css"                 -> ".css"
    | "text/html"                -> ".html"
    | "text/javascript"          -> ".js"
    | "text/plain"               -> ".txt"
    | "text/xml"                 -> ".xml"
    | _                          -> ""

ファイルの抽出

ここまで調べればファイルが抽出できます。

let extractHar (har:string) (outdir:string) =
    if not <| Directory.Exists outdir then
        Directory.CreateDirectory outdir |> ignore
    use sr = new StreamReader(har, Text.Encoding.UTF8)
    let jp = JSONParser sr
    if not <| jp.Find "entries" then () else
    use sw = new StreamWriter(Path.Combine(outdir, "_index.tsv"), false, Text.Encoding.UTF8)
    seq {for _ in jp.Each() -> entry jp}
    |> Seq.filter (fun e -> e.url.Length > 0 && e.text.Length > 0 && e.size > 0)
    |> Seq.iteri (fun i e ->
        let fn = string (i + 1) + mimeToExt e.mimeType
        fprintfn sw "%s\t%d\t%s\t%s\t%s" fn e.size e.mimeType e.encoding e.url
        let fn = Path.Combine(outdir, fn)
        match e.encoding with
        | "base64" ->
            File.WriteAllBytes(fn, Convert.FromBase64String e.text)
        | _ ->
            File.WriteAllText(fn, e.text, Text.Encoding.UTF8))

次のように使用します。

extractHar "入力.har" "出力先フォルダ"

出力先フォルダには _index.tsv というファイル一覧が出力されます。

まとめ

コード全体は以下に掲載します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした