Twitterのデータダウンロード機能を使ってみたら1GBのデータ量に圧倒されるも肝心の全ツイートのデータが欠けていた話

虚偽DMCA申請による凍結騒動

虚偽のDMCA申請によって複数アカウントが一斉に凍結され、そのうちの一部が数日後に凍結を解除される*1事件があった。

今朝メールを見てみたら、Twitterから「あなたが投稿した画像が著作権の侵害にあたると申請があったので削除した」と連絡がありました。

心当たりがないので一体どの画像?と情報を見てみたら、小学生の娘がカスタムキャストで描いた東方キャラの絵と、森が描いた東方「博麗霊夢」の絵が「著作権侵害」にあたるのだと言われました。

気になって調べてみましたが、記載のあった岐阜県の住所をGoogleマップで見ても険しい自然、山しかない。

法人登録もありませんでした。もちろん社名で検索してもまったく情報が出てきません。個人名にしたっておそらくは偽名でしょう。

これまでもTwitterの凍結を脅威に感じつつも、どこか素朴な印象として凍結されてもやむなしな言動をしたアカウントが凍結されているという感覚があったけれど、今回の事件には自分も何かのはずみで凍結されるかもと思わせるものがあった。

凍結されるとツイートデータの復元は困難か

Twitterは公式でデータダウンロード機能を用意しているけれど、凍結されると使えなくなるらしい。

f:id:fuyu77:20200222193605p:plain

またツイートを記録する外部サービスのTwilogも凍結されると非表示になる*2

突然凍結されたらツイートデータを喪失する可能性もあると考え、Twitterのデータダウンロード機能を利用してバックアップを取っておこうと思った。

1GB以上のデータ量に驚愕する

f:id:fuyu77:20200222195106p:plain

設定からデータダウンロードの準備を行い、しばらくすると準備完了の通知が届いたけれど、表示されたデータ量は何と1643MBで、1GB以上。

f:id:fuyu77:20200222195451p:plain

ダウンロードに2時間以上かかった。

データの全容

f:id:fuyu77:20200222200211p:plain

ダウンロードしたデータは、大量の謎のJSファイルと画像や動画が入っているディレクトリによる構成だった。

ツイート等のコンテンツはJSファイルにJSONの配列形式で入っていて、通常のツイートに加えてDMのメッセージやフォロー、フォロワー、いいね、リスト、モーメント、ミュート、ブロックから広告とのマッチング情報までありとあらゆるアカウント情報がダウンロードされていた。

しかし肝心のツイートのデータが一部欠けている

tweet.jsというファイルに入っているツイートの履歴を表示するビュワーがいくつかネット上に公開されていて、試してみたところ、Twitterを始めてから半年分のデータが欠落していた*3。無駄にデータが重い上に肝心の全ツイートのデータがないのでは話にならないではないか。

代替案

画面上部の「ログイン」より管理画面に入り、「ログのダウンロード」からダウンロードすることができます。
CSV(SJIS)、CSV(UTF8)、XML(UTF8)の3つのフォーマット・文字コードを選ぶことができます。

含まれる情報は、投稿ID、投稿日時、テキスト本文のみです。

TwilogCSVダウンロード機能があり、こちらは7MBのサイズでRTを含む全ツイートテキストがダウンロードできて「これこれ、こういうのでいいんだよ」という感じだった。

結論としては、Twilogにデータ連携して定期的にCSVダウンロードするのが良さそうだ。

*1:2020/02/22現在、森哲平さんはまだ凍結されている。

*2:凍結でTwilogのデータダウンロード機能が使えなくなるのかどうかは不明。

*3:どういうロジックで欠落しているのかは不明。

PuppeteerでWebスクレイピングが捗る ―株マップ.comの銘柄スクリーニング結果を取得する―

趣味の株式投資で投資対象の選定にクォンツ・リサーチ社が運営する株マップ.comの銘柄スクリーニングを便利に使わせて貰っているけれど、これまで

  1. ブラウザのブックマークからスクリーニング結果にアクセス
  2. 表の文字列を手元のテキストエディタにコピペ
  3. 正規表現で扱いやすいデータに置換
  4. 手元に保存しているデータとの差分をチェックし、新たに注目すべき銘柄がないか確認
  5. 上記のフローを複数のスクリーニング条件について確認

という作業に数分かかっていて、Webスクレイピングでの自動化を試みたいと思った。

GASでできそうだったが…

まずGoogle Apps Script(GAS)のUrlFetchApp.fetchでサクッとスクレイピングできそうだったので試してみたけれど、株マップ.comのスクリーニングの表はHTMLのレスポンスに含まれておらずJSで描画されているので、UrlFetchApp.fetchの機能では取得できなかった。

ちなみに今回は試さなかったけれど、PhantomJs Cloudを使えばGASでもできるらしい。

Puppeteerという選択肢

JSの描画に対応できる選択肢としてPythonまたはRubySeleniumを使えばできそうだと分かったけれど、Seleniumは昔触って割と面倒だった印象があり、若干躊躇していると

Python Webスクレイピング テクニック集「取得できない値は無い」JavaScript対応@追記あり6/12 - Qiita

取得だけなら簡単だけどidがとことん無かったりnth-childだと壊れやすいんだよなあ。Chromeなら今ならpuppeteer が楽チンでオススメ。

2018/02/22 19:54

ブコメでPuppeteerを推す声があったのでチェックしてみたところ

  • ヘッドレスChromeを操作するGoogle製のNode.jsライブラリ
  • npm i puppeteerするだけで動くので環境構築が簡単
    • その他のNode.jsライブラリと組み合わせたプログラムの機能拡張も容易
  • APIドキュメントが公式でキッチリ揃っている

お手軽、高機能、Google製の三拍子揃った便利ライブラリだった。

株マップ.comをスクレイピングするプログラム

使い方に慣れるのにそこそこ苦労したけれど、目的のプログラムを作ることができた。

$ node main.js
[
  '1720', '1805', '1814', '1847', '1882',
  '1887', '1888', '2768', '3023', '3228',
  '3254', '3284', '3294', '3388', '3408',
  '4004', '4042', '4215', '5021', '5101',
  '5288', '5351', '5391', '5702', '6463',
  '7525', '7940', '8002', '8020', '8031',
  '8053', '8091', '8354', '8570', '8591',
  '8604', '8928', '8935', '8999', '9504',
  '9810'
]

上のサンプルプログラムを実行すると、このようにスクリーニングされた銘柄の証券コードの一覧の配列が表示される。GitHubにアップした実際のプログラムでは、fsライブラリを使って手元に保存した証券コードの一覧と比較してスクリーニング結果に注目すべき更新がないか確認できるようにしている。

Puppeteerを使う上での注意点

Promise、async/awaitの知識は必須

APIドキュメントを見れば分かるように、ほとんどのメソッドがPromiseを返していて、メソッドはawaitを付けて呼び出すのが基本になるのでPromiseを用いたJSの非同期処理の仕組みは理解しておかないとPuppeteerを使いこなすのは難しい。

Promiseについては、『JavaScript Promiseの本』が分かりやすい。取り敢えずPromiseの仕組みをザックリ理解したら中間は読み飛ばしてasync/awaitの5章に進むみたいな感じでパパッと読み飛ばしてもPuppeteerを使う上では十分な知識が得られた。

ElementHandleの性質に注意

const elementHandle = await page.$('#example-id')
const elementHandles = await page.$$('.example-class')

のようにpage.$page.$$メソッドを用いてjQueryのような記法でElementHandleを取得できるけれど、このPuppeteer独自クラスのElementHandleは、JSの通常のNode(DOM)とは異なるので、例えばNodeのinnerTextプロパティでデータを取得するというようなことができない。

Nodeとして扱いたいときは、evalがついている方のpage.$evalpage.$$evalメソッドを使うと良い。

const text = await page.$('#example-id', (node) => node.innerText)
const texts = await page.$$('.example-class', (nodes) => nodes.map((node) => node.innerText))

こんな感じで、page.$evaldocument.querySelectorpage.$$evalArray.from(document.querySelectorAll) *1の結果が第2引数の関数の引数として渡されるイメージだ。より柔軟に書けるpage.evaluateメソッドもある。

GASと連携した定期実行も可能か

Google Cloud FunctionsでPuppeteerが使えるので、GASと連携してスクレイピングを定期実行して結果をメール通知というようなこともできそうだ。

*1:querySelectorAllで返るNodeListはArrayではないため、 Array.fromで変換して使いやすくしていると思われる。

オタクの原点としての一方的な愛とこき下ろし ―Amazonレビューの評価に惑わされて高級ヘッドホンを2つ買ってしまった話―

ビックカメラでヘッドホンを視聴

気に入って使っていたオーディオテクニカのATH-A500Xが壊れて片耳が聴こえなくなってしまったので、同じオーディオテクニカでヘッドホンを買い換えたいと思っていた。

年始の帰省帰りに名古屋駅近くのビックカメラに寄る機会があったので、ヘッドホンを試聴してみる。オーディオテクニカのヘッドホンに的を絞って手持ちのiPhoneに挿して聴き比べてみる。最初にビックカメラおすすめヘッドホンランキング的なコーナーにあるヘッドホンを聴いてみた(機種は確認しておらず不明)。音に迫力はあるものの低音がゴツ過ぎてちょっとキツい。

次に元々持っていたATH-A500Xに見た目が似ているヘッドホンを聴いてみる。これは素晴らしく感動的な音のように感じた。帰省帰りで荷物が増えるのは嫌だったので、機種をメモしてAmazonで買おうと思った。ATH-A900Zというらしい。

Amazonで評価をチェック

東京の自宅に帰ってからAmazonのATH-A900Zのページを見てみる。レビューの様子がどうも怪しい。

衰えましたよ、確実に

テクニカは往年のユーザーをなめてるね?分かるからねこーいうのは

開発スタッフはもう一度原点を見つめ直した方がいい。

低音寄りの方が受けがいいのだろうか、最近ほかのメーカーでもそうだが
マイナーチェンジがあると高い確率で低音が強まるだけな安易な調整が入るのがホントにいやだ。

何をトチ狂ったかしょーもないありきたりな低音寄りヘッドホンになってしまった、しかも特別チューニングも上手くない、こんな音オーテクでやる意味がない

まず前提に、私はこの商品を買っていません。
何故なら買う気前提で視聴した結果あまりの酷さに購入を中止したからです。

これ、アートモニターシリーズでやることか?
音の作り方が完全にポータブル用途です。

何だかボロッカスに言われているけれど大丈夫なのだろうか、もっと良いヘッドホンがあるのではないかと感じてしまった。

Amazonで検索し直してみたところ、評価が1万件以上も付いていてかつ高評価なヘッドホンがあった。

購入して後悔しない間違い無しのヘッドホンの一つだと思います。

生活が変わりました。「音楽」を楽しめるようになりなりました。

レビューもATH-A900Zと違って手放しの絶賛が多い。結局こちらを買うことにした。

届いたヘッドホンは…

届いたATH-M50xを聴いてみる。

…あれ?

期待したのと違う。クリアな音質だけれど、心に響いてくるものがない。

名古屋のビックカメラで試聴したATH-A900Zはもっと心に沁みる感動的な音を聴かせてくれた気がした。でもそれは思い出補正かも知れない…いや、ビックカメラ店内でiPhoneに挿して試聴した環境で、マイナス補正はあれどプラス補正とかあるか…?きっとあっちのヘッドホンの方が良かったんだ…でもこのヘッドホンも高いしもう一つ買うというのは…と葛藤しつつ、結局ATH-A900Zも買った。

ATH-A900Zを聴く

届いたATH-A900Zを、失敗だったらどうしようと不安になりながら聴いてみる。

ビックカメラで試聴したときと同じように、心に深く響く音が流れて来た。思い切って買い直して本当に良かったと思う。

ATH-A900ZのAmazonレビューを読み直す

ここでもう一度ATH-A900ZのAmazonレビューを読み直してみる。

こと前900に関してはフラットかつ高解像度な音を「長時間」ストレスフリーで聴けるということで紛れもなく名機だった

まずA900に求める事
・やや高音寄りフラットな密閉型
・フルサイズなボディを生かした音場
この二つがかなり重要で初代a900はこの条件を満たしていた

A900がなぜ評価されていたのか、正しく認識してほしい。

私はA900無印を使用しています。
A900の透き通る高音、他の音を邪魔しない上品な低音。高すぎない分解能が逆に有効に作用した高い汎用性。
そして何よりこの価格でこのレベルの音が出せるのかという衝撃を持って愛用していました。
A900Xが発売された時にも順当進化を認めながらも無印に愛着があり、無印がボロくなったら買おうという気持ちで今まで過ごしていました。
そして今、無印がボロくなってきていざXを買おうと思った時にZが発売していることを知り、意気揚々と店に向かった結果がこれでした・・・。

A900ZのZとは最高傑作という意味ではなく、もうこのシリーズは終わりだよという事なのですね・・・。
こんな出来では続編は無いと思います。Xを買えるうちに買いましょう。900シリーズのファンだけに悲しいな・・・。

音自体は悪くないんです。ただ、もう昔のA900の音では無いんです。

どうやらATH-A900→ATH-A900X→ATH-A900Zという順番にモデルチェンジしていて、酷評している人々は初代ATH-A900の長年の愛用者のようだ。愛着と期待が裏切られた怒りをATH-A900Zにぶつけている訳だ。

これは完全な相対評価で、「音自体は悪くない」がATH-A900と比べて…という話なので、そもそもATH-A900を知らない私がレビューの酷評を気にしすぎる必要はなかったという訳だ。

一方的な愛とディス ―オタクの原点―

それにしてもこのATH-A900ZのAmazonレビュー欄、読み返すほどに面白い。痛烈なディスと一方的な愛、謎のこだわり、それらをどこかポエティックに表現する感性、オタクの原点という感じだ。

ネガティブなフィードバックに神経質にならざるを得ないTwitterで、最近は歯に衣を重ね着している私からすると、彼らの様子はのびのびとしていて実に好ましく感じられる。私も今一度原点に立ち還ってみたいような気がした。現実にはもうあの頃には戻れないとしても。

colspan="100%"の罠 ―W3CのHTML5仕様書を読んでみた―

項目1 項目2 項目3
データが登録されていません

仕事で、上のようなテーブル(データがないときに、データがない旨のメッセージを表示する)に列を追加したら

項目1 項目2 項目3 追加された項目
データが登録されていません

このようにテーブルが欠けるバグがあった。

<td colspan="4">登録されていません</td>

項目1 項目2 項目3 項目4
登録されていません

こうすれば直るけれど、列を追加した際に同様のバグがまた混入するのでメンテナンス性が悪い。

<td colspan="100%">登録されていません</td>

項目1 項目2 項目3 項目4
登録されていません

ググったらcolspan="100%"で行けると言っている人がいて、一見すると良さそうだけれど

<td colspan="50%">登録されていません</td>

項目1 項目2 項目3 項目4
登録されていません

50%にしても半分にならない。

<td colspan="3abc">登録されていません</td>

項目1 項目2 項目3 項目4
登録されていません

ここまで試すと、100%はただの100と同じだったらしいということが読めて来る。

では何故そうなっているのかについては、W3CHTML5の仕様書を読めば分かった。

1. Let input be the string being parsed.
2. Let position be a pointer into input, initially pointing at the start of the string.
3. Let sign have the value "positive".
4. Skip white space.
5. If position is past the end of input, return an error.
6. If the character indicated by position (the first character) is a U+002D HYPHEN-MINUS character (-):
 1. Let sign be "negative".
 2. Advance position to the next character.
 3. If position is past the end of input, return an error.
Otherwise, if the character indicated by position (the first character) is a U+002B PLUS SIGN character (+):
 1. Advance position to the next character. (The "+" is ignored, but it is not conforming.)
 2. If position is past the end of input, return an error.
7. If the character indicated by position is not an ASCII digit, then return an error.
8. Collect a sequence of characters that are ASCII digits, and interpret the resulting sequence as a base-ten integer. Let value be that integer.
9. If sign is "positive", return value, otherwise return the result of subtracting value from zero.

数値を解釈するアルゴリズムが示されている。簡単にまとめると、先頭が数字の場合に、連続する数字を表す文字(ASCII digit)の塊を10進数の数値として評価しているという訳だ。

<td colspan="abc4abc">登録されていません</td>

項目1 項目2 項目3 項目4
登録されていません

7. If the character indicated by position is not an ASCII digit, then return an error.

先頭の文字が数字でない場合はエラーを返すとあるので、上のように先頭が数字でないパターンを試してみると、確かに中間に置いた数値は評価されないことが分かる。

話題としては些細なものだけれど、仕様書のアルゴリズムに基本的に忠実な形で各ブラウザが実装されていることを知れて何だか感慨深い。

特定のディレクトリ下のファイル内の文字列をまとめて置換して保存する

$ sh replace.sh

特定のディレクトリ下のファイル内の文字列をまとめて置換する必要があったので、シェルスクリプトで書いてみた。

$ for file in `find ./ -maxdepth 1 -type f`; do; sed -i '' -e 's/置換前/置換後/g' $file; done

改行部分を;で区切って一行のコマンドとして実行することもできる。

逆引用符(``)を使用すると、コマンドの標準出力への出力内容をコマンド行に含める事ができます。

$ echo `pwd`
/opt

`で括ってfindコマンドの出力結果を配列として渡してループしている。今までシェルスクリプトをちゃんと書いた経験が少なかったけれど、色々書いてみると面白そうだと思った。

参考記事

Webエンジニアというナイスな職種 ―業務未経験から1年が経って―

前職を辞めてRailsを独学し、業務未経験からWebエンジニアになって7月で1年になり、なかなか順調なので、Webエンジニアという職種(実際には私の個別の働き方に関する内容を含むけれど)の良いところを書いてみたい。

何をしているか

家事代行サービスを提供するベンチャー企業で、ユーザー向け予約マッチングシステムと、家事代行を実際に担うスタッフ向けのスケジュール管理システム、社員向け業務管理システムの開発を行っている(BtoCの事業会社のシステム開発部門)。

サーバーサイドはRails、フロントエンドはTypeScript/Backbone.js/jQuery/Reduxという謎構成のSPAで開発を行っている。分業はエンジニア/デザイナーという程度で、社員エンジニアには、インフラ含めてフルスタックな役割が求められている。

最近の業務の流れとしては、事業計画に沿って新機能の開発をビジネスサイドの関係者とチームを組んで進めている感じだ。ベンチャーらしく、事業の推進にあたってもエンジニア/ビジネスサイドの区別は曖昧で、エンジニア主導で機能の要件が決まることも多い。

Webエンジニアの良いところ

コードがたくさん書ける、ゴリゴリ開発できる

前職はIT企業にも関わらず業務でプログラミングする機会がまったくなかったけれど、現職では入社直後から毎日ゴリゴリコードを書く機会に恵まれている。

すべてのコードを書くのに長年の経験が必要なわけではない。良いコードを書く場合にだって必要ない。すべてのプログラムには、ありふれたやり方で入力と出力を結びつける「にかわのようなコード」が存在する。こういうのは誰が書いたって大差ない。時給 $28 の人に頼んでも時給 $59 の人に頼んでも結果は同じだ。もし熟練エンジニアしか雇わないのなら、そういった軽い仕事に余分なお金を払っているということになる。

上の記事でRailsの生みの親のDHHも言っているように、プログラミングの仕事というのは簡単なタスクから難しいタスクまで様々で、開発リソースと費用対効果の兼ね合いで手付かずで放置されているタスクも大量にあるので、業務未経験から入ってもすぐに取りかかれる開発タスクはいくらでもあるのだ。少しずつレベルを上げて、段々と難しいタスクが出来るようになって行くのは楽しい。

裁量が大きい

個別の機能開発のコーディングは基本的に一人のエンジニアが担当して行い、ビジネスサイドと話し合って大枠の仕様を決定した後は、「あとはエンジニア判断でいい感じに作って」という感じなので、自分で決めて開発できる範囲が非常に大きい。また、このような実装方法における裁量に限らず、そもそもベンチャーは発言力があれば何でもできるようなところがあるので、「自分のアイディアで、より良いサービスを作る」ということにかなり主体的にコミットできている感覚がある。

健全な議論ができる

議論はあくまで前提を共有した人間同士が目的をもって行うものであり、前提も目的も共有しない他者との議論を活発化させても議論は無限に後退するだけである。

京太郎さんが上の記事で、ネットでは目的を共有しない者同士による、議論として成立していない議論が横行しているという分析を書いているけれど、逆にエンジニアの議論は目的を高度に共有しているので、極めて健全にわだかまりなく行えることが多い。技術的な観点でどのような方法が望ましいかについては、世界中のエンジニアに共有された前提が豊富に用意されているためだ。

自由度の高い働き方

これは会社にもよるだろうけれど、今は週3リモート、週2出社の働き方で、11時までに出社すればOKなフレックスタイム制なので、かなり伸び伸びと働けている。毎朝目覚まし時計をセットしないですむ生活は素晴らしい。エンジニアは成果さえ出していれば文句は言われないので、全般的に柔軟に働ける部分が大きい。

まとめ

前職では、仕事というものは「嫌々やらされるつらい営み」という感じだったけれど、Webエンジニアになってから、主体的に働く感覚がつかめて、労働者として本当のスタートを切れたと感じている。仮に今の会社での労働環境が悪くなっても転職が容易にできそうな点も安心感があって良い。

短歌投稿サイトUtakataがリリース後1周年になりました! ―現状の課題―

短歌投稿サイトUtakataをリリースして今日でちょうど1年が経った。

この1年で、ユーザー数416、短歌投稿数7720のサイト規模になり、なかなか順調に使っていただけている感じだけれど、その一方で以下の課題にも直面している。

スケールしない

Google Analyticsでアクセス数を見ていると、ある時期からほぼ横ばいになってしまった。実感としては、飽きてやめる人と新しく入ってくる人がほぼ同数という感じで、これ以上スケールする気配がない。

原因としては、機能があまりにもシンプルでコミュニケーション面が弱く、コミュニティとして機能しづらいというのがあると思う。

仕事が忙しくなって来て開発の時間が確保できない ―開発者としてのスキル向上を趣味の開発に還元できない問題―

上のコミュニケーション面が弱い問題を解決するために、「歌会」機能を開発する構想が去年からあるのだけれど、仕事が忙しくてなかなか着手できない状態にある。

エンジニアとしてのスキルはどんどん上がっているにも関わらず、そのスキルを趣味の開発に活かすリソースがないのがつらい。趣味の開発のコミット度に限定して言えば、素人の無職>業務経験一年の会社員という構図があるのだ。

マネタイズできない

また、HerokuとAWS S3、ドメイン維持の費用で毎月赤字を垂れ流している有様で、少しでもマネタイズできたらと思うのだけれど、Google AdSenseにもAmazonアソシエイトにも審査ではじかれてしまった。トップページにある短歌の文字数が少なく、スパムサイトと自動判定されているようだ。

仮にスケールしても、増大する費用をすべて自費で賄うのはなかなかにつらい。マネタイズについて、何か知見を持っている方がいたらぜひ教えていただきたい。