Future Insight RSSフィード Twitter

- 過去記事一覧はこちら

2009-05-03

シリコンバレー在住コンサルティングのポジショントークに日本のエンジニアはどのように向き合ったらよいか

先日のエントリーは読み返してみて、恥ずかしいくらい感情にまかせたエントリーでした。

そこまで怒りっぽい方でもないのに、何でこういうことを書く気分になったのかをちょっと考えてみましたが、最近僕が抱いてるシリコンバレー在住コンサルティングの発信するメッセージに対してかなり強い懐疑心を抱くようになってしまったのが原因なのではないかと思いました。

最近のシリコンバレー在住コンサルティングの言説

はてな界隈でシリコンバレー在住コンサルティングと言えば渡辺千賀氏、海部美知氏、梅田望夫氏などが有名なのではないないかと思います。最近、目についたこの方達の主張と言えば、以下のようなものでした。

  • パラダイス鎖国、日本のガラパゴス化 (海部美知氏)

パラダイス鎖国 忘れられた大国・日本 (アスキー新書 54)
パラダイス鎖国 忘れられた大国・日本 (アスキー新書 54)海部 美知

おすすめ平均
stars正論だけど薄い内容
stars「内なる黒船」への期待
starsブログから生まれた軽いタッチの日本論
stars開国への第一歩
stars日本にシリコンバレー的文化を開花できるか?

Amazonで詳しく見る
by G-Tools

日本語が亡びるとき―英語の世紀の中で
日本語が亡びるとき―英語の世紀の中で水村 美苗

おすすめ平均
starsレベルの低い”日本語擁護”アジテーション
stars辺境の地、極東の文化の強さを理解していないかも
stars偏屈者は、村上春樹を思う
starsともに読み、ともに語りましょう
stars賛否両論は当たり前

Amazonで詳しく見る
by G-Tools

上記のようにこちらの方達はある意味、日本に問題提起をすることが仕事な訳です。日本語が亡びるときははてな界隈ではトンデモ本認定を受けてしまったので、それほどセンセーショナルな話にはならなかった訳ですが、ガラパゴス化などは最近はありきたりになりすぎてしまい、コンサルティングファームでは陳腐化により使用禁止になっているという話を聞いたりもします。

[追記]ポッドキャスト内でガラパゴス化について語ってみました。よろしければご試聴下さい。個人的にガラパゴス化はかなり良い問題提起だったと思ってます。

どうして感情的になってしまったのかの分析

上記のようなトピックが投稿されてきたなかで、渡辺千賀氏が以下のようなエントリーを掲載しました。

これまでずっとなるべく言わないようにしていたのだが、もう平たく/明快に言うことにしました。

1)日本はもう立ち直れないと思う。

だから、

2)海外で勉強してそのまま海外で働く道を真剣に考えてみて欲しい。

これまでは、1)は言わずに、2)だけ言ってきた。で、「海外で働く」の中でも、私が知っている「シリコンバレーで働く」ことの楽しさ、働くための方法をなるべく具体的に紹介するようにしてきた訳なのであるが、前半も言うことにしました。その理由は、若い人に早く気づいて欲しいから。年を取ったら駄目、というわけではないが、扶養家族が増えて、引退までの年数の方が働いてきた年数より短くなってきたりすると、みるみると進路変更は大変になる。ところが、多くの人が「もはや国内に機会はない」と気づく頃には、そういう「進路変更大変状態」になってしまっていることが多い訳です。

ある意味、今まで問題提起を繰り返してきたシリコンバレー在住コンサルティングの中でも最終兵器とでも言うべき意見です。上のような意見の後に接するにはかなりヘビーなものでした。読んでいて口がふさがりませんでしたが、最後に以下のような書き込みがあり、かなり感情的になってしまったのだと思います。

もちろん、上述した通り、日本は貧富の差が広がりつつあるし、今後もどんどん広がると思うので、「日本で勝てる!」と思う人は、是非国内でその道を邁進してください。誘拐には気をつけてね。

思い返してみれば、「誘拐には気をつけてね。」など、特に怒るような内容でもないのですが、ある意味仕事として、日本に問題提起を繰り返している人に、ここまでおちょくるようなことを言われていいのか、という気持ちもあったかと思います。まぁ、そんな訳で、以下のようなエントリーを書いてしまった訳です。全くお恥ずかしい限りです。

ポジショントークに対する考え方

最近ポジショントークについて、ちょっと接し方が変わる意見に触れました。

まずは、Twitterを眺めていたら見かけた、及川卓也さんの以下のようなエントリーです。

ある行動をとった人がいて、それが邪推すれば、その人のあるビジネスにマネーとして還流することがあるかもしれないというだけの理由で、その人の行動をビジネスのためだと決め付けてしまうことは、その人の行動の価値を適切に判断することにならないのではないかと思う。以前そうされた被害者として

また、以下のようなエントリーが広告βさんからありました。

私はここで、ポジショントークでもいいではないか、という視点を提供したい。問題は、ポジショントークかどうかではなくて、ポジションがよくわからないまま情報が出回るということではないだろうか。冒頭にもどると、購読していたニュース誌のスタンスがわかってさえいれば、その偏り補正は受け手の私が引き受けるということである。受け手の私からすれば、複数の異なる立場から主張される、あるトピックに関する意見を眺めることで、自分の中である程度の客観性をたもつことはできるかもしれないからだ。

二つとも、ポジショントークをポジショントークとだけ捉えるよりも情報に対する心構えとして、3歩は先に行っている意見だと思います。非常に勉強になりました。

シリコンバレー在住コンサルティングのポジショントークに日本のエンジニアはどのように向き合うか

長くなりましたが、話をまとめると、僕たち日本在住のエンジニアは、シリコンバレー在住コンサルティングの意見にしっかりと耳を傾ける必要があるという事です。ポジショントークをポジショントークだといって切って捨てるのは非常に簡単ですが、改めて考えてみるとかなりナイーブな態度です。意見を発信する人の立場をしっかり認識した上で、客観的な判断が難しい場面においても意見をかみ砕いていくような態度を心がけるという辺りを意識する必要があると思いました。

(書いてみて思ったのですが、あんまりエンジニアと関係ないエントリーになってしまいました。まぁ、でも自分が日本在住のエンジニアなので、自戒も含めてこのタイトルにしておこうかと思います。)

[追記]切込隊長のエントリーが面白かったです。あまりポジショントーク云々とかの話は書くものではないですね。

結論を先に言うと、それはポジショントークじゃなくて、ミッショナリーによるプロパガンダでしょう、というお話。梅田望夫氏の言説や、gamella氏が例示した一連の流れは、それぞれ個人のオリジナルというより、米・民主党系と新自由主義のアイノコのようなプロパガンダに近しい内容を踏襲していると考えたほうが良いかなと。

考えてみると誰も得のしなそうなトピックだし、いたずらに人を傷つけるだけだと思うのでポジショントーク云々はもう止めて、何か意見がある場合は具体的な提案に対して反論をするようにしたいと思います。

本文抽出ライブラリWebstemmerのblog本文抽出用特化スクリプト「blogstemmer」を書いてみた

以前のエントリーで本文抽出ライブラリWebstemmerを使ってみました。

Webstemmerは非常に興味深い本文抽出ライブラリなのですが、ニュースサイトなどの複雑な階層構造を持っているサイトの本文抽出に特化しているため、逆にblogのようなシンプルなケースでの本文抽出に用いるには、ちょっとオーバースペックです。

Webstemmer はニュースサイトから記事本文と記事のタイトルをプレインテキスト形式で自動的に抽出するソフトウェアです。サイトのトップページの URL さえ与えれば全自動で解析するため、人手の介入はほとんど必要ありません。

そのあたりのことを考慮して、本文抽出ライブラリWebstemmerのblog本文抽出用特化スクリプト「blogstemmer」を作成してみました。相変わらず長いので興味のある方だけ、ご覧下さい。あと、あくまでエントリーの本文をRSSにある他のエントリーとレイアウトを比較して推定するためのラッパーであり、特に機能追加などはしておりません。(はやくgitの使い方覚えて、githubに入れていこう)

Webstemmerがblogの本文抽出にはオーバースペックなポイント

Webstemmerを一通り触ってみて、オーバースペックだと感じた点は以下の3点です。

  • blogの本文抽出だと各エントリーのレイアウトはほぼ同じと仮定してよいので、各ページのレイアウトを分類する精度はそこまで必要ない。広告エントリー、一行だけ、写真だけなどあきらかなごみエントリーをはじけばよい。
  • blogはRSSがあるのでタイトルの推定は基本的に必要ない。
  • blogはRSSがあるのでリンク構造を解析するタイプのクローラは必要ない。
  • 逆に文字エンコード情報が含まれていないページが多いので、文字エンコード推定機能は必須。

そんなわけで、上記のオーバースペックな点をカバーするblogの本文抽出専用のラッパースクリプトを作成してみました。含まれている機能は、

  • RSSを指定すればエントリーを取得し、後は勝手にパターンファイルを作成してくれる。
  • レイアウト解析においてレイアウトがマッチしていると判断するスレッショルドをデフォルトで下げておく。0.97から0.7とかなり低くしておきました。これは、各エントリーの構造は基本的に同じであるという前提を利用したものです。いろいろ試した感じ、0.7あれば、広告エントリーははじくことができつつ、基本的に各エントリーは同じレイアウトを持つと判断されるようです。
  • エントリーの文字コードは自動判断してくれる(Universal Encoding Detectorを使用:http://chardet.feedparser.org/)

Webstemmerのblog本文抽出用特化スクリプト「Blogstemmer」

そんなわけで以下がソースコードです。Universal Encoding Detectorをあらかじめインストールしておいてください。Webstemmer-0.6.1内に以下のスクリプトを設置してご利用下さい。Webstemmerに合わせて、Python2.4までの機能しか使ってません。

# -*- coding: utf-8; -*-
#!/usr/bin/env python

import sys
import os
import feedparser
import urllib2
import urllib
import zipfile
import datetime
import md5
import time
import popen2
import chardet
import extract

def getBlogEntry(rss):
    d = feedparser.parse(rss)
    entries = {}
    index = 0
        
    for item in d["items"]:
        print >>sys.stderr, "get:"+item["link"]
        response = urllib2.urlopen(item["link"])
        entries[item["link"]] = response.read()
        index = index + 1
        if index == 8:
            break
        time.sleep(1)
    return entries

def createPackageDir(dirname, entries, rss):
    try:
        os.makedirs(dirname)
    except:
        pass
    filelist = []
    for key in entries.keys():
        filename = str(md5.new(key).hexdigest())+".html"
        fileobj = open(os.path.join(dirname, filename), "w")
        filelist.append(os.path.join(dirname, filename))
        fileobj.write(entries[key])
        fileobj.close()
    return filelist

def createZipFile(filename, filelist):
    if os.path.exists(filename):
        os.remove(filename)
    zip = zipfile.ZipFile(filename, "w")
    for path in filelist:
        print >>sys.stderr, "zip:"+path
        zip.write(path)
    zip.close()

def analize(rss):
    entries = getBlogEntry(rss)
    d = datetime.datetime.today()
    
    datename = "%04d%02d%02d%02d%02d"%(d.year, d.month, d.day, d.hour, d.minute)
    basename = urllib.quote("_".join( (rss.replace("http://", "")).split("/")))
    filelist = createPackageDir(os.path.join(datename, basename), entries, rss)
    createZipFile(basename+"."+datename+".zip", filelist)
    char = chardet.detect(entries[entries.keys()[0]])
    print >>sys.stderr, "python analyze.py -c %s -t 0.5 %s"%(char["encoding"] ,basename+"."+datename+".zip")
    (stdout, stdin, stderr) = popen2.popen3("python analyze.py -c %s -t 0.7 %s"%
                                            (char["encoding"] ,basename+"."+datename+".zip"))
    lines = []
    for line in stdout:
        lines.append(line)
    try:
        os.remove(zipfilename)
    except:
        pass
    return lines

def extract(rss, entryurl, pattern):
    response = urllib2.urlopen(entryurl)
    entry = response.read()
    d = datetime.datetime.today()
    datename = "%04d%02d%02d%02d%02d"%(d.year, d.month, d.day, d.hour, d.minute)
    basename = urllib.quote("_".join( (rss.replace("http://", "")).split("/"))) 
    filelist = createPackageDir(os.path.join(datename, basename), {entryurl:entry}, rss)
    zipfilename = createZipFile(basename+"."+datename+".zip", filelist)
    charenc = chardet.detect(entry)
    default_charset=charenc["encoding"]

    print >>sys.stderr, "python extract.py -c %s -t 0.5 %s %s"%(charenc["encoding"], pattern, basename+"."+datename+".zip")
    (stdout, stdin, stderr) = popen2.popen3("python extract.py -c %s -t 0.7 %s %s"%(charenc["encoding"], pattern, basename+"."+datename+".zip"))
    lines = []
    for line in stdout:
        lines.append(line)
    try:
        os.remove(zipfilename)
    except:
        pass

    title = []
    maintext = []
    subtext = []
    for line in lines:
        if line.startswith("TITLE"):
            title.append(":".join(line.split(":")[1:]))
        elif line.startswith("MAIN"):
            maintext.append(":".join(line.split(":")[1:]))
        elif line.startswith("SUB"):
            subtext.append(":".join(line.split(":")[1:]))
        else:
            print >>sys.stderr, line
    return (title, maintext, subtext)

def main():
  import getopt
  def usage():
      print '''usage: blogstemmer.py [-t ][-p pattern_file] [-r rss_url] [-e entry_url]
      if pattern output is needed, please set only -r option.
      if entry extract result is needed, please set all options(-p, -r, -e).'''
      sys.exit(2)
  try:
      (opts, args) = getopt.getopt(sys.argv[1:], 'p:r:e:')
  except getopt.GetoptError:
      usage()
    
  (patternfile, rss, entry) = ("", "", "")
  
  for (k, v) in opts:
      if k == '-p': patternfile = v
      elif k == '-r': rss = v
      elif k == '-e': entry = v

  if rss != "" and entry == "" and patternfile == "":
      print "".join(analize(rss))
  elif rss != "" and entry != "" and patternfile != "":
      (title, maintext, subtext ) = extract(rss, entry, patternfile)
      print "TITLE:" + "".join(title)
      print "MAIN:" + "".join(maintext)
      print "SUB:" + "".join(subtext)      
  else:
      usage()

if __name__=="__main__":
    main()

ファイルはこちら

使い方は以下の通りです。

  • レイアウトパターンファイルをRSSから生成する場合
python blogstemmer.py -r http://d.hatena.ne.jp/gamella/rss > gamella.pat
python blogstemmer.py -r http://d.hatena.ne.jp/gamella/rss -p gamella.pat -e http://d.hatena.ne.jp/gamella/20090324/1237826978

また、上のスクリプトを見ていただけるとわかりますが、main関数で呼んでいるanalyze()とextract()にすべて集約されており、この2関数を呼べばあとは、本文抽出が行えるようになっているので、ライブラリとしても利用可能なはずです。

実際に使うとなると修正が必要だろうなと思う点は以下の通りです。

  • エントリー取得部分にタイムアウト処理を設定
  • レイアウト解析処理(analyze()内)にタイムアウト処理を設定
  • 解析失敗などのエラー処理(設定を変えてもういちど試すか、RSS側に含まれるデータを本文にするとか)
  • そのほかのエラー処理たくさん追加
  • ディレクトリのお掃除処理を追加
  • RSSに本文が全て含まれているケースも多々あるので、そもそもそんなに必要ない?

まぁ、しかし上のスクリプトをベースにいろいろいじれば良いかと思いますので、もしよろしければお使い下さい。手元では、ライブドアブログ、はてな、MTなどの10個くらいのサイトで本文抽出に成功することを確認しています。