向こう岸

最近はもっぱら同人イベントの参加記録

コミックマーケット96サークル参加告知

コミックマーケット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)