コミックマーケット95のサークル参加告知です。
サークル名:バイトニック
参加日:12/30(2日目)
配置場所:東ト20a
新刊:
「Webカタログ用ツール作ってみた 周辺ツール編」
私が開発した下記3ツールを紹介する技術系同人誌です。
- ブラウザ上で選択した単語をWebカタログで検索するChrome拡張機能
- 同人誌委託書店通販履歴ページ記載のサークル名をWebカタログで検索するChrome拡張機能
- コミケ以外の同人イベントのサークルリストページからWebカタログのお気に入りサークルCSVに入っているサークルを抽出するPythonスクリプト
頒布価格:200円
上記同人誌にソースを載せていますが、コピペできるようにここでもソースのみ載せます。
ブラウザ上で選択した単語をWebカタログで検索するChrome拡張機能
manifest.json
{ "name": "Comike Catalog Search Context Menu", "version": "0.1.0", "permissions": ["contextMenus"], "background": { "scripts": ["background.js"], "persistent": false }, "manifest_version": 2 }
background.js
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.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=832&c.gls=833&c.gls=835&c.gls=911&c.gls=912&c.gls=998&page=1&c.SortOrderBy=0`; window.open(url); } });
同人誌委託書店通販履歴ページ記載のサークル名をWebカタログで検索するChrome拡張機能
manifest.json
{ "name": "Melon To Comike Catalog Search", "version": "0.1.0", "content_scripts": [ { "matches": ["https://www.melonbooks.co.jp/mypage/history.php"], "js": ["content_script.js"] } ], "manifest_version": 2 }
content_script.js
var search = function(event) { var circleList = event.target.parentNode.parentNode.querySelectorAll( "table tbody tr td p.circle"); circleList.forEach(function(elem) { // サークル名を検索 // 全日程及び落選が対象 // 全ジャンル対象 var keyword = encodeURIComponent(elem.innerText); // ジャンルコード追加変更時にはパラメータ変更要 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.dl=true&c.dl=false&c.cn=true&c.cn=false&c.ck=false&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=832&c.gls=833&c.gls=835&c.gls=911&c.gls=912&c.gls=998&page=1&c.SortOrderBy=0`; window.open(url); }); }; // 各注文のステータス・配送業者部分のdiv要素 var orderStatusElems = document.querySelectorAll("div.status"); // 各div要素にボタン追加 orderStatusElems.forEach(function(elem) { var searchButton = document.createElement("button"); searchButton.type = "button"; searchButton.innerText = "Webカタログでサークルを検索"; searchButton.style ="margin-top: 5px;"; searchButton.addEventListener("click", search, false); elem.appendChild(searchButton); });
コミケ以外の同人イベントのサークルリストページから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_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 get_circle_list(url): """サークルリストページのデータを取得し、リストを抽出する""" response = requests.get(url) # 文字コード判定はBeautifulSoupで行うため、バイト列で渡す soup = BeautifulSoup(response.content, 'html.parser') parsefuncs = [ ('.*comitia.*', parse_circle_list_comitia), ('.*sdf-event.*', parse_circle_list_sdf), ] 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)