LoginSignup
4
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-27

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 というファイル一覧が出力されます。

まとめ

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3