BeautifulSoupとIllegal Multibyte Sequenceエラーの解決法

BeautifulSoupで「Illegal multibyte sequence」エラーが出る原因と解決方法

BeautifulSoup は、Python で HTML や XML を扱うときによく使われるパーサーライブラリです。
Web ページのスクレイピングや、HTML から特定の要素を抜き出す処理などで頻繁に利用されます。

しかし、日本語などのマルチバイト文字を含むページを扱うと、
Illegal multibyte sequence というエラーが発生することがあります。
このエラーは多くの場合、「文字コードの扱い」が原因です。

この記事では、BeautifulSoup で Illegal multibyte sequence が発生する典型的なパターンと、
その考え方・解決方法をまとめていきます。

BeautifulSoupとは

BeautifulSoup は、Python のライブラリの 1 つで、HTML や XML などのテキストデータを解析し、
DOM ツリーとして扱いやすくしてくれるツールです。タグ名やクラス名、属性などを指定して、
必要な情報だけを取り出すことができます。

Illegal multibyte sequenceエラーとは?

Illegal multibyte sequence エラーは、文字コード関連のエラーの一種です。
日本語や中国語のようなマルチバイト文字を含むテキストを、
間違った文字コードでデコード・変換しようとしたときに発生します。

例えば、本来は Shift_JIS や EUC-JP でエンコードされているデータに対して、
無理やり UTF-8 として解釈しようとすると、バイト列の組み合わせがおかしくなり、
「不正なマルチバイトシーケンス(Illegal multibyte sequence)」と判断されます。

Illegal multibyte sequenceエラーが発生する典型的なコード例

import requests
from bs4 import BeautifulSoup

url = "http://example.com"
res = requests.get(url)
soup = BeautifulSoup(res.content, "html.parser")
print(soup.prettify())
  

上記のように、レスポンスボディを res.content(バイト列)で取得し、
そのまま BeautifulSoup に渡しているコードは、一見すると問題なさそうに見えます。

しかし、レスポンスの実際の文字コードと、内部で使用されるデコーダやライブラリの想定する文字コードが食い違うと、
途中の変換処理で Illegal multibyte sequence が発生することがあります。

なぜIllegal multibyte sequenceが発生するのか

このエラーの根本原因は、「バイト列をどの文字コードとして解釈するか」という部分にあります。

  • サーバーが返している HTML が Shift_JIS なのに、UTF-8 として扱おうとしている
  • レスポンスヘッダの Content-Type に書かれている charset と、実際の中身のエンコードが一致していない
  • 中間で別ライブラリ(iconv など)が変換を行い、その際に不正なバイト列として弾かれる

BeautifulSoup 自体は様々なパーサー(html.parserlxmlhtml5lib など)と組み合わせて動作しますが、
どの場合でも、「どのエンコーディングでデコードするか」を間違えると、同様の問題が起こり得ます。

解決パターンの全体像

BeautifulSoup で Illegal multibyte sequence が発生する場合、多くは次のような方針で解決できます。

  1. レスポンスの文字コードを正しく指定する(res.encoding を適切に設定するなど)。
  2. バイト列(res.content)を自分でデコードしてから BeautifulSoup に渡す。
  3. 文字コードが不明な場合は、推定ライブラリを使って文字コードを推定したうえでデコードする。

解決方法1:レスポンスの文字コードを正しく設定してから解析する

まずは、レスポンスの文字コードを正しく設定したうえで、res.text を使って解析する方法です。

import requests
from bs4 import BeautifulSoup

url = "http://example.com"

res = requests.get(url)

# サーバーが返すヘッダや中身から推定されたエンコーディングを使用
# サイトによっては "shift_jis" などを明示的に指定した方がよい場合もある
if res.encoding is None or res.encoding == "ISO-8859-1":
    res.encoding = res.apparent_encoding

soup = BeautifulSoup(res.text, "html.parser")
print(soup.prettify())
  

ポイントは、闇雲に "utf-8" を指定するのではなく、
res.encodingres.apparent_encoding を確認したうえで、
実際のページのエンコーディングに合わせることです。

例えば、対象のページが Shift_JIS で配信されていることが分かっているなら、
以下のように明示的に指定してもかまいません。

res = requests.get(url)
res.encoding = "shift_jis"  # 実際のページのエンコーディングに合わせる
soup = BeautifulSoup(res.text, "html.parser")
  

この方法は、レスポンスの文字コードが比較的素直なサイトであればシンプルに動作します。
一方で、ヘッダ情報と実際の中身が食い違っているような「クセのあるサイト」では、
後述の方法も併用した方が安全です。

解決方法2:バイト列を自分でdecodeしてからBeautifulSoupに渡す

文字コードを自分でコントロールしたい場合は、
res.content(バイト列)を一度デコードしてから、BeautifulSoup に渡す方法が有効です。

import requests
from bs4 import BeautifulSoup

url = "http://example.com"

res = requests.get(url)

# 本来のエンコーディングが分かっている場合はそれを指定する
html = res.content.decode("utf-8", errors="strict")

soup = BeautifulSoup(html, "html.parser")
print(soup.prettify())
  

errors="strict" は、デコード時に不正なバイト列があれば例外を投げる設定です。
もし、どうしてもエラーを避けたいケースでは、
いったん "replace""ignore" を使うこともありますが、
その場合は文字が欠落・変換される可能性がある点に注意してください。

# 不正なバイト列があっても処理を続けたい場合の例(あくまで最終手段)
html = res.content.decode("utf-8", errors="replace")
soup = BeautifulSoup(html, "html.parser")
  

重要なのは、「どの文字コードでデコードしているか」を明示的に意識することです。
実際のサイトのエンコーディングに合わせて "shift_jis""euc-jp" を指定する場面もあります。

解決方法3:文字コードが不明な場合に推定ライブラリを使う

古いサイトや独自実装のサイトなどでは、レスポンスヘッダと中身が一致していなかったり、
そもそも charset が書かれていなかったりします。
そのような場合、charset-normalizerchardet といったライブラリで
文字コードを推定してからデコードすると、エラーを避けやすくなります。

import requests
from bs4 import BeautifulSoup
import charset_normalizer

url = "http://example.com"

res = requests.get(url)

# バイト列からエンコーディングを推定する
result = charset_normalizer.from_bytes(res.content).best()
html = str(result)  # 推定されたエンコーディングでデコードされた文字列

soup = BeautifulSoup(html, "html.parser")
print(soup.prettify())
  

文字コードが混在していたり、ヘッダ情報が信用できないサイトを扱う場合は、
このような推定処理を挟むことで Illegal multibyte sequence の発生を抑えられることがあります。

パーサーの種類とIllegal multibyte sequence

BeautifulSoup は内部処理で、指定したパーサーに HTML を渡して解析します。
よく使われるのは以下の 3 つです。

  • html.parser(標準ライブラリ)
  • lxml
  • html5lib

Illegal multibyte sequence は、これらのパーサー内部で利用される
文字コード変換処理(iconv など)が、不正なバイト列を受け取ったときにも発生します。
そのため、どのパーサーを使っていても、根本的な対策は「正しい文字コードでデコードする」ことです。

トラブルシューティングのチェックリスト

実際にエラーが出た場合は、次の順番で確認していくと原因を絞り込みやすくなります。

  • 対象のサイトの HTML に <meta charset="..."> がどう書かれているか確認する。
  • レスポンスヘッダの Content-Type にどの charset が指定されているか確認する。
  • res.encodingres.apparent_encoding の値を print して比較する。
  • res.text ではなく res.content を使っていないか確認する。
  • 自分で decode() する場合、指定しているエンコーディングが正しいか見直す。
  • 必要に応じて charset-normalizerchardet で推定結果を見てみる。

よくある質問(FAQ)

Q. とりあえず UTF-8 にしておけば問題ありませんか?

A. いいえ。実際のページが UTF-8 であれば問題ありませんが、
Shift_JIS や EUC-JP のサイトを UTF-8 として扱うと、Illegal multibyte sequence を含む
さまざまな文字化けの原因になります。必ず、対象サイトの実際の文字コードを確認してから指定してください。

Q. errors="ignore"errors="replace" を常に使っても大丈夫ですか?

A. 文字化けや情報欠落が許容できる場面なら使うこともありますが、基本的にはおすすめできません。
デバッグや一時的な回避策として使い、本番コードでは極力、正しいエンコーディングを指定して
正常にデコードできるようにする方が安全です。

Q. どのパーサーを使うとエラーが出にくいですか?

A. パーサーによってエラーメッセージの種類は変わることがありますが、
文字コードが原因のエラーそのものを根本的に避けるには、
どのパーサーを使う場合でも「正しい文字コードでデコードする」ことが重要です。
まずはエンコーディングを見直し、そのうえで用途に応じて
html.parserlxmlhtml5lib を選ぶとよいでしょう。

まとめ

BeautifulSoup で Illegal multibyte sequence エラーが出るときは、
ほぼ確実に「文字コードの扱い」に原因があります。

  • レスポンスの文字コードを正しく設定する(res.encoding を見直す)。
  • res.content を自分で decode() し、正しいエンコーディングで文字列にする。
  • 文字コードが不明な場合は、推定ライブラリでエンコーディングを推定する。

上記のポイントを意識して実装すれば、
マルチバイト文字を含むページでも安定して BeautifulSoup で解析できるようになります。