見出し画像

19【Pythonクローラー開発】APIを用いたNCBIデータ取得で直面した課題とその解決方法

1. はじめに

こんにちは!TechCommitメンバーの友季子です!

API(Application Programming Interface)を使ってNCBI(National Center for Biotechnology Information)のデータを取得するのは、非常に便利ですが、時にはエラーが発生することもありますよね。私も開発の過程でいくつかのトラブルに遭遇しましたが、その原因と対処方法をシェアしたいと思います。

今回は、私が実際に経験したエラーの原因と、それに対して行った対処法をまとめました。もし、NCBIのAPIを使っていて困っていることがあれば、この情報が少しでも役立てば嬉しいです

▼NCBIのWebサイト※アメリカ国立生物工学情報センター

それでは、いくつかのエラーとその解決方法についてお話ししますね。


2. 全体の完成コード(サンプル)とAPI周りのコード解説

まずは、全体のコードをご覧ください。以下のコードは、NCBIからデータを取得し、取得結果をExcelファイルとして保存するものです。
私はざっくりお伝えすると、PubMed APIを使用して、指定されたジャーナル、発行日、ボリューム、号数、および著者に基づいて論文データを取得し、そのデータをExcelファイルに保存します。また、追加のデータを取得するためにCitmatch API等も利用しています。

①全体のコード

import requests
import os
import pandas as pd
from datetime import datetime
from xml.etree import ElementTree as ET

def format_date(date_str):
    for fmt in ("%Y/%m/%d", "%Y年%m月%d日", "%Y年%m月", "%Y年"):
        try:
            return datetime.strptime(date_str, fmt).strftime("%Y/%m/%d")
        except ValueError:
            continue
    return date_str

def fetch_pubmed_data(journal=None, pdat=None, volume=None, issue=None, authors=None):
    base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
    db = "pubmed"
    retmode = "json"

    params = {
        "db": db,
        "retmode": retmode,
        "term": f"{journal}[journal]" if journal else None,
        "mindate": pdat,
        "maxdate": pdat,
        "volume": volume,
        "issue": issue,
        "author": authors
    }

    filtered_params = {key: value for key, value in params.items() if value}
    response = requests.get(base_url, params=filtered_params)
    if response.status_code == 200:
        return response.json()
    else:
        print(f"Error: {response.status_code}")
        print(response.text)
        return {"error": f"Failed to fetch data, status code: {response.status_code}", "details": response.text}

def fetch_article_details(uid):
    base_url = f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
    db = "pubmed"
    retmode = "xml"

    params = {
        "db": db,
        "retmode": retmode,
        "id": uid
    }

    response = requests.get(base_url, params=params)
    if response.status_code == 200:
        return response.text
    else:
        print(f"Error fetching article {uid}: {response.status_code}")
        return None

def parse_article_details(xml_data):
    root = ET.fromstring(xml_data)
    title = root.findtext(".//ArticleTitle")
    term = title.replace(" ", "%20")  # スペースを%20に置き換え
    link = f"https://pubmed.ncbi.nlm.nih.gov/?term={term}"
    
    return title, link

def fetch_citmatch_data(journal, volume, page):
    url = f"https://pubmed.ncbi.nlm.nih.gov/api/citmatch/?method=field&journal={journal}&volume={volume}&page={page}"
    response = requests.get(url)
    if response.status_code == 200:
        return response.json()
    else:
        print(f"Error: {response.status_code}")
        print(response.text)
        return {"error": f"Failed to fetch data, status code: {response.status_code}", "details": response.text}

def main():
    print("PubMed Search")
    journal = input("Enter journal name (or leave blank): ")
    pdat = input("Enter publication date (YYYY/MM/DD, YYYY年MM月DD日, etc., or leave blank): ")
    volume = input("Enter volume (or leave blank): ")
    issue = input("Enter issue (or leave blank): ")
    authors = input("Enter authors (format: Surname Initial, or leave blank): ")

    if not (journal or pdat or volume or issue or authors):
        print("Error: At least one search parameter must be provided.")
        return

    result = fetch_pubmed_data(journal, pdat, volume, issue, authors)
    
    if "error" in result:
        print(result)
        return
    
    titles = []
    links = []
    if "esearchresult" in result and "idlist" in result["esearchresult"]:
        for uid in result["esearchresult"]["idlist"]:
            details = fetch_article_details(uid)
            if details:
                title, link = parse_article_details(details)
                titles.append(title)
                links.append(link)
    
    # Fetch additional data using citmatch API
    citmatch_result = fetch_citmatch_data("Front Immunol", "13", "826091")
    if "result" in citmatch_result and "uids" in citmatch_result["result"]:
        for item in citmatch_result["result"]["uids"]:
            uid = item["pubmed"]
            details = fetch_article_details(uid)
            if details:
                title, link = parse_article_details(details)
                titles.append(title)
                links.append(link)
    
    data = {
        'Title': titles,
        'Link': links
    }

    df = pd.DataFrame(data)

    output_directory = os.path.join(os.path.expanduser('~'), 'Documents')
    output_file_path = os.path.join(output_directory, 'ncbi_result_info_api.xlsx')

    df.to_excel(output_file_path, index=False, engine='openpyxl')

    print(f"Excelファイル '{output_ile_path}' に保存されたよ!")

if __name__ == "__main__":
    main() #2024_08 :09_ncbiのデータ取得でAPI版成功_LINKの文字列を作成してデータとれる_テスト進めてPROに出す

②APIまわりの解説をコメントで綴りました。

import requests
import os
import pandas as pd
from datetime import datetime
from xml.etree import ElementTree as ET

def format_date(date_str):
    for fmt in ("%Y/%m/%d", "%Y年%m月%d日", "%Y年%m月", "%Y年"):
        try:
            return datetime.strptime(date_str, fmt).strftime("%Y/%m/%d")
        except ValueError:
            continue
    return date_str

def fetch_pubmed_data(journal=None, pdat=None, volume=None, issue=None, authors=None):
    base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"  # PubMed Search APIのエンドポイントURL
    db = "pubmed"  # データベースとしてPubMedを指定
    retmode = "json"  # レスポンス形式をJSONに指定

    params = {
        "db": db,  # 使用するデータベース
        "retmode": retmode,  # レスポンス形式
        "term": f"{journal}[journal]" if journal else None,  # ジャーナル名で検索する場合のクエリ
        "mindate": pdat,  # 発行日(最小日付)
        "maxdate": pdat,  # 発行日(最大日付)
        "volume": volume,  # ボリューム番号
        "issue": issue,  # イシュー番号
        "author": authors  # 著者名
    }

    filtered_params = {key: value for key, value in params.items() if value}
    response = requests.get(base_url, params=filtered_params)
    if response.status_code == 200:
        return response.json()  # 成功時はJSONデータを返す
    else:
        print(f"Error: {response.status_code}")
        print(response.text)
        return {"error": f"Failed to fetch data, status code: {response.status_code}", "details": response.text}

def fetch_article_details(uid):
    base_url = f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"  # PubMed Fetch APIのエンドポイントURL
    db = "pubmed"  # データベースとしてPubMedを指定
    retmode = "xml"  # レスポンス形式をXMLに指定

    params = {
        "db": db,  # 使用するデータベース
        "retmode": retmode,  # レスポンス形式
        "id": uid  # 取得する記事のID
    }

    response = requests.get(base_url, params=params)
    if response.status_code == 200:
        return response.text  # 成功時はXMLデータを返す
    else:
        print(f"Error fetching article {uid}: {response.status_code}")
        return None

def parse_article_details(xml_data):
    root = ET.fromstring(xml_data)
    title = root.findtext(".//ArticleTitle")  # XMLから記事タイトルを抽出
    term = title.replace(" ", "%20")  # スペースを%20に置き換え、URLエンコード用
    link = f"https://pubmed.ncbi.nlm.nih.gov/?term={term}"  # PubMedのリンクを生成
    
    return title, link

def fetch_citmatch_data(journal, volume, page):
    url = f"https://pubmed.ncbi.nlm.nih.gov/api/citmatch/?method=field&journal={journal}&volume={volume}&page={page}"  # Citmatch APIのエンドポイントURL
    response = requests.get(url)  # Citmatch APIへのリクエストを送信
    
    if response.status_code == 200:
        return response.json()  # 成功時はJSONデータを返す
    else:
        print(f"Error: {response.status_code}")
        print(response.text)
        return {"error": f"Failed to fetch data, status code: {response.status_code}", "details": response.text}

def main():
    print("PubMed Search")
    journal = input("Enter journal name (or leave blank): ")
    pdat = input("Enter publication date (YYYY/MM/DD, YYYY年MM月DD日, etc., or leave blank): ")
    volume = input("Enter volume (or leave blank): ")
    issue = input("Enter issue (or leave blank): ")
    authors = input("Enter authors (format: Surname Initial, or leave blank): ")

    if not (journal or pdat or volume or issue or authors):
        print("Error: At least one search parameter must be provided.")
        return

    result = fetch_pubmed_data(journal, pdat, volume, issue, authors)  # PubMed Search APIを使用して検索
    
    if "error" in result:
        print(result)
        return
    
    titles = []
    links = []
    if "esearchresult" in result and "idlist" in result["esearchresult"]:
        for uid in result["esearchresult"]["idlist"]:
            details = fetch_article_details(uid)  # PubMed Fetch APIを使用して文献の詳細を取得
            if details:
                title, link = parse_article_details(details)
                titles.append(title)
                links.append(link)
    
    # Fetch additional data using citmatch API
    citmatch_result = fetch_citmatch_data("Front Immunol", "13", "826091")  # Citmatch APIを使用して追加データを取得
    if "result" in citmatch_result and "uids" in citmatch_result["result"]:
        for item in citmatch_result["result"]["uids"]:
            uid = item["pubmed"]
            details = fetch_article_details(uid)  # PubMed Fetch APIを使用して文献の詳細を取得
            if details:
                title, link = parse_article_details(details)
                titles.append(title)
                links.append(link)
    
    data = {
        'Title': titles,
        'Link': links
    }

    df = pd.DataFrame(data)

    output_directory = os.path.join(os.path.expanduser('~'), 'Documents')
    output_file_path = os.path.join(output_directory, 'ncbi_result_info_api.xlsx')

    df.to_excel(output_file_path, index=False, engine='openpyxl')

    print(f"Excelファイル '{output_file_path}' に保存されたよ!")

if __name__ == "__main__":
    main()

3.API周りの仕様

①PubMed E-Utilities API (esearch.fcgi)

目的: PubMedデータベースで文献を検索し、文献IDを取得します。

def fetch_pubmed_data(journal=None, pdat=None, volume=None, issue=None, authors=None): base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi" db = "pubmed" retmode = "json" params = { "db": db, "retmode": retmode, "term": f"{journal}[journal]" if journal else None, "mindate": pdat, "maxdate": pdat, "volume": volume, "issue": issue, "author": authors } filtered_params = {key: value for key, value in params.items() if value} response = requests.get(base_url, params=filtered_params) if response.status_code == 200: return response.json() else: print(f"Error: {response.status_code}") print(response.text) return {"error": f"Failed to fetch data, status code: {response.status_code}", "details": response.text}

解説:

  • base_url: esearch.fcgiはPubMedで文献を検索するエンドポイントです。

  • params: リクエストパラメータにはデータベース (db)、返却形式 (retmode)、検索クエリ (term)、日付範囲 (mindate, maxdate)、巻号 (volume)、号 (issue)、著者 (author) があります。

  • requests.get(): APIリクエストを送信し、レスポンスをJSON形式で受け取ります。

②PubMed E-Utilities API (efetch.fcgi)

目的: 文献IDを基に文献の詳細情報を取得します。

def fetch_article_details(uid): base_url = f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi" db = "pubmed" retmode = "xml" params = { "db": db, "retmode": retmode, "id": uid } response = requests.get(base_url, params=params) if response.status_code == 200: return response.text else: print(f"Error fetching article {uid}: {response.status_code}") return None

解説

  • base_url: efetch.fcgiは文献の詳細情報を取得するエンドポイントです。

  • params: リクエストパラメータにはデータベース (db)、返却形式 (retmode)、文献ID (id) があります。

  • requests.get(): APIリクエストを送信し、レスポンスをXML形式で受け取ります。

③PubMed Citmatch API

目的: 特定のジャーナル、巻号、ページに基づいて文献を検索します。

def fetch_citmatch_data(journal, volume, page): url = f"https://pubmed.ncbi.nlm.nih.gov/api/citmatch/?method=field&journal={journal}&volume={volume}&page={page}" response = requests.get(url) if response.status_code == 200: return response.json() else: print(f"Error: {response.status_code}") print(response.text) return {"error": f"Failed to fetch data, status code: {response.status_code}", "details": response.text}

解説:

  • url: citmatchエンドポイントを使って、指定されたジャーナル、巻号、ページに基づく文献を検索します。

  • requests.get(): APIリクエストを送信し、レスポンスをJSON形式で受け取ります。

これらのAPIを組み合わせて、PubMedから文献の検索と詳細取得を行い、結果をExcelファイルに保存しています。

4.NCBI APIのエラーとトラブルシューティング

APIの設定で悩んだこと共有

  • APIの設定が上手くいかない問題があるとき、下記の対応をしました。

①Excelに出力されたLINK文字列を開いてみると開けない・・・

【対処方法】URLが以下のサンプルリクエストのようになっているか確認する。

#URLが以下のサンプルリクエストのようになっているか確認する。

https://pubmed.ncbi.nlm.nih.gov/api/citmatch/?method=field&journal=Front%20Immunol&volume=13&page=826091

②クリックが上手くいかないときや、スクレイピングがうまくできない・・・

【対処方法】取得したい値がどこまで正しく取れているのかprint文を用いてデバッグする。

③Excelで出力できない問題のコード※実装の修正

【対処方法】65行目あたりを以下に修正したら、Excel出力できました。

link = f"https://pubmed.ncbi.nlm.nih.gov/{title}/"

④ターミナルにジャーナル名を一部だけいれてデータ取得が出来るようにもしたいが、うまくいかない

(※最終的にExcel出力したLink列をブラウザに入力して、データ取得をしたいが、開発の段階ではターミナルに出力してデータ取得を確認していた状況です。)
【対処方法】APIの仕様書通り、Linkの文字列を生成してあげるようにする。
サンプルでは以下の通りとなっていました。

リクエスト
https://pubmed.ncbi.nlm.nih.gov/api/citmatch/?method=field&journal=Front%20Immunol&volume=13&page=826091
レスポンス
{"version":"1.0","operation":"citmatch","success":true,"result":{"count":1,"type":"uids","uids":[{"pubmed":"35251006"}]}}

※リクエストをブラウザで入力すると、上記レスポンスが返ってくることも確認できました。

5.スクレイピングのトラブルシューティング

  1. エラーメッセージの確認

    • エラーメッセージは問題の手がかりになります。例えば、認証エラー、リクエストフォーマットの誤り、アクセス制限などの情報が含まれることがあります。

  2. URLとパラメータの確認

    • URLが正しいか、必要なパラメータがすべて含まれているか確認します。URLエンコードが必要な場合もあります。

  3. HTTPリクエストの確認

    • リクエストメソッド(GETやPOSTなど)が正しいか、リクエストヘッダーやクエリパラメータが適切に設定されているか確認します。

  4. デバッグ

    • スクリプトにprint文やログを追加し、実際に送信しているリクエストや受け取ったレスポンスを確認します。Jupyter Notebookを使用している場合は、セルごとに実行してデバッグするのが便利です。

  5. APIドキュメントの確認

    • APIの公式ドキュメントを確認し、仕様が変更されていないか、使用方法が正しいか確認します。

  6. サンプルコードのテスト

    • APIのサンプルコードや公式のテストコードを試して、基本的な動作を確認します。

6.公式サイト

以上です。
あなたのお役に立てば嬉しいです!


この記事が気に入ったらサポートをしてみませんか?