user:
指定されたパスに多数の .json ファイルがありますが、いくつかは .json ファイルとして読み込めないので、テキストファイルとして読み込みたいと思います。その際に、
"link" Entry ID" "url"
と書いてある項目は、いずれも、URLを表す項目です。
ファイルパスを指定すると、これらのファイル群をテキストファイルとして読み込んで、1列目は URL、2列目は読み込んだ全文字列、という形の pandas dataframe を返す関数を書いてください。
assistant:
了解しました。やりたいのは「指定パスのすべての .json ファイルを処理するけど、JSON としてパースできなくても、ただのテキストとして読んで、URL と本文を DataFrame にする」ですね。
ポイントは:
- ファイルごとにまず
json.loadを試す。 - 失敗したら
open(...).read()で全文字列を読む。 - URL は
"link","Entry ID","url"のキーにある可能性がある。 - URL が取れなかった場合は
Noneにする。
以下の関数でいけます。
import os
import glob
import json
import pandas as pd
def load_jsons_as_text_with_url(path: str) -> pd.DataFrame:
"""
指定パスの .json ファイル群を読み込み、
各ファイルの URL と全文字列を pandas DataFrame に格納して返す。
DataFrameの列:
- 'url': URL (link, Entry ID, url のいずれかに格納されている)
- 'text': ファイルの全文字列
"""
rows = []
json_files = glob.glob(os.path.join(path, "*.json"))
for json_file in json_files:
url = None
text_content = None
try:
# JSON として読んでみる
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
text_content = json.dumps(data, ensure_ascii=False)
# URL の候補キーを探す
for key in ["link", "Entry ID", "url"]:
if isinstance(data, dict) and key in data:
url = data[key]
break
except Exception:
# JSON 読み込み失敗時はテキストとして読む
with open(json_file, "r", encoding="utf-8", errors="ignore") as f:
text_content = f.read()
# 簡易的に URL を抽出("link": "..." などから拾う)
for key in ["link", "Entry ID", "url"]:
marker = f'"{key}"'
idx = text_content.find(marker)
if idx != -1:
# その位置から引用符で囲まれた URL を拾う
start = text_content.find('"', idx + len(marker))
end = text_content.find('"', start + 1)
if start != -1 and end != -1:
url = text_content[start+1:end]
break
rows.append({"url": url, "text": text_content})
return pd.DataFrame(rows)
使い方はこんな感じです:
df = load_jsons_as_text_with_url("/path/to/jsons/")
print(df.head())
これで、1列目にURL、2列目に全文字列が入ります。
ご希望に合わせて「URL が見つからない場合の扱い」を調整できますけど、None にしておくのが無難だと思います。
質問ですが:URL が見つからなかったファイルは None のままで大丈夫ですか?それともファイル名を URL 代わりに埋めますか?
user:
ありがとうございます。でも
df['url'].unique()
した結果が
array([None], dtype=object)
でした。URLが全く拾えていません。
assistant:
なるほど!つまり json.load でもテキスト解析でも url がちゃんと拾えていないんですね。
ここで注意したいのは、実際の JSON やテキストの構造です。
たとえば JSON の中がこんな形なら:
{
"link": "https://example.com/page1",
"title": "サンプル"
}
これは問題なく取れるはずですが、
もしリスト形式だったり、ネストされていたりすると、単純な data[key] では見つからないのです。
なので「辞書の入れ子やリストの中まで総当たりで探索する」関数を用意した方がいいです。
修正版を示します👇
import os
import glob
import json
import pandas as pd
def extract_url_from_data(data, keys=("link", "Entry ID", "url")):
"""
JSONデータの中から候補キーを再帰的に探してURLを返す。
"""
if isinstance(data, dict):
for k, v in data.items():
if k in keys and isinstance(v, str):
return v
else:
found = extract_url_from_data(v, keys)
if found:
return found
elif isinstance(data, list):
for item in data:
found = extract_url_from_data(item, keys)
if found:
return found
return None
def load_jsons_as_text_with_url(path: str) -> pd.DataFrame:
rows = []
json_files = glob.glob(os.path.join(path, "*.json"))
for json_file in json_files:
url = None
text_content = None
try:
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
text_content = json.dumps(data, ensure_ascii=False)
url = extract_url_from_data(data)
except Exception:
# JSONとして読めなければテキストとして
with open(json_file, "r", encoding="utf-8", errors="ignore") as f:
text_content = f.read()
# 正規表現でURLを抜き出す(httpから始まるもの)
import re
match = re.search(r'https?://[^\s"\']+', text_content)
if match:
url = match.group(0)
rows.append({"url": url, "text": text_content})
return pd.DataFrame(rows)
このバージョンでは:
- JSON として読めたら、中を再帰的に探索して
"link","Entry ID","url"を探す。 - JSON として読めなかった場合は 正規表現で http(s)://... を拾う。
これなら URL をかなりの確率で拾えるはずです。
確認したいのですが、あなたの .json ファイルの中身は「1階層の dict」形式が多いですか?それとも「リストやネストされた dict」が多いですか?
user:
どっちも多いですね。いくらかURLを拾えるようになりましたが、もっと拾えるようにできませんか?
assistant:
なるほど、了解です。今の方法でも URL を拾える確率は上がりましたが、まだ取りこぼしがあるのですね。原因は大きく分けて2つです:
-
JSONとして読めないテキストに URL が書かれている場合、今は単純に
"http://"か"https://"から始まる文字列しか拾っていないこと。実際には"link": "..."のような書き方や、改行や全角スペースで区切られている場合もあります。 - ネスト構造やリストの深い中にある URL は、今の再帰探索でも取りこぼす場合があること。特に文字列として JSON 内に埋め込まれている場合は拾えません。
もっと拾うためのアプローチ:
-
正規表現を強化して URL らしきものを幅広く拾う
-
http:///https://以外にwww.から始まるものも対象にする -
"link"\s*:\s*".*?"など、"キー" : "値"の形式も正規表現で拾う
-
-
JSON 内文字列まで解析して URL を抜く
- JSON の値として文字列が入っている場合、その中に URL が埋まっていることがあるので、文字列も正規表現で検索する
改良版を作ってみました。これなら、JSON が正しく読める場合も、テキストとして読んだ場合も URL の取得率がかなり上がります:
import os
import glob
import json
import pandas as pd
import re
def extract_urls_from_text(text, keys=("link", "Entry ID", "url")):
"""
テキストから URL を幅広く抽出
"""
urls = set()
# "キー":"URL" の形式をまず探す
for key in keys:
pattern = re.compile(rf'"{key}"\s*:\s*"([^"]+)"', re.IGNORECASE)
urls.update(pattern.findall(text))
# http(s):// 形式の URL も拾う
urls.update(re.findall(r'https?://[^\s"\'<>]+', text))
# www. 形式も拾う
urls.update(re.findall(r'www\.[^\s"\'<>]+', text))
return list(urls)
def extract_url_from_data(data, keys=("link", "Entry ID", "url")):
"""
JSONデータの中から再帰的に URL を探索
"""
if isinstance(data, dict):
for k, v in data.items():
if k in keys and isinstance(v, str):
return v
else:
found = extract_url_from_data(v, keys)
if found:
return found
elif isinstance(data, list):
for item in data:
found = extract_url_from_data(item, keys)
if found:
return found
elif isinstance(data, str):
urls = extract_urls_from_text(data, keys)
if urls:
return urls[0]
return None
def load_jsons_as_text_with_url(path: str) -> pd.DataFrame:
rows = []
json_files = glob.glob(os.path.join(path, "*.json"))
for json_file in json_files:
url = None
text_content = None
try:
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
text_content = json.dumps(data, ensure_ascii=False)
url = extract_url_from_data(data)
# URL が見つからなければ、文字列中も探索
if not url:
url_list = extract_urls_from_text(text_content)
if url_list:
url = url_list[0]
except Exception:
# JSON 読み込み失敗時
with open(json_file, "r", encoding="utf-8", errors="ignore") as f:
text_content = f.read()
url_list = extract_urls_from_text(text_content)
if url_list:
url = url_list[0]
rows.append({"url": url, "text": text_content})
return pd.DataFrame(rows)
ポイント:
- JSON 内の文字列まで再帰的に検索
- 正規表現で
"link": "..."やhttp(s)://、www.形式の URL を幅広く拾う - URL が複数あってもとりあえず最初の1つを
urlに格納
この方法なら、かなりのファイルで URL が取得できるはずです。
希望をもう少し言うと、複数 URL をすべて拾って DataFrame に複数行として展開することも可能です。
その方が後で分析しやすいですが、やりますか?
user:
ありがとうございます。だいぶいい線いってますね。日本語の文字はURLに含まれません、という制限を加えてもらっていいですか。
assistant:
なるほど、URL に日本語文字(全角文字など)が入っていると拾わないようにしたいわけですね。
それなら正規表現を少し変えて、「ASCII 文字のみの URL」に限定すればOKです。
具体的には、先ほどの extract_urls_from_text を次のように修正します:
def extract_urls_from_text(text, keys=("link", "Entry ID", "url")):
"""
テキストから URL を幅広く抽出
- URL に日本語文字が含まれていたら除外
"""
urls = set()
# "キー":"URL" の形式をまず探す
for key in keys:
pattern = re.compile(rf'"{key}"\s*:\s*"([^"]+)"', re.IGNORECASE)
for u in pattern.findall(text):
if all(ord(c) < 128 for c in u): # ASCII のみ
urls.add(u)
# http(s):// 形式の URL
for u in re.findall(r'https?://[^\s"\'<>]+', text):
if all(ord(c) < 128 for c in u):
urls.add(u)
# www. 形式も同様
for u in re.findall(r'www\.[^\s"\'<>]+', text):
if all(ord(c) < 128 for c in u):
urls.add(u)
return list(urls)
ポイントはこの部分です:
if all(ord(c) < 128 for c in u)
- URL の文字列
uを 1 文字ずつチェック - ASCII(半角英数字・記号)だけなら採用
- 日本語や全角文字が入っていればスキップ
この修正を先ほどの load_jsons_as_text_with_url 関数に組み込めば、URL 列に日本語入りの URL は出てこなくなります。
希望なら、このバージョンを 最初から最後まで統合した完成版 を作ってお渡しすることもできます。作りますか?
user:
ありがとうございます。これで解決です。今夜はこの辺にしときましょう。ここまでの内容をブログにまとめたいので、タイトル案をいくつか考えていただけますか?
assistant:
うん、いい締めくくりですね。今回のテーマは「壊れたりフォーマットがバラバラな JSON をどうにか読み込んで、URL とテキストを pandas で整理する」でしたから、その軸でいくつかタイトルを考えてみました。
- 「壊れた JSON も怖くない:URL 抽出と全文テキストを DataFrame にまとめる方法」
- 「pandas で JSON をテキストとして扱う ― URL 抽出の実践」
- 「JSON が読めなくても大丈夫!URL と本文をきれいに整理する Python 関数」
- 「混在する JSON ファイル群から URL とテキストを抽出する pandas 活用術」
- 「JSON として読めないファイルも扱う:URL 抽出 + DataFrame 化の実装」
この中なら、検索性や実用感を考えると「壊れた JSON も怖くない…」がキャッチーですが、技術ブログとしては「混在する JSON ファイル群から…」が一番まじめで引きがあると思います。
どちらのトーンでまとめたいですか?キャッチー寄りか、実務寄りか。
user:
「壊れた JSON も怖くない:URL 抽出と全文テキストを DataFrame にまとめる方法」でいきましょう。このタイトルに沿って、ここまでの内容を詳細に解説してください。
assistant:
壊れた JSON も怖くない:URL 抽出と全文テキストを DataFrame にまとめる方法
目的:指定したフォルダ内の .json ファイル群を読み込み、
- 1列目:URL(
"link","Entry ID","url"のいずれか、または本文中に現れる URL) - 2列目:そのファイルから読み取った全文字列(JSON として読み込めた場合は
json.dumpsの結果、読み込めなければテキストそのもの)
という形の pandas.DataFrame を返す関数を作ります。
追加要件として「URL に日本語文字(全角文字など)が含まれるものは除外する(ASCII のみ許容)」という制約を入れます。
アプローチ(ざっくり)
-
各ファイルをまず
json.load()で読み込もうとする。- 成功したら:内部を再帰的に探索して
link/Entry ID/urlキーを探す。 - 見つからなければ、JSON を文字列化したもの(
json.dumps)に対して正規表現で URL を探す。
- 成功したら:内部を再帰的に探索して
-
json.load()が失敗(壊れたファイルや JSONL など)したら、テキストとして全文を読み、正規表現で URL を探す。 -
URL 抽出時は 日本語(非 ASCII)を含む候補を除外。また末尾の句読点などのゴミを取り除く。
-
最終的に
pandas.DataFrameを作成し、列順は['url', 'text']とする(ご指定に合わせる)。
実装(完成版コード)
以下がそのまま使える関数一式です。path に対象ディレクトリを渡すと DataFrame が返ります。
import os
import glob
import json
import re
from typing import List, Optional, Tuple
import pandas as pd
def _is_ascii(s: str) -> bool:
"""文字列が ASCII のみかを判定(True: ASCII のみ)。"""
try:
s.encode('ascii')
return True
except UnicodeEncodeError:
return False
def _normalize_candidate_url(u: str, ascii_only: bool = True) -> Optional[str]:
"""
候補 URL をトリムし、末尾の句読点を取り、ASCII 制約を満たすかチェックして返す。
不正なら None を返す。
"""
if not isinstance(u, str) or not u:
return None
u = u.strip().strip('\'"') # 周囲の引用符を除去
u = u.rstrip('.,;:!?)\]}\''"') # 末尾の余分な句読点を除去
if u.startswith('<') and u.endswith('>'):
u = u[1:-1].strip()
# 最低限の妥当性(スキームか www. かドメインっぽさ)
if not (u.startswith('http://') or u.startswith('https://') or u.startswith('www.') or '.' in u):
return None
if ascii_only and (not _is_ascii(u)):
return None
return u
def extract_urls_from_text(text: str,
keys: Tuple[str, ...] = ("link", "Entry ID", "url"),
ascii_only: bool = True) -> List[str]:
"""
テキストから URL 候補を幅広く抽出してリストで返す(重複除去済み、順序保持)。
- keys に対する "key":"value" 形式の抽出を優先
- 次に http(s)://...、www.... を抽出
- 抽出した文字列は _normalize_candidate_url でフィルタする
"""
if not text:
return []
found = []
# "key":"value"(シングル/ダブル両対応)
for key in keys:
pattern = re.compile(rf'["\']{re.escape(key)}["\']\s*:\s*["\']([^"\']+)["\']', re.IGNORECASE)
for m in pattern.findall(text):
u = _normalize_candidate_url(m, ascii_only=ascii_only)
if u and u not in found:
found.append(u)
# http(s):// 形式
for m in re.findall(r'https?://[^\s"\'<>]+', text):
u = _normalize_candidate_url(m, ascii_only=ascii_only)
if u and u not in found:
found.append(u)
# www. 形式(先頭/空白の後に現れることを想定)
for m in re.findall(r'(?:(?<=\s)|^)(www\.[^\s"\'<>]+)', text):
u = _normalize_candidate_url(m, ascii_only=ascii_only)
if u and u not in found:
found.append(u)
return found
def extract_url_from_data(data, keys: Tuple[str, ...] = ("link", "Entry ID", "url"),
ascii_only: bool = True) -> Optional[str]:
"""
JSON パース済みオブジェクト(dict/list/str)を再帰的に検索して、最初に見つかった URL を返す。
文字列値の中に潜む URL も正規表現で探す。
"""
if isinstance(data, dict):
for k, v in data.items():
# キー一致で文字列値ならそれを正規化して返す
if k in keys and isinstance(v, str):
u = _normalize_candidate_url(v, ascii_only=ascii_only)
if u:
return u
# 再帰探索
found = extract_url_from_data(v, keys=keys, ascii_only=ascii_only)
if found:
return found
elif isinstance(data, list):
for item in data:
found = extract_url_from_data(item, keys=keys, ascii_only=ascii_only)
if found:
return found
elif isinstance(data, str):
urls = extract_urls_from_text(data, keys=keys, ascii_only=ascii_only)
if urls:
return urls[0]
return None
def load_jsons_as_text_with_url(path: str,
keys: Tuple[str, ...] = ("link", "Entry ID", "url"),
ascii_only: bool = True,
recursive: bool = False,
filename_fallback: bool = False) -> pd.DataFrame:
"""
指定パスの .json ファイル群を読み込み、URL と全文テキストを持つ DataFrame を返す。
Parameters
----------
path : str
対象ディレクトリ(末尾に / を含めても可)。ファイル名にワイルドカードは不要。
keys : tuple
JSON 内で URL を示す可能性のあるキーのタプル。
ascii_only : bool
True の場合、日本語(非 ASCII)を含む URL 候補を除外する。
recursive : bool
True にするとサブディレクトリも再帰的に検索する(glob の ** を使用)。
filename_fallback : bool
URL が検出できなかった場合にファイル名を代わりに埋めるかどうか。
Returns
-------
pandas.DataFrame
列順は ['url', 'text']
"""
pattern = '**/*.json' if recursive else '*.json'
search_path = os.path.join(path, pattern)
json_files = glob.glob(search_path, recursive=recursive)
rows = []
for jf in json_files:
url = None
text_content = None
# まず JSON として読み込む試行
try:
with open(jf, 'r', encoding='utf-8') as f:
data = json.load(f)
# テキスト化(可読性のため ensure_ascii=False)
text_content = json.dumps(data, ensure_ascii=False)
# JSON 内を再帰探索して URL を探す
url = extract_url_from_data(data, keys=keys, ascii_only=ascii_only)
# 見つからなければ JSON を文字列として再度正規表現検出
if not url:
url_list = extract_urls_from_text(text_content, keys=keys, ascii_only=ascii_only)
if url_list:
url = url_list[0]
except Exception:
# JSON パース失敗時はテキストとして読む(エンコーディング問題は ignore)
with open(jf, 'r', encoding='utf-8', errors='ignore') as f:
text_content = f.read()
url_list = extract_urls_from_text(text_content, keys=keys, ascii_only=ascii_only)
if url_list:
url = url_list[0]
if not url and filename_fallback:
url = os.path.basename(jf)
rows.append({'url': url, 'text': text_content})
df = pd.DataFrame(rows, columns=['url', 'text'])
return df
使い方例
# ディレクトリ直下の .json を処理
df = load_jsons_as_text_with_url('/path/to/jsons')
# サブディレクトリも含めて再帰的に処理し、URLが見つからない場合はファイル名を代替値にする
df = load_jsons_as_text_with_url('/path/to/jsons', recursive=True, filename_fallback=True)
# 確認
print(df.shape)
print(df.head())
print(df['url'].nunique())
この関数は要求通り、返される DataFrame の1列目が URL(None が許容されます)、2列目がファイルの全文字列です。
実装上のポイント解説(なぜこうしたか)
-
まず
json.load()を試す理由:- 正しい JSON として読めた場合、その構造を使って確実に
link等のキーを取り出せる。文字列中にエスケープされた URL が埋まっている場合も検出しやすい。
- 正しい JSON として読めた場合、その構造を使って確実に
-
壊れたファイルはテキスト読みでカバー:
- 実務では部分的に壊れた JSON や JSONL、CSV 化されて拡張子だけ
.jsonになっているケースがあり得る。errors='ignore'で読み切って正規表現で抽出するのが現実的。
- 実務では部分的に壊れた JSON や JSONL、CSV 化されて拡張子だけ
-
正規表現と正規化:
-
https?://...やwww.の検出は基本だが、後段で末尾の句読点や山括弧<...>を取り除くことで誤検出を減らす。
-
-
ASCII(非日本語)制約:
- 「日本語文字を含む URL を除外する」という要望は
_is_asciiチェックで実現。Punycode(xn--)や%エンコードは ASCII の範囲なので問題なく残る。
- 「日本語文字を含む URL を除外する」という要望は
-
優先順位:
- JSON のキー(
"link": "...")をまず探し、それでダメなら本文中の URL を探す。確度の高いデータを優先的に採用するため。
- JSON のキー(
追加でできる改善・拡張案(すぐ使えるヒント)
-
すべての URL を列挙して
explode()する- 今の実装は「最初に見つかった URL」を
urlに入れます。すべての URL を出したい場合はextract_urls_from_textを用いてリストを作成し、df.explode('all_urls')する形で long 形式にできます。
- 今の実装は「最初に見つかった URL」を
-
エンコーディング検出
- 必要なら
chardetやcharset-normalizerを使って読み込み時のエンコーディングを推定し、errors='ignore'を減らすとデータ品質が向上します。
- 必要なら
-
並列処理
- ファイル数が非常に多ければ
concurrent.futures.ThreadPoolExecutorで並列化して高速化できます(I/O バウンドなのでスレッド有利)。
- ファイル数が非常に多ければ
-
正規化(正規化ライブラリ)
-
urllib.parseを使って URL を正規化(スキームの付与、末尾スラッシュ除去など)すると後処理が楽になります。
-
-
ログ/進捗表示
-
tqdmで進捗バーを出すと処理中の安心感が上がります。
-
注意点(現場でよくある落とし穴)
- 「URL とみなしたい表現」が非常に多様なため、完璧な抽出は困難。特に HTML の断片やエスケープが多い場合は正規表現で取りこぼすことがあります。
- 日本語ドメイン(国際化ドメイン名)を素の全角で保存しているファイルは今回の
ascii_only=Trueの設定で除外されます。必要ならascii_only=Falseに切り替え、 punycode 変換等で扱ってください。 - ファイルが巨大(数十 MB~)だとメモリや速度の点で工夫が必要(ストリーミング処理や chunk 読み)。
まとめ
- 壊れた JSON もテキスト読みで拾えばかなりの精度で URL を回収できます。
- 本稿の関数は「JSON として読み込める場合は構造を活用」「読み込めない場合は本文を正規表現で検索」を組み合わせ、かつ日本語を含む URL を除外する仕様になっています。
- 必要に応じて「全 URL を列挙する」「エンコーディングをきちんと検出する」「並列化する」などの拡張が可能です。
お疲れさまでした — 今夜はここまでで大丈夫ですね。必要なら、上のコードをあなたの環境で動かしやすい形に小分けしてお渡しします(サンプルファイルを想定したテストコードなど)。
user:
ありがとうございます。これで大丈夫です。今夜もお疲れ様でした!
assistant:
お疲れさまでした!無事に仕上がってよかったです。
壊れた JSON を相手にするのは骨が折れる作業だけど、その分データ整形の腕もぐんと鍛えられますね。
また明日以降も一緒に遊びましょう。
ゆっくり休んでくださいね 🌙