マオヤン映画ボックスオフィスデータスクレイピング技術

はじめに

最近、スクレイピング技術を学び始め、今では何でもスクレイプしたくなるほどです。特にマオヤンのボックスオフィスデータがフォント暗号化されていると聞き、この技術を解明してみることにしました。

技術の進化に伴い、スクレイピング対策も日々進化しています。データ収集前に多くの専門家の記事を読みましたが、多くが時代遅れで使用できませんでした。主な変更点は、ウェブサイトが動的レンダリングを採用し、requestsライブラリではソースコードを取得できないことです。第二に、フォントファイルとデータファイルを取得した後、どのようにデコードするかという問題です。これら2つの問題が本日のテーマです。以下に、これらのステップを段階的に分析していきます。完全なコードはGitHubリポジトリを参照してください:

https://github.com/Rasrea/python-spider

ブラウザ自動化ツール:Selenium

スクレイピング学習者にとってSeleniumがどれほど便利かは言うまでもありません(ブラウザのシミュレーション、IPブロックの大幅な軽減)。使用する前にpipでインストールするだけでなく、chromedriverもダウンロードする必要があります。ここでは通常Chromeブラウザを使用しますが、Edgeブラウザを使用する場合は対応するドライバーをダウンロードできます。ただし、ブラウザのバージョンと一致させる必要があります(当初は自分のコンピュータが壊れたと思っていました)。以下は最新のchromedriverのリンクです:Chrome for Testing availability

準備が整ったので、実際の操作に入りましょう。マオヤンプロフェッショナル版(マオヤンプロフェッショナル版-リアルタイムボックスオフィス)を開き、ウェブサイトのソースコードを確認します。fontタグがフォントファイルの位置に対応しています。

ここで誰かが「タグの位置がわかっているなら、Beautiful Soupで検索すればいいだけでは?」と言うかもしれません。しかし、ウェブサイトは実際にはAjaxレンダリングされており、完全なソースコードを要求できません。この見えないが触れるものをどうやって取得するかが鍵となります。私はブラウザ自体にダウンロードさせることにしました。Networkページに入りFrontカテゴリをクリックすると、これらがすべてフォントファイルであり、2秒ごとに新しいフォントファイルが更新され、対応するデータファイルも更新されることがわかります。

これらのフォントファイルはダウンロード可能(驚きですね)であることがわかります。つまり、ソースコードからダウンロードできないとしても、ブラウザからはダウンロードできます。OK、考え方が浮かびました。ブラウザにこれらのフォントファイルを要求させます。リアルタイム更新なので、最新のwoffファイル(フォントファイルの拡張子)を要求して保存するだけです。コードはGitHubを参照してください(自分で完全なコードを見てください)。同様に、最新のデータファイル(xhrタイプ)もダウンロードできます。これで最初の問題は解決し、次はメインイベントのデコードです。

数字の特定

このタイトルは最初は理解しにくいかもしれませんが、これは私を長らく悩ませて問題でした。さて、woffファイルを取得しましたが、ダブルクリックして開くと(コンピュータが高性能であることが前提です)、コンピュータが開ける形式に変換する必要があります。コードは以下の通りです:

# XML形式に変換して解析しやすくする
font = TTFont(font_filename)
font.saveXML(font_filename + '.xml')
xml_file = font_filename + '.xml'
print("フォントファイルのフォーマット変換完了!\n")

ここでfont_filenameはwoffファイルのパスです。OK、ダブルクリックして開きます(開けない?良いことです、あなたのコンピュータに問題があるようです)。その前に、データファイルを見てみましょう。

numの後の2つの文字列がそれぞれ総合ボックスオフィスと分配ボックスオフィスのデータに対応していることがわかります。比較すると、各セミコロン前の文字列が一つの数字に対応していることがわかります。したがって、対応関係を見つけるだけです。つまり、&#xf16b=5、&#xf23f=2、&#xe886=1であれば、それらを組み合わせて521になるということです。この考え方に基づいて、新しいフォントファイルを開いてみましょう。目に飛び込んでくるのは、各文字列に対応する情報です。その中でuniE132=2、uniE83D=3(ここで誤解しないでください、ウェブサイトでは&#xで始まりますが、今はuniで始まっています。これは別の形式で、これらのuniを&#xに変えると、先頭だけが変わって後の4文字は変わらないことがわかります).......

本当にこれだけ簡単なら、私はもっと早く省略記号を書いていたはずです(嘻嘻)。さらに下にスクロールすると、uniE132が多くのxとyに関連するデータに対応していることがわかります。よく考えてみて、中学生の時にこれほど多くのx,yを見たら、まず何を思い浮かべますか?そう、座標です。うん、それも一つの考え方です。試してみましょう。コードは以下の通りです:

import re
import matplotlib.pyplot as plt

# XMLファイルを開きテキスト形式で出力
with open('2a70c44b.woff.xml', 'r') as file:
    xml_text = file.read()

# XMLファイルの暗号化情報を抽出
pattern = re.compile(r'<TTGl.*?name="(uni.*?)".*?xMin=.*?>(.*?)</TTGlyph>', re.S)
image = re.findall(pattern, xml_text)

# 各暗号化情報をループ処理
for item in image:
    # 0が暗号化コンテンツ、1が復号化コンテンツに対応
    print(item[0])
    x = [int(i) for i in re.findall(r'<pt x="(.*?)" y=', item[1])]
    y = [int(i) for i in re.findall(r'y="(.*?)" on=', item[1])]

    # 新しい画像オブジェクトを作成
    plt.figure()
    plt.plot(x, y)
    plt.fill(x, y, color='black')  # グラフの内部を塗りつぶす

    # 現在の画像オブジェクトを閉じて、次のループで新しいオブジェクトを作成できるようにする
    plt.show()
    plt.close()

2a70c44b.woff.xmlフォントファイルを例に取り、各文字列に対応するx,y座標を抽出してグラフ化すると、最終的にこれらの結果が得られます。

見てください、最初のものを除いて、各文字列が一つの数字に対応しており、私たちの推測が裏付けられました。次に、それらを対応付けるだけです。しかし、新しい問題が発生します。コンピュータにこの数字を認識させるにはどうすればよいでしょうか。コンピュータは人間ではありません。人間は123を学びましたが、コンピュータは123がどう書かれるか知りません。ここでまた私たちの想像力を発揮する時が来ました。深層学習でモデルを訓練できないでしょうか?また、既存のOCR技術認識ソフト(tesseract)もあります。まず、これら2つの方法は両方とも実行可能ですが、どちらも少し実行不可能ですなぜなら、面倒だからです。これらの方法を使用する前に、まず訓練する必要がありますさもなければ認識できません。では、他の方法はあるでしょうか?もちろん、さもなければ私はどうしてスクレイピング精神を広められるでしょうか(自慢)。

フォントに対応するxmlファイルを開き、これらの座標点にどんな規則性があるか詳しく分析してみてください。よく見てください。規則性はなさそうです。やはり諦めよう、プロフェッショナル版は私には向いていないようです。ネットでさらに探してみましょう。どうやら普通版のマオヤンもあるようです!このボックスオフィスデータは暗号化されていません。very good、映画の数はプロフェッショナル版ほど多くありませんが、とりあえず我慢して見ることができます。終了。

正直に言うと、以前にデコードしようとした時にも少し諦めそうになりました。さて、元に戻りましょう。大きな問題を分析する際には、それをいくつかの小さな問題に分解してみるのはどうでしょうか。例えば、これらの数字に対応する座標点はすべてタグ内にあり、1対、2対、最大で3対あります。各フォントファイルを調べてみると、数字8だけが3つのを持つことがわかります。OK、数字8のデコードは成功し、次に1対のものはそれぞれ数字:7、3、5、2、1に対応し、2対のものは:0、9、6、4に対応します。完璧に私たちは彼らを3つのグループに分けることができました。最初のグループには8しかなく、つまり8はデコードされました。次に、残りの2つのグループを分析します。

皆さんが私の絵を見たことがあるかどうか(あるはずがない)、数字1は一筆で終わり、数字7はさらに一横を加えます。数字7は本当にインクを無駄遣いしますよね。。。そうそう!インクの問題です!数字1は数字7よりインクが少なく、つまり座標点が少なく、各行が一組の座標に対応します。わあ!考えがまた浮かびました。各数字が何行に対応するかを見つけて、ソートすれば残りの2つのグループのデータをデコードできるかもしれません。やってみましょう。コードは書きませんが、比較的簡単です。これらのフォントファイルをすべて調べてみると、1対のものの行数の大きさはそれぞれ数字3>2>5>7>1であることがわかります。素晴らしい!いきなり5つもデコードできました。次に2対のもの:9>6>0>4、6>9>0>4、what?どうして2種類あるのでしょう?元々6を逆にすると9になりますか?まあ、6と9は個別に研究する必要がありますね。

さっきのグラフ化コードを使って6と9を再度描いてみましょう、今度は座標値も含めて。図のように:

これら2つの図から、6を逆さにすると9になることがわかります。本当に。。。似ています。しかし、どうして私がスクレイピング精神を広められないのでしょうか(自慢)?よく見ると、座標系は同じで、私たちが描画する際、6の小さなおなかは下にあり、9の小さなおなかは上にあります。そして、2つのの2番目がこのおなかを描くためにあります。完璧!2番目の部分のy座標の和を比較するだけです。大きい方が9で、小さい方が6です。コードは以下の通りです。完全なデコードコードは以下の通りです:

import re
import matplotlib.pyplot as plt

# XMLファイルを開きテキスト形式で出力
with open("e3dfe524.woff.xml", 'r') as file:
    xml_text = file.read()

# XMLファイルの暗号化情報を抽出
pattern = re.compile(r'<TTGl.*?name="(uni.*?)".*?xMin=.*?>(.*?)</TTGlyph>', re.S)
image_text = re.findall(pattern, xml_text)
# 暗号化情報を'contour'文字数で分類
two_count = []
one_count = []
key_thr = {}  # 辞書形式でデコード情報を保存

# 各暗号化情報をループ処理
for i, str_text in enumerate(image_text):
    count_text = re.findall(r'<contour>', str_text[1])
    # それぞれのリストに保存
    if len(count_text) == 3:
        key_thr[str_text[0]] = 8
    elif len(count_text) == 2:
        two_count.append(str_text)
    else:
        if i == len(image_text):  # 最後を除外
            break
        else:
            one_count.append(str_text)

# 画像データの行数に基づいてリスト内の値をさらに区別
line_dict = {}
list_two = [9, 6, 0, 4]
for data in two_count:
    line_count = data[1].count('\n') + 1
    line_dict[data[0]] = line_count

# sorted関数で辞書のキーバリューペアをソート、値に基づいて、reverse=Trueを降順に設定
key_two = dict(sorted(line_dict.items(), key=lambda item: item[1], reverse=True))

# ループでデコード結果を対応させる
for key, value in zip(key_two.keys(), list_two):
    key_two[key] = value

# 数字9と6を区別、2番目の部分y座標の大きさを比較
# 辞書のキーリストを取得
keys = list(key_two.keys())
nin_text = ''
six_text = ''
for text in image_text:
    if text[0] == keys[0]:
        nin_text = text
    if text[0] == keys[1]:
        six_text = text

# 数字9の2番目の部分y座標値の合計を取得
sec_nin_text = re.search('</cont.*?tour>(.*?)</contour>', nin_text[1], re.S).group(1)
nin_y = [int(i) for i in re.findall(r'y="(.*?)" on=', sec_nin_text)]
# 同様に数字6を取得
sec_six_text = re.search('</cont.*?tour>(.*?)</contour>', six_text[1], re.S).group(1)
six_y = [int(i) for i in re.findall(r'y="(.*?)" on=', sec_six_text)]

# 2番目の部分y座標の値を比較
if sum(nin_y) < sum(six_y):
    # 9, 6を入れ替える
    keys = list(key_two.keys())[:2]  # 辞書の最初の2つのキーを取得
    key_two[keys[0]], key_two[keys[1]] = 6, 9

# 同様にデコード
line_dict = {}
list_one = [3, 2, 5, 7, 1]
for data in one_count:
    line_count = data[1].count('\n') + 1
    line_dict[data[0]] = line_count
key_one = dict(sorted(line_dict.items(), key=lambda item: item[1], reverse=True))
for key, value in zip(key_one.keys(), list_one):
    key_one[key] = value

# デコード辞書にマージ
key_dict = {**key_thr, **key_two, **key_one}
for key, value in key_dict.items():
    print(f'{key}: {value}')

これで、すべての数字がデコードされ、辞書にまとめられました。後はデータファイルと一つずつ対応させるだけです。これは結果の図で、csvファイルにインポートされています:

まとめ

あっという間に!私の長々とした説明は終わりました。皆さんは理解できましたか?理解できなくても問題ありません。番号1314521にダイヤルすれば、私があなたの元に駆けつけて助けてくれます。

タグ: スクレイピング ウェブクローリング フォントデコード Python Selenium

5月14日 06:15 投稿