ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English
ZoteroのAPIとStreamlitを使ったアプリ開発

ZoteroのAPIとStreamlitを使ったアプリ開発

概要 ZoteroのAPIとStreamlitを使ったアプリを試作しました。 https://nakamura196-zotero.streamlit.app/ 本記事は、このアプリ開発におけるメモです。 Streamlit 以下の記事がとても参考になりました。 https://qiita.com/sypn/items/80962d84126be4092d3c ZoteroのAPI ZoteroのAPIについて、以下で説明されています。 https://www.zotero.org/support/dev/web_api/v3/start 今回は上記のページで紹介されている以下のライブラリを使用しました。 https://github.com/urschrei/pyzotero APIの利用にあたっては、personal library IDやAPI keyを取得する必要がありますが、READMEのQuickstartの手順に従うと、それらを取得することができました。 以下は、API keyを発行した際の画面です。 Streamlitを用いた開発 以下のリポジトリでソースコードを公開しています。 https://github.com/nakamura196/zotero_streamlit 機密情報 機密情報は、/.streamlit/secrets.tomlというファイルを作成し、そこに記載するようでした。.gitignoreに記載することも忘れないようにしてください。 [zotero] library_id="xxx" library_type="user" api_key="xxx" そして、以下のように呼び出すことができました。 def init_zotero(): library_id = st.secrets["zotero"]["library_id"] library_type = st.secrets["zotero"]["library_type"] api_key = st.secrets["zotero"]["api_key"] return zotero.Zotero(library_id, library_type, api_key) Zoteroライブラリのコレクション一覧の取得 以下ので、Zoteroライブラリからコレクションの一覧を取得することができました。 def fetch_collections(zot): """ Zoteroライブラリからコレクション一覧を取得する """ collections = zot.collections() # 各コレクションからタイトルとキー(ID)を取得 collection_list = [{"name": collection['data']['name'], "key": collection['data']['key']} for collection in collections] return collection_list コレクション内のアイテムの取得 以下でコレクション内のアイテムを取得できました。itemTypeがattachmentのものはスキップする処理を加えています。 # 文献データをDataFrameに変換 def create_df(zot, collection_id): if not collection_id: return pd.DataFrame() try: items = zot.collection_items(collection_id) rows = [{ 'title': item['data']['title'], "itemType": item['data']['itemType'], "creators": ", ".join(f"{creator['firstName']} {creator['lastName']}" for creator in item['data'].get('creators', [])), "date": item['data'].get('date', "") } for item in items if item['data']['itemType'] != "attachment"] return pd.DataFrame(rows) except Exception as e: st.error(f"Failed to load items from collection: {e}") return pd.DataFrame() メタタグ設定 以下のような形で、タイトルなどのページ設定を行うことができました。 ...

Content Negotiationを使って、PythonでURIからRDFを取得する

Content Negotiationを使って、PythonでURIからRDFを取得する

概要 WikidataのエンティティのURIからRDFデータを取得する機会がありましたので、備忘録です。 Content Negotiationを使用しない まず以下のように、headersを空のままリクエストします。 import requests # URL for the Wikidata entity in RDF format url = "http://www.wikidata.org/entity/Q12418" headers = { } # Sending a GET request to the URL response = requests.get(url, headers=headers) # Checking if the request was successful if response.status_code == 200: text = response.text print(text[:5000]) else: print("Failed to retrieve RDF data. Status code:", response.status_code) この場合、以下のように、json形式のテキストデータを取得することができます。 {"entities":{"Q12418":{"pageid":14002,"ns":0,"title":"Q12418","lastrevid":2176343952,"modified":"2024-06-11T11:43:44Z","type":"item","id":"Q12418","labels":{"fr":{"language":"fr","value":"La Joconde"},"es":{"language":"es","value":"La Gioconda"},"en":{"language":"en","value":"Mona Lisa"},"af":{"language":"af","value":"Mona Lisa"},"am":{"language":"am","value":"\u121e\u1293 \u120a\u12db"},"ar": ... ただし、求めているRDFデータは取得できません。 Content Negotiationを使用する 次にheadersにRDF/XMLを指定します。 import requests from lxml import etree # URL for the Wikidata entity in RDF format url = "http://www.wikidata.org/entity/Q12418" # Headers to request RDF/XML format headers = { 'Accept': 'application/rdf+xml' } # Sending a GET request to the URL response = requests.get(url, headers=headers) # Checking if the request was successful if response.status_code == 200: tree = etree.fromstring(response.content) # Convert _Element to _ElementTree tree = etree.ElementTree(tree) # Now you can use the write method tree.write("output.xml", pretty_print=True, xml_declaration=True, encoding='UTF-8') else: print("Failed to retrieve RDF data. Status code:", response.status_code) 結果、以下のようにRDFデータを取得することができました。 ...

iiif-prezi3を試す

iiif-prezi3を試す

概要 IIIF Presentation API 3が普及しつつありますが、その仕様を理解しつつ、JSONファイルを直接作成することが難しく感じるようになりました。 そこで、以下のPythonライブラリを使用してみましたので、備忘録です。 https://github.com/iiif-prezi/iiif-prezi3 以下の記事で紹介した東寺百合文書WEBで公開されているデータのIIIFへの変換にあたり、本ライブラリを使用しています。 読みにくいもので恐縮ですが、ソースコードも以下のリポジトリで公開していますので、参考になりましたら幸いです。 https://github.com/nakamura196/toji_iiif コレクションの作成 以下のようなコードにより、IIIFコレクションを作成できました。 import iiif_prezi3 iiif_prezi3.config.configs['helpers.auto_fields.AutoLang'].auto_lang = "ja" collection = iiif_prezi3.Collection( id=f"{origin}/set/3/collection.json", label="東寺百合文書", viewingDirection="right-to-left", provider=iiif_prezi3.ProviderItem( id=self.homepage, label=self.attribution, ), homepage=iiif_prezi3.HomepageItem( id=self.homepage, type="Text", label=self.attribution, format="text/html", language="ja" ), metadata=[ iiif_prezi3.KeyValueString(label="Attribution", value=self.attribution), iiif_prezi3.KeyValueString(label="Rights", value=self.rights), ], rights=self.rights, ) opath = f"{self.docs_dir}/iiif/set/3/collection.json" os.makedirs(os.path.dirname(opath), exist_ok=True) with open(opath, "w") as f: f.write(collection.json(ensure_ascii=False, indent=2 if IS_DEBUG else None)) iiif_prezi3.config.configs['helpers.auto_fields.AutoLang'].auto_langにjaを与えることで、labelやmetadataの言語フィールドがjaになりました。 { "@context": "http://iiif.io/api/presentation/3/context.json", "id": "https://nakamura196.github.io/toji_iiif/iiif/set/3/collection.json", "type": "Collection", "label": { "ja": [ "東寺百合文書" ] }, "metadata": [ { "label": { "ja": [ "Attribution" ] }, "value": { "ja": [ "京都府立京都学・歴彩館 東寺百合文書WEB" ] } }, { "label": { "ja": [ "Rights" ] }, "value": { "ja": [ "https://creativecommons.org/licenses/by/2.1/jp/" ] } } ], "rights": "https://creativecommons.org/licenses/by/2.1/jp/", "provider": [ { "id": "https://hyakugo.pref.kyoto.lg.jp/", "type": "Agent", "label": { "ja": [ "京都府立京都学・歴彩館 東寺百合文書WEB" ] } } ], "homepage": [ { "id": "https://hyakugo.pref.kyoto.lg.jp/", "type": "Text", "label": { "ja": [ "京都府立京都学・歴彩館 東寺百合文書WEB" ] }, "format": "text/html", "language": [ "ja" ] } ], "items": [ { "id": "https://nakamura196.github.io/toji_iiif/iiif/3/1/manifest.json", "label": { "ja": [ "イ函/1/:山城国紀伊郡司解案" ] }, "type": "Manifest" } ] } iiif_prezi3.KeyValueString関数を使用することで、フィールドや値が配列として出力される点も有用かと思いました。 ...

「ARC2によるRDFグラフの視覚化」をPythonで利用する

「ARC2によるRDFグラフの視覚化」をPythonで利用する

概要 神崎正英氏が公開する「ARC2によるRDFグラフの視覚化」をPythonで利用させていただく機会がありましたので、備忘録です。 「ARC2によるRDFグラフの視覚化」の公開ページは以下です。 https://www.kanzaki.com/works/2009/pub/graph-draw Turtle、RDF/XML、JSON-LD、TriGもしくはMicrodataで記述したRDFを入力として、可視化結果をpngやsvgファイルで取得することができます。 Pythonでの利用例 import requests text = "@prefix ns1: <http://example.org/propery/> .\n\n<http://example.org/bbb> ns1:aaa \"ccc\" ." output_path = "./graph.png" # POSTリクエストに必要なデータ url = "https://www.kanzaki.com/works/2009/pub/graph-draw" data = { "RDF": text, "rtype": "turtle", "gtype": "png", "rankdir": "lr", "qname": "on", } # POSTリクエストを送信 response = requests.post(url, data=data) # 応答がPNG画像でない場合、内容を確認 if response.headers['Content-Type'] != 'image/png': print("応答はPNG画像ではありません。内容を表示します:") # print(response.text[:500]) # 最初の500文字を表示 # [:500] else: os.makedirs(os.path.dirname(output_path), exist_ok=True) # 応答をPNGファイルとして保存 with open(output_path, 'wb') as f: f.write(response.content) まとめ RDFデータの可視化にあたり、参考になりましたら幸いです。

Hugging Face SpacesとYOLOv5モデル(NDL-DocLデータセットで学習済み)を使った推論アプリの修正

Hugging Face SpacesとYOLOv5モデル(NDL-DocLデータセットで学習済み)を使った推論アプリの修正

概要 以下の記事でHugging Face Spacesと、以下の記事で紹介したYOLOv5モデル(NDL-DocLデータセットで学習済み)を使った推論アプリを紹介しました。 このアプリが動作しなくなっていたため、動作するように修正しました。 https://huggingface.co/spaces/nakamura196/yolov5-ndl-layout この修正で行なった対応についてメモします。 修正点 修正を加えたapp.pyは以下です。 import gradio as gr from PIL import Image import yolov5 import json model = yolov5.load("nakamura196/yolov5-ndl-layout") def yolo(im): results = model(im) # inference df = results.pandas().xyxy[0].to_json(orient="records") res = json.loads(df) im_with_boxes = results.render()[0] # results.render() returns a list of images # Convert the numpy array back to an image output_image = Image.fromarray(im_with_boxes) return [ output_image, res ] inputs = gr.Image(type='pil', label="Original Image") outputs = [ gr.Image(type="pil", label="Output Image"), gr.JSON() ] title = "YOLOv5 NDL-DocL Datasets" description = "YOLOv5 NDL-DocL Datasets Gradio demo for object detection. Upload an image or click an example image to use." article = "<p style='text-align: center'>YOLOv5 NDL-DocL Datasets is an object detection model trained on the <a href=\"https://github.com/ndl-lab/layout-dataset\">NDL-DocL Datasets</a>.</p>" examples = [ ['『源氏物語』(東京大学総合図書館所蔵).jpg'], ['『源氏物語』(京都大学所蔵).jpg'], ['『平家物語』(国文学研究資料館提供).jpg'] ] demo = gr.Interface(yolo, inputs, outputs, title=title, description=description, article=article, examples=examples) demo.launch(share=False) まず、Gradioのバージョンアップに伴い、gr.inputs.Imageをgr.Imageなどに変更しました。 ...

PythonでXML文字列を整形する

PythonでXML文字列を整形する

概要 PythonでXML文字列を整形するプログラムの備忘録です。 プログラム1 以下を参考にしました。 https://hawk-tech-blog.com/python-learn-prettyprint-xml/ 不要な空行を削除する処理などを加えています。 from xml.dom import minidom import re def prettify(rough_string): reparsed = minidom.parseString(rough_string) pretty = re.sub(r"[\t ]+\n", "", reparsed.toprettyxml(indent="\t")) # インデント後の不要な改行を削除 pretty = pretty.replace(">\n\n\t<", ">\n\t<") # 不要な空行を削除 pretty = re.sub(r"\n\s*\n", "\n", pretty) # 連続した改行(空白行を含む)を単一の改行に置換 return pretty プログラム2 以下を参考にしました。 https://qiita.com/hrys1152/items/a87b4ca3c74ec4997f66 TEI/XMLを処理する場合には、名前空間の登録をおすすめします。 import xml.etree.ElementTree as ET # 名前空間の登録 ET.register_namespace('', "http://www.tei-c.org/ns/1.0") tree = ET.ElementTree(ET.fromstring(xml_string)) ET.indent(tree, space=' ') tree.write('output.xml', encoding='UTF-8', xml_declaration=True) まとめ 参考になりましたら幸いです。

「SAT大蔵経DB 2018」で公開されているテキストの分析例

「SAT大蔵経DB 2018」で公開されているテキストの分析例

概要 「SAT大蔵経DB 2018」は以下のように説明されています。 https://21dzk.l.u-tokyo.ac.jp/SAT2018/master30.php このサイトは、SAT大蔵経テキストデータベース研究会が提供するデジタル研究環境の2018年版です。 SAT大蔵経テキストデータベース研究会は、2008年4月より、大正新脩大藏経テキスト部分85巻の全文検索サービスを提供するとともに、各地のWebサービスとの連携機能を提供することにより、利便性を高めるとともに、Webにおける人文学研究環境の可能性を追求してきました。 2018年版となるSAT2018では、近年広まりつつある機械学習の技術と、IIIFによる高精細画像との連携、高校生でもわかる現代日本語訳の公開及び本文との連携、といった新たなサービスに取り組んでみました。また、本文の漢字をUnicode10.0に対応させるとともに、すでに公開していたSAT大正蔵図像DBの機能の大部分も統合いたしました。ただし、今回は、コラボレーションを含む仕組みの提供という側面もあり、今後は、この輪組に沿ってデータを増やし、より利便性を高めていくことになります。 当研究会が提供するWebサービスは、さまざまな関係者が提供するサービスや支援に依拠しています。SAT2018で新たに盛り込んだサービスでは、機械学習及びIIIF対応に関しては一般財団法人人文情報学研究所、現代日本語訳の作成に関しては公益財団法人全日本仏教会の支援と全国の仏教研究者の方々のご協力をいただいております。 SAT2018が、仏教研究者のみなさまだけでなく、仏典に関心を持つ様々な方々のお役に立つことを願っております。さらに、ここで提示されている文化資料への技術の適用の仕方が、人文学研究における一つのモデルになることがあれば、なお幸いです。 今回は、上記のDBが公開するテキストデータを対象として、簡単な分析を試みます。 説明 以下の「T0220 大般若波羅蜜多經」のテキストを対象にします。 方法 テキストデータの取得 ネットワークを確認したところ、以下のようなURLでテキストデータを取得することができました。 https://21dzk.l.u-tokyo.ac.jp/SAT2018/satdb2018pre.php?mode=detail&ob=1&mode2=2&useid=0220_,05,0001 0220_,05,0001の部分について、05を06に変えると6巻のデータが取得できました。また、末尾の0001を0011に変更すると、0011の前後を含むテキストが取得できました。 この傾向を踏まえて、以下のようなプログラムを実行しました。 import os import requests import time from bs4 import BeautifulSoup def fetch_soup(url): """Fetches and parses HTML content from the given URL.""" time.sleep(1) # Sleep for 1 second before making a request response = requests.get(url) return BeautifulSoup(response.text, "html.parser") def write_html(soup, filepath): """Writes the prettified HTML content to a file.""" with open(filepath, "w") as file: file.write(soup.prettify()) def read_html(filepath): """Reads HTML content from a file and returns its parsed content.""" with open(filepath, "r") as file: return BeautifulSoup(file.read(), "html.parser") def process_volume(vol): """Processes each volume by iterating over pages until no new page is found.""" page_str = "0001" while True: url = f"https://21dzk.l.u-tokyo.ac.jp/SAT2018/satdb2018pre.php?mode=detail&ob=1&mode2=2&useid=0220_{vol}_{page_str}" id = url.split("useid=")[1] opath = f"html/{id}.html" if os.path.exists(opath): soup = read_html(opath) else: soup = fetch_soup(url) write_html(soup, opath) new_page_str = get_last_page_id(soup) if new_page_str == page_str: break page_str = new_page_str def get_last_page_id(soup): """Extracts the last page ID from the soup object.""" spans = soup.find_all("span", class_="ln") if spans: last_id = spans[-1].text return last_id.split(".")[-1][0:4] return None def main(): vols = ["05", "06", "07"] for vol in vols: process_volume(vol) if __name__ == "__main__": main() 上記の処理により、HTMLファイルをダウンロードすることができます。 ...

researchmapのapiを使う

researchmapのapiを使う

概要 researchmapのapiを使って、業績リストを作成する機会がありましたので、備忘録です。 researchmapのapiに対するクエリ例 researchmapのapiに対するクエリ例をいくつか紹介します。 論文の一覧を取得する https://api.researchmap.jp/nakamura.satoru/published_papers 上限を指定する(limitの使用) https://api.researchmap.jp/nakamura.satoru/published_papers?limit=5 x件以降の結果を取得する(startの使用) https://api.researchmap.jp/nakamura.satoru/published_papers?limit=5&start=6 出版年月日を指定する(from_dateとto_date) https://api.researchmap.jp/nakamura.satoru/published_papers?from_date=2023-04-01&to_date=2024-03-31 Pythonでの使用例 指定したユーザと出版年月日に基づき、published_papersとpresentationsをExcelに書き出します。 #| export import requests import pandas as pd import os class Client: def __init__(self, slug, date_start, date_end): self.slug = slug self.date_start = date_start self.date_end = date_end self.output_dir = f"data/{self.slug}/{self.date_start}_{self.date_end}" os.makedirs(self.output_dir, exist_ok=True) @staticmethod def main(slug, date_start, date_end): client = Client(slug, date_start, date_end) client.process_data() def process_data(self): self.df_paper = self.fetch_data('published_papers', self.paper_processing_logic) self.df_presentation = self.fetch_data('presentations', self.presentation_processing_logic) self.write_to_excel() def fetch_data(self, data_type, processing_function): url = f"https://api.researchmap.jp/{self.slug}/{data_type}" params = { "limit": 100, "start": 0, "from_date": self.date_start, "to_date": self.date_end, } response = requests.get(url, params=params) if response.status_code == 200: data = response.json().get("items", []) return processing_function(data) else: raise Exception(f"Error fetching {data_type}: {response.status_code}") def paper_processing_logic(self, papers): rows = [] for item in papers: rows.append(self.process_paper_item(item)) return pd.DataFrame(rows) def process_paper_item(self, item): author_list = [auth["name"] for auth in item.get('authors', {}).get("ja", [])] c1 = '''1.掲載論文のDOI (デジタルオブジェクト識別子)''' c2 = '''2.著者名''' c3 = '''3.論文標題''' c4 = '''4.雑誌名''' c5 = '''5.巻 (半角数字)''' c6 = '''6.発行年 (半角数字)''' c7 = '''7.最初と最後の頁 (半角数字)''' c8 = '''8.査読の有無 (1:有 0:無)''' c9 = '''9.国際共著 (1:有 0:無)''' c10 = '''10.オープンアクセス (1:有 0:無)''' return { c1: item.get('identifiers', {}).get('doi', [None])[0], c2: ", ".join(author_list), c3: item.get('paper_title', {}).get('ja', ''), c4: item.get('publication_name', {}).get('ja', ''), c5: item.get('volume', None), c6: item['publication_date'][:4], c7: f"{item.get('starting_page', '')}-{item.get('ending_page', '')}", c8: 1 if item.get('referee', False) else 0, c9: 1 if item.get('is_international_collaboration', False) else 0, c10: 1 if item.get('rm:is_open_access', False) else 0 } def presentation_processing_logic(self, presentations): rows = [] for item in presentations: rows.append(self.process_presentation_item(item)) return pd.DataFrame(rows) def process_presentation_item(self, item): author_list = [auth["name"] for auth in item.get('presenters', {}).get("ja", [])] c1 = '''1.発表者名''' c2 = "2.発表標題" c3 = "3.学会等名" c4 = '''4.発表年(開始) (半角数字)''' c5 = '''5.発表年(終了) (半角数字)''' c6 = '''6.招待講演 (1:有 0:無)''' c7 = '''7.国際学会 (1:有 0:無)''' return { c1: ", ".join(author_list), c2: item.get('presentation_title', {}).get('ja', ''), c3: item.get('event', {}).get('ja', ''), c4: item['publication_date'][:4], c5: item['publication_date'][:4], c6: 1 if item.get('invited', False) else 0, c7: 1 if item.get('is_international_presentation', False) else 0 } def write_to_excel(self): with pd.ExcelWriter(f'{self.output_dir}/merged.xlsx', engine='openpyxl') as writer: self.df_paper.to_excel(writer, sheet_name='Papers', index=False) self.df_presentation.to_excel(writer, sheet_name='Presentations', index=False) self.df_paper.to_csv(f"{self.output_dir}/papers.csv", index=False) self.df_presentation.to_csv(f"{self.output_dir}/presentations.csv", index=False) 使用例は以下です。dataフォルダにcsvやexcelが出力されます。 ...

AIPscanを試す

AIPscanを試す

概要 以下のツールを試します。 https://github.com/artefactual-labs/AIPscan 本ツールは以下のように説明されています。 AIPscan was developed to provide a more in-depth reporting solution for Archivematica users. It crawls METS files from AIPs in the Archivematica Storage Service to generate tabular and visual reports about repository holdings. It is designed to run as a stand-alone add-on to Archivematica. It only needs a valid Storage Service API key to fetch source data. AIPscanは、Archivematicaユーザーにより詳細なレポーティングソリューションを提供するために開発されました。それはArchivematicaストレージサービスのAIPからMETSファイルをクロールして、リポジトリ保有物についての表形式および視覚的レポートを生成します。これはArchivematicaへのスタンドアロンアドオンとして実行されるように設計されています。ソースデータを取得するために有効なストレージサービスAPIキーのみが必要です。 Archivematicaのストレージサービスに格納されたAIPの内容を視覚化します。 本記事は、その使い方について説明します。 セットアップ 以下の記載の通りに進めることでセットアップすることができました。 https://github.com/artefactual-labs/AIPscan?tab=readme-ov-file#installation 試す:ストレージサービスの追加 Archivematica Storage Servicesメニューをクリックし、以下にアクセスします。画面下部のNew storage serviceボタンをクリックします。 ...

デジタル源氏物語における校異源氏物語と現代語訳の対応づけ

デジタル源氏物語における校異源氏物語と現代語訳の対応づけ

概要 「デジタル源氏物語」は『源氏物語』に関する様々な関連データを収集・作成し、それらを結びつけることで、『源氏物語』研究はもちろん、古典籍を利用した教育・研究活動の一助となる環境の提案を目指したサイトです。 https://genji.dl.itc.u-tokyo.ac.jp/ 本サイトが提供する機能の一つとして、「校異源氏物語と現代語訳の対応づけ」があります。以下のように、「校異源氏物語」と青空文庫で公開されている与謝野晶子訳の対応箇所がハイライト表示されます。 この記事では、上記の機能を実現するための手順について説明します。 データ 以下のようなデータを作成します。 https://genji.dl.itc.u-tokyo.ac.jp/data/tei/koui/54.xml 「校異源氏物語」のテキストデータに対して、anchorタグを使って、与謝野晶子訳のファイルとIDのペアを対応付ます。 <text> <body> <p> <lb/> <pb facs="#zone_2055" n="2055"/> <lb/> <seg corresp="https://w3id.org/kouigenjimonogatari/api/items/2055-01.json"> <anchor corresp="https://genji.dl.itc.u-tokyo.ac.jp/api/items/tei/yosano/56.xml#YG5600000300"/> やまにおはしてれいせさせ給やうに経仏なとくやうせさせ給 <anchor corresp="https://genji.dl.itc.u-tokyo.ac.jp/api/items/tei/yosano/56.xml#YG5600000400"/> 又の日はよかはに </seg> <lb/> ... このデータの作成にあたっては、以下のツールを開発して使用しました。 https://github.com/tei-eaj/parallel_text_editor 残念ながら2024-01-07時点では動作しませんが、動作イメージは以下をご確認ください。今後、本ツールの改修を行いたいと思います。 https://youtu.be/hOp_PxYUrZk 上記の作業の結果、以下のようなGoogleドキュメントが作成されます。 https://docs.google.com/document/d/1DxKItyugUIR3YYUxlwH5-SRVA_eTj7Gh_LJ4A0mzCg8/edit?usp=sharing 「校異源氏物語」の各行について、対応する与謝野晶子訳のIDを\[YG(\d+)\]の形で挿入されます。 2055-01 [YG5600000300]やまにおはしてれいせさせ給やうに経仏なとくやうせさせ給[YG5600000400]又の日はよかはに 2055-02 おはしたれはそうつおとろきかしこまりきこえ給[YG5600000500]としころ御いのりなとつけか 2055-03 たらひたまひけれとことにいとしたしきことはなかりけるをこのたひ一品の宮 2055-04 の御心ちのほとにさふらひ給へるにすくれたまへるけん物し給けりとみたまひ 2055-05 てよりこよなうたうとひたまひていますこしふかきちきりくはへ給てけれはお 2055-06 も〱しくおはするとのゝかくわさとおはしましたることゝもてさはきゝこえ 2055-07 給[YG5600000600]御物かたりなとこまやかにしておはすれは御ゆつけなとまいり給[YG5600000700]すこし人 2055-08 〱しつまりぬるに[YG5600000800]をのゝわたりにしり給へるやとりや侍とゝひ給へはしか侍 ... 源氏物語の冊毎のGoogleドキュメントがGoogleドライブに保存されます。 https://drive.google.com/drive/folders/1QgS4z_5vk8AEz95iA3q7j41-U3oDdfpx 処理 Googleドライブからのファイル名とIDの一覧を取得 Googleドライブとの接続 #| export import os.path from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError #| export class GoogleDriveClient: def __init__(self, credential_path): # If modifying these scopes, delete the file token.json. SCOPES = [ "https://www.googleapis.com/auth/drive.metadata.readonly" ] creds = None # The file token.json stores the user's access and refresh tokens, and is # created automatically when the authorization flow completes for the first # time. if os.path.exists("token.json"): creds = Credentials.from_authorized_user_file("token.json", SCOPES) # If there are no (valid) credentials available, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( credential_path, SCOPES ) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open("token.json", "w") as token: token.write(creds.to_json()) try: service = build('drive', 'v3', credentials=creds) self.drive_service = service except HttpError as e: print(e) print("Error while creating API client") raise e 一覧の取得 ...

AttributeError: 'ImageDraw' object has no attribute 'textsize'への対応

AttributeError: 'ImageDraw' object has no attribute 'textsize'への対応

PythonのPillowで以下を使用していた際、 textsize = 14 font = ImageFont.truetype("Arial Unicode.ttf", size=textsize) txw, txh = draw.textlength(label, font=font) 以下のエラーが発生しました。 AttributeError: ‘ImageDraw’ object has no attribute ’textsize’ この対処法として、以下が参考になりました。 https://stackoverflow.com/questions/77038132/python-pillow-pil-doesnt-recognize-the-attribute-textsize-of-the-object-imag 具体的には、以下のように書き直しました。 textsize = 14 font = ImageFont.truetype("Arial Unicode.ttf", size=textsize) txw = draw.textlength(label, font=font) txh = textsize 参考になりましたら幸いです。

Amazon SNSを用いたEC2上のVirtuosoの再起動

Amazon SNSを用いたEC2上のVirtuosoの再起動

概要 以下の記事で、ヘルスチェックを行う方法について記述しました。 また、Virtuosoが停止した際の再起動のためのコマンドを以下に記述しました。 今回は、Amazon SNSを用いた通知に合わせて、Virtuosoを再起動してみます。 方法 EC2インスタンスにsudo rm -rf /usr/local/var/lib/virtuoso/db/virtuoso.lck && ...のようなコマンドを送信するには、SSM(AWS Systems Manager)に関する設定が必要でした。 IAMロールとポリシー IAMロールを新規に作成して、AmazonSSMFullAccessというポリシーを許可しました。はじめ、AmazonSSMManagedInstanceCoreというポリシーを許可していましたが、後述するlambda実行時に以下のようなエラーが発生して、うまく動作させることができませんでした。 An error occurred (InvalidInstanceId) when calling the SendCommand operation: Instances [[i-xxxxxx]] not in a valid state for account xxxxxx EC2インスタンスの「IAMロールを変更」から、作成したIAMロールを選択して更新しました。 AWS SAM: lamda関数の作成 AWS SAMを用いました。以下でのプロジェクトを作成します。 sam init hello_world/app.pyを以下のように作成しました。instance_idの部分には、要修正です。 import boto3 import time def lambda_handler(event, context): # EC2クライアントの初期化 ec2 = boto3.client('ec2') # 特定のEC2インスタンスIDを指定 instance_id = 'i-xxxxxxxx' # EC2インスタンスのステータスチェック response = ec2.describe_instance_status(InstanceIds=[instance_id]) if len(response['InstanceStatuses']) == 0: print(f"インスタンス {instance_id} は停止中です。") return # Define the command to be executed on the instance (e.g., restart software) command = 'sudo rm -rf /usr/local/var/lib/virtuoso/db/virtuoso.lck && sudo /usr/local/bin/virtuoso-t +configfile /usr/local/var/lib/virtuoso/db/virtuoso.ini' # SSMを通じてEC2インスタンスにコマンドを送信 ssm_client = boto3.client('ssm') response = ssm_client.send_command( InstanceIds=[instance_id], DocumentName='AWS-RunShellScript', Parameters={'commands': [command]} ) time.sleep(1.0) command_id = response['Command']['CommandId'] output = ssm_client.get_command_invocation( CommandId=command_id, InstanceId=instance_id, ) print("コマンドを実行しました。") # Extract only the necessary information from the output simplified_response = { "Output": output['StandardOutputContent'], "Error": output['StandardErrorContent'], } return simplified_response また、hello_word/requirements.txtにboto3を追記しました。 ...

Google Mapsの短縮URLから緯度経度を取得する際の備忘録

Google Mapsの短縮URLから緯度経度を取得する際の備忘録

概要 以下のようなGoogle Mapsの短縮URLから緯度経度を取得する機会がありました。 https://goo.gl/maps/aPxUgDJ9KP2FLFkN7 https://goo.gl/maps/aPxUgDJ9KP2FLFkN7 この時、2つの緯度経度が取得できたので、その備忘録です。 抽出方法 GPT-4から以下の回答が得られました。 -– 以下、回答 — Google Mapsの短縮URL (goo.gl/maps/...) から直接緯度経度を抽出することはできません。しかし、この短縮URLを展開して元のURLを取得することで、そのURLから緯度経度を抽出することができます。 以下の手順で緯度経度を取得することができます: 短縮URLを展開します。 展開されたURLから緯度経度を抽出します。 以下はPythonでこれを実現する一例です: import requests import re def get_lat_lon_from_google_url(short_url): # 短縮URLを展開 response = requests.get(short_url, allow_redirects=False) if response.status_code != 302: return None long_url = response.headers['location'] # 展開されたURLから緯度経度を抽出 match = re.search(r'@([\-0-9.]+),([\-0-9.]+)', long_url) if match: lat, lon = match.groups() return float(lat), float(lon) else: return None url = "https://goo.gl/maps/aPxUgDJ9KP2FLFkN7" lat_lon = get_lat_lon_from_google_url(url) print(lat_lon) このコードは、短縮URLを展開して緯度経度を抽出するための基本的なものです。しかし、実際の使用環境や目的に応じて、エラーハンドリングや例外処理を追加することが推奨されます。 -– 回答、ここまで — 上記により、以下のURLに含まれる緯度経度(ここでは、28.3410487,129.9769402)を取得できました。 https://www.google.com/maps/place/鹿児島県大島郡喜界町大字伊砂/@28.3410487,129.9769402,3868m/data=!3m2!1e3!4b1!4m5!3m4!1s0x351f80a0620f4335:0x35d406a5040d7197!8m2!3d28.3439714!4d129.9838012?shorturl=1 しかし、この座標を指定すると、以下の場所が指定され、求めている座標からずれているようでした。 修正 以下のURLを確認すると、末尾にも!3d28.3439714!4d129.9838012という座標が見られました。 https://www.google.com/maps/place/鹿児島県大島郡喜界町大字伊砂/@28.3410487,129.9769402,3868m/data=!3m2!1e3!4b1!4m5!3m4!1s0x351f80a0620f4335:0x35d406a5040d7197!8m2!3d28.3439714!4d129.9838012?shorturl=1 そこで、先ほどプログラムについて、prefixとdelimiterを指定できるように修正しました。 import requests import re def get_lat_lon_from_google_url(short_url, prefix= "@", delimiter = ","): # 短縮URLを展開 response = requests.get(short_url, allow_redirects=False) if response.status_code != 302: return None long_url = response.headers['location'] print(long_url) # 展開されたURLから緯度経度を抽出 pattern = r'{prefix}([\-0-9.]+){delimiter}([\-0-9.]+)'.format(prefix=re.escape(prefix), delimiter=re.escape(delimiter)) match = re.search(pattern, long_url) if match: lat, lon = match.groups() return float(lat), float(lon) else: return None url = "https://goo.gl/maps/aPxUgDJ9KP2FLFkN7" lat_lon = get_lat_lon_from_google_url(url, prefix="!3d", delimiter="!4d") print(lat_lon) 結果、28.3439714,129.9838012が取得できました。この座標を使用してみたところ、以下のように、より目的に近い座標にピンが立ちました。 ...

Omeka Classic IIIF Toolkitにデータを一括登録する

Omeka Classic IIIF Toolkitにデータを一括登録する

概要 Omeka Classic IIIF Toolkitにデータを一括登録する方法を説明します。Omeka Classic IIIF Toolkitのセットアップについては、以下を参考にしてください。 また、以下の記事の内容を発展させ、excelデータを入力データとして、より簡便に使用できるようにしたものです。 excelファイルの準備 以下のようなexcelファイルを準備します。 https://github.com/nakamura196/000_tools/blob/main/data/sample.xlsx 「collection」「item」「annotation」の3つのシートを用意します。 collection manifest_uri https://d1fasenpql7fi9.cloudfront.net/v1/manifest/3437686.json item title canvas_uri width height image_url manifest_uri 校異源氏物語. 巻一 [4] https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/4 6890 4706 https://www.dl.ndl.go.jp/api/iiif/3437686/R0000004/full/full/0/default.jpg https://d1fasenpql7fi9.cloudfront.net/v1/manifest/3437686.json 校異源氏物語. 巻一 [4] https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 6890 4706 https://www.dl.ndl.go.jp/api/iiif/3437686/R0000005/full/full/0/default.jpg https://d1fasenpql7fi9.cloudfront.net/v1/manifest/3437686.json annotation chars x y w h canvas_uri tag 3125 4898 4008 241 79 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/4 序 2910 868 147 140 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/4 913.36 たH 2228 226 586 156 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/4 或は撮影して、 897 3517 83 434 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/4 一 810 3528 30 17 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/4 序 6018 1055 65 65 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 二 6025 3535 49 39 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 殆んどあらゆる現存貴重資料に及び、 5889 707 86 1090 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 一字をも忽にしない細緻な〓究は、 5889 1837 86 1012 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 フイルム等 5501 3614 76 323 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 大島雅太郞氏· 916 3491 89 436 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 序 807 1048 65 61 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 三 815 3517 61 57 https://www.dl.ndl.go.jp/api/iiif/3437686/canvas/5 python 以下のライブラリをインストールします。 ...

WikibaseSyncを試す

WikibaseSyncを試す

概要 以下のWikibaseSyncを試す機会がありましたので、その備忘録です。 https://github.com/the-qa-company/WikibaseSync 以下の論文で、上記のツールを知りました。 https://doi.org/10.11517/jsaisigtwo.2022.SWO-056_04 インストール ソースコードや関連ライブラリをインストールします。 !get clone https://github.com/the-qa-company/WikibaseSync cd WikibaseSync !pip install -r requirements.txt Bot Accountの作成 事前に用意したWikibaseにアクセスし、「Special pages」から、「Bot passwords」をクリックします。 以下の画面で、「Bot name」を入力します。 次の画面で、以下の3つにチェックを入れます。 Give him the follwing rights: “High-volume editing”, “Edit existing pages” and “Create, edit, and move pages” 結果、以下のようなメッセージが表示され、パスワードを取得することができます。 The bot password for bot name "WikidataUpdater" of user "Nakamura" was created. The new password to log in with Nakamura@WikidataUpdater is xxxxxxxxxxxxxx. Please record this for future reference. (For old bots which require the login name to be the same as the eventual username, you can also use Nakamura as username and WikidataUpdater@xxxxxxxxxxxxxx as password.) Return to Special:BotPasswords. そして、以下の説明にしたがって、インストールしたソースコードの設定ファイルに、上記のBotに関する情報を入力します。 ...

wikibaseのapiをつかってみる

wikibaseのapiをつかってみる

概要 wikibaseのapiをpythonクライアントから使用する機会がありましたので、その備忘録です。 以下のライプラリを使用しました。 https://wikibase-api.readthedocs.io/en/latest/index.html インストール 以下でインストールします。 !pip install wikibase-api Read 今回は、以下のwikibaseインスタンスに対して処理を行います。 https://nakamura196.wikibase.cloud/ from wikibase_api import Wikibase api_url = "https://nakamura196.wikibase.cloud/w/api.php" wb = Wikibase(api_url=api_url) r = wb.entity.get("Q1") print(r) 上記により、Q1に関する情報を取得することができました。 Create 認証情報の取得 アイテムの作成などを行う際には、以下のどちらかの方法で認証を行う必要がありました。 Authentication using OAuth Authentication with a user account 後者はBot passwordsを作成する方法で、以下の記事でも作成方法を紹介しています。 今回は前者の方法を試します。 「Special pages」の「OAuth consumer registration」を選択します。 「Request a token for a new OAuth 1.0a consumer.」と「Request a token for a new OAuth 2.0 client.」の2つの作成方法がありますが、前者を選択します。 項目を埋めていきます。 今回はテストのため、すべての項目にチェックを入れました。 結果、以下のようなページが表示され、認証情報を取得できます。 実行 .envファイルを作成します。 consumer_key=xxx consumer_secret=yyy access_token=zzz access_secret=aaa load_dotenvを使って認証情報をロードして、設定します。 ...

Dataverseを試す

Dataverseを試す

概要 Dataverseを試す機会がありましたので、その備忘録です。以下のデモ環境を使用します。 https://demo.dataverse.org/ アカウント作成 Sign Upからアカウントを作成します。 Dataverseの作成 Dataverseを作成してみます。 以下のようなDataverseを作成しました。 https://demo.dataverse.org/dataverse/nakamura196 Datasetの作成 Add Dataから、データセットを作成します。 以下が登録画面です。 以下が登録結果の画面です。 URLは以下になっており、自動的に付与されるDOIをシステム内の識別子として利用しているようでした。 https://demo.dataverse.org/dataset.xhtml?persistentId=doi:10.70122/FK2/HBXQPZ Whole Tale Access Datasetボタンのオプションに、Whole Taleというものがありました。 アカウントを作成して、Compute EnvironmentをJupyter Notebookに設定してみます。 Acknowledge allボタンを押します。 結果、以下のようなJupyter Notebookの環境が作成されました。 Pythonを使って、ダウンロードした画像を表示してみます。 上記のように、データセットやソースコードに対する動作確認や検証もシームレスに実行できるようでした。 Widgets 以下の「Thumbnails + Widgets」を押してみます。 以下の「Widgets」タブを表示してみます。 上記のコードを埋め込んでみると、以下に示すように、データセットに関する情報を埋め込み表示することができました。 https://codepen.io/satoru196/pen/NWEMvEV?editors=1000 API Pythonのライブラリを使ってみます。 https://pydataverse.readthedocs.io/en/latest/ ライブラリをインストールします。 !pip install pyDataverse データのダウンロード 以下のページを参考にしました。 https://pydataverse.readthedocs.io/en/latest/user/basic-usage.html#download-and-save-a-dataset-to-disk from pyDataverse.api import NativeApi, DataAccessApi from pyDataverse.models import Dataverse base_url = 'https://demo.dataverse.org' api = NativeApi(base_url) data_api = DataAccessApi(base_url) DOI = "doi:10.70122/FK2/HBXQPZ" dataset = api.get_dataset(DOI) files_list = dataset.json()['data']['latestVersion']['files'] for file in files_list: filename = file["dataFile"]["filename"] file_id = file["dataFile"]["id"] print("File name {}, id {}".format(filename, file_id)) response = data_api.get_datafile(file_id) with open(filename, "wb") as f: f.write(response.content) データセットの作成(demo.dataverse.orgでは権限がありませんでした。) demo.dataverse.orgでは権限がありませんでしたが、実施した内容をメモします。 ...

Omeka Classicでコレクションを一括削除する方法

Omeka Classicでコレクションを一括削除する方法

概要 Omeka Classicでコレクションを一括削除する方法の一例を紹介します。Omeka Classic (Version 3.1.1)では、コレクションを複数選択して削除するGUIは提供されていません。アイテムについては、同機能が提供されています。 そこで、APIを用いてコレクションの一括削除を行います。 APIキーの取得 以下を参考に、APIの有効化とAPIキーの発行を行います。 https://omeka.org/classic/docs/Admin/Settings/API_Settings/ 具体的には、まず以下のページにアクセスします。 /admin/settings/edit-api そして、ユーザ毎のページにアクセスして、「API Keys」のタブを選択します。「New key label」からAPIキーを発行します。 /admin/users/api-keys/1 Pythonスクリプト 以下のようなPythonスクリプトにより、コレクションの一括削除を行うことができます。以下では、アイテム数が0のコレクションのみを削除するように設定しています。 import requests api_key = "<取得したAPIキー>" # 要変更 endpoint = "https://example.org/omeka/api" # 要変更 params = { "key": api_key } # コレクション一覧の取得 url = f"{endpoint}/collections" collections = requests.get(url, params=params).json() for collection in collections: # コレクションに含まれるアイテム数を取得 items_count = collection["items"]["count"] # アイテム数が0の場合 if items_count == 0: url_collection = collection["url"] requests.delete(f"{url_collection}", params=params) まとめ Omeka Classicでコレクションを一括削除する際の参考になりましたら幸いです。

bagit-pythonを試す

bagit-pythonを試す

bagitは以下のように説明されています。 bagit is a Python library and command line utility for working with BagIt style packages. 本ライブラリを試すGoogle Colabのノートブックを作成しました。 https://colab.research.google.com/github/nakamura196/ndl_ocr/blob/main/bagit_python.ipynb bagitの利用にあたり、参考になりましたら幸いです。

DjangoとAWS OpenSearchを接続する

DjangoとAWS OpenSearchを接続する

概要 DjangoとAWS OpenSearchを接続する方法に関するメモです。以下の記事が参考になりました。 https://testdriven.io/blog/django-drf-elasticsearch/ ただし、上記の記事はElasticsearchを対象にした設定のため、OpenSearchに応じた変更が必要です。 変更点 以下のElasticsearch Setupの部分から、OpenSearchに応じた変更が必要でした。 https://testdriven.io/blog/django-drf-elasticsearch/#elasticsearch-setup 具体的には、以下の2つのライブラリが必要でした。 (env)$ pip install opensearch-py (env)$ pip install django-opensearch-dsl その後は、django_elasticsearch_dslとなっている箇所をdjango-opensearch-dslに、elasticsearch_dslをopensearchpyに書き換えることで、記事の通りに進めることができました。 例えば、以下のような形です。 # blog/documents.py from django.contrib.auth.models import User from django_opensearch_dsl import Document, fields # opensearchに変更 from django_opensearch_dsl.registries import registry # opensearchに変更 from blog.models import Category, Article @registry.register_document class UserDocument(Document): class Index: name = 'users' settings = { 'number_of_shards': 1, 'number_of_replicas': 0, } class Django: model = User fields = [ 'id', 'first_name', 'last_name', 'username', ] @registry.register_document class CategoryDocument(Document): id = fields.IntegerField() class Index: name = 'categories' settings = { 'number_of_shards': 1, 'number_of_replicas': 0, } class Django: model = Category fields = [ 'name', 'description', ] @registry.register_document class ArticleDocument(Document): author = fields.ObjectField(properties={ 'id': fields.IntegerField(), 'first_name': fields.TextField(), 'last_name': fields.TextField(), 'username': fields.TextField(), }) categories = fields.ObjectField(properties={ 'id': fields.IntegerField(), 'name': fields.TextField(), 'description': fields.TextField(), }) type = fields.TextField(attr='type_to_string') class Index: name = 'articles' settings = { 'number_of_shards': 1, 'number_of_replicas': 0, } class Django: model = Article fields = [ 'title', 'content', 'created_datetime', 'updated_datetime', ] Populate Elasticsearch Elasticsearchを対象にした上記の記事では、以下のコマンドが紹介されています。 ...