web-dev-qa-db-ja.com

BashでXMLを解析する方法は?

理想的には、私ができることは:

cat xhtmlfile.xhtml |
getElementViaXPath --path='/html/head/title' |
sed -e 's%(^<title>|</title>$)%%g' > titleOfXHTMLPage.txt
123
asdfasdfasdf

これは実際には Yuzem's answerの説明にすぎませんが、他の誰かにこのような編集を行うべきだとは思わず、コメントではフォーマットが許可されないため、...

rdom () { local IFS=\> ; read -d \< E C ;}

「rdom」ではなく「read_dom」と呼び、少しスペースを空けて、より長い変数を使用します。

read_dom () {
    local IFS=\>
    read -d \< ENTITY CONTENT
}

さて、read_domという関数を定義しています。最初の行は、IFS(入力フィールド区切り記号)をこの関数に対してローカルにし、>に変更します。つまり、スペース、タブ、または改行で自動的に分割されるのではなく、データを読み取ると、「>」で分割されます。次の行は、stdinから入力を読み取ることを示しており、改行で停止する代わりに、 '<'文字(デリミネーターフラグの-d)が表示されたら停止します。読み取られたものは、IFSを使用して分割され、変数ENTITYおよびCONTENTに割り当てられます。したがって、次のことを行ってください。

<tag>value</tag>

read_domの最初の呼び出しは、空の文字列を取得します(「<」が最初の文字であるため)。 「>」文字がないため、これはIFSによって単に ''に分割されます。読み取りは、両方の変数に空の文字列を割り当てます。 2番目の呼び出しは、文字列 'tag> value'を取得します。それはIFSによって2つのフィールド「タグ」と「値」に分割されます。読み取りは、次のような変数を割り当てます:ENTITY=tagおよびCONTENT=value。 3番目の呼び出しは、文字列「/ tag>」を取得します。これはIFSによって2つのフィールド「/ tag」と「」に分割されます。読み取りは、次のような変数を割り当てます:ENTITY=/tagおよびCONTENT=。ファイルの終わりに達したため、4番目の呼び出しはゼロ以外のステータスを返します。

現在、彼のwhileループは上記に一致するように少しクリーンアップしています。

while read_dom; do
    if [[ $ENTITY = "title" ]]; then
        echo $CONTENT
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt

最初の行は、「read_dom関数がゼロステータスを返している間に、次のことを行う」とだけ言っています。 2行目では、今見たエンティティが「タイトル」かどうかを確認します。次の行は、タグの内容をエコーし​​ます。 4行が終了します。タイトルエンティティではない場合、ループは6行目で繰り返されます。 「xhtmlfile.xhtml」を標準入力(read_dom関数の場合)にリダイレクトし、標準出力を「titleOfXHTMLPage.txt」(ループの早い段階からのエコー)にリダイレクトします。

ここで、input.xmlについて(S3でバケットをリストすることで得られるものと同様に)以下を指定します。

<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>sth-items</Name>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>[email protected]</Key>
    <LastModified>2011-07-25T22:23:04.000Z</LastModified>
    <ETag>&quot;0032a28286680abee71aed5d059c6a09&quot;</ETag>
    <Size>1785</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

そして次のループ:

while read_dom; do
    echo "$ENTITY => $CONTENT"
done < input.xml

あなたが得る必要があります:

 => 
ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/" => 
Name => sth-items
/Name => 
IsTruncated => false
/IsTruncated => 
Contents => 
Key => [email protected]
/Key => 
LastModified => 2011-07-25T22:23:04.000Z
/LastModified => 
ETag => &quot;0032a28286680abee71aed5d059c6a09&quot;
/ETag => 
Size => 1785
/Size => 
StorageClass => STANDARD
/StorageClass => 
/Contents => 

したがって、Yuzemのようなwhileループを記述した場合:

while read_dom; do
    if [[ $ENTITY = "Key" ]] ; then
        echo $CONTENT
    fi
done < input.xml

S3バケット内のすべてのファイルのリストを取得します。

EDIT何らかの理由でlocal IFS=\>が機能せず、グローバルに設定する場合は、次のように関数の最後でリセットする必要があります。

read_dom () {
    ORIGINAL_IFS=$IFS
    IFS=\>
    read -d \< ENTITY CONTENT
    IFS=$ORIGINAL_IFS
}

そうしないと、スクリプトの後半で行う行分割が台無しになります。

EDIT 2属性の名前/値のペアを分割するには、次のようにread_dom()を拡張できます。

read_dom () {
    local IFS=\>
    read -d \< ENTITY CONTENT
    local ret=$?
    TAG_NAME=${ENTITY%% *}
    ATTRIBUTES=${ENTITY#* }
    return $ret
}

次に、次のように必要なデータを解析して取得する関数を作成します。

parse_dom () {
    if [[ $TAG_NAME = "foo" ]] ; then
        eval local $ATTRIBUTES
        echo "foo size is: $size"
    Elif [[ $TAG_NAME = "bar" ]] ; then
        eval local $ATTRIBUTES
        echo "bar type is: $type"
    fi
}

その後、read_domparse_domを呼び出している間に:

while read_dom; do
    parse_dom
done

次に、次のマークアップの例を示します。

<example>
  <bar size="bar_size" type="metal">bars content</bar>
  <foo size="1789" type="unknown">foos content</foo>
</example>

次の出力が得られます。

$ cat example.xml | ./bash_xml.sh 
bar type is: metal
foo size is: 1789

EDIT 3別の ser FreeBSDで問題が発生したと述べ、読み取りから終了ステータスを保存し、 read_domの終わり:

read_dom () {
    local IFS=\>
    read -d \< ENTITY CONTENT
    local RET=$?
    TAG_NAME=${ENTITY%% *}
    ATTRIBUTES=${ENTITY#* }
    return $RET
}

それがうまくいかない理由がわからない

138
chad

これは、bashのみを使用して非常に簡単に実行できます。この関数を追加するだけです:

rdom () { local IFS=\> ; read -d \< E C ;}

これで、readのようにrdomを使用できますが、htmlドキュメントに使用できます。呼び出されると、rdomは要素を変数Eに、コンテンツを変数Cに割り当てます。

たとえば、やりたいことをするには:

while rdom; do
    if [[ $E = title ]]; then
        echo $C
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt
59
Yuzem

シェルスクリプトから呼び出すことができるコマンドラインツールには、次のものがあります。

  • 4xpath -Pythonのコマンドラインラッパー 4Suite パッケージ
  • XMLStarlet
  • xpath-PerlのXPathライブラリのコマンドラインラッパー
  • Xidel -ファイルだけでなくURLでも動作します。 JSONでも動作します

また、xmllintとxsltprocを小さなXSL変換スクリプトとともに使用して、コマンドラインまたはシェルスクリプトでXML処理を行います。

53
Nat

Xpathユーティリティを使用できます。 Perl XML-XPathパッケージとともにインストールされます。

使用法:

/usr/bin/xpath [filename] query

または XMLStarlet 。 opensuseにインストールするには、次を使用します。

Sudo zypper install xmlstarlet

または、他のプラットフォームでcnf xmlを試してください。

21
Grisha

これで十分です...

xpath xhtmlfile.xhtml '/html/head/title/text()' > titleOfXHTMLPage.txt
8
teknopaul

XMLを行に変換する http://www.ofb.net/~egnor/xml2/ からXML2を確認してください。指向のフォーマット。

5
simon04

チャドの答えから始めて、コメントを適切に処理し、わずか2つの小さな機能(2つ以上のすべてを混在させることができます)でUMLを解析する完全な作業ソリューションを以下に示します。 chadの1つがまったく機能しなかったとは言いませんが、不適切にフォーマットされたXMLファイルでは問題が多すぎます。したがって、コメントや置き忘れたスペース/ CR/TAB /などを処理するにはもう少し注意が必要です。

この回答の目的は、Perlやpythonなどを使用した複雑なツールなしで、UMLを解析する必要がある人にすぐに使えるbash関数を提供することです。私に関しては、cpanも、私が取り組んでいる古い本番OS用のPerlモジュールもインストールできず、pythonは利用できません。

まず、この投稿で使用されるUML単語の定義:

<!-- comment... -->
<tag attribute="value">content...</tag>

編集:更新された関数、ハンドル:

  • Websphere xml(xmiおよびxmlns属性)
  • 256色の互換性のある端末が必要です
  • 24階調のグレー
  • iBM AIX bash 3.2.16(1)に追加された互換性

関数は、最初はxml_readによって再帰的に呼び出されるxml_read_domです:

xml_read_dom() {
# https://stackoverflow.com/questions/893585/how-to-parse-xml-in-bash
local ENTITY IFS=\>
if $ITSACOMMENT; then
  read -d \< COMMENTS
  COMMENTS="$(rtrim "${COMMENTS}")"
  return 0
else
  read -d \< ENTITY CONTENT
  CR=$?
  [ "x${ENTITY:0:1}x" == "x/x" ] && return 0
  TAG_NAME=${ENTITY%%[[:space:]]*}
  [ "x${TAG_NAME}x" == "x?xmlx" ] && TAG_NAME=xml
  TAG_NAME=${TAG_NAME%%:*}
  ATTRIBUTES=${ENTITY#*[[:space:]]}
  ATTRIBUTES="${ATTRIBUTES//xmi:/}"
  ATTRIBUTES="${ATTRIBUTES//xmlns:/}"
fi

# when comments sticks to !-- :
[ "x${TAG_NAME:0:3}x" == "x!--x" ] && COMMENTS="${TAG_NAME:3} ${ATTRIBUTES}" && ITSACOMMENT=true && return 0

# http://tldp.org/LDP/abs/html/string-manipulation.html
# INFO: oh wait it doesn't work on IBM AIX bash 3.2.16(1):
# [ "x${ATTRIBUTES:(-1):1}x" == "x/x" -o "x${ATTRIBUTES:(-1):1}x" == "x?x" ] && ATTRIBUTES="${ATTRIBUTES:0:(-1)}"
[ "x${ATTRIBUTES:${#ATTRIBUTES} -1:1}x" == "x/x" -o "x${ATTRIBUTES:${#ATTRIBUTES} -1:1}x" == "x?x" ] && ATTRIBUTES="${ATTRIBUTES:0:${#ATTRIBUTES} -1}"
return $CR
}

そして2つ目:

xml_read() {
# https://stackoverflow.com/questions/893585/how-to-parse-xml-in-bash
ITSACOMMENT=false
local MULTIPLE_ATTR LIGHT FORCE_PRINT XAPPLY XCOMMAND XATTRIBUTE GETCONTENT fileXml tag attributes attribute tag2print TAGPRINTED attribute2print XAPPLIED_COLOR PROSTPROCESS USAGE
local TMP LOG LOGG
LIGHT=false
FORCE_PRINT=false
XAPPLY=false
MULTIPLE_ATTR=false
XAPPLIED_COLOR=g
TAGPRINTED=false
GETCONTENT=false
PROSTPROCESS=cat
Debug=${Debug:-false}
TMP=/tmp/xml_read.$RANDOM
USAGE="${C}${FUNCNAME}${c} [-cdlp] [-x command <-a attribute>] <file.xml> [tag | \"any\"] [attributes .. | \"content\"]
${nn[2]}  -c = NOCOLOR${END}
${nn[2]}  -d = Debug${END}
${nn[2]}  -l = LIGHT (no \"attribute=\" printed)${END}
${nn[2]}  -p = FORCE PRINT (when no attributes given)${END}
${nn[2]}  -x = apply a command on an attribute and print the result instead of the former value, in green color${END}
${nn[1]}  (no attribute given will load their values into your Shell; use '-p' to print them as well)${END}"

! (($#)) && echo2 "$USAGE" && return 99
(( $# < 2 )) && ERROR nbaram 2 0 && return 99
# getopts:
while getopts :cdlpx:a: _OPT 2>/dev/null
do
{
  case ${_OPT} in
    c) PROSTPROCESS="${DECOLORIZE}" ;;
    d) local Debug=true ;;
    l) LIGHT=true; XAPPLIED_COLOR=END ;;
    p) FORCE_PRINT=true ;;
    x) XAPPLY=true; XCOMMAND="${OPTARG}" ;;
    a) XATTRIBUTE="${OPTARG}" ;;
    *) _NOARGS="${_NOARGS}${_NOARGS+, }-${OPTARG}" ;;
  esac
}
done
shift $((OPTIND - 1))
unset _OPT OPTARG OPTIND
[ "X${_NOARGS}" != "X" ] && ERROR param "${_NOARGS}" 0

fileXml=$1
tag=$2
(( $# > 2 )) && shift 2 && attributes=$*
(( $# > 1 )) && MULTIPLE_ATTR=true

[ -d "${fileXml}" -o ! -s "${fileXml}" ] && ERROR empty "${fileXml}" 0 && return 1
$XAPPLY && $MULTIPLE_ATTR && [ -z "${XATTRIBUTE}" ] && ERROR param "-x command " 0 && return 2
# nb attributes == 1 because $MULTIPLE_ATTR is false
[ "${attributes}" == "content" ] && GETCONTENT=true

while xml_read_dom; do
  # (( CR != 0 )) && break
  (( PIPESTATUS[1] != 0 )) && break

  if $ITSACOMMENT; then
    # oh wait it doesn't work on IBM AIX bash 3.2.16(1):
    # if [ "x${COMMENTS:(-2):2}x" == "x--x" ]; then COMMENTS="${COMMENTS:0:(-2)}" && ITSACOMMENT=false
    # Elif [ "x${COMMENTS:(-3):3}x" == "x-->x" ]; then COMMENTS="${COMMENTS:0:(-3)}" && ITSACOMMENT=false
    if [ "x${COMMENTS:${#COMMENTS} - 2:2}x" == "x--x" ]; then COMMENTS="${COMMENTS:0:${#COMMENTS} - 2}" && ITSACOMMENT=false
    Elif [ "x${COMMENTS:${#COMMENTS} - 3:3}x" == "x-->x" ]; then COMMENTS="${COMMENTS:0:${#COMMENTS} - 3}" && ITSACOMMENT=false
    fi
    $Debug && echo2 "${N}${COMMENTS}${END}"
  Elif test "${TAG_NAME}"; then
    if [ "x${TAG_NAME}x" == "x${tag}x" -o "x${tag}x" == "xanyx" ]; then
      if $GETCONTENT; then
        CONTENT="$(trim "${CONTENT}")"
        test ${CONTENT} && echo "${CONTENT}"
      else
        # eval local $ATTRIBUTES => eval test "\"\$${attribute}\"" will be true for matching attributes
        eval local $ATTRIBUTES
        $Debug && (echo2 "${m}${TAG_NAME}: ${M}$ATTRIBUTES${END}"; test ${CONTENT} && echo2 "${m}CONTENT=${M}$CONTENT${END}")
        if test "${attributes}"; then
          if $MULTIPLE_ATTR; then
            # we don't print "tag: attr=x ..." for a tag passed as argument: it's usefull only for "any" tags so then we print the matching tags found
            ! $LIGHT && [ "x${tag}x" == "xanyx" ] && tag2print="${g6}${TAG_NAME}: "
            for attribute in ${attributes}; do
              ! $LIGHT && attribute2print="${g10}${attribute}${g6}=${g14}"
              if eval test "\"\$${attribute}\""; then
                test "${tag2print}" && ${print} "${tag2print}"
                TAGPRINTED=true; unset tag2print
                if [ "$XAPPLY" == "true" -a "${attribute}" == "${XATTRIBUTE}" ]; then
                  eval ${print} "%s%s\ " "\${attribute2print}" "\${${XAPPLIED_COLOR}}\"\$(\$XCOMMAND \$${attribute})\"\${END}" && eval unset ${attribute}
                else
                  eval ${print} "%s%s\ " "\${attribute2print}" "\"\$${attribute}\"" && eval unset ${attribute}
                fi
              fi
            done
            # this trick prints a CR only if attributes have been printed durint the loop:
            $TAGPRINTED && ${print} "\n" && TAGPRINTED=false
          else
            if eval test "\"\$${attributes}\""; then
              if $XAPPLY; then
                eval echo "\${g}\$(\$XCOMMAND \$${attributes})" && eval unset ${attributes}
              else
                eval echo "\$${attributes}" && eval unset ${attributes}
              fi
            fi
          fi
        else
          echo eval $ATTRIBUTES >>$TMP
        fi
      fi
    fi
  fi
  unset CR TAG_NAME ATTRIBUTES CONTENT COMMENTS
done < "${fileXml}" | ${PROSTPROCESS}
# http://mywiki.wooledge.org/BashFAQ/024
# INFO: I set variables in a "while loop" that's in a pipeline. Why do they disappear? workaround:
if [ -s "$TMP" ]; then
  $FORCE_PRINT && ! $LIGHT && cat $TMP
  # $FORCE_PRINT && $LIGHT && Perl -pe 's/[[:space:]].*?=/ /g' $TMP
  $FORCE_PRINT && $LIGHT && sed -r 's/[^\"]*([\"][^\"]*[\"][,]?)[^\"]*/\1 /g' $TMP
  . $TMP
  rm -f $TMP
fi
unset ITSACOMMENT
}

最後に、rtrim、trim、echo2(stderrへ)機能:

rtrim() {
local var=$@
var="${var%"${var##*[![:space:]]}"}"   # remove trailing whitespace characters
echo -n "$var"
}
trim() {
local var=$@
var="${var#"${var%%[![:space:]]*}"}"   # remove leading whitespace characters
var="${var%"${var##*[![:space:]]}"}"   # remove trailing whitespace characters
echo -n "$var"
}
echo2() { echo -e "$@" 1>&2; }

色付け:

ああ、あなたは最初にいくつかのきちんとした色付け動的変数を定義し、エクスポートする必要があります:

set -a
TERM=xterm-256color
case ${UNAME} in
AIX|SunOS)
  M=$(${print} '\033[1;35m')
  m=$(${print} '\033[0;35m')
  END=$(${print} '\033[0m')
;;
*)
  m=$(tput setaf 5)
  M=$(tput setaf 13)
  # END=$(tput sgr0)          # issue on Linux: it can produces ^[(B instead of ^[[0m, more likely when using screenrc
  END=$(${print} '\033[0m')
;;
esac
# 24 shades of grey:
for i in $(seq 0 23); do eval g$i="$(${print} \"\\033\[38\;5\;$((232 + i))m\")" ; done
# another way of having an array of 5 shades of grey:
declare -a colorNums=(238 240 243 248 254)
for num in 0 1 2 3 4; do nn[$num]=$(${print} "\033[38;5;${colorNums[$num]}m"); NN[$num]=$(${print} "\033[48;5;${colorNums[$num]}m"); done
# piped decolorization:
DECOLORIZE='eval sed "s,${END}\[[0-9;]*[m|K],,g"'

すべてをロードする方法:

関数を作成し、FPATH(ksh)またはFPATHのエミュレーション(bash)でロードする方法を知っている

そうでない場合は、コマンドラインですべてをコピーして貼り付けてください。

仕組み:

xml_read [-cdlp] [-x command <-a attribute>] <file.xml> [tag | "any"] [attributes .. | "content"]
  -c = NOCOLOR
  -d = Debug
  -l = LIGHT (no \"attribute=\" printed)
  -p = FORCE PRINT (when no attributes given)
  -x = apply a command on an attribute and print the result instead of the former value, in green color
  (no attribute given will load their values into your Shell as $ATTRIBUTE=value; use '-p' to print them as well)

xml_read server.xml title content     # print content between <title></title>
xml_read server.xml Connector port    # print all port values from Connector tags
xml_read server.xml any port          # print all port values from any tags

デバッグモード(-d)では、コメントと解析された属性がstderrに出力されます

5
scavenger

別のコマンドラインツールは、新しい Xidel です。既に述べたxpath/xmlstarletとは異なり、XPath 2とXQueryもサポートしています。

タイトルは次のように読むことができます:

xidel xhtmlfile.xhtml -e /html/head/title > titleOfXHTMLPage.txt

また、複数の変数をbashにエクスポートするクールな機能もあります。例えば

eval $(xidel xhtmlfile.xhtml -e 'title := //title, imgcount := count(//img)' --output-format bash )

$titleをタイトルに設定し、$imgcountをファイル内の画像の数に設定します。これは、bashで直接解析するのと同じくらい柔軟でなければなりません。

4
BeniBela

純粋なシェルXML解析ツールについては知りません。そのため、おそらく他の言語で書かれたツールが必要になります。

私のXML :: Twig Perlモジュールには、xml_grepというツールが付属しています。xml_grep -t '/html/head/title' xhtmlfile.xhtml > titleOfXHTMLPage.txt-tオプションを使用すると、xmlではなくテキストとして結果が得られます)

4
mirod

必要なことを実行できる既製のコンソールユーティリティはかなりありますが、Pythonなどの汎用プログラミング言語で数行のコードを記述するのに時間がかかることはおそらくありません。簡単に拡張してニーズに適応できます。

解析に lxml を使用するpythonスクリプトは次のとおりです。最初のパラメーターとしてファイルまたはURLの名前を取り、2番目のパラメーターとしてXPath式を使用して、指定された式に一致する文字列/ノード。

例1

#!/usr/bin/env python
import sys
from lxml import etree

tree = etree.parse(sys.argv[1])
xpath_expression = sys.argv[2]

#  a hack allowing to access the
#  default namespace (if defined) via the 'p:' prefix    
#  E.g. given a default namespaces such as 'xmlns="http://maven.Apache.org/POM/4.0.0"'
#  an XPath of '//p:module' will return all the 'module' nodes
ns = tree.getroot().nsmap
if ns.keys() and None in ns:
    ns['p'] = ns.pop(None)
#   end of hack    

for e in tree.xpath(xpath_expression, namespaces=ns):
    if isinstance(e, str):
        print(e)
    else:
        print(e.text and e.text.strip() or etree.tostring(e, pretty_print=True))

lxmlpip install lxmlでインストールできます。 UbuntuではSudo apt install python-lxmlを使用できます。

使用法

python xpath.py myfile.xml "//mynode"

lxmlは、入力としてURLも受け入れます。

python xpath.py http://www.feedforall.com/sample.xml "//link"

:XMLにプレフィックスのないデフォルトのネームスペース(xmlns=http://abc...など)がある場合は、pプレフィックス( 'hack'で提供)を使用する必要がありますあなたの表現で、例えば//p:moduleは、pom.xmlファイルからモジュールを取得します。 pプレフィックスが既にXMLにマップされている場合、別のプレフィックスを使用するようにスクリプトを変更する必要があります。


例2

Apache mavenファイルからモジュール名を抽出するという狭い目的に役立つ1回限りのスクリプト。ノード名(module)にデフォルトの名前空間{http://maven.Apache.org/POM/4.0.0}が接頭辞として付加されていることに注意してください。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.Apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.Apache.org/POM/4.0.0 http://maven.Apache.org/xsd/maven-4.0.0.xsd">
    <modules>
        <module>cherries</module>
        <module>bananas</module>
        <module>pears</module>
    </modules>
</project>

module_extractor.py

from lxml import etree
for _, e in etree.iterparse(open("pom.xml"), tag="{http://maven.Apache.org/POM/4.0.0}module"):
    print(e.text)
2
ccpizza

XMLファイル内のファイルパスのLinux形式とWindows形式の間の翻訳に関するいくつかの調査の後、私は以下に関する興味深いチュートリアルとソリューションを見つけました。

2
user485380

さて、xpathユーティリティを使用できます。 PerlのXML :: Xpathに含まれていると思います。

2
alamar

Yuzemのメソッドは、rdom関数内の<および>記号の順序と変数の割り当てを逆にすることで改善できます。

rdom () { local IFS=\> ; read -d \< E C ;}

になる:

rdom () { local IFS=\< ; read -d \> C E ;}

解析がこのように行われない場合、XMLファイルの最後のタグに到達することはありません。 whileループの最後に別のXMLファイルを出力する場合、これは問題になる可能性があります。

0
michaelmeyer

これは、XML属性が必要な場合に機能します。

$ cat alfa.xml
<video server="asdf.com" stream="H264_400.mp4" cdn="limelight"/>

$ sed 's.[^ ]*..;s./>..' alfa.xml > alfa.sh

$ . ./alfa.sh

$ echo "$stream"
H264_400.mp4
0
Steven Penny