nikkie-ftnextの日記

イベントレポートや読書メモを発信

ねえ、ChatGPT? Webページの本文を取得するJavaScriptで実装された関数があるんだけど、Pythonではどう書いたらいいかな?

はじめに

これが、ChatGPTとのペアプロ...! nikkieです

Webページの本文を取得するというめちゃめちゃステキ✨な関数の実装を見かけました。
「なにこれ、めっちゃ便利!私も使いたい!!」
その実装はJavaScriptです。
Pythonでおんなじことできないかな〜」と考えた私は、ChatGPTの力を借りることにしました。

目次

経緯

このブログで何回か取り上げているこちらの記事。

ChatGPTでできることとして、要約も挙がっています。

要約の実装を見たところ、URLから本文を抽出していました。

https://github.com/newspicks/learn-chatgpt-api/blob/92b377c79eadc0ec5654df544c15cd62f966c197/src/util.ts#L15-L28 (抜粋版を示します)

export async function extractArticleContent(url: string): Promise<string> {
    const dom = await JSDOM.fromURL(url);
    const reader = new Readability(dom.window.document);
    const article = reader.parse();

    const content = new dom.window.DOMParser()
        .parseFromString(article.content, "text/html")
        .body
        .textContent || "";
    return content;
}

URLから本文抽出、これは私がかなり欲しかった実装です。
JavaScriptの実装をPythonに書き換えられるのでは」と、プログラミング言語間の書き換えで(私の中で)定評のある1ChatGPTの力を借りることにしました。

結論(暫定解)

以下のスクリプトで本文が取得できます。

% python extract_body.py https://www.itmedia.co.jp/business/articles/2303/14/news050.html

仕事に役立つ調査データ:
(以下略)

動作環境

  • Python 3.10.9
  • requests 2.28.2
  • readability-lxml 0.8.1
  • beautifulsoup4 4.12.2
  • chardet 5.1.0

ChatGPTとのペアプロ

初手「JavaScriptの実装、解説して」🙏

先のJavaScriptの実装を解説してもらうところから始めます。
関数の主要部分を抜き出しました(※JavaScript素人目に主要と判断した部分です)

>>>以下のJavaScriptのコードを解説して私を助けてください

const dom = await JSDOM.fromURL(url);
const reader = new Readability(dom.window.document);
const article = reader.parse();

const content = new dom.window.DOMParser()
    .parseFromString(article.content, "text/html")
    .body
    .textContent || "";

丁寧な解説に定評のあるChatGPT氏。
各行を解説してくれました。

ここで知ったのは2つのライブラリを使っているということ

今回取り上げた実装ができることは、@mozilla/readabilityhttps://github.com/mozilla/readability/tree/0.4.4#nodejs-usage に書いてあるんですね!

Pythonのライブラリreadability-lxmlを知った!

Pythonでどうやるかを聞いたところ、readability-lxmlというライブラリを教えてもらいます。

>>>なるほど。私はJavaScriptよりPythonが得意なのですが、Pythonに書き換えたらどうなるのか教えてほしいです
Pythonで同じ処理を行う場合は、以下のようなコードになります。
import requests
from readability import Document

url = "https://example.com"

response = requests.get(url)
doc = Document(response.text)

content = doc.summary()

(※続く解説は省略します)

生成されたこのコード、ライブラリのREADMEのサンプルコードと同様なんですよ!

from readability import Documentの行を書くにはライブラリのインストールが必要だと思いますが、「readability」で検索しても見つからず。
pip install readabilityでいいのかなと思い質問。

>>>Pythonのreadabilityライブラリはどのようにインストールすればいいのでしょうか
Pythonのreadabilityライブラリは、Pythonのパッケージ管理ツールであるpipを使ってインストールすることができます。以下のコマンドをターミナルで実行してください。

pip install readability-lxml

これで、readabilityライブラリがインストールされ、Pythonで利用することができるようになります。

PyPIでも裏が取れ、無事インストールできました。

requests -> readability-lxml -> beautifulsoup4

先のPythonを動かすと、contentにはHTMLタグを含んだ文字列が取得されています(ページ本文部分なの、すごい!)。
「HTMLタグをどう除けばいいかな、BeautifulSoupとかでいけたよな」と思いつつ聞いてみます。

>>>doc.summary()が返す値はHTMLタグをもった文字列でした。

'<html><body><div><body id="readabilityBody">\n<div>\n    <h1>Example Domain</h1>\n

HTMLタグを除くにはどんな方法があるでしょうか
doc.summary()が返す値は、HTMLタグを含んだ文字列であるため、HTMLタグを除去するためには、Pythonのbeautifulsoup4ライブラリを使うことができます。

以下のように、beautifulsoup4ライブラリを使って、HTMLタグを取り除いた文字列を得ることができます。
import requests
from readability import Document
from bs4 import BeautifulSoup

url = "https://example.com"

response = requests.get(url)
doc = Document(response.text)

# HTMLタグを取り除く
soup = BeautifulSoup(doc.summary(), "html.parser")
text = soup.get_text()

print(text)

おお、やはりBeautifulSoup
そうか、get_textで全てのHTMLタグがstripされるのか!

https://www.crummy.com/software/BeautifulSoup/bs4/doc/#get-text

If you only want the human-readable text inside a document or tag, you can use the get_text() method.

文字コード!(文字化け😱)

ここまででほしいスクリプトはできたので、参考記事と同じURLを対象に動かしてみます。
ChatGPT、使用している経営者は1割未満 「調べものに活用」「質の低いプログラマー淘汰」の声も

すると、文字化け

dÉð§Â²¸f[^F

対象ページのソース(HTML)を見てみると、charsetがshift_jisなのです(ここを意識しないで済むJavaScript実装、すごすぎますね)。

>>>ありがとうございます。たいへん助かっています。HTMLのmetaタグでcharset=shift_jisだと文字化しちゃうんですが、どんな対策が考えられますか?

感想戦:ここでもっと具体を与えるとよかったかも)

shift_jisは日本語の文字コードであり、日本語のWebページでは比較的よく使われますが、文字コードがshift_jisであると、日本語以外の文字が正しく表示されない可能性があります。

この問題を解決するには、以下のような対策が考えられます。

1. requestsライブラリを使ってWebページの文字コードを明示的に指定する
2. HTMLのmetaタグから文字コードを取得し、それに基づいて文字コードを変換する

具体的には、以下のようなコードを書くことができます。
import requests
from readability import Document
from bs4 import BeautifulSoup
import chardet

url = "https://example.com"

# Webページの文字コードを明示的に指定する
response = requests.get(url, headers={"Content-Type": "text/html; charset=utf-8"})

# 文字コードを取得する
soup = BeautifulSoup(response.content, "html.parser")
meta_charset = soup.find("meta", attrs={"charset": True})
if meta_charset:
    charset = meta_charset["charset"]
else:
    # chardetライブラリを使って文字コードを自動判別する
    charset = chardet.detect(response.content)["encoding"]

# 文字コードを変換する
doc = Document(response.content.decode(charset))
summary = doc.summary()

# HTMLタグを取り除く
soup = BeautifulSoup(summary, "html.parser")
text = soup.get_text()

print(text)

いくつか教えてくれました

  1. requests.getするときにheadersを使ってcharsetを指定
    • IMO:これって機能するんです? ちょっと試して無理筋っぽかったので見送り
  2. BeautifulSoupmetaタグを見つけて、charsetを取得
  3. chardetを使ってcharsetを自動判別

2か3でcharsetが分かったら、それを指定してページのコンテンツをdecodeすればいいわけですね!2

chardetによる文字コード自動判定に至る

ライブラリを追加したくなかったので、2のmetaタグを見つける路線で進めます。

>>>なるほど、こんな方法が!
meta_charset = soup.find("meta", attrs={"charset": True}) で見つからないみたいなのですが、個々の解説お願いします

(「個々」は変換ミスです)

metaタグには、charset属性が指定されていることがあります。この属性には、Webページの文字コードが指定されています。

BeautifulSoupライブラリを使ってmetaタグを検索するには、find()メソッドを使います。このメソッドは、指定した属性が存在する最初の要素を返します。

以下のように、find()メソッドにattrsパラメータを指定することで、metaタグの属性に対する検索を行うことができます。

python

meta_charset = soup.find("meta", attrs={"charset": True})

attrsパラメータには、charset属性が存在することを表す{"charset": True}を指定しています。

ただし、すべてのWebページでcharset属性が指定されているわけではないため、この方法だけでは文字化けが解決できない場合があります。その場合は、chardetライブラリを使って自動判別するなど、別の方法を試す必要があります。
>>><meta http-equiv="content-type" content="text/html;charset=shift_jis"> からcharsetを取得するにはどうすればいいでしょうか
metaタグのhttp-equiv属性には、content-typeが指定されていることがあります。この場合、文字コードはcontent属性に含まれています。

以下のように、http-equiv属性がcontent-typeであるmetaタグを検索し、content属性の値から文字コードを取得することができます。
meta_content_type = soup.find("meta", attrs={"http-equiv": "content-type"})
if meta_content_type:
    content = meta_content_type["content"]
    charset_pos = content.find("charset=")
    if charset_pos >= 0:
        charset = content[charset_pos + 8:].lower()
find()メソッドを使って、charset=という文字列が含まれている位置を検索し、その後ろに続く文字列を切り出しています。また、取得した文字コードは小文字に変換しています。

ここまで聞いて、2の路線はけっこう処理を書かないといけずめんどくさいなと感じ、3のchardetに切り替えました。
依存は増えますが、1行だけで済みます!

charset = chardet.detect(response.content)["encoding"]

こうして文字化けにも対処できました!

終わりに

URLから本文抽出するJavaScriptの実装(とても欲しかったもの)を見つけ、powered by ChatGPTでPythonに書き直しました。
HTMLから本文を取り出してくれるreadability-lxml、未来ずら〜。
そしてchardetを使って、文字コードutf-8以外でも対応できるように!

ChatGPT、ありがとう〜
「ぴょんぴょん」会話する3だけじゃなく、サンプルコードを錬成してくれるのマジで便利ですね、加速する!

P.S. ChatGPT関連勉強会紹介ゾーン!

「ChatGPTとのペアプロ」は id:karaage さんの記事で事例を知っていました。

この事例でからあげさんは「ChatGPT チャレンジ」コンペ制覇、おめでとうございます!

4/28(金)にナレッジ共有会!

また、TypeScriptではじめるChatGPT API:入門から一歩進んだテクニックまで - Uzabase for Engineers関連では、4/27(木)に勉強会があります!

エントリの執筆者の中村さんも登壇です。
LTの数がヤバい!

今週は4/25(火)にMLOpsコミュニティでもLT大会があったりと、毎日のようにChatGPTの事例共有ありますね、大盛り上がり!


  1. 過去の小さな成功体験です
  2. 最近素振りしたのでdecodeもしっくり来ました