たつをの ChangeLog

40 件 見つかりました。

1 2 3 4 5 6 7 8 [ 次へ ]

ユニークな文字列がたくさんあって、それぞれをいくつかのバケットになるべく均等に振り分けたい。

ユニークな文字列とは、例えばユニークなユーザ ID とか商品 ID とか。
タスクの要件はこんな感じ。

  • ユニークな文字列たちを N 個のバケットに振り分ける
  • なるべくランダムに振り分ける
  • それぞれのバケットの文字列の数はなるべく同じに
  • シェルのワンライナーで済ませたい

いろいろと試行錯誤した結果、対象文字列をMD5のハッシュ値に変換して整数にして mod するのが一番手軽かな。

  1. 16バイトのバイナリが出てくる
    $ echo foobar | perl -MDigest::MD5 -nle 'print Digest::MD5::md5($_)' | od -t x1
    0000000    38  58  f6  22  30  ac  3c  91  5f  30  0c  66  43  12  c6  3f
    0000020    0a
    0000021
    
  2. その最初の4バイトを整数 (unsigned long) にする
    $ echo foobar | perl -MDigest::MD5 -nle 'print unpack("L",Digest::MD5::md5($_))'
    586569784
    
  3. それをバケット数 N で割って余り(=バケット番号)を得る
    $ echo foobar | perl -MDigest::MD5 -nle 'print unpack("L",Digest::MD5::md5($_)) % 5'
    4
    

エンディアンの問題とかあるかもだけど、まあ困ったら考えればいいや。

バケット数ごとの振り分け結果です。
約122万のユニーク文字列数を使用。
だいたい均等の分配できてるっぽい。
厳密さは求めないのでこれでOK。
N = 3
408313 0
408764 1
408370 2

N = 4
306157 0
307042 1
305954 2
306294 3

N = 5
244633 0
245089 1
245880 2
244960 3
244885 4

N = 6
204129 0
204480 1
203698 2
204184 3
204284 4
204672 5

N = 10
122542 0
122427 1
122685 2
122694 3
121956 4
122091 5
122662 6
123195 7
122266 8
122929 9

過去記事


Yahoo!デベロッパーネットワークのテキスト解析 API の日本語形態素解析 (V2) を Linux や macOS などのターミナルから使うためのコマンドを用意しました。
Perl で書いた超簡単なやつです。


実行例
echo '走れ' | ./yapima2.pl

{"id":1,"jsonrpc":"2.0","result":{"tokens":[["走れ","はしれ","走る","動詞","*","子音動詞ラ行","命令形"]]}}


実行例(jq で加工)
echo '読み仮名だけ抜き出します' | ./yapima2.pl \
  | jq -cr '[ .result.tokens[] | .[1] ] | join("")'

よみがなだけぬきだします
echo '行番号と品詞です\n晩御飯が美味しい' | ./yapima2.pl \
  | jq -cr '[ .id, [ .result.tokens[] | .[3] ] ] | flatten | @csv'

1,"名詞","名詞","助詞","名詞","判定詞"
2,"名詞","名詞","助詞","形容詞"
echo '文章から名詞だけ抽出して要約っぽく見せかけます' | ./yapima2.pl \
  | jq -cr '[ .result.tokens[] | select(.[3] | contains("名詞")) | .[0] ] | join("")'

文章名詞抽出要約

参考

この記事に言及しているこのブログ内の記事

「1月」〜「12月」、「1日」〜「31日」が1文字文の全角幅で表示できるUnicode文字。
幅固定なのでレイアウトの細かい調整がなくて良い。

%E3%8B%80 (12992)%E3%8B%8B (13003)
%E3%8F%A0 (13280)%E3%8F%BE (13310)

例1:

(日付部分テンプレ、中心寄せが難しいケース、等幅便利)

例2:
(カレンダーは㉛などの囲み数字も使えるけど囲みはいらない……)

月(1-12)、日(1-31)を入力として月日文字を出す Perl での簡単な方法:
pack("U", 12991 + $month)
pack("U", 13279 + $day_of_month)

サンプルプログラム:
#!/usr/bin/env perl
# -*- coding: utf-8 -*-
use strict;
use warnings;
use utf8;
binmode STDOUT, ":utf8";
print join(",", "㋀", ord("㋀"), "㏠", ord("㏠"))."\n";
print join(",", map {"$_:".pack("U", 12991 + $_)} (1..12))."\n";
print join(",", map {"$_:".pack("U", 13279 + $_)} (1..31))."\n";

出力結果:
㋀,12992,㏠,13280
1:㋀,2:㋁,3:㋂,4:㋃,5:㋄,6:㋅,7:㋆,8:㋇,9:㋈,10:㋉,11:㋊,12:㋋
1:㏠,2:㏡,3:㏢,4:㏣,5:㏤,6:㏥,7:㏦,8:㏧,9:㏨,10:㏩,11:㏪,12:㏫,13:㏬,14:㏭,15:㏮,16:㏯,17:㏰,18:㏱,19:㏲,20:㏳,21:㏴,22:㏵,23:㏶,24:㏷,25:㏸,26:㏹,27:㏺,28:㏻,29:㏼,30:㏽,31:㏾

追記: 月・日・曜日
perl -C -Mutf8 -le '$month=3;$day=11;$dow=5;print pack("U*",12991+$month,13279+$day,12842+($dow-1)%7)'
㋂㏪㈮

ついでに囲み数字も。50まである。
1〜20, 21〜35, 36〜50 で分割されている。

1〜20%E2%91%A0 (9312)%E2%91%B3 (9331)
21〜35%E3%89%91 (12881)%E3%89%9F (12895)
36〜50%E3%8A%B1 (12977)%E3%8A%BF (12991)

1から20までなら:
pack("U", 9311 + $_)
1から50までなら:
pack("U", $_ + ($_ < 21 ? 9311 : $_ < 36 ? 12860 : 12941))

参考


手元でのちょっとした用途で類似テキスト検索をやりたいのですが、
Linux環境であれこれインストールしなくても動かせて、
気ままにカスタマイズできる気が利いたやつがなかったので、
改めて作ってみました。
過去に何度も書いたことのあるプログラムなので目新しさはありませんが。
(「車輪の再発明を気にしない」が私の行動指針です!)

simpii


私の母プログラミング言語(母語)である Perl で書いています。
標準ライブラリしか使っていないので、
Perl さえインストールすればどこでも動くはずです。

転置インデックス(+リランキング)用のスクリプトと、リランキングだけするスクリプトがあります。
リランキング時のスコア計算方法は README.md を参照されたし。

関連記事


CSV ファイルで、テキストのカラムにカンマが入っているんだけど、そのカラムがダブルクォート文字で囲まれていないときの対処法について。

ときどきそういうデータがあるのです。
適当なその場しのぎスクリプトで CSV 出力した結果とか。
で、それをなんとかして使わないといけない場面もあるのです。
怒ってないです。

例えば、全部で10カラムのCSV。
第4カラムにテキスト。

0,2,0,こんにちは,50,0,0,0,0,0
1,1,1,さようなら,0,0,0,0,0,0

CSV だからカンマ区切りなんだけど、テキストにカンマが含まれてる場合あり。
しかもダブルクォートで囲まれていない。
区切りのカンマと区別つかなくて困る。

理想:
1,1,0,"ああ,いい,ううう。",21,0,0,0,0,0
0,2,0,"あれれれ,",50,0,0,0,0,0

現実:
1,1,0,ああ,いい,ううう。,21,0,0,0,0,0
0,2,0,あれれれ,,50,0,0,0,0,0

こういうときは、前から3カラム(=4-1)、後ろから6カラム(=10-4)を除いた残りすべてを一つのテキストとする作戦で。

1,1,0,ああ,いい,ううう。,21,0,0,0,0,0

↓ カンマで分割してリストにする

1 1 0 ああ いい ううう。 21 0 0 0 0 0

↓ 前と後ろのカラムを取る

[1 1 0] ああ いい ううう。 [21 0 0 0 0 0]

↓

ああ いい ううう。

↓ 残りをカンマで繋ぎ直し、ダブルクォートで囲む

"ああ,いい,ううう。"

↓ さっき取った、前と後ろのカラムと合わせて最終的な CSV にする

1,1,0,"ああ,いい,ううう。",21,0,0,0,0,0

Perl


Perl で書くとこんな感じ。
my $IX = 4; # 4番目のカラムがカンマ入りテキスト
my $N = 10; # 全部で10カラム
while (<>) {
    chomp; # 末尾の改行削除
    my @F = split(",", $_, -1); # 読み込んだ CSV 行をカンマで切ってリストへ
    my @pre = @F[0..($IX-2)]; # 前から IX-1 個分のカラム
    my @post = @F[$#F-($N-$IX-1)..$#F]; # 後ろから N-IX 個分のカラム
    my $text = join(",", @F[($IX-1)..($#F-($N-$IX))]); # カンマ入りテキスト
    print join(",", @pre, "\"$text\"", @post)."\n"; # CSV に戻す
}

splice 関数を使ったバージョン。
my $IX = 4; # 4番目のカラムがカンマ入りテキスト
my $N = 4; # 全部で10カラム
while (<>) {
    chomp; # 末尾の改行削除
    my @F = split(",", $_, -1); # 読み込んだ CSV 行をカンマで切ってリストへ
    my @ts = join(",", splice(@F, $IX-1, (@F-$N) + 1)); # テキスト部分の抜き出し
    splice(@F, $IX-1, 0, qq(").join(",", @ts).qq(")); # ""をつけて戻す
    print join(",", @F)."\n"; # CSV に戻す
}

Perl の split 関数の第3引数の "-1" についてはこちらを参照。

Python


Python で書くとこんな感じ。
右側から分割してくれる rsplit 関数がこの用途に便利。
ID = 4
N = 10
with open('sample.csv') as f:
    for line in f.read().splitlines():
        pre = line.split(",", ID - 1)
        post = pre[-1].rsplit(",", N - ID)
        l = pre[:-1]
        l.append('"' + post[0] + '"')
        l.extend(post[1:])
        print(",".join(l))

1,1,0,ああ,いい,ううう。,21,0,0,0,0,0

↓ 最初の split で前から4分割(3箇所で切断)

1 / 1 / 0 / ああ,いい,ううう。,21,0,0,0,0,0

↓ 一番右の塊を次の rsplit で後ろから7分割(6箇所で切断)。

ああ,いい,ううう。 / 21 / 0 / 0 / 0 / 0 / 0

↓

1 / 1 / 0 / ああ,いい,ううう。 / 21 / 0 / 0 / 0 / 0 / 0

↓

1,1,0,"ああ,いい,ううう。",21,0,0,0,0,0

おわりに


カンマ入りテキストカラムが複数ある場合はちょっと難しい。めんどくさいのであきらめましょう。桁区切りカンマ入りの数値がクォート無しで複数カラムあるとかも。

あと、上のスクリプトではテキストにダブルクォートが含まれててエスケープされてない場合は考慮してません。join で "\t" を使って TSV にしとくのが良いかと。テキストに "\t" がない前提で。

1 2 3 4 5 6 7 8 [ 次へ ]

たつをの ChangeLog
Powered by chalow