BeautifulSoupでXMLをパース(解析)加工する – Python



XMLとは?

XMLとは、文書やデータの意味や構造を記述するためのマークアップ言語の一つ。 マークアップ言語とは、「タグ」と呼ばれる特定の文字列で地の文に情報の意味や構造、装飾などを埋め込んでいく言語のことで、XMLはユーザが独自のタグを指定できることから、マークアップ言語を作成するためのメタ言語とも言われる。

https://www.graffe.jp/blog/2911/

一帯何に使うマークアップなのか?という方もいらっしゃるかもしれませんが、よくあるのはAPIでのレスポンスです。

割とよく見る馴染み深いレスポンス形式といえば「JSON」という方も少なくないと思います。

最近ではJSONに押され気味ながらも、まだまだXMLも健在です。

PythonでのXML解析にBeautifulSoupを使ってみる

BeautifulSoupはHTMLのパースというイメージがあるかと思いますが、実はXMLの解析にも使えます。

サンプルはAmazonMWSへのアクセス時にレスポンスとして帰ってくるXMLを使います。

<?xml version="1.0"?>
<GetProductCategoriesForASINResponse xmlns="http://mws.amazonservices.com/schema/Products/2011-10-01">
  <GetProductCategoriesForASINResult>
    <Self>
      <ProductCategoryId>2509478051</ProductCategoryId>
      <ProductCategoryName>FOSSIL(フォッシル)</ProductCategoryName>
      <Parent>
        <ProductCategoryId>2509471051</ProductCategoryId>
        <ProductCategoryName>F</ProductCategoryName>
        <Parent>
          <ProductCategoryId>2509395051</ProductCategoryId>
          <ProductCategoryName>Brand Stores</ProductCategoryName>
          <Parent>
            <ProductCategoryId>333346011</ProductCategoryId>
            <ProductCategoryName>COOP</ProductCategoryName>
            <Parent>
              <ProductCategoryId>333334011</ProductCategoryId>
              <ProductCategoryName>Stores</ProductCategoryName>
              <Parent>
                <ProductCategoryId>324025011</ProductCategoryId>
                <ProductCategoryName>腕時計</ProductCategoryName>
              </Parent>
            </Parent>
          </Parent>
        </Parent>
      </Parent>
    </Self>
    <Self>
      <ProductCategoryId>333009011</ProductCategoryId>
      <ProductCategoryName>メンズ腕時計</ProductCategoryName>
      <Parent>
        <ProductCategoryId>331952011</ProductCategoryId>
        <ProductCategoryName>カテゴリー別</ProductCategoryName>
        <Parent>
          <ProductCategoryId>324025011</ProductCategoryId>
          <ProductCategoryName>腕時計</ProductCategoryName>
        </Parent>
      </Parent>
    </Self>
    <Self>
      <ProductCategoryId>2131417051</ProductCategoryId>
      <ProductCategoryName>メンズ</ProductCategoryName>
      <Parent>
        <ProductCategoryId>361245011</ProductCategoryId>
        <ProductCategoryName>カテゴリー別</ProductCategoryName>
        <Parent>
          <ProductCategoryId>352484011</ProductCategoryId>
          <ProductCategoryName>服&ファッション小物</ProductCategoryName>
        </Parent>
      </Parent>
    </Self>
  </GetProductCategoriesForASINResult>
  <ResponseMetadata>
    <RequestId>*******-****-****-****-**********</RequestId>
  </ResponseMetadata>
</GetProductCategoriesForASINResponse>

MWS-GetProductCategoriesForASINで取得したXMLです。

どういったソースなのかというと、商品が属するカテゴリノードを示したXMLです。Amazonでは一商品一カテゴリという分類ではなく、複数のカテゴリ分類に属している可能性があり、上記は一商品分のソースで、3つのカテゴリ分類に属しています。

BeautifulSoupにXMLを渡す

まずは解析準備です。

MWSアクセスのコードは省略します。res.contentがXMLの文字列本体だと思ってください。

BeautifulSoupオブジェクトの作成

import requests
from bs4 import BeautifulSoup as bs4

res = requests.post(MWS)
#詳しくは省略、上のXMLが返ってくるものと思ってください

soup = bs4(res.content,'lxml-xml')

上記記事でHTMLをパースした時とsoupオブジェクトの作り方が若干違うのがわかるかと思いますが、bs4のパーサに'lxml-xml'を指定してます。

僕は基本的に'lxml'パーサを使いますが、それのXML版です。

’lxml’のままでもパースできなくはないですが、キャメルバックになったタグが使えなくなったり、色々弊害が出てきたりするので、こちらをオススメします。

BeautifulSoupでXMLをパースする

基本的にHTMLのパースと要領は同じですが、復習を兼ねて、逆引きリファレンス的にまとめてみます。

上記コードの続きからです。

XMLのタグ名で検索し、最初に見つかる要素へアクセス

soup.Self
soup.find('Self')
#どちらも同じ

上記はどちらも同じ挙動で、「最初の<Self>タグ」にアクセスします。

Outは以下の通り。

Out[1]: 
<Self>
<ProductCategoryId>2509478051</ProductCategoryId>
<ProductCategoryName>FOSSIL(フォッシル)</ProductCategoryName>
<Parent>
<ProductCategoryId>2509471051</ProductCategoryId>
<ProductCategoryName>F</ProductCategoryName>
<Parent>
<ProductCategoryId>2509395051</ProductCategoryId>
<ProductCategoryName>Brand Stores</ProductCategoryName>
<Parent>
<ProductCategoryId>333346011</ProductCategoryId>
<ProductCategoryName>COOP</ProductCategoryName>
<Parent>
<ProductCategoryId>333334011</ProductCategoryId>
<ProductCategoryName>Stores</ProductCategoryName>
<Parent>
<ProductCategoryId>324025011</ProductCategoryId>
<ProductCategoryName>腕時計</ProductCategoryName>
</Parent>
</Parent>
</Parent>
</Parent>
</Parent>
</Self>

XMLのタグ名で検索し、一致した全ての要素のリストを返す

soup('Self')
soup.find_all('Self')
#どちらも同じ

上記はどちらも同じ挙動で、「全ての<Self>タグ」にアクセスし、リスト形式で返却します。

Outは以下の通り。

Out[2]:
[<Self>
 <ProductCategoryId>2509478051</ProductCategoryId>
 <ProductCategoryName>FOSSIL(フォッシル)</ProductCategoryName>
 <Parent>
 <ProductCategoryId>2509471051</ProductCategoryId>
 <ProductCategoryName>F</ProductCategoryName>
 <Parent>
 <ProductCategoryId>2509395051</ProductCategoryId>
 <ProductCategoryName>Brand Stores</ProductCategoryName>
 <Parent>
 <ProductCategoryId>333346011</ProductCategoryId>
 <ProductCategoryName>COOP</ProductCategoryName>
 <Parent>
 <ProductCategoryId>333334011</ProductCategoryId>
 <ProductCategoryName>Stores</ProductCategoryName>
 <Parent>
 <ProductCategoryId>324025011</ProductCategoryId>
 <ProductCategoryName>腕時計</ProductCategoryName>
 </Parent>
 </Parent>
 </Parent>
 </Parent>
 </Parent>
 </Self>, <Self>
 <ProductCategoryId>333009011</ProductCategoryId>
 <ProductCategoryName>メンズ腕時計</ProductCategoryName>
 <Parent>
 <ProductCategoryId>331952011</ProductCategoryId>
 <ProductCategoryName>カテゴリー別</ProductCategoryName>
 <Parent>
 <ProductCategoryId>324025011</ProductCategoryId>
 <ProductCategoryName>腕時計</ProductCategoryName>
 </Parent>
 </Parent>
 </Self>, <Self>
 <ProductCategoryId>2131417051</ProductCategoryId>
 <ProductCategoryName>メンズ</ProductCategoryName>
 <Parent>
 <ProductCategoryId>361245011</ProductCategoryId>
 <ProductCategoryName>カテゴリー別</ProductCategoryName>
 <Parent>
 <ProductCategoryId>352484011</ProductCategoryId>
 <ProductCategoryName>服&ファッション小物</ProductCategoryName>
 </Parent>
 </Parent>
 </Self>]

リスト形式ですので、返却されたオブジェクトを操作する場合はリストと同じようにインデックスアクセスして操作します。

例えば最後の<Self>タグにアクセスしたい場合は以下のように。

Self = soup('Self')
Self[-1]

Out[3]: 
<Self>
<ProductCategoryId>2131417051</ProductCategoryId>
<ProductCategoryName>メンズ</ProductCategoryName>
<Parent>
<ProductCategoryId>361245011</ProductCategoryId>
<ProductCategoryName>カテゴリー別</ProductCategoryName>
<Parent>
<ProductCategoryId>352484011</ProductCategoryId>
<ProductCategoryName>服&ファッション小物</ProductCategoryName>
</Parent>
</Parent>
</Self>

[-1]は最後の要素、[-2]なら最後から二番目の要素を返します。

子要素をリスト形式で返却

要素直下の各子要素をリスト形式で返却するメソッドもあります。

soup.GetProductCategoriesForASINResult.contents
#直下子要素をリスト形式で返却

上記コードはsoup('Self')とした時と全く同じ結果が返ってきます。

XMLのテキストで検索する

soup.find(text='メンズ')
Out[4]: 'メンズ'

テキストがわかっている時そのテキストを持つ要素にアクセスしたい場合は上記の通り。

ただ、そのテキスト本体にアクセスしてしまうので、基本的にこれだけでは何してるのか一切意味が分かりません(笑)

普通は、ここから更に、別の要素に展開させて行く使い方をします。

#テキスト一致の親要素(タグ)
soup.find(text='メンズ').parent
Out[5]: <ProductCategoryName>メンズ</ProductCategoryName>
#テキスト一致の親要素(タグ)の「タグ名」
soup.find(text='メンズ').parent.name
Out[6]: 'ProductCategoryName'
#テキスト一致の親要素(タグ)から更に直前の兄弟要素
soup.find(text='腕時計').parent.previous_sibling
Out[7]: <ProductCategoryId>324025011</ProductCategoryId>
#テキスト一致の親要素(タグ)から更に直後の兄弟要素
soup.find(text='352484011').parent.next_sibling
Out[8]: <ProductCategoryName>服&ファッション小物</ProductCategoryName>

実践的にBeautifulSoupでXMLを加工してみる

大体使い方もわかったところで、少し実践的に加工してみます。

ProductCategoryIdと、ProductCategoryNameのテキストを一組2要素のタプルとして、階層順にリストとして保存。更に3つの各カテゴリ属性全てを同様に加工し別リストとして保存してみます。

複雑そうに見えますが、慣れればこの加工、2行で出来ます。

Self = soup('Self ')
item_nodes = [[(tag.text,tag.next_sibling.text) for tag in item('ProductCategoryId')] for item in Self]

1行目は各カテゴリ分類をリスト形式で「Self」という変数に保存。

2行目のリスト内包表記は 「Self」に格納された各要素にアクセス。
各要素は「item」要素に格納されます。

「item('ProductCategoryId')」としてProductCategoryIdのタグ全てを取得していますが、そのままではタグごと取得してしまい、テキストを抜き出すことが出来ないため、更にリスト内包表記を使ってタグ内のtextにアクセスします。

ProductCategoryNameは必ずProductCategoryIdと一対の兄弟要素となっているため、Idタグにさえアクセスできれば「.next_sibling」で取得可能になります。

item_nodesに結果が保存されています。

item_nodes
Out[9]: 
[[('2509478051', 'FOSSIL(フォッシル)'),
  ('2509471051', 'F'),
  ('2509395051', 'BrandStores'),
  ('333346011', 'COOP'),
  ('333334011', 'Stores'),
  ('324025011', '腕時計')],
 [('333009011', 'メンズ腕時計'), ('331952011', 'カテゴリー別'), ('324025011', '腕時計')],
 [('2131417051', 'メンズ'), ('361245011', 'カテゴリー別'), ('352484011', '服&ファッション小物')]]

BeautifulSoupによるXMLパース(解析) - 総括

HTMLのパースと感覚はほとんど一緒ですね。

'lxml-xml'を使うと、要素へのアトリビュートアクセスの時にキャメルバックのタグもそのままアクセスできますが、’lxml’だと小文字でしかアクセスできなかったりと、細かい違いはところどころに存在します。

今回はパース(解析)の方法を紹介しましたが、実はBeautifulSoupはXMLの作成もできたりします。

こちらの記事ではYahooAPIを題材にBeautifulSoupでゼロから作成したXMLドキュメントでリクエストを行う方法を紹介していますので、ぜひ参考にしてみてください。

created by Rinker
¥755 (2022/07/01 16:28:44時点 Amazon調べ-詳細)