以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/09/23/162351より取得しました。


PythonでProtocol Buffersを扱う

これは、なにをしたくて書いたもの?

PythonでProtocol Buffersを扱ってみようかな、ということで。

Javaでなら以前扱ったことがあります。とはいえ、久しぶりに触るので情報も見返してみましょう。

Protocol Buffers

Protocol BuffersのWebサイトはこちら。

Protocol Buffers Documentation

GitHubリポジトリーはこちら。

GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format

Protocol Buffersは、言語およびプラットフォームに非依存の構造化データをシリアライズするメカニズムです。

Protocol Buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.

C++C#Java、Goなど様々なプログラミング言語で生成されたコードをサポートしています。

特徴は以下のとおりです。

  • JSONに似た、サイズが小さく高速
  • 言語ネイティブなバインディングを生成する
  • データ構造を定義すれば、様々な言語で読み書きできるようになる
  • 後方互換性だけではなく、前方互換性も意識して定義を更新可能

Overview | Protocol Buffers Documentation

プログラミングガイドはこちら。

Programming Guides | Protocol Buffers Documentation

よく見ると、現在のProtocol Buffersには以下の種類があるようです。

こちらが言語リファレンスで、各言語向けにコードを生成した場合の型のマッピングなども書かれていますね。

Editionsには2023、そして開発中の2024があります。

Editionsとproto 2、proto 3との違いはこちらを見るとよさそうです。

Protobuf Editions Overview | Protocol Buffers Documentation

現時点で、大きくproto 3と違うわけではなさそうですが。

今回は、proto 3を扱うことにします。

Language Guide (proto 3) | Protocol Buffers Documentation

言語はPythonを使います。

Python Reference | Protocol Buffers Documentation

チュートリアルはこちら。

Protocol Buffer Basics: Python | Protocol Buffers Documentation

今回は簡単な例で試してみることにします。

環境

今回の環境はこちら。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.3 LTS
Release:        24.04
Codename:       noble


$ uname -srvmpio
Linux 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux


$ python3 --version
Python 3.12.3


$ uv --version
uv 0.8.19

Protocol Buffersコンパイラー(Protocol Buffer Compiler、protoc)をインストールする

まずはProtocol Buffersのコンパイラー、Protocol Buffer Compiler(protoc)をインストールします。

Protocol Buffer Compiler Installation | Protocol Buffers Documentation

コンパイル済みのバイナリーを使うか、OSのパッケージマネージャーを使うかしてインストールします。

Ubuntu Linux 24.04 LTSではaptでインストールできるバージョンが最新版と同じようなので、aptでインストールすることにします。

$ apt show protobuf-compiler
Package: protobuf-compiler
Version: 3.21.12-8.2ubuntu0.2
Priority: extra
Section: universe/devel
Source: protobuf
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Laszlo Boszormenyi (GCS) <gcs@debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 116 kB
Depends: libc6 (>= 2.34), libgcc-s1 (>= 3.3.1), libprotoc32t64 (= 3.21.12-8.2ubuntu0.2), libstdc++6 (>= 13.1)
Recommends: libprotobuf-dev
Suggests: protobuf-mode-el
Homepage: https://github.com/google/protobuf/
Download-Size: 29.0 kB
APT-Sources: http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 Packages
Description: compiler for protocol buffer definition files
 Protocol buffers are a flexible, efficient, automated mechanism for
 serializing structured data - similar to XML, but smaller, faster, and
 simpler. You define how you want your data to be structured once, then you can
 use special generated source code to easily write and read your structured
 data to and from a variety of data streams and using a variety of languages.
 You can even update your data structure without breaking deployed programs
 that are compiled against the "old" format.
 .
 Google uses Protocol Buffers for almost all of its internal RPC protocols and
 file formats.
 .
 This package contains the protocol buffer compiler that is used for
 translating from .proto files (containing the definitions) to the language
 binding for the supported languages.

N: 追加レコードが 1 件あります。表示するには '-a' スイッチを付けてください。

Ubuntu Manpage: protoc - compile protocol buffer description files

インストール。

$ sudo apt install protobuf-compiler

バージョン。

$ protoc --version
libprotoc 3.21.12

これでprotocを使う準備はできました。

uvプロジェクトを作成して、Protocol Buffersの定義からソースコードを生成する

uvプロジェクトを作成します。

$ uv init --vcs none protobuf-getting-started
$ cd protobuf-getting-started
$ rm main.py

依存関係をいろいろとインストールする前に、ソースコードを生成してしまいましょう。

こちらを見ながら進めていきます。

Protocol Buffer Basics: Python | Protocol Buffers Documentation

出力先はsrc/genとします。

$ mkdir -p src/gen

定義はこのようなものにしました。書籍がお題ですね。

book.proto

syntax="proto3";

package tutorial;

message Book {
  string isbn13 = 1;
  string title = 2;
  repeated Author authors = 3;
  int32 price = 4;

  enum Tag {
    BEGGINER = 0;
    ADVANCED = 1;
    PYTHON = 2;
    JAVA = 3;
    MYSQL = 4;
  }

  repeated Tag tags = 5;
}

message Author {
  string first_name = 1;
  string last_name = 2;
}

ソースコードを生成。--proto_pathは、Protocol Buffersの定義ファイルがどこにあるかを指定します。--python_outは生成する
Pythonコードの出力先、--pyi_outはスタブファイル(型定義)の出力先です。

$ protoc --proto_path=. --python_out=src/gen --pyi_out=src/gen book.proto

結果はこうなりました。

src/gen/book_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: book.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nbook.proto\x12\x08tutorial\"\xbd\x01\n\x04\x42ook\x12\x0e\n\x06isbn13\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12!\n\x07\x61uthors\x18\x03 \x03(\x0b\x32\x10.tutorial.Author\x12\r\n\x05price\x18\x04 \x01(\x05\x12 \n\x04tags\x18\x05 \x03(\x0e\x32\x12.tutorial.Book.Tag\"B\n\x03Tag\x12\x0c\n\x08\x42\x45GGINER\x10\x00\x12\x0c\n\x08\x41\x44VANCED\x10\x01\x12\n\n\x06PYTHON\x10\x02\x12\x08\n\x04JAVA\x10\x03\x12\t\n\x05MYSQL\x10\x04\"/\n\x06\x41uthor\x12\x12\n\nfirst_name\x18\x01 \x01(\t\x12\x11\n\tlast_name\x18\x02 \x01(\tb\x06proto3')

_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'book_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:

  DESCRIPTOR._options = None
  _BOOK._serialized_start=25
  _BOOK._serialized_end=214
  _BOOK_TAG._serialized_start=148
  _BOOK_TAG._serialized_end=214
  _AUTHOR._serialized_start=216
  _AUTHOR._serialized_end=263
# @@protoc_insertion_point(module_scope)

スタブファイル。

src/gen/book_pb2.pyi

from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union

DESCRIPTOR: _descriptor.FileDescriptor

class Author(_message.Message):
    __slots__ = ["first_name", "last_name"]
    FIRST_NAME_FIELD_NUMBER: _ClassVar[int]
    LAST_NAME_FIELD_NUMBER: _ClassVar[int]
    first_name: str
    last_name: str
    def __init__(self, first_name: _Optional[str] = ..., last_name: _Optional[str] = ...) -> None: ...

class Book(_message.Message):
    __slots__ = ["authors", "isbn13", "price", "tags", "title"]
    class Tag(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
        __slots__ = []
    ADVANCED: Book.Tag
    AUTHORS_FIELD_NUMBER: _ClassVar[int]
    BEGGINER: Book.Tag
    ISBN13_FIELD_NUMBER: _ClassVar[int]
    JAVA: Book.Tag
    MYSQL: Book.Tag
    PRICE_FIELD_NUMBER: _ClassVar[int]
    PYTHON: Book.Tag
    TAGS_FIELD_NUMBER: _ClassVar[int]
    TITLE_FIELD_NUMBER: _ClassVar[int]
    authors: _containers.RepeatedCompositeFieldContainer[Author]
    isbn13: str
    price: int
    tags: _containers.RepeatedScalarFieldContainer[Book.Tag]
    title: str
    def __init__(self, isbn13: _Optional[str] = ..., title: _Optional[str] = ..., authors: _Optional[_Iterable[_Union[Author, _Mapping]]] = ..., price: _Optional[int] = ..., tags: _Optional[_Iterable[_Union[Book.Tag, str]]] = ...) -> None: ...

ちなみにスタブファイルは、mypy向けに生成することもできます。こちらは後で置き換えます。

では、生成されたコードを使っていきます。

準備

必要な依存関係をインストールします。

Protocol Buffers。

$ uv add protobuf

protobuf

スタブファイルもインストール。

$ uv add --dev types-protobuf

テストや各種チェックツールも入れておきます。

$ uv add --dev pytest mypy ruff

今回mypyを使うのですが、protocが生成するファイルでは型情報が弱く、mypyでのチェックを通すことができません。

そこで今回はmypy-protobufを使います。

$ uv tool install mypy-protobuf

GitHub - nipunn1313/mypy-protobuf: open source tools to generate mypy stubs from protobufs

こちらをインストールするとprotoc-gen-mypyというコマンドが使えるようになります。

$ which protoc-gen-mypy
$HOME/.local/bin/protoc-gen-mypy


$ protoc-gen-mypy --version
mypy-protobuf 3.6.0

この状態だと、protocコマンドに--mypy_outというオプションが指定できるようになり、型情報が強化されたスタブファイルを生成できます。

$ protoc --proto_path=. --python_out=src/gen --mypy_out=src/gen book.proto

結果はこうなりました。

src/gen/book_pb2.pyi

"""
@generated by mypy-protobuf.  Do not edit manually!
isort:skip_file
"""

import builtins
import collections.abc
import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import sys
import typing

if sys.version_info >= (3, 10):
    import typing as typing_extensions
else:
    import typing_extensions

DESCRIPTOR: google.protobuf.descriptor.FileDescriptor

@typing.final
class Book(google.protobuf.message.Message):
    DESCRIPTOR: google.protobuf.descriptor.Descriptor

    class _Tag:
        ValueType = typing.NewType("ValueType", builtins.int)
        V: typing_extensions.TypeAlias = ValueType

    class _TagEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Book._Tag.ValueType], builtins.type):
        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
        BEGGINER: Book._Tag.ValueType  # 0
        ADVANCED: Book._Tag.ValueType  # 1
        PYTHON: Book._Tag.ValueType  # 2
        JAVA: Book._Tag.ValueType  # 3
        MYSQL: Book._Tag.ValueType  # 4

    class Tag(_Tag, metaclass=_TagEnumTypeWrapper): ...
    BEGGINER: Book.Tag.ValueType  # 0
    ADVANCED: Book.Tag.ValueType  # 1
    PYTHON: Book.Tag.ValueType  # 2
    JAVA: Book.Tag.ValueType  # 3
    MYSQL: Book.Tag.ValueType  # 4

    ISBN13_FIELD_NUMBER: builtins.int
    TITLE_FIELD_NUMBER: builtins.int
    AUTHORS_FIELD_NUMBER: builtins.int
    PRICE_FIELD_NUMBER: builtins.int
    TAGS_FIELD_NUMBER: builtins.int
    isbn13: builtins.str
    title: builtins.str
    price: builtins.int
    @property
    def authors(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Author]: ...
    @property
    def tags(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___Book.Tag.ValueType]: ...
    def __init__(
        self,
        *,
        isbn13: builtins.str = ...,
        title: builtins.str = ...,
        authors: collections.abc.Iterable[global___Author] | None = ...,
        price: builtins.int = ...,
        tags: collections.abc.Iterable[global___Book.Tag.ValueType] | None = ...,
    ) -> None: ...
    def ClearField(self, field_name: typing.Literal["authors", b"authors", "isbn13", b"isbn13", "price", b"price", "tags", b"tags", "title", b"title"]) -> None: ...

global___Book = Book

@typing.final
class Author(google.protobuf.message.Message):
    DESCRIPTOR: google.protobuf.descriptor.Descriptor

    FIRST_NAME_FIELD_NUMBER: builtins.int
    LAST_NAME_FIELD_NUMBER: builtins.int
    first_name: builtins.str
    last_name: builtins.str
    def __init__(
        self,
        *,
        first_name: builtins.str = ...,
        last_name: builtins.str = ...,
    ) -> None: ...
    def ClearField(self, field_name: typing.Literal["first_name", b"first_name", "last_name", b"last_name"]) -> None: ...

global___Author = Author

コメントをよく見ると、mypy-protobufによって生成されたことが書かれています。

"""
@generated by mypy-protobuf.  Do not edit manually!
isort:skip_file
"""

ここまででインストールした依存関係はこちら。

$ uv pip list
Package           Version
----------------- ---------------
iniconfig         2.1.0
mypy              1.18.2
mypy-extensions   1.1.0
packaging         25.0
pathspec          0.12.1
pluggy            1.6.0
protobuf          6.32.1
pygments          2.19.2
pytest            8.4.2
ruff              0.13.1
types-protobuf    6.32.1.20250918
typing-extensions 4.15.0

pyproject.toml

[project]
name = "protobuf-getting-started"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "protobuf>=6.32.1",
]

[dependency-groups]
dev = [
    "mypy>=1.18.2",
    "pytest>=8.4.2",
    "ruff>=0.13.1",
    "types-protobuf>=6.32.1.20250918",
]

[tool.ruff]
extend-exclude = ["src/gen"]

[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true
explicit_package_bases = true
mypy_path = ["src"]

[tool.pytest.ini_options]
pythonpath = ["src"]

生成されたコードを使ってみる

それでは、生成されたコードを使ってみます。

参考にするのは、このあたりです。

Protocol Buffer Basics: Python | Protocol Buffers Documentation

Python Generated Code Guide | Protocol Buffers Documentation

この2つを見ると、だいたい使い方はわかるような気がします。

APIリファレンスはこちらですね。

Protocol Buffers Python API Reference — Protocol Buffers 4.21.1 documentation

オブジェクトの作成、JSONでのシリアライズ、Protocol Buffersとしてのシリアライズをそれぞれ行っています。

rc/main.py

from google.protobuf import json_format
from gen import book_pb2


def main() -> None:
    python_book = book_pb2.Book()
    python_book.isbn13 = "978-4873119328"
    python_book.title = "入門 Python 3 第2版"
    python_book.price = 4180
    python_book.tags.append(book_pb2.Book.BEGGINER)
    python_book.tags.append(book_pb2.Book.Tag.PYTHON)

    python_book_author1 = book_pb2.Author()
    python_book_author1.first_name = "Bill"
    python_book_author1.last_name = "Lubanovic"
    python_book.authors.append(python_book_author1)

    python_book_author2 = python_book.authors.add()
    python_book_author2.first_name = "駿 "
    python_book_author2.last_name = "鈴木"
    python_book_author3 = python_book.authors.add()
    python_book_author3.first_name = "高弘"
    python_book_author3.last_name = "長尾"

    print("==================== Python Book ====================")
    print("Plain")
    print(python_book)

    print()

    print("JSON:")
    print(json_format.MessageToJson(python_book))

    print()

    print("Serialize:")
    print(python_book.SerializeToString())

    print()

    java_book = book_pb2.Book()
    java_book.isbn13 = "978-4621303252"
    java_book.title = "Effective Java 第3版"
    java_book.price = 4400
    java_book.tags.append(book_pb2.Book.Tag.Value("ADVANCED"))
    java_book.tags.append(book_pb2.Book.Tag.Value("JAVA"))

    java_book_author1 = book_pb2.Author()
    java_book_author1.first_name = "Joshua"
    java_book_author1.last_name = "Bloch"
    java_book.authors.append(java_book_author1)

    java_book_author1 = book_pb2.Author()
    java_book_author1.first_name = "Joshua"
    java_book_author1.last_name = "Bloch"
    java_book.authors.append(java_book_author1)

    print("==================== Java Book ====================")
    print("Plain")
    print(java_book)

    print()

    print("JSON:")
    print(json_format.MessageToJson(java_book))

    print()

    print("Serialize:")
    print(java_book.SerializeToString())


if __name__ == "__main__":
    main()

リストへの追加方法は2種類あったりします。

    python_book_author1 = book_pb2.Author()
    python_book_author1.first_name = "Bill"
    python_book_author1.last_name = "Lubanovic"
    python_book.authors.append(python_book_author1)

    python_book_author2 = python_book.authors.add()
    python_book_author2.first_name = "駿 "
    python_book_author2.last_name = "鈴木"
    python_book_author3 = python_book.authors.add()
    python_book_author3.first_name = "高弘"
    python_book_author3.last_name = "長尾"

あと、Enumの扱いが最初わかりませんでした…。

シリアライズJSONとProtocol Buffersでのシリアライズ。順番が逆な気がしますが。

    print("JSON:")
    print(json_format.MessageToJson(java_book))

    print()

    print("Serialize:")
    print(java_book.SerializeToString())

結果。

$ uv run src/main.py
==================== Python Book ====================
Plain
isbn13: "978-4873119328"
title: "入門 Python 3 第2版"
authors {
  first_name: "Bill"
  last_name: "Lubanovic"
}
authors {
  first_name: "駿 "
  last_name: "鈴木"
}
authors {
  first_name: "高弘"
  last_name: "長尾"
}
price: 4180
tags: BEGGINER
tags: PYTHON


JSON:
{
  "isbn13": "978-4873119328",
  "title": "\u5165\u9580 Python 3 \u7b2c2\u7248",
  "authors": [
    {
      "firstName": "Bill",
      "lastName": "Lubanovic"
    },
    {
      "firstName": "\u99ff ",
      "lastName": "\u9234\u6728"
    },
    {
      "firstName": "\u9ad8\u5f18",
      "lastName": "\u9577\u5c3e"
    }
  ],
  "price": 4180,
  "tags": [
    "BEGGINER",
    "PYTHON"
  ]
}

Serialize:
b'\n\x0e978-4873119328\x12\x17\xe5\x85\xa5\xe9\x96\x80 Python 3 \xe7\xac\xac2\xe7\x89\x88\x1a\x11\n\x04Bill\x12\tLubanovic\x1a\x0e\n\x04\xe9\xa7\xbf \x12\x06\xe9\x88\xb4\xe6\x9c\xa8\x1a\x10\n\x06\xe9\xab\x98\xe5\xbc\x98\x12\x06\xe9\x95\xb7\xe5\xb0\xbe \xd4 *\x02\x00\x02'

==================== Java Book ====================
Plain
isbn13: "978-4621303252"
title: "Effective Java 第3版"
authors {
  first_name: "Joshua"
  last_name: "Bloch"
}
authors {
  first_name: "芳樹"
  last_name: "柴田"
}
price: 4400
tags: ADVANCED
tags: JAVA


JSON:
{
  "isbn13": "978-4621303252",
  "title": "Effective Java \u7b2c3\u7248",
  "authors": [
    {
      "firstName": "Joshua",
      "lastName": "Bloch"
    },
    {
      "firstName": "\u82b3\u6a39",
      "lastName": "\u67f4\u7530"
    }
  ],
  "price": 4400,
  "tags": [
    "ADVANCED",
    "JAVA"
  ]
}

Serialize:
b'\n\x0e978-4621303252\x12\x16Effective Java \xe7\xac\xac3\xe7\x89\x88\x1a\x0f\n\x06Joshua\x12\x05Bloch\x1a\x10\n\x06\xe8\x8a\xb3\xe6\xa8\xb9\x12\x06\xe6\x9f\xb4\xe7\x94\xb0 \xb0"*\x02\x01\x03'

テストでは、シリアライズしてファイルに保存、ファイルからデシリアライズしてみることを確認。

tests/test_main.py

import os
from gen import book_pb2


def test_book_deser() -> None:
    python_book = book_pb2.Book()
    python_book.isbn13 = "978-4873119328"
    python_book.title = "入門 Python 3 第2版"
    python_book.price = 4180
    python_book.tags.append(book_pb2.Book.BEGGINER)
    python_book.tags.append(book_pb2.Book.PYTHON)

    python_book_author1 = book_pb2.Author()
    python_book_author1.first_name = "Bill"
    python_book_author1.last_name = "Lubanovic"
    python_book.authors.append(python_book_author1)

    python_book_author2 = python_book.authors.add()
    python_book_author2.first_name = "駿 "
    python_book_author2.last_name = "鈴木"
    python_book_author3 = python_book.authors.add()
    python_book_author3.first_name = "高弘"
    python_book_author3.last_name = "長尾"

    with open("/tmp/book.pb", "wb") as f:
        f.write(python_book.SerializeToString())

    with open("/tmp/book.pb", "rb") as f:
        restore_book = book_pb2.Book()
        restore_book.ParseFromString(f.read())

        assert restore_book.isbn13 == "978-4873119328"
        assert restore_book.title == "入門 Python 3 第2版"
        assert restore_book.price == 4180

        assert len(restore_book.tags) == 2
        assert restore_book.tags == [book_pb2.Book.BEGGINER, book_pb2.Book.PYTHON]

        author1 = book_pb2.Author()
        author1.first_name = "Bill"
        author1.last_name = "Lubanovic"

        author2 = book_pb2.Author()
        author2.first_name = "駿 "
        author2.last_name = "鈴木"

        author3 = book_pb2.Author()
        author3.first_name = "高弘"
        author3.last_name = "長尾"

        assert len(restore_book.authors) == 3
        assert restore_book.authors == [
            author1,
            author2,
            author3,
        ]

        assert os.path.exists("/tmp/book.pb")
        os.remove("/tmp/book.pb")
        assert not os.path.exists("/tmp/book.pb")

こんなところでしょうか。

おわりに

PythonでProtocol Buffersを扱ってみました。

生成されたソースコードやProtocol BuffersのAPIに戸惑ったところもありましたが、それ以上にRuffやmypyとの組み合わせで困っていた時間の方が
長かった気がします。

このあたりのツールの設定に慣れていないだけですが。

ひとまず使い方の雰囲気はわかったのでよしとしましょう。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/09/23/162351より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14