コミックマーケット96のサークル参加告知です。
サークル名:バイトニック
参加日:8/12(月・4日目)
配置場所:東ト20a
新刊:
「Webカタログ用ツール作ってみた その3」
自作の新しいChrome拡張と既存のツール改修について書いているプログラミングの本です。
下記ツールについて書いています。
- Webカタログ用Chrome拡張機能【新規開発】
- Webカタログを検索するChrome拡張機能【既存改修】
- WebカタログAPIを利用した地図作成ツール【既存改修】
- コミケ以外の同人イベントのサークルリストページからWebカタログのお気に入りサークルCSVに入っているサークルを抽出するPythonスクリプト【既存改修】
頒布価格:300円
また、少部数ですが既刊も頒布します。
上記同人誌にツールのソースを掲載していますが、コピペできるようにここでもソースのみ掲載します。「WebカタログAPIを利用した地図作成ツール」についてはWebカタログAPIが非公開のため、掲載しません。
Webカタログ用Chrome拡張機能
manifest.json
{ "name": "Comike Catalog Utils", "version": "0.1.0", "content_scripts": [ { "matches": ["https://webcatalog.circle.ms/Search/Result*"], "js": ["content_script_search.js"] }, { "matches": ["https://webcatalog.circle.ms/Spa*"], "js": ["content_script_circle_list.js"] } ], "manifest_version": 2 }
content_script_search.js
同人誌に載せたソースにバグがあり、ここには修正版を掲載しています。修正した部分は先頭のreferrerの判定です。
var referrer = document.referrer; if (!referrer || referrer.split("/")[2] !== "webcatalog.circle.ms") { // 直接検索ページにアクセスされた場合 // 検索結果にスクロール document.getElementsByClassName("m-result").item(0).scrollIntoView(); // 検索結果が一件だった場合はサークル詳細にジャンプ if (document.querySelectorAll(".h-text--large").length === 1) { document.querySelectorAll(".c-table--list tr a").item(0).click(); } }
content_script_circle_list.js
const COLUMN_OF_LIST = 6; const KEEP_ROW = 30; var releaseElems = function(event) { // 画面より上部の要素の解放 var listElems = document.querySelectorAll("li.cut-tile"); var parentElem = document.querySelector("ul.m-cutblock-list") // 削除する行数を求める var releaseRowNum = Math.floor(listElems.length / COLUMN_OF_LIST) - KEEP_ROW; if (releaseRowNum <= 0) { return; } // 削除する要素数を求める var releaseElemNum = releaseRowNum * COLUMN_OF_LIST; // 要素削除の実行 for(var i = 0; i < releaseElemNum; i++) { parentElem.removeChild(listElems.item(i)); } }; // ボタンの作成 var releaseButton = document.createElement("button"); releaseButton.type = "button"; releaseButton.innerText = "上部要素解放"; releaseButton.style ="margin-top: 5px; position: fixed; top: 5px; left: 10px;"; releaseButton.addEventListener("click", releaseElems, false); document.body.appendChild(releaseButton);
Webカタログを検索するChrome拡張機能
manifest.json
{ "name": "Comike Catalog Search Context Menu", "version": "0.1.1", "permissions": ["contextMenus"], "background": { "scripts": ["background.js"], "persistent": false }, "manifest_version": 2 }
chrome.runtime.onInstalled.addListener(function() { chrome.contextMenus.create({ id: "searchCatalog", title: "コミケWebカタログで検索", contexts: ["selection"], type: "normal", }); }); chrome.contextMenus.onClicked.addListener(function (info) { if (info.menuItemId === "searchCatalog") { // サークル名、ヨミガナ、執筆者名を検索 // 全日程及び落選が対象 // 全ジャンル対象 var keyword = encodeURIComponent(info.selectionText); // ジャンルコード追加変更時にはパラメータ変更要 var url = `https://webcatalog.circle.ms/Search/Result?c.Keyword=${keyword}&c.op=0&c.d1=true&c.d1=false&c.d2=true&c.d2=false&c.d3=true&c.d3=false&c.d4=true&c.d4=false&c.dl=true&c.dl=false&c.cn=true&c.cn=false&c.ck=true&c.ck=false&c.ca=true&c.ca=false&c.cb=false&c.ct=false&c.ctw=false&c.cpi=false&c.cu=false&c.cm=false&c.cno=false&c.cfm=false&c.cmo=0&c.gls=111&c.gls=112&c.gls=113&c.gls=114&c.gls=115&c.gls=116&c.gls=211&c.gls=212&c.gls=213&c.gls=221&c.gls=232&c.gls=233&c.gls=234&c.gls=300&c.gls=311&c.gls=312&c.gls=313&c.gls=314&c.gls=315&c.gls=321&c.gls=331&c.gls=332&c.gls=333&c.gls=334&c.gls=400&c.gls=431&c.gls=432&c.gls=433&c.gls=500&c.gls=511&c.gls=531&c.gls=532&c.gls=533&c.gls=534&c.gls=535&c.gls=600&c.gls=611&c.gls=700&c.gls=711&c.gls=811&c.gls=812&c.gls=813&c.gls=831&c.gls=833&c.gls=835&c.gls=836&c.gls=911&c.gls=912&c.gls=998&page=1&c.SortOrderBy=0`; window.open(url); } });
コミケ以外の同人イベントのサークルリストページからWebカタログのお気に入りサークルCSVに入っているサークルを抽出するPythonスクリプト
circleListFavoriteMatchTool.py
import sys import requests from bs4 import BeautifulSoup import csv import unicodedata import pprint import re def normalize(str): """文字列のUnicode正規化""" return unicodedata.normalize('NFKC', str) def make_unique_list(seq): """リストの重複要素を削除""" seen = [] return [x for x in seq if x not in seen and not seen.append(x)] def parse_circle_list_creation(soup): """クリエイション(サンクリ、コミクリ)のサークルリストを抽出""" circlelist = [] alltr = soup.find("table", attrs={"border":"1"}).tbody.find_all('tr') for tr in alltr: if tr.td.has_attr('colspan'): # ブロック文字行は飛ばす continue alltd = tr.find_all('td') # 比較のため正規化したサークル名も取得 circlelist.append({'position': alltd[0].string, 'name': alltd[1].string, 'normalizedname': normalize(alltd[1].string), 'author': alltd[2].string, }) return circlelist def parse_circle_list_comitia(soup): """コミティアのサークルリストを抽出""" circlelist = [] alltr = soup.main.table.find_all('tr') for tr in alltr: if tr.td.has_attr('colspan'): # ブロック文字行は飛ばす continue alltd = tr.find_all('td') # 比較のため正規化したサークル名も取得 circlelist.append({'position': alltd[0].string, 'name': alltd[1].string, \ 'normalizedname': normalize(alltd[1].string)}) return circlelist def parse_circle_list_sdf(soup): """SDFのサークルリストを抽出""" circlelist = [] alltr = soup.find_all('tr') for tr in alltr: alltd = tr.find_all('td') if len(alltd) != 2: # サークル情報以外(カットなど)は飛ばす continue # サークル情報(先頭にサークル名、末尾に配置、途中はイベント名等) infos = list(str.strip() for str in alltd[0].strings) author = alltd[1].string circlelist.append({'position': infos[-1], 'name': infos[0], 'normalizedname': normalize(infos[0]), 'author': author }) return circlelist def parse_circle_list_bsmatsuri(soup): """BS祭のサークルリストを抽出""" circlelist = [] alltr = soup.find_all('tr') for tr in alltr: alltd = tr.find_all('td') if len(alltd) != 6: # サークル情報以外(イベントタイトル)は飛ばす continue if alltd[0].string == "配置ジャンル" or alltd[1].string is None: # 見出し行は飛ばす continue # サークル情報(イベント名、サークル名、ふりがな、ペンネーム、サイト情報、配置番号) circlelist.append({'position': alltd[5].string, 'name': alltd[1].string, \ 'normalizedname': normalize(alltd[1].string), 'author': alltd[3].string }) return circlelist def get_circle_list(url): """サークルリストページのデータを取得し、リストを抽出する""" response = requests.get(url) # 文字コード判定はBeautifulSoupで行うため、バイト列で渡す soup = BeautifulSoup(response.content, 'html.parser') parsefuncs = [ ('.*creation.*', parse_circle_list_creation), ('.*comitia.*', parse_circle_list_comitia), ('.*sdf-event.*', parse_circle_list_sdf), ('.*bs-fes.*', parse_circle_list_bsmatsuri), ] for parsefunc in parsefuncs: if re.match(parsefunc[0], url): return parsefunc[1](soup) return null def match_circle_list(circlelist, favoritefilepath): """お気に入りサークルリストファイルの内容を取得し、サークルリストとマッチングする""" with open(favoritefilepath, 'r') as f: reader = csv.reader(f) header = next(reader) # サークルリストの比較 checklist = [] for row in reader: if row[0] == 'Circle': name = normalize(row[10]) elif row[0] == 'UnKnown': name = normalize(row[1]) else: continue for circle in circlelist: if name == circle['normalizedname']: checklist.append(circle) checklist = make_unique_list(checklist) return checklist def print_check_list(checklist): """チェックリストを出力""" checklist = sorted(checklist, key=lambda circle: normalize(circle['position'])) pprint.pprint(['{0}:{1}'.format(c['position'], c['name']) for c in checklist]) circlelist = get_circle_list(sys.argv[1]) checklist = match_circle_list(circlelist, sys.argv[2]) print_check_list(checklist)