ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English

TEI XMLのスタンドオフ注釈をインライン化する際の落とし穴とDOM操作による解決

デジタル延喜式は、延長5年(927年)に完成した律令の施行細則集『延喜式』を TEI (Text Encoding Initiative) XML で符号化し、Web上で閲覧・検索できるようにするプロジェクトです。国立歴史民俗博物館を中心に、校訂文・現代語訳・英訳を TEI でマークアップし、Nuxt.js(Vue.js)ベースのビューアで公開しています。 この開発の中で、TEI XML のスタンドオフ(standoff)注釈をインライン注釈に変換する処理において、XML の文書構造が崩壊するバグに遭遇しました。本記事では、その原因と DOM 操作ベースの解決策を記録します。 スタンドオフ注釈とは TEI XML では、テキスト中の校異(variant readings)を記録する方法として、スタンドオフ方式がよく使われます。デジタル延喜式では、複数の写本間のテキストの異同を <app> 要素で記録しており、テキスト中に <anchor> 要素で範囲を示し、対応する <app> 要素を別の場所に置く構造になっています。 <p> 前テキスト <anchor xml:id="app001"/> 校異対象のテキスト <anchor xml:id="app001e"/> 後テキスト </p> <!-- 別の場所に校異情報 --> <app from="#app001" to="#app001e"> <lem>校異対象のテキスト</lem> <rdg wit="#写本A">異なるテキスト</rdg> </app> この方式は、XML のネスト制約を回避できる利点があります。校異の範囲が要素境界をまたぐ場合(overlapping hierarchy)でも、anchor はどこにでも置けるためです。 インライン化の理由 XML ツリーと UI コンポーネントツリーの対応 デジタル延喜式のビューアは Vue.js で構築しています。Vue.js や React のようなコンポーネントベースのフレームワークでは、UI はツリー構造で記述されます。TEI XML もツリー構造なので、XML の各要素を UI コンポーネントに 1:1 でマッピングする再帰レンダリングが自然なアプローチになります。 <!-- TEI.vue: XML要素を再帰的にコンポーネントにマッピング --> <template> <component v-for="child in element.children" :is="getComponent(child.tagName)" :element="child" /> </template> この設計では、<app> 要素がテキスト中にインラインで存在すれば、ツリーの走査だけでレンダリングできます。 ...

Apple Sales Reports APIのデータ反映時刻とYouTube APIのクォータリセットを実測した

外部APIを使って日次のデータ取得を自動化する場合、「いつデータが利用可能になるか」「いつクォータがリセットされるか」を把握しておくとスケジューリングに役立ちます。この記事では、Apple App Store Connect Sales Reports APIとYouTube Data API v3について、実際に観測したタイミングを記録します。 Apple App Store Connect Sales Reports API 公式ドキュメントの記載 Appleの公式ドキュメントでは、日次の売上レポートは翌日の太平洋時間 午前8時まで(by 8:00 AM Pacific Time)に利用可能になるとされています。 参照: Sales and Trends Reports Availability - Apple Developer 実測結果 2026年3月22日に、前日(2026年3月21日)分のデータがいつ取得可能になるかを1時間ごとに確認しました。 16:00 JST (0:00 PT) → 取得不可 17:00 JST (1:00 PT) → 取得不可 18:00 JST (2:00 PT) → 取得不可 19:00 JST (3:00 PT) → 取得不可 20:00 JST (4:00 PT) → 取得不可 21:09 JST (5:09 PT) → 取得可能 今回の計測では、太平洋時間の午前5時頃(日本時間 21時頃)にデータが利用可能になっていました。公式に記載されている午前8時よりも約3時間早いタイミングです。 ...

App Storeのスクリーンショット生成をPython+UIテストで完全自動化する

App Storeのスクリーンショット生成をPython+UIテストで完全自動化する

TL;DR XCUITestでiPhone・iPadシミュレータのスクリーンショットを日英両言語で自動撮影 PythonのPillowでグラデーション背景+デバイスフレーム+テキストオーバーレイのマーケティング画像を生成 xcrun simctl io recordVideoでデモ動画も録画 App Store Connect APIで自動アップロード すべてをシェルスクリプト1本で実行可能 はじめに iOSアプリのApp Storeスクリーンショットは、iPhone 6.7インチ、iPad 12.9インチの各サイズを日英2言語分用意すると、それだけで12枚以上の画像を作る必要があります。 アプリを更新するたびに手作業でスクリーンショットを撮り直し、FigmaやPhotoshopでマーケティング画像を作り、App Store Connectに1枚ずつアップロードするのは手間がかかります。 本記事では、撮影 → 画像生成 → アップロードの全工程をコマンド一発で実行できるパイプラインの構築方法を解説します。 全体構成 capture_screenshots.sh ├── Step 1: シミュレータの準備(起動 + テスト画像追加) ├── Step 2: XCUITestでスクリーンショット撮影(JA/EN × iPhone/iPad) ├── Step 3: sipsでApple規定サイズにリサイズ ├── Step 4: Pillow でマーケティング画像を生成 ├── Step 5: xcrun simctl io でデモ動画を録画 └── Step 6: App Store Connect APIでアップロード Step 1: XCUITestでスクリーンショットを撮影する テストコードの設計 UIテスト用のテストクラスを作成します。ポイントは以下の3つです。 テスト画像の自動読み込み: TEST_IMAGE_PATH環境変数で画像パスを渡し、PHPickerを経由せずに画像を直接ロードします 言語切り替え: xcodebuild -testLanguageで設定された言語を-AppleLanguagesとしてアプリに渡します レビューダイアログの抑制: 起動引数で不要なダイアログを抑制します final class ScreenshotTests: XCTestCase { private var app: XCUIApplication! private let screenshotDir = ProcessInfo.processInfo.environment["SCREENSHOT_DIR"] ?? "/tmp/myapp_screenshots" override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() // オンボーディングをスキップ、レビューダイアログを抑制 app.launchArguments += ["-hasCompletedOnboarding", "YES"] // テスト言語をアプリの言語設定に反映 let preferredLang = Locale.preferredLanguages.first ?? "ja" let langCode = preferredLang.components(separatedBy: "-").first ?? "ja" app.launchArguments += ["-AppleLanguages", "(\(langCode))", "-AppleLocale", langCode] // テスト用画像パスを環境変数で渡す app.launchEnvironment["TEST_IMAGE_PATH"] = "/path/to/test_sample.jpg" try FileManager.default.createDirectory( atPath: screenshotDir, withIntermediateDirectories: true ) } func testCaptureScreenshots() throws { app.launch() // 処理完了を待機 let backButton = app.buttons["back_button"] XCTAssertTrue(backButton.waitForExistence(timeout: 300)) sleep(2) // メイン画面のスクリーンショット saveScreenshot(name: "04_result") // 他の画面に遷移してスクリーンショットを撮る // ... backButton.tap() sleep(1) saveScreenshot(name: "01_camera") } private func saveScreenshot(name: String) { let screenshot = app.windows.firstMatch.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.name = name attachment.lifetime = .keepAlways add(attachment) let path = "\(screenshotDir)/\(name).png" try? screenshot.pngRepresentation.write(to: URL(fileURLWithPath: path)) } } saveScreenshotメソッドは、XCTestのXCTAttachmentとしてテスト結果に添付すると同時に、指定ディレクトリにPNGファイルとして保存します。ファイル名のプレフィックス(01_, 04_等)は、後のマーケティング画像生成で優先順位を制御するために使います。 ...

LoRAによる書名からのNDC(日本十進分類法)自動分類の試み

LoRAによる書名からのNDC(日本十進分類法)自動分類の試み

ノートブック: Google Colab で開く / GitHub TL;DR 国立国会図書館サーチAPI(SRU)を用いて617件の書誌データを収集 llm-jp-3-1.8b に LoRA(全パラメータの0.67%)を適用し、書名からNDC第1次区分への分類を学習 学習前 22.0% → 学習後 78.0%(+56ポイント) LoRAはドメイン知識の注入ではなく、タスク遂行のための振る舞いを獲得させる手法 NDC(日本十進分類法)とは 日本の図書館で広く使われている書籍の分類体系です。すべての本に0〜9の第1次区分(類目)が割り当てられます。 NDC ジャンル 0 総記(百科事典・情報学など) 1 哲学・宗教 2 歴史・地理 3 社会科学(法律・経済・教育) 4 自然科学(数学・物理・医学) 5 技術・工学 6 産業(農業・商業・運輸) 7 芸術・スポーツ 8 言語 9 文学 図書館において資料の整理(目録作成)時にNDCコードを付与する作業は、主題分析の専門的知識を要する業務です。書名のみから大まかな分類を自動推定できるモデルがあれば、分類作業の初期スクリーニングとして有用です。 LoRAとは何か LoRA(Low-Rank Adaptation)は、大規模言語モデルを効率的にファインチューニングするための手法です。 通常のファインチューニングではモデルの全パラメータ(18億個など)を更新しますが、LoRAでは元のモデルを凍結し、Attention層に小さな「アダプター」行列を挿入してそこだけを学習させます。 モデル本体 (18億パラメータ) → 凍結(更新対象外) ↓ LoRAアダプター (数百万パラメータ) → 学習対象 今回の設定では全パラメータの約0.67%(12,582,912 / 1,880,197,120)だけを学習対象にしています。これにより、GPUメモリの消費を抑えつつ、タスク特化の性能を得ることができます。 Step 1. 国立国会図書館サーチAPIからデータ取得 国立国会図書館サーチのSRU APIは誰でも無料で利用可能です。各NDCカテゴリから最大80件ずつ取得し、タイトル文字数(3〜80文字)によるフィルタリング後に合計617件の書誌データを収集しました。カテゴリごとの取得件数は以下の通りで、均等ではありません。 NDC カテゴリ 取得件数 0 総記 65件 1 哲学 67件 2 歴史 73件 3 社会科学 59件 4 自然科学 52件 5 技術・工学 63件 6 産業 65件 7 芸術・スポーツ 57件 8 言語 67件 9 文学 49件 なお、APIの特性上、取得される書誌には書名が極めて短いものや内容が判別しにくいものも含まれるため、学習データの品質には一定のノイズが存在します。 ...

researchmapの科研費と業績の紐付けをPlaywrightで自動化した

researchmapの科研費と業績の紐付けをPlaywrightで自動化した

はじめに researchmap は、日本の研究者が業績を管理・公開するためのプラットフォームです。論文や講演などの業績を登録するだけでなく、科研費(共同研究・競争的資金等の研究課題)との紐付けを行うことで、研究課題ごとの成果一覧を集約できます。 この紐付けについて、APIやCSVインポートでの一括設定ができないか調べたところ、調査した限りでは現時点ではWeb UIからの手動操作に限られるようでした。そこで、Playwrightによる自動化を試みました。 紐付けの方法を調べる researchmapでは、業績データの一括登録はJSONLやCSVファイルのインポートで可能です。一方、科研費と業績の紐付けについては、調査した限り、一括インポートでは設定できないようでした。 API設計書での確認 researchmap v2 API設計書を確認すると、以下のフィールドはすべて「更新不可」と記載されています。 research_projects(科研費)側: rm:published_paper_id(紐づいた論文の業績ID)→ 更新不可 rm:presentation_id(紐づいた講演の業績ID)→ 更新不可 rm:work_id(紐づいたWorksの業績ID)→ 更新不可 業績(論文・講演等)側: rm:research_project_id(紐づいた科研費ID)→ 更新不可 実際に検証 念のため、JSOLNインポートで identifiers.research_project_id を指定して検証しました。 {"insert":{"type":"presentations","id":"52101757"},"merge":{"identifiers":{"research_project_id":["51361068"]}}} 結果:インポート自体は「完了」と表示されましたが、紐付けは反映されませんでした。このフィールドは無視されるようです。 Web UIでは可能 一方、researchmapのWeb UI(編集画面)では、業績ごとに「共同研究・競争的資金等の研究課題」のプルダウンから科研費を選択して紐付けることができます。 この操作は1件ずつ手動で行う必要があり、件数が多いと手間がかかります。 Playwrightで自動化する Web UIでの手動操作を、Playwrightを使って自動化するPythonスクリプトを作成しました。 仕組み .env ファイルからresearchmapのログイン情報を読み込み Playwrightでブラウザを起動し、自動ログイン 各業績の編集ページ(/{slug}/{type}/{id}/edit)に移動 selectize.jsで実装されたプルダウンから、対象の科研費を自動選択 「決定」ボタンをクリックして保存 既に紐付け済みの業績は自動スキップ 編集画面のHTML構造 researchmapの編集画面では、科研費のプルダウンはselectize.jsで実装されています。 <select name="data[PublishedPapersIndex][_source][identifiers][research_project_id][]" class="form-control selectized" multiple="multiple" style="display: none;"> <option value="50040755" selected="selected">TEIを中心とした高度な歴史テキスト構築</option> </select> 実際の <select> は非表示で、selectize.jsが生成するカスタムUIで操作します。Playwrightでは以下のようにselectize入力をクリックし、ドロップダウンからオプションを選択します。 # selectize の入力エリアをクリックしてドロップダウンを開く selectize_input = page.locator(f'#{selectize_id}-selectized') await selectize_input.click() # ドロップダウンから該当の科研費を選択 option = page.locator(f'.selectize-dropdown .option[data-value="{project_id}"]') await option.click() # 「決定」ボタンで保存 submit = page.locator('button[name="save"][type="submit"]') await submit.click() 紐付け設定ファイル 紐付け対象はJSONファイルで管理します。 ...

App Store Connect APIでiOSアプリのアップデートを審査提出する方法

App Store Connect APIでiOSアプリのアップデートを審査提出する方法

TL;DR iOSアプリのアップデート版を ビルド → アップロード → ビルド紐付け → whatsNew設定 → 審査提出 まで、すべてコマンドラインとApp Store Connect REST APIで完結させた。初回リリース時と異なり、メタデータやスクリーンショットは既存のものが引き継がれるため、更新時に必要な操作は少ない。 前提: App Store Connect APIだけでiOSアプリを審査提出する完全ガイドのセットアップ(APIキー取得・JWT生成・ヘルパー関数)が完了しているものとする。 全体の流れ ビルド番号のインクリメント アーカイブ・IPA書き出し・アップロード(xcodebuild + xcrun altool) ビルドの処理完了を確認(API) ビルドをバージョンに紐付け(API) 暗号化コンプライアンスの設定(API) whatsNew(新機能)の設定(API) 審査提出(API) 1. ビルド番号のインクリメント App Store Connectは同じビルド番号のアップロードを拒否する。CURRENT_PROJECT_VERSION を上げる必要がある。 XcodeGenを使っている場合は project.yml を編集する: # project.yml settings: base: MARKETING_VERSION: "1.1.0" CURRENT_PROJECT_VERSION: "4" # 3 → 4 に変更 ポイント: マーケティングバージョン(1.1.0)はユーザーに見えるバージョン番号、ビルド番号(4)は同一バージョン内の連番。不具合修正の再提出など、ユーザーから見た変更がない場合はビルド番号だけ上げればよい。 2. アーカイブ・アップロード # 環境変数を設定 export APP_STORE_API_KEY="YOUR_KEY_ID" export APP_STORE_API_ISSUER="YOUR_ISSUER_ID" # XcodeGenでプロジェクト再生成 xcodegen generate # アーカイブ xcodebuild archive \ -project KotenOCR.xcodeproj \ -scheme KotenOCR \ -archivePath build/KotenOCR.xcarchive \ -destination "generic/platform=iOS" \ -quiet # IPA書き出し xcodebuild -exportArchive \ -archivePath build/KotenOCR.xcarchive \ -exportPath build/export \ -exportOptionsPlist scripts/ExportOptions.plist \ -quiet # App Store Connectへアップロード xcrun altool --upload-app \ --type ios \ --file build/export/KotenOCR.ipa \ --apiKey "$APP_STORE_API_KEY" \ --apiIssuer "$APP_STORE_API_ISSUER" 注意: xcrun altool に渡す環境変数は export しておく必要がある。source .env だけではサブプロセスに渡らない。 ...

App Store Connect APIでiOSアプリにチップ(Tip Jar)機能を追加する完全ガイド

App Store Connect APIでiOSアプリにチップ(Tip Jar)機能を追加する完全ガイド

TL;DR iOSアプリにチップ(Tip Jar)機能を追加した。SwiftUI + StoreKit 2 でアプリ側を実装し、App Store Connect REST API を使って商品登録・ローカライズ・価格設定・審査用スクリーンショット・配信地域設定・TestFlight配信までをコマンドラインから完了させた。本記事ではその全手順を再現可能な形で記載する。 前提: App Store Connect APIだけでiOSアプリを審査提出する完全ガイドの続編として、APIキーの取得・JWT生成は既にセットアップ済みとする。 全体の流れ アプリ側の実装(StoreKit 2 + SwiftUI) App Store Connect APIで商品登録(3つの消費型アイテム) ローカライズ設定(日本語・英語) 価格設定($0.99 / $2.99 / $6.99) 審査用スクリーンショットのアップロード 配信地域の設定 有料アプリ契約の締結 TestFlightでの動作確認 1. アプリ側の実装 1.1 StoreKit設定ファイル Xcodeのテスト環境用に TipJar.storekit を作成する。これによりシミュレータでStoreKitのテストが可能になる。 { "products" : [ { "displayPrice" : "0.99", "familyShareable" : false, "internalID" : "tip_small_001", "localizations" : [ { "description" : "開発を応援する小さなチップ", "displayName" : "小さな応援", "locale" : "ja" }, { "description" : "A small tip to support development", "displayName" : "Small Tip", "locale" : "en_US" } ], "productID" : "com.example.app.tip.small", "referenceName" : "Small Tip", "type" : "Consumable" } ] } XcodeGen を使っている場合は project.yml の scheme に StoreKit 設定を追加する: ...

Chrome Headless の --screenshot で画面下部に白い余白が出る問題と対処法

Chrome Headless の --screenshot で画面下部に白い余白が出る問題と対処法

問題 Chrome の Headless モードで HTML を PNG 画像にキャプチャする際、画面下部に**白い余白(白帯)**が発生することがあります。 google-chrome --headless --screenshot=output.png \ --window-size=1920,1080 \ --hide-scrollbars \ --force-device-scale-factor=1 \ file:///path/to/slide.html HTML 側で width: 1920px; height: 1080px を指定しているにもかかわらず、生成された画像の下部に白い帯が残り、bottom で配置した要素(テロップ、フッターなど)が切れてしまいます。 原因 --window-size=1920,1080 はブラウザの外部ウィンドウサイズを指定するオプションであり、実際の**ビューポート(描画領域)**は若干小さくなります。Headless モードでもこのズレは発生します。 つまり: --window-size=1920,1080 → 実際のビューポートは約 1920×1058 程度 HTML は 1080px の高さで描画しようとする ビューポートに収まらない下部のコンテンツが見切れる スクリーンショットは 1920×1080 で出力されるが、下部はデフォルト背景色(白)で埋まる html や body に height: 1080px を指定しても、Chrome が実際に確保するビューポート高さとは一致しないため、CSS だけでは解決できません。 解決方法 ウィンドウサイズを大きめに設定し、スクリーンショット後に Pillow でクロップするのが最も確実です。 1. Chrome のウィンドウサイズを大きくする google-chrome --headless --screenshot=output.png \ --window-size=1920,1280 \ --hide-scrollbars \ --force-device-scale-factor=1 \ file:///path/to/slide.html 高さを 1280 に変更することで、1080px のコンテンツが確実にビューポート内に収まります。 ...

Three.js + Puppeteer で VRM キャラクターを動かして動画を自動生成する

Three.js + Puppeteer で VRM キャラクターを動かして動画を自動生成する

はじめに 技術ブログの記事を VTuber 風の解説動画に自動変換できたら面白いのでは――そんな思いつきから、Three.js + Puppeteer で VRM キャラクターをフレーム単位でレンダリングし、VOICEVOX の音声とリップシンクさせて動画を生成するパイプラインを作りました。 この記事では、実装で得られた知見とハマりどころを共有します。 全体のパイプライン 処理の流れは以下の通りです。 Markdown 記事を読み込み → LLM(OpenRouter API)でセクション分割された台本を生成 VOICEVOX でセクションごとに音声(WAV)と音素タイミングを生成 Three.js + @pixiv/three-vrm でヘッドレス Chrome 上に VRM モデルを描画し、音素データに基づくリップシンクアニメーションをフレーム連番 PNG として出力 スライド画像を自動生成(HTML → Chrome ヘッドレス → PNG) FFmpeg でスライド背景 + VRM アニメーション + 音声を合成し、MP4 動画を出力 Python スクリプトがオーケストレーション役を担い、VRM レンダリングは Node.js スクリプトを子プロセスとして呼び出す構成です。 使用技術 役割 技術 3D レンダリング Three.js v0.172 VRM 読み込み @pixiv/three-vrm v3.3.3 ヘッドレスブラウザ puppeteer-core (SwiftShader) 音声合成 VOICEVOX Engine (Docker) 動画合成 FFmpeg パイプライン制御 Python VRM モデル AvatarSample_C (VRoid Hub / 無料ライセンス) ヘッドレス Chrome で VRM を読み込む 課題: file:// の CORS 制限 最初の壁は、ヘッドレス Chrome 上で VRM ファイルを読み込む方法でした。ローカルの .vrm ファイルを file:// プロトコルで読もうとすると CORS エラーで弾かれます。 ...

App Store Connect APIだけでiOSアプリを審査提出する手順

App Store Connect APIだけでiOSアプリを審査提出する手順

TL;DR App Store Connect の REST API を使い、コマンドラインからiOSアプリの審査提出に必要なほぼ全作業(メタデータ・スクリーンショット・年齢レーティング・ビルド紐付け・URL設定・暗号化コンプライアンス・価格設定)を完了させた。本記事ではその手順を再現可能な形で記載する。 注意: 「App Privacy(アプリのプライバシー)」のデータ使用状況の宣言だけは、2026年3月時点でAPIが提供されておらず、App Store Connectのブラウザから設定する必要がある。 前提条件 Apple Developer Program に登録済み App Store Connect で API キーを発行済み アプリの Bundle ID が登録済み Xcode でアーカイブ・アップロード済みのビルドが存在する(xcodebuild -exportArchive でアップロード可能) Python 3 + PyJWT + cryptography がインストール済み pip install PyJWT cryptography 1. API キーの準備 1.1 API キーの発行 App Store Connect → ユーザーとアクセス → 統合 → App Store Connect API から新しいキーを発行する。 名前: 任意(例: deploy-key) アクセス: Admin(メタデータ更新・提出に必要) 発行後、以下の情報をメモする: 項目 説明 Key ID APIキーの識別子(10文字程度の英数字) Issuer ID 組織の識別子(UUID形式) ダウンロードした .p8 ファイルは安全な場所に保存する(一度しかダウンロードできない): ...

はてなブログの記事を一括で非公開にする方法(AtomPub API)

はてなブログの記事を一括で非公開にする方法(AtomPub API)

はてなブログの記事を別サイトに移行した後、旧記事を一括で非公開にしたいケースがあります。 注意点:下書きには戻せない はてなブログのAtomPub APIでは、公開済みの記事を下書き(draft)に戻すことはできません。PUTリクエストで <app:draft>yes</app:draft> を送ると 400 Cannot Change into Draft エラーになります。 そのため、以下の2つの方法があります。 方法1:記事本文を「移転しました」に書き換える AtomPub APIのPUTで記事の <content> を書き換えることは可能です。 import requests import xml.etree.ElementTree as ET import time HATENA_ID = "your_hatena_id" BLOG_ID = "your_blog_id.hatenablog.com" API_KEY = "your_api_key" NEW_SITE_URL = "https://your-new-site.com" ATOM_NS = "http://www.w3.org/2005/Atom" def fetch_all_entries(): entries = [] url = f"https://blog.hatena.ne.jp/{HATENA_ID}/{BLOG_ID}/atom/entry" while url: resp = requests.get(url, auth=(HATENA_ID, API_KEY), timeout=30) resp.raise_for_status() root = ET.fromstring(resp.text) for entry in root.findall(f"{{{ATOM_NS}}}entry"): title_el = entry.find(f"{{{ATOM_NS}}}title") title = title_el.text or "" if title_el is not None else "" edit_link = entry.find(f"{{{ATOM_NS}}}link[@rel='edit']") edit_url = edit_link.get("href") if edit_link is not None else None if edit_url: entries.append({"title": title, "edit_url": edit_url}) next_el = root.find(f"{{{ATOM_NS}}}link[@rel='next']") url = next_el.get("href") if next_el is not None else None return entries def replace_content(entry): title = entry["title"] update_xml = f"""<?xml version="1.0" encoding="utf-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app"> <title>{title}</title> <content type="text/plain">この記事は {NEW_SITE_URL} に移転しました。</content> </entry>""" resp = requests.put( entry["edit_url"], auth=(HATENA_ID, API_KEY), data=update_xml.encode("utf-8"), headers={"Content-Type": "application/atom+xml; charset=utf-8"}, timeout=30, ) return resp.status_code entries = fetch_all_entries() print(f"Found {len(entries)} entries") for i, e in enumerate(entries): status = replace_content(e) print(f"[{i+1}/{len(entries)}] {status}: {e['title'][:50]}") time.sleep(0.5) 方法2:はてなブログの管理画面から一括削除 記事数が少なければ、管理画面の「記事の管理」から手動で削除する方法もあります。ただし一括選択機能がないため、大量の記事には向きません。 ...

OpenITI mARkdownからTEI XMLへの自動変換ツール「oitei」を試す

OpenITI mARkdownからTEI XMLへの自動変換ツール「oitei」を試す

はじめに イスラーム圏の歴史テキストを扱う OpenITI(Open Islamicate Texts Initiative) プロジェクトでは、TEI/XMLの代わりに mARkdown という軽量記法でテキストをタグ付けできます。 TEI/XMLは構造化の国際規格として強力ですが、特にアラビア語のような右から左に書く言語(RTL)では、XMLタグとの混在でエディタ上の表示が乱れるという問題があります。mARkdownはこの課題を解決する記法です。 本記事では、mARkdownで書かれたテキストを TEI XMLに自動変換 するPythonツール oitei を実際に動かしてみます。 oiteiとは OpenITI mARkdown → TEI XML の変換ライブラリ(Python) OpenITI TEI Schema に準拠したXMLを出力 PyPIで公開されており pip install で導入可能 依存ライブラリ: oimdp(mARkdownパーサー)、lxml https://github.com/OpenITI/oitei インストール pip install oitei Python 3.8以上が必要です。oimdp(OpenITI mARkdown Parser)と lxml が依存関係として自動インストールされます。 OpenITI mARkdownの記法 mARkdownファイルは以下の3部構成です。 マジックバリュー (1行目): ######OpenITI# メタデータ : #META# で始まる行 本文 : #META#Header#End# の後に記述 主なタグ 記法 意味 `### ` `### ### $ 伝記エントリ # 段落の開始 @P02 名前 人物名(後続2語を含む) @T11 地名 地名(後続1語を含む) @YB732 誕生年(ヒジュラ暦732年) @YD808 没年(ヒジュラ暦808年) %~% 詩行(hemistich)の区切り 固有表現タグ(@P, @T 等)の後ろの 2桁の数字 は、1桁目がエンティティ番号、2桁目が「後続する何単語を名前に含むか」を指定します。例えば @P02 Ibn Khaldun は「後続2語(Ibn Khaldun)を人名として含む」という意味です。 ...

Deep Zoom画像を完全復元:タイル画像からBigTIFFへの変換技術

Deep Zoom画像を完全復元:タイル画像からBigTIFFへの変換技術

はじめに Webサイト上で高解像度画像をスムーズにズーム表示するために使用されるDeep Zoom技術。Microsoft Deep Zoom Composerなどで生成されたタイル化された画像データから、元の高解像度画像を復元する必要に迫られることがあります。 本記事では、Deep Zoom形式で公開されている画像データから、元の高解像度TIFF画像を復元する技術について解説します。 Deep Zoom画像の仕組み タイル構造 Deep Zoom画像は、1枚の大きな画像を複数の小さなタイル画像に分割し、ピラミッド構造で保存します: レベル0 : 最も低解像度(通常1タイル) レベルN : 最高解像度(元画像の解像度に相当) 各レベルで解像度が2倍になる ファイル構成 dzc_output.xml # メタデータ dzc_output_files/ ├── 0/ │ └── 0_0.jpg # レベル0の唯一のタイル ├── 1/ │ ├── 0_0.jpg │ └── ... └── 16/ # 最高レベル ├── 0_0.jpg ├── 0_1.jpg └── ... # 数万枚のタイル 実装の課題と解決策 課題1: XMLメタデータの名前空間の違い Deep Zoomには複数のバージョンがあり、XMLの名前空間が異なります: http://schemas.microsoft.com/deepzoom/2008 http://schemas.microsoft.com/deepzoom/2009 解決策 : 複数の名前空間に対応した柔軟なXMLパーサーを実装 ...

GitHub File History Analyzerの紹介:ファイル編集履歴をAIで分析するツール

GitHub File History Analyzerの紹介:ファイル編集履歴をAIで分析するツール

本記事はAIが作成しました。 はじめに GitHubリポジトリで管理されているファイルの編集履歴を分析したいと思ったことはありませんか?特に長期間にわたって更新されているファイルの変更パターンや、プロジェクトの進化の過程を理解したい場合があります。 GitHub File History Analyzerは、このようなニーズに応えるために開発したコマンドラインツールです。 ツールの概要 このツールは以下の機能を提供します: GitHubのAPIを使用して特定ファイルのコミット履歴を取得 変更内容の統計的な分析(追加・削除行数、変更タイプの分類など) OpenRouter経由でAI(Gemini 2.5 Proなど)による編集パターンの分析 分析結果のMarkdown/JSON形式での出力 開発の背景 デジタルアーカイブプロジェクトで、XMLファイルの長期的な編集作業を追跡する必要がありました。単純なgit logでは得られない、より深い洞察(編集の傾向、作業の質、進捗状況など)を得たいという要求から、このツールの開発に至りました。 技術的な実装 使用技術 言語 : Python 3.8+ 主要ライブラリ : PyGithub(GitHub API wrapper) requests(HTTP通信) python-dotenv(環境変数管理) アーキテクチャ ツールは主に2つのコンポーネントで構成されています: GitHubFileHistoryAnalyzer : GitHub APIを使用してファイル履歴を取得・分析 OpenRouterClient : AI分析のためのクライアント # 基本的な使用例 analyzer = GitHubFileHistoryAnalyzer(github_token) commits = analyzer.get_file_history("owner/repo", "path/to/file.xml") analysis = analyzer.analyze_patches(commits) prompt = analyzer.generate_ai_prompt(commits, analysis) 実際の使用例 基本的なコマンド # ファイル履歴の取得と表示 python main.py --repo owner/repo --file path/to/file.py # AI分析の実行 python main.py --repo owner/repo --file path/to/file.py --analyze # 結果をMarkdown形式で保存 python main.py --analyze --ai-output analysis.md 分析結果の例 ツールは以下のような情報を提供します: ...

DHConvalidatorにおける'ref'に関する不具合への対応

DHConvalidatorにおける'ref'に関する不具合への対応

本記事は、一部AIが執筆しました。 概要 DHConvalidatorは、デジタル人文学(DH)会議の抄録を一貫したTEI(Text Encoding Initiative)テキストベースに変換するためのツールです。 https://github.com/ADHO/dhconvalidator このツールの利用において、Microsoft Word形式(DOCX)からTEI XML形式への変換処理中に以下のようなエラーが発生するケースがありました: ERROR: nu.xom.ParsingException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'ref' この原因と対処方法について共有します。 原因の特定 調査の結果、問題の原因はWord文書内に埋め込まれた INCLUDEPICTUREフィールドコード であることが判明しました。 具体的には、Googleドキュメントから画像をコピー&ペーストした際に、以下のようなフィールドコードが文書内に残存していました: INCLUDEPICTURE "https://lh7-rt.googleusercontent.com/docsz/..." \* MERGEFORMATINET これらの外部画像参照リンクがTEI変換プロセスで適切に処理されず、XML検証エラーを引き起こしていました。 解決方法 この問題を解決するため、DOCXファイル内の問題のあるフィールドコードを自動的に除去するPythonスクリプトを開発しました。 スクリプトの特徴 安全な処理 : 画像コンテンツ自体は保持し、フィールドコード部分のみを削除 ZIP形式対応 : DOCXファイルの内部構造(ZIP + XML)を適切に処理 名前空間対応 : Word文書のXML名前空間を考慮した正確な要素検索 主要な処理ロジック DOCXファイルを一時ディレクトリに展開 word/document.xml内のフィールドコード構造を解析 INCLUDEPICTUREを含むフィールドを特定 フィールド制御要素(begin/separate/end)のみを削除し、画像要素は保持 修正されたXMLで新しいDOCXファイルを生成 実装のポイント フィールドコード判定 def is_includepicture_field(field_runs, ns): for run in field_runs: instr_text = run.find('.//w:instrText', ns) if instr_text is not None and instr_text.text: if 'INCLUDEPICTURE' in instr_text.text: return True return False 削除対象の選別 def should_remove_run(run, ns): # フィールド制御要素を持つか確認 has_field_control = (run.find('.//w:fldChar', ns) is not None or run.find('.//w:instrText', ns) is not None) # 実際の画像コンテンツを持つか確認 has_image_content = (run.find('.//w:drawing', ns) is not None or run.find('.//w:pict', ns) is not None) # フィールド制御要素があり、画像コンテンツがない要素を削除 return has_field_control and not has_image_content 結果 このスクリプトにより、問題のあるフィールドコードが除去され、TEI変換プロセスが正常に完了するようになりました。画像は適切に文書内に埋め込まれた状態で保持されます。 ...

ArchivematicaのPreservation planningにおいて、Normalizationのルールを追加する

ArchivematicaのPreservation planningにおいて、Normalizationのルールを追加する

概要 ArchivematicaのPreservation planningにおいて、Normalizationのルールを追加する方法の備忘録です。 背景 拡張子が.jpgである画像をArchivematicaに投入した際、以下のようにFormatがJPEGのものに対してtifファイルを保存用に作成するルールを用意しているにもかかわらず、tifファイルが作成されないことがありました。 そこで、以下のような履歴の画面から、タスクの内容を確認しました。 結果は以下です。 具体的には以下のような記載になっており、該当するルールが存在しない、ということが記載されています。 File format: Image (Raster): Exchangeable Image File Format (Compressed): EXIF Compressed Image 2.2.1 (big-endian) (fmt/645) Not normalizing 11ecf05d-8fc6-4704-a6e9-4a26ef98f186.jpg - No rule or default rule found to normalize for preservation そこで、fmt/645に対するルールを追加します。 ルールの追加 「Create new rule」のリンクをクリックします。 そして、以下のように入力します。 今回は以下を「The related format」として指定します。 Image (Raster): Exchangeable Image File Format (Compressed): EXIF Compressed Image 2.2.1 (big-endian) (fmt/645) 結果、以下のようにルールが新規に追加され、以降、保存用のtifファイルが生成されるようになりました。 まとめ ArchivematicaのPreservation planningにおけるルール追加の一例を紹介しました。参考になりましたら幸いです。 ...

AtoM(Access to Memory)のAPIを使って、オブジェクトを登録してみる

AtoM(Access to Memory)のAPIを使って、オブジェクトを登録してみる

概要 AtoM(Access to Memory)のAPIを使って、オブジェクトを登録する方法の備忘録です。 APIの有効化 以下にアクセスします。 /sfPluginAdminPlugin/plugins arRestApiPluginを有効にします。 APIキーの取得 以下に、APIキーを生成する方法が説明されています。 https://www.accesstomemory.org/en/docs/2.9/dev-manual/api/api-intro/#generating-an-api-key-for-a-user ユーザ名とパスワードでもAPI接続できるようですが、今回はREST API Keyを発行しました。 エンドポイント AtoMでは、「典拠レコード」や「機能」など、複数のメニューが提供されていますが、APIによって利用できるのは、以下のみのようです。 See the subsequent pages for more details on each endpoint, and available parameters. There are three endpoints available: Browse taxonomy terms Browse information objects Read information object Download digital objects Add physical objects この点は、ArchivesSpaceのほうが豊富なAPIが提供されており、軍配が上がるかもしれません。 https://archivesspace.github.io/archivesspace/api/ また、以下のソースコードを確認すると、CreateActionが可能なものは、informationobjectsとphysicalobjects、digitalobjectsに限定されているようでした。 https://github.com/artefactual/atom/tree/qa/2.x/plugins/arRestApiPlugin/modules/api/actions ただ機械的に一括登録を行いたい場面は、主にinformationobjectsだと考えられるため、これらの機能のみで十分かもしれません。 physical objectsの登録 以下のようなクラスを用意します。 #| export class ApiClient: def __init__(self): load_dotenv(override=True) self.url = os.getenv("atom_url") username = os.getenv("username") password = os.getenv("password") api_key = os.getenv("api_key") if api_key: self.headers = { "REST-API-Key": api_key, "Content-Type": "application/json" } else: # Basic 認証のヘッダーを作成 auth_string = f"{username}:{password}" auth_bytes = auth_string.encode('ascii') auth_b64 = base64.b64encode(auth_bytes).decode('ascii') self.headers = { "Authorization": f"Basic {auth_b64}", "Content-Type": "application/json" } def add_physical_objects(self, physical_objects): url = f"{self.url}/api/physicalobjects" print(url, self.headers, physical_objects) response = requests.post(url, headers=self.headers, json=physical_objects) # レスポンスを確認 if response.status_code in [200, 201]: print("物理オブジェクトが作成されました!") print(f"ステータスコード: {response.status_code}") print(f"レスポンス: {response.text}") # 作成されたオブジェクトの情報 result = response.json() print(f"作成された物理オブジェクトID: {result.get('id')}") print(json.dumps(result, indent=4)) else: print(f"エラー: {response.status_code}") print(f"レスポンス: {response.text}") 以下で実行します。 ...

WordファイルをTEI XMLに変換する方法:TEIgarage APIの活用ガイ

WordファイルをTEI XMLに変換する方法:TEIgarage APIの活用ガイ

この記事は、AIが作成し、一部を人が修正したものです。 はじめに デジタル人文学の世界では、文書をTEI(Text Encoding Initiative)形式で保存することが一般的になっています。TEIは学術的なテキストを構造化するための標準規格です。今回は、Microsoft Wordで作成した文書をTEI XML形式に変換する方法を、Pythonを使って解説します。 TEIgarageとは? TEIgarageは、さまざまな形式の文書をTEI XMLに変換するためのオンラインサービスです。このサービスはAPIを提供しており、プログラムから直接利用することができます。今回はこのAPIをPythonから呼び出して、Wordファイルを変換してみましょう。 必要なもの Python 3.6以上 requestsライブラリ(APIリクエスト用) インターネット接続 変換したいWordファイル(.docx形式) 手順 1. 必要なライブラリをインストールする まず、必要なライブラリをインストールしましょう。コマンドプロンプトやターミナルで以下のコマンドを実行します。 pip install requests 2. Pythonスクリプトを作成する 次に、以下のPythonコードをword_to_tei.pyなどの名前で保存します。 import requests import os import zipfile from io import BytesIO def convert_docx_to_tei_xml(file_path, output_path): # OxGarageのエンドポイント input_document_type = "docx%3Aapplication%3Avnd.openxmlformats-officedocument.wordprocessingml.document" output_document_type = "TEI%3Atext%3Axml" TEIGARAGE_URL = f"https://teigarage.tei-c.org/ege-webservice/Conversions/{input_document_type}/{output_document_type}/" # .docx ファイルを開いてAPIに送信 with open(file_path, "rb") as file: files = {"file": file} response = requests.post(TEIGARAGE_URL, files=files) # 変換結果をファイルとして保存せずに展開 if response.status_code == 200: # zipファイルをメモリ上で展開 with zipfile.ZipFile(BytesIO(response.content)) as zip_ref: # tei.xmlファイルを保存 for member in zip_ref.namelist(): if member.endswith("tei.xml"): zip_ref.extract(member, os.path.dirname(output_path)) tei_xml_path = os.path.join(os.path.dirname(output_path), member) os.rename(tei_xml_path, output_path) print("TEI/XML 変換成功!tei.xml に保存されました。") break else: print("エラー: tei.xml ファイルが見つかりません。") else: print("エラー:", response.status_code, response.text) # メイン処理 if __name__ == "__main__": # 変換したいWordファイルのパスを指定 word_file = "documents/sample.docx" # ここを実際のファイルパスに変更してください # 出力ファイルのパスを指定 output_file = "output/sample_tei.xml" # 出力先を指定 try: # Wordファイルを変換 convert_docx_to_tei_xml(word_file, output_file) except Exception as e: print(f"エラーが発生しました: {e}") 3. スクリプトを実行する スクリプト内のword_file変数を、変換したいWordファイルの実際のパスに変更します。同様に、output_file変数も希望の出力先に変更してください。 ...

DrupalのJSON:APIを用いて、ユーザ名とパスワードでデータ登録を行う

DrupalのJSON:APIを用いて、ユーザ名とパスワードでデータ登録を行う

概要 過去に、DrupalのJSON:APIを用いて、Pythonによるデータ登録を行う記事を執筆しました。 以下は、Basic認証を用いた方法です。 また以下は、API Keyを用いた方法です。 これらに加えて、通常のログインによる登録を行うことができたので、備忘録です。 コード 以下の通りです。ログインやCSRFトークンを取得した上で、コンテンツを登録します。 import requests import json import os from dotenv import load_dotenv class ApiClient: def __init__(self): load_dotenv(override=True) # DrupalサイトのURL(例) self.DRUPAL_BASE_URL = os.getenv("DRUPAL_BASE_URL") # エンドポイント(JSON:API) # self.JSONAPI_ENDPOINT = f"{self.DRUPAL_BASE_URL}/jsonapi/node/article" # 認証情報(Basic認証) self.USERNAME = os.getenv("USERNAME") self.PASSWORD = os.getenv("PASSWORD") def login(self): # ログインリクエスト login_url = f"{self.DRUPAL_BASE_URL}/user/login?_format=json" login_response = requests.post( login_url, json={"name": self.USERNAME, "pass": self.PASSWORD}, headers={"Content-Type": "application/json"} ) if login_response.status_code == 200: self.session_cookies = login_response.cookies def get_csrf_token(self): # CSRFトークンを取得 csrf_token_response = requests.get( f"{self.DRUPAL_BASE_URL}/session/token", cookies=self.session_cookies # ここでログインセッションを渡す ) if csrf_token_response.status_code == 200: # return csrf_token_response.text # self.csrf_token = csrf_token_response.text self.headers = { "Content-Type": "application/vnd.api+json", "Accept": "application/vnd.api+json", "X-CSRF-Token": csrf_token_response.text, } else: # raise Exception(f"CSRFトークン取得失敗: {csrf_token_response.status_code} {csrf_token_response.text}") self.csrf_token = None def create_content(self, data: dict): # 記事作成リクエスト url = f"{self.DRUPAL_BASE_URL}/jsonapi/{data['data']['type'].replace('--', '/')}" response = requests.post( # self.JSONAPI_ENDPOINT, url, headers=self.headers, cookies=self.session_cookies, json=data ) if response.status_code == 201: print("コンテンツが作成されました!") else: print("エラー:", response.status_code, response.text) これにより、以下で、コンテンツを登録することができました。 ...

大きな画像から部分画像の座標を取得する方法

大きな画像から部分画像の座標を取得する方法

概要 大きな画像の一部が切り出された複数の画像から、元の画像内での座標を取得する機会がありました。本記事では、そのための方法についての備忘録をまとめます。 OpenCV の SIFT (Scale-Invariant Feature Transform) を用いて、テンプレート画像と元の画像を特徴点マッチングし、アフィン変換を推定して座標を取得する方法を紹介します。 実装 必要なライブラリ pip install opencv-python numpy tqdm Pythonコード 以下のコードでは、指定した大きな画像 (image_path) に対して、テンプレート画像 (templates_dir 内の PNG 画像) を SIFT でマッチングし、元の画像内の座標を取得します。 import cv2 import numpy as np from glob import glob from tqdm import tqdm import os # 画像読み込み def load_image_gray(path): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: print(f"画像が見つかりません: {path}") return img # 特徴点抽出 def extract_features(image, detector): return detector.detectAndCompute(image, None) # マッチング処理 def match_features(des1, des2, matcher, ratio_test=0.7, min_matches=4): matches = matcher.knnMatch(des1, des2, k=2) good_matches = [m for m, n in matches if m.distance < ratio_test * n.distance] return good_matches if len(good_matches) >= min_matches else None # アフィン変換推定 def estimate_affine_transform(kp1, kp2, good_matches): src_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) M_affine, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts, method=cv2.RANSAC, ransacReprojThreshold=5.0) return M_affine # 画像上にマッチング結果を描画 def draw_matched_rectangle(image, M_affine, templ_shape): h, w = templ_shape rect_pts = np.float32([[0, 0], [w, 0], [w, h], [0, h]]) # 長方形の四隅 transformed_pts = cv2.transform(np.array([rect_pts]), M_affine)[0] # 変換後の座標 cv2.polylines(image, [np.int32(transformed_pts)], isClosed=True, color=(0, 0, 255), thickness=2) return transformed_pts # メイン処理 def main(image_path, templates_dir, output_path): # 画像とテンプレート一覧の読み込み img = load_image_gray(image_path) templ_paths = glob(templates_dir) dst_img = cv2.imread(image_path) # SIFT特徴量検出器 & BFMatcher 設定 sift = cv2.SIFT_create() bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False) kp1, des1 = extract_features(img, sift) # 特徴点が見つからなかった場合 if des1 is None: print("対象画像の特徴点が見つかりませんでした。") return for templ_path in tqdm(templ_paths): templ = load_image_gray(templ_path) if templ is None: continue kp2, des2 = extract_features(templ, sift) if des2 is None: continue good_matches = match_features(des1, des2, bf) if good_matches is None: print(f"特徴点のマッチングが不足: {templ_path}") continue # アフィン変換推定 M_affine = estimate_affine_transform(kp1, kp2, good_matches) if M_affine is None: print(f"アフィン変換推定に失敗: {templ_path}") continue # 矩形描画 best_dst = draw_matched_rectangle(dst_img, M_affine, templ.shape) # ファイル名を矩形の近くに表示 x, y, _, _ = cv2.boundingRect(best_dst) base_name = os.path.splitext(os.path.basename(templ_path))[0] cv2.putText(dst_img, base_name, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) # 結果を保存 cv2.imwrite(output_path, dst_img) print(f"結果画像を保存しました: {output_path}") 実行 # 実行 if __name__ == "__main__": # パラメータ設定 IMAGE_PATH = "/xxx/default.jpg" TEMPLATES_DIR = "/xxx/*.png" OUTPUT_PATH = "/xxx/match_result.jpg" main(IMAGE_PATH, TEMPLATES_DIR, OUTPUT_PATH) まとめ 本記事では、SIFT を用いた特徴点マッチング によって、部分画像が元画像のどこに位置するかを推定し、アフィン変換 で位置を特定する方法を紹介しました。 ...