Pythonで駅コード調査用データベースから情報をスクレイピングした

blog.zuckey17.org

前回、 Suicaの利用履歴をパースするリポジトリ中の StationCode.csvについて、

StationCode.csvがコミュニティーの有志によって作成されたものらしく、最新のものではないのか、完全ではないようです。

と書きました。

よくよく調べると、路線・駅コード一覧・登録 というサイトがあり、

路線・駅コード一覧・登録のページに遷移すると、駅コードのデータベースのようなものにたどり着きます。 ここで随時更新されているようなので、今回はこれをスクレイピングして、真・StationCode.csvを作成してみました。

Pythonによるスクレイピング、ちゃんとするのが初めてでいろいろハマりましたが以下のようなコードになりました。

import csv
import re
import urllib2
from bs4 import BeautifulSoup

current_page = 1

html = urllib2.urlopen("http://www.denno.net/SFCardFan/index.php")
soup = BeautifulSoup(html, "html.parser")
last_page_num = int(re.findall('[\d]+', soup.find("a", title="last page").string)[0])

file = open('StationCode.csv', 'w')

for num in range(last_page_num):
    html = urllib2.urlopen("http://www.denno.net/SFCardFan/index.php?pageID=" + str(current_page))
    soup = BeautifulSoup(html, "html.parser")
    rows = soup.find_all("table")[1].find_all("tr")
    del rows[0:2]
    for row in rows:
        datas = map(lambda x: not(isinstance(x, type(None))) and not(isinstance(x.string, type(None))) and x.string.encode('utf-8'), row.find_all("td")) # エンコード変換
        writer = csv.writer(file, lineterminator='\n')
        writer.writerow(datas)
    current_page += 1

file.close()

ファイルに書き出す際に UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128) と文字エンコードで怒られたので、空文字列でない場合にUTF-8エンコードを変換する処理を書きました。

それ以外は意外とスムーズに進んだので、pythonすげーとなりました。

まとめ

周辺知識なしからSuicaの利用履歴を読み込むデモ( macOS )

f:id:zuckey_17:20181104184308j:plain:w200

Suicaの利用履歴を読み込み、入場駅やためていたデータの履歴を表示するところまでをとりあえずやってみたので、手順をまとめておきます。

お願い

僕はPythonをほとんど触ったことがないので、ライブラリの利用方法や読み込みなどあまり理解していないので、温かい目でみていただき、間違った部分があれば指摘していただければ嬉しいです。

用意するもの

ソニー SONY 非接触ICカードリーダー/ライター PaSoRi RC-S380

ソニー SONY 非接触ICカードリーダー/ライター PaSoRi RC-S380

PaSoRiICカードリーダーでよく見るのでこれにしてみました。

nfcpy

今回は nfcpyというライブラリを利用します。

https://nfcpy.readthedocs.io/en/latest/index.html

NFC(Near Field Communication)で受け取った生データを読み込んで出力するためのライブラリのようです。

こちらを使います。

python3.x系への対応はまだらしく、2.x系を利用します。

python & pipの準備

僕はHomebrewにてpython2系をインストールしました。

$ brew install python@2
...()...

$ which python2
/usr/local/bin/python2

$ which pip2
/usr/local/bin/pip2

pip(pythonのパッケージ管理ツール)も一緒に入ります。

その他の必要な環境を準備

USB読み込み系

PaSoRiはUSBデバイスなのでそれを扱うためのライブラリをインストールしておきます。

$ brew install libusb
$ brew install libusb-compat
$ sudo pip2 install pyusb libusb1 pyserial

OpenSSL を新しくする

macOSに標準で入っているOpenSSLはバージョンが古いらしく、下記の方法で新しくします。

$ brew install openssl
$ ln -s /usr/local/opt/openssl/bin/openssl /usr/local/bin/openssl
$ ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/libcrypto.dylib
$ ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/libssl.dylib

バージョン管理ツールBazaar

nfcpy を利用するための方法は、 Bazaarというgitのような分散型のバージョン管理ツールを利用することが公式で紹介されています。 このツールはHomebrewで入れることができます。

$ brew install bzr

nfcpyを利用する

まず、Bazaarnfcpyを落とします。 trunkというディレクトリにnfcpyのライブラリのリポジトリが落とせます。

$ bzr branch lp:nfcpy trunk
$ cd trunk
$ tree -L 1
.
├── HISTORY.rst
├── LICENSE
├── README.rst
├── docs
├── examples
├── nfc
├── setup.py
├── tests
└── tools

examples以下にこのライブラリを用いたサンプルがあるので、実行してみます。 まず先にmacPaSoRiを接続しておきましょう。

$ python2 examples/tagtool.py
[nfc.clf] searching for reader on path usb
[nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:020:025
** waiting for a tag ** <<< ここで入力待ちをするのでICカードをPaSoRiに近づけます
Type3Tag 'FeliCa Standard (RC-S???)' ID=**************** PMM=**************** SYS=0003

これでFeliCa対応のカードの情報を取得することができました。 ↑は

  • IDm (製造ID/Manufacture ID)
  • PMm (製造パラメータ/Manufacture Parameter)

だそうです。

Suicaの利用履歴を読み込む

さて、本題のSuicaの利用履歴です。Suicaは過去20件の利用履歴が取り出せるようになっています。 nfcpyを利用してSuicaの情報を取得し、生データをパースして見やすくしてくれているライブラリが以下です。

https://github.com/m2wasabi/nfcpy-suica-sample

中を見ていただければわかりますが、StationCode.csvというところに、生データと駅との組み合わせがあり、それによって入出場駅を出力できているようです。

clone

このリポジトリを任意の場所にcloneし、中でnfcpyを落としてきます。 その際ディレクトリ名をnfcpyにしているのを間違えないようにします。

$ git clone git@github.com:m2wasabi/nfcpy-suica-sample.git
Cloning into 'nfcpy-suica-sample'...
remote: Enumerating objects: 13, done.
remote: Counting objects: 100% (13/13), done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 13 (delta 3), reused 13 (delta 3), pack-reused 0
Receiving objects: 100% (13/13), 65.33 KiB | 484.00 KiB/s, done.
Resolving deltas: 100% (3/3), done.

$ cd nfcpy-suica-sample
$ bzr branch lp:nfcpy nfcpy # ディレクトリ名をnfcpyに
Not checking SSL certificate for xmlrpc.launchpad.net.
You have not informed bzr of your Launchpad ID, and you must do this to
write to Launchpad or access private data.  See "bzr help launchpad-login".
Branched 284 revisions.

そして、直下あるsuica_read.pyを実行します。

python2 suica_read.py
Type3Tag 'FeliCa Standard (RC-S???)' ID=**************** PMM=**************** SYS=0003
=== 00 ===
端末種: None
処理: 物販
日付: 18-11-02
入線区: None-None
入駅順: None
出線区: None-None
出駅順: None
残高: 1734
BIN:
c7 46 00 00 25 62 6e a7 d0 59 c6 06 00 03 4a 00
=== 01 ===
端末種: 改札機
処理: 運賃支払
日付: 18-11-01
入線区: *********
入駅順: ***
出線区: *********
出駅順: ***
残高: 2643
BIN:
16 01 00 02 25 61 e0 09 e4 3e 53 0a 00 03 49 00

...(略)...

=== 19 ===
端末種: 改札機
処理: None
日付: 18-10-25
入線区: *******
入駅順: ***
出線区: *******
出駅順: **
残高: 3958
BIN:
16 14 02 01 25 59 ce 25 00 00 76 0f 00 03 31 00

と20件のSuica利用履歴が出力されました。 端末機: 物販などはコンビニで利用したときのもののようです。 また、 StationCode.csvがコミュニティーの有志によって作成されたものらしく、最新のものではないのか、完全ではないようです。

まとめ

  • Suicaの利用履歴が読み込めました
  • Pythonのエコシステムについて詳しくなかったためハマりました
  • Raspberry Piを購入したのでこれを使って楽しめたらなというのが今後の展望です

パブリックなページのpathにidを入れるかどうか検討した

パブリックなページのpathにidを入れるかどうか検討しました。
idといっても、DBのプライマリ・キーという意味なので以降 idというのはその意味で見てほしいです。

調べてみても案外エントリがなかったので書いておいておこうと思いました。
もしかしたら、間違っていたり考慮が足りなかったりすると思うので、そのときはコメントなどで教えてほしいです。

idを使う場合と使わない場合?

idを使わない場合

ユーザー個別のリソースを作成するページ

ユーザー個別のリソースに対する編集や削除などが他のユーザーからできてはいけません。
そういった場合は認証をかけると思いますが、単純にユーザーが登録などをしていない場合にも、ユーザー個別のリソースを作成したい場合があります。
何らかの質問項目に回答をしてもらう際などがそうです。(アパレルショップなどで経験があります。)
直接やメールなどでそのURLを送信することもありますが、その際、別の人がそのページを踏んでしまうと回答が混線してしまって大変なことになるので、避けるためにハッシュのようなものをidの代わりに利用することが多いです。

SEOや見た目を重視する場合

ユーザーフレンドリーなURLにすることはSEOにプラスに働くと言われています。
また、URLがわかりやすいとそのページがどういうものなのかを想像しやすく、またイトの世界観を壊さないために数字よりも意味のある文字列にすることもあります。

そのリソースの規模やKPIを悟られたくない場合

idがインクリメントであるならばリソースの数がそのままidとなります。
そのため、サービスの規模を隠したい場合などには隠すことが有効な手段というふうに考えられます。(ブログサービスの記事数など)
また、毎日のように新しいリソースを作るようにするとサービスの成長具合もわかってしまうこともあります。
そのことによって、KPIを推測されるというリスクもあり、それを避けるためにも有効な手段です。

idを使う場合

簡単に言うと上記以外なのですが、認証などはあるものの、個々のユーザーに関係なく同様のリソースを表現している場合です。
Railsなどのフレームワークでは複数存在するリソースに対してルーティングを作成するとデフォルトでidによってアクセスできるpathを用意してくれたりします。
not availableなリソースにアクセスした場合は、どちらの場合も404を返すようにします。
そのような前提に立つならば特に理由のない場合、ハッシュ化する意味はないように思いました。

参考にしたページ

connpass

https://shiganai.connpass.com/event/82050/

eventはすべてのconnpassユーザーが参照可能。

gitHub

https://github.com/rails/rails/issues/34300

Railsはパブリックなリポジトリなのでissuesはすべての人が参照可能。

はてなブログ

https://blog.zuckey17.org/entry/2018/10/21/140909

はてなブログは見た目重視で、デフォルトでは投稿日付をエントリへのパスにしてくれます。
またユーザーが個別にエントリへのパスを指定することもできます。

note.mu

https://note.mu/ruiu/n/n76729d8930c9

詳しく使ったことがないのでわからないですが、noteでは記事に投げ銭ができたり、購入機能があるためidをハッシュ化しているのだと思いました。 また、ブログサービスということで全体の記事数を隠したいという意図もあるのかもしれません。

まとめ

  • これまで深く考えたことがなかったのですが、仕事で必要になり少し意識的に詳しく調べてみました
  • 調査の際、相談にのってもらった方々ありがとうございました

冒頭にも書きましたが、間違っていたり考慮漏れたりしている可能性もあると思うので、コメントなどで指摘いただけると嬉しいです!!

技術書典5に「しがないラジオ」としてサークル参加参加してきました

f:id:zuckey_17:20181021141145p:plain:w300

技術書典5に「しがないラジオ」としてサークル参加参加してきました。
出した本は「完全SIer脱出マニュアル」という本です。
技術書典は技術にまつわる同人誌の即売会で、技術系コミケ *1というとわかりやすいかと思います。
半年に1回のペースで開催されており、第4回の前回は一般参加者として書籍を買う側として参加しましたが、今回第5回はサークル参加ということで書籍を出す側で参加しました。

もともと出たいなと思ってはいましたが具体的には何もなく、Podcastでともにパーソナリティをしているgamiさんが勢いで申し込んでくれ、参加が決定しました。

完全SIer脱出マニュアル

BOOTHで買えます 👇

booth.pm

しがないラジオというのは技術系のポッドキャストで、僕とgamiさんがパーソナリティをつとめており、最近では楽しく働いているエンジニアのキャリアなどを話しています。
タイトルどおりの本で、SIer(=楽しくない職場。といってもいいと思います)から脱出したい人のためにノウハウや考え方を、しがないラジオでこれまで聞いてきたエピソードトークから抽象化して書いた本です。

目次なんかは、BOOTHのリンクからも見れますし、読んだ方々の感想は、togertterにもまとまっているので良ければ見てみてください。

togetter.com

担当

ノウハウや考え方を書いてある本編を僕自身は書いておらず、ディスカッションの場にいたりチェックくらいで、僕はこれらの本編を読んだあとにそのもととなったしがないラジオのエピソードをピックアップして紹介する付録を担当しました。
また、もう1つ付録の特典エピソードについてはいつものごとく編集作業などを担当しました。

売れたのか

結論から言うと、想像以上に売れました。

売れた要因?

gamiさんの煽りタイトルをつける能力が炸裂し、お願いしたデザイナーさんの表紙 *2がわかりやすくマッチしていました。
やはり人の流れの早いイベントだったので、ポッドキャストのリスナーさん以外にも購入していただけたり立ち読みしていただけていたのは、そのパッと目に入るわかりやすい煽りが良かったのだと思いました。

同人誌即売イベントで多い立ち読みですが、そこで読んでいただいたかたに買ってくださったのは大きいと思います。
タイトルに反してすごく客観的に、適切なエクスキューズをしつつ書かれた本だったのでそのへんがギャップとして刺さった部分なのかなと思いました。
ディスカッションの中でもかなり意識していたところでもあったし、そのへんもgamiさんの良さなのかなと思います。

リスナー層の一部にお買い上げいただくための特典エピソードをとれたというのが僕の唯一の売上への貢献だったかなと思います。

焦りと後悔

とそんなふうに結果としてはよかったのですが、とあるポッドキャストも聞いてくださっているかたのブログにこのように書かれていました。

リスナー勢としてあえて欠点を上げるならばずっきー色が薄いことでしょうか(笑)

iwasiman.hatenablog.com

また、他のブログの方もこのようなことを書かれていました。

今回、私と同じく初のサークル参加でかなりバズっていたのが gami さんの『完全 SIer 脱出マニュアル』。

note.mu

確かにgamiさんが申し込んでくれたところもありますし、あまり熱が盛り上がらなかったというのは実際にPodcastでも話しましたが *3 、本当にそのとおりでしたし、他エピソードの編集もあり意外と忙しく乗れなかったというところでした。
そういうあれこれで、終わってみると僕らの *4 本がすごく好評なのをみてすごく悔しいなり焦るなりをしました。

  • 自分が書いたのではこのようなことにはならないのだろう
  • 同人誌なのに自分の子供感がなくて即売会にちょっと居づらい

そんな思いをもって帰路についたので、おそらく次回も僕らは何かしら頒布するつもりではいますが *5、次は僕も本を書こうと思います。
なので何を書くかもまだきまっていないですが、とりあえずリポジトリは作り、gamiさんの手順を参考にして、なにもないPDFを出力できるようにしました。

そういえば今朝公開されたgamiさんのブログに技術書典サークル参加へのいろいろなノウハウが乗っていたので参考にしたいと思います。笑

jumpei-ikegami.hatenablog.com

最後に

技術書典5で「完全SIer脱出マニュアル」でかってくださった皆様本当にありがとうございました。
あまり制作には多く関わっていませんでしたが、しがないラジオというのが愛されていることもわかったので非常に嬉しかったです。

また、次自分も出すという宣言をここでしたのでしっかり頑張ります。

*1:コミケでも技術書はうられているようですが。

*2:ありがとうございます

*3:記事公開時点ではリリースされておりませんが

*4:あえてそれでも僕らのといいますが

*5:あまり決まってはいないですが

textlint のルール作成 はじめました

f:id:zuckey_17:20181007235919j:plain

前回はこちらでtextlintを簡単に触ってみました。

公式サイトのCreating Rulesを参考にします。

【注意】

説明の都合上、ASTなどの単語が出てきておりますが、僕自身あまり深く理解しておりません。 細かな理解が誤っていれば教えていただきたいです。

no-todoを作る

前回も利用したno-todoというルールを自分で作成してみます。

no-todoというルールは

  • - [ ]がリストのすべての項目に含まれないこと
  • todo:がプレーンなテキストの一部に含まれないこと

を確かめるためのLintルールでした。

準備

create-textlint-ruleのインストールとプロジェクトの作成

// 公式ではグローバルにインストールしていますが、ここではローカルにインストールしています。
$ npm install create-textlint-rule

// create-textlint-rule <ルール名> を実行すると、
// "textlint-rule-<ルール名>"というプロジェクトディレクトリが作成されます。
$ npx create-textlint-rule no-todo

$ ls textlint-rule-no-todo

-rw-r--r--    1 zuckey  staff     598 10  7 23:12 README.md
-rw-r--r--    1 zuckey  staff  179575 10  7 23:12 package-lock.json
drwxr-xr-x    4 zuckey  staff     128 10  7 23:12 lib
drwxr-xr-x  392 zuckey  staff   12544 10  7 23:12 node_modules
-rw-r--r--    1 zuckey  staff     471 10  7 23:12 package.json
drwxr-xr-x    3 zuckey  staff      96 10  7 23:12 test
drwxr-xr-x    3 zuckey  staff      96 10  7 23:12 src

コードを書く

先程作成した、プロジェクト直下src/の中にコードを記載していきます。

Stringノードのチェック

/**
 * @param {RuleContext} context
 */
export default function(context) {
    // さまざまなプロパティが定義されているので事前に受け取っておく
    const { Syntax, getSource, RuleError, report } = context;
    
    // ルールオブジェクトを返す。
    return {
        // [Syntax.Str]は、markdown文書を解析して得られたASTのStr(ing)ノードに対して
        // ルールを設定したいときに返却するオブジェクトのプロパティとして定義します。
        // ここの中で report メソッドを呼ぶと、エラーの表示が可能です。
        [Syntax.Str](node) { 
            const text = getSource(node); // ノードから文字列だけを取り出しています。

            const match = text.match(/todo:/i);
            // "todo:"という文字列がないかどうかを、正規表現で大文字小文字関係なくチェックします。
            // 存在すれば、↓ のif文の中でreportを呼びます。
            if (match) {
                report(
                    node,
                    new RuleError(`Found TODO: '${text}'`, {
                        index: match.index
                    })
                );
            }
        },
    };
}

ListItem ノードのチェック

/**
 * @param {RuleContext} context
 */
export default function(context) {
    const { Syntax, getSource, RuleError, report } = context;
    return {
        ...,
        // [Syntax.ListItem]は、markdown文書を解析して得られたASTのListItemノードに対して
        // ルールを設定したいときに返却するオブジェクトのプロパティとして定義します。
        [Syntax.ListItem](node) {
            const text = context.getSource(node); // ノードから文字列だけを取り出しています。
            const match = text.match(/\[\s+\]\s/i);
            // "[ ]"という文字列がないかどうかを、正規表現でチェックします。
            // 存在すれば、↓ のif文の中でreportを呼びます。
            if (match) {
                report(
                    node,
                    new context.RuleError(`Found TODO: '${text}'`, {
                        index: match.index
                    })
                );
            }
        }
    };
}

コードのビルド

$ npm run build

と実行すると、トランスパイルされ、lib/以下にコードが書き込まれます。

実行

例のによって以下のファイルをチェックします。

# file.md

- [ ] textlintのブログを書く

`- [ ]` これはエラーにならない

# 接続詞

僕は、眠い。
しかし、ブログを書かないと行けない。
僕は2あれ。
しかし、眠い。
$ npx textlint --rulesdir lib/ file.md -f pretty-error
no-todo: Found TODO: '- [ ] textlintのブログを書く'
/Users/zuckey/.ghq/github.com/zuckeyM-17/textlint-sandbox/file.md:3:3
         v
    2.
    3. - [ ] textlintのブログを書く
    4.
         ^

いい感じですね。

しかし、本来であれば、この実行ではわからないのですが、todo:という文字列が、コードブロックや、リンクの中にある場合に反応しないようにしたいです。 そのような高度なルールの作成は次回以降に譲りたいと思います。

まとめ

  • textlintルールの作り方の概要
    • 特定のプロパティをもったルールオブジェクトを返す
    • そのプロパティの中で各ノードへの処理を書く
    • reportメソッドを読んでエラーを出力する
  • これで簡単なルールは作れそうです

textlint に入門した

久しぶりのブログ更新。 最近入門して、ちょっとだけ書いているtextlint、入門したときのメモをまとめました。

f:id:zuckey_17:20180930233703j:plain

参考

公式サイト

textlint.github.io

ドットインストール

既存のものを使うだけならドットインストールでも学習できます。

https://dotinstall.com/lessons/basic_textlint

前提

  1. すべてのルールはプラグインになっており、textlintはそれらを読み込むようにして利用する
  2. textlintはMarkdownやテキスト、HTMLをプラグインを利用してパースする
  3. textlintはテキストのパターンを評価するためにASTとしてテキストを扱う
  4. textlintはエラーや注意を表示する

npm を使ってインストールして適当なルールを使ってみる

npmとルールのインストール

$ mkdir textlint-test && cd textlint-test
$ npm init --yes
$ npm install -D textlint
$ npm install -D textlint-rule-no-todo
$ ./node_modules/.bin/textlint --rule no-todo file.md

マークダウンをファイル(file.md)に保存

# file.md

- [ ] textlintのブログを書く

`- [ ]` これはエラーにならない

はじめてのチェック

$ npx textlint --rule no-todo file.md

/Users/zuckey/.ghq/github.com/zuckeyM-17/textlint-sandbox/file.md
  3:3  error  Found TODO: '- [ ] textlintのブログを書く'  no-todo

✖ 1 problem (1 error, 0 warnings)

設定ファイル

.textlintrcを作成する

$ npx textlint --init
.textlintrc is created.

$ cat .textlintrc
{
  "filters": {},
  "rules": {
    "no-todo": true
  }
}%

先程、 no-todoをインストールしているので、initの時点で追加されている

ルールの指定は必要ない

$ npx textlint file.md

設定ファイルの書き方

{
  "rules": {
    "no-todo": true, // ルールを有効に
    "< name >": false, # ルールを無効に
    "< name >": { // ルールには細かいオプションがあるものもある
        "key": "value" // JSONのkey valueで表される
    },
    "< name >":  {
        "severity": "warning" // error か warningルールの重要度を設定できる
    }
  }
}

ルールは作成できることができ、ルール名は以下のようになっている必要がある

  • textlint-rule-< name >
  • < name >
  • @scope/textlint-rule-< name >
  • @scope/< name >

プリセットルール

プリセットルールはルールの集まりで、以下のようにして使う

集まりすべてを有効にする場合

{
  "rules": {
    "preset-example": true
  }
}

もちろん各ルールで設定可能

{
  "rules": {
    "preset-example": {
        "foo": true // preset-exampleに含まれる、"text-lint-rule-foo" はこのようにして指定する
    }
  }
}

実際にやってみる

連続する接続詞でwarning

ルールのインストール

$ npm install -D  textlint-rule-no-start-duplicated-conjunction

設定を書く

.textlintrc

{
  "filters": {},
  "rules": {
    "no-todo": true,
    "no-start-duplicated-conjunction": { // text-lint-< name > のnameをkeyにする
      "interval": 2 # 同じ接続詞を使うには、 2文以上間に挟まないといけない
    }
  }
}

以下のファイルを保存

# file.md

- [ ] textlintのブログを書く

`- [ ]` これはエラーにならない

## 接続詞

僕は、眠い。
しかし、ブログを書かないといけない。
なぜなら週1でブログを書くと決めているからだ。
しかし、眠い。

動かしてみる

$ npx textlint file.md

/Users/zuckey/.ghq/github.com/zuckeyM-17/textlint-sandbox/file.md
   3:3  error  Found TODO: '- [ ] textlintのブログを書く'  no-todo
  11:1  error  Don't repeat "しかし" in 2 phrases          no-start-duplicated-conjunction

✖ 2 problems (2 errors, 0 warnings)

他の設定

.textlintrc

{
  "filters": {},
  "rules": {
    "no-todo": true,
    "no-start-duplicated-conjunction": {
      "interval": 2,
      "severity": "warning" # severityをwarningにしてみる
    }
  }
}
$ npx textlint file.md

/Users/zuckey/.ghq/github.com/zuckeyM-17/textlint-sandbox/file.md
   3:3  error    Found TODO: '- [ ] textlintのブログを書く'  no-todo
  12:1  warning  Don't repeat "しかし" in 2 phrases          no-start-duplicated-conjunction

✖ 2 problems (1 error, 1 warning)

まとめ

  • 簡単にtextlintに入門した
  • 次はルールを作成したい

CSSだけでアコーディオンを実装する

僕はCSSが苦手なんですが、必要にかられてアコーディオンの実装をしたいということになり、少し前にCSS単体で実現できると聞いたのでやってみました。

  • CSS上級者はこんな実装しない
  • この実装はこういうときに困る

などありましたらご指摘いただければ嬉しいです。

動き

FAQページによくありがちです。

f:id:zuckey_17:20180702000417g:plain

実装

<div class="faq">
    <input type="checkbox" id="question1" class="checkbox" />
    <label for="question1">
        <div class="question">
            <span class="text">質問①</span>
            <span class="collapse-handle"></span>
        </div>
    </label>
    <div class="answer">
        質問①の答えです
    </div>
    <input type="checkbox" id="question2" class="checkbox" />
    <label for="question2">
        <div class="question">
            <span class="text">質問②</span>
            <span class="collapse-handle"></span>
        </div>
    </label>
    <div class="answer">
        質問②の答えです
    </div>
</div>
  .faq .question {
      height: 4em;
      display: flex;
      position: relative;
  }
  
  .faq label {
    display: block;
    cursor :pointer;
    transition: all 0.5s;
  }
  
  /* チェックボックスは表示しない */
  .faq input {
    display: none;
  }

  /* 開けることを示すための下矢印と答えをチェックボックスが入る前の状態にセット */

  .faq .collapse-handle {
    height: 0.7em;
    width: 1.5em;
    background: image-url("collapse_allow_close.svg") no-repeat;
    position: absolute;
    right: 1em;
    top: 40%;
  }
  
  .faq .answer {
    height: 0;
    padding: 0;
    overflow: hidden;
    opacity: 0;
    transition: 0.8s;
  }

  /* チェックボックスにチェックが入ったら、以下の要素が変更になる */
  
  .checkbox:checked + label .collapse-handle {
    height: 0.7em;
    width: 1.5em;
    background: image-url("collapse_allow_open.svg") no-repeat;
    position: absolute;
    right: 1em;
    top: 40%;
  }
  
  .checkbox:checked + label + .answer {
    height: auto;
    opacity: 1;
    background-color: #FAFAFA;
    color: #0A0E1F;
    display: flex;
    align-items: flex-start;
    padding: 1em 1em 1em 0;
  }

まとめ

意外と簡単にできました。
普段ならJSでやってしまいがちですが、CSSでやるのは新鮮でした。
これから頑張っていきたい気持ち💪