へぬもへメモ

https://twitter.com/henumohe

Webstemmer使おうとして文字コードに悩まされたけど解決

昨日の記事で書いた、Webstemmerステップ4の文字コードエラーが治った。

昨日の状況

『ステップ4. 学習したパターンを使って本文を抽出する』を実行すると文字化け

C:\Python27\Lib\site-packages\webstemmer>extract.py -C euc-jp asahi.pat asahi.201107111623.zip
PATTERN: 201107111602/www.asahi.com/digital/av/TKY201106280357.html
MAIN-6: 召ツ宵エ。・将ツ。・。・娼。将苡ェイ召エ宵ョ将」召ェツ。ェ。。ェゥ。ェメ。ェ。\将「\宵ェ\召エ召ツ娼ャ。ェ。。
  • 元の文字列はUnicode文字列
  • EUC-JP,shift-jis,utf-8などひと通りエンコード試したけど全て文字化け
  • PyhtonはUnicode文字列を直接表示できない
    • 直接表示しようとすると"UnicodeEncodeError: 'cp932' codec can't encode character u'\xa1' in position 0:illegal multibyte sequence"
  • try-except文を使って1文字ずつ↑のエラー吐かせてみた

新たに分かったこと

  • 元の文字列はEUC-JPっぽい "\xa1\xca\xa3\xb6\xb7\xee\xa3\xb2" = "(6月2"
  • extract.pyは文字コード自動認識しない
  • オプション-cでデフォルトの文字コードを指定する
    • 公式の説明
      • "HTML ファイル内に charset の指定がない場合、 デフォルトで使用する文字コードの名前 ("euc-jp", "utf-8" など)を指定します。文字コードの自動認識機能はありません。"
    • オプション-Cは出力テキストの文字コード指定なので今回は関係なさそう

とりあえず、文字コード周りをEUC-JPに統一してみることにした。
↓元のコードのエンコードを外して

  def dump_text(self, name, tree,
                pat_threshold, diffscore_threshold, main_threshold,
                codec_out='utf-8', strict=True):
   #enc = lambda x: x.encode(codec_out, 'replace')
    enc = lambda x: x
    ........
          if pat1.title_sectno < sectno and main_threshold <= sect.mainscore:
            for b in sect.blocks:
              print 'MAIN-%d: %s' % (sect.id, enc(b.orig_text))
          else:
            for b in sect.blocks:
              print 'SUB-%d: %s' % (sect.id, enc(b.orig_text))
    print
    return

オプションに-c euc-jpを追加して

C:\Python27\Lib\site-packages\webstemmer>extract.py -c euc-jp asahi.pat asahi.201107111623.zip

実行すると

...
!MATCHED: 201107111623/www.asahi.com/digital/av/TKY201106280357.html
PATTERN: 201107111602/www.asahi.com/digital/av/TKY201106280357.html
MAIN-6: (6月29日発売のAERAムック『AERA×Apple アップルはお好き
ですか』から抜粋した記事です)
MAIN-6: 死者3025人、行方不明者2770人、全壊家屋2万8千戸……(6月10日現
在)。3月11日に起きた東日本大震災でもっとも被害の大きかった自治体のひとつ、宮城
県石巻市。海に近い市街地は石巻市立病院などいくつかの建物を除き、街は跡形もなく流
された。
...

治ったー!ようやく先に進めます。
色々分からない所もあるけど今は放置。余裕ができたら細かいところも追って行きたい。

Webstemmer使おうとして引数エラーと文字コードに悩まされる

Webページの本文を抽出したくて、MOONGIFTで見つけたWebstemmerを使おうと思ったら、いろんな障害にぶつかった。Python歴3日なので、ソース読むのも一苦労。

エラー1

『ステップ1. 学習するためのHTMLページを取得する』をやろうとしたら早速エラーが。

C:\Python27\Lib\site-packages\webstemmer> textcrawler.py -o asahi http://asahi.com/
Writing: 'asahi.201107111850.zip'
Traceback (most recent call last):
  File "C:\Python27\Lib\site-packages\webstemmer\textcrawler.py", line 444, in <module>
    if __name__ == '__main__': main()
  File "C:\Python27\Lib\site-packages\webstemmer\textcrawler.py", line 436, in main
    debug=debug).run()
  File "C:\Python27\Lib\site-packages\webstemmer\textcrawler.py", line 356, in __init__
    cookie_file, acldb, urldb, default_charset, delay, timeout, debug)
  File "C:\Python27\Lib\site-packages\webstemmer\textcrawler.py", line 113, in __init__
    self.robotstxt.read()
  File "C:\Python27\lib\robotparser.py", line 57, in read
    f = opener.open(self.url)
  File "C:\Python27\lib\urllib.py", line 205, in open
    return getattr(self, name)(url)
  File "C:\Python27\lib\urllib.py", line 342, in open_http
    h.endheaders(data)
TypeError: endheaders() takes exactly 1 argument (2 given)

エラーの意味は「引数2つあるよ!1つにしてよ!」らしい。(参考:『初めてのPython』 IV部 演習問題 - ケーズメモ)
試しにendheaders()の元であるhttplib.pyの中身を覗くと、

class HTTPConnection:
  ...
  def endheaders(self, message_body=None):
    ...

となってる。引数がちゃんと2つあるのでこれはおかしい。
試行錯誤の結果、httplib.pyがそこかしこのフォルダに存在していることがわかったため、まずimportの優先順位を確認する。

import sys
sys.path

結果がこう。

['C:\\Python27\\Lib\\site-packages\\webstemmer', 'C:\\Python27\\Lib\\site-packages', 
 'C:\\Windows\\system32\\python27.zip', 'C:\\Python27\\DLLs', 'C:\\Python27\\lib', 
 'C:\\Python27\\lib\\plat-win', 'C:\\Python27\\lib\\lib-tk', 'C:\\Python27']

で、C:\Python27\Lib\site-packages\webstemmerの中を見ると、しっかりhttplib.pyが入ってる。こっちのendheaders()を確認すると、

class HTTPConnection:
  ...
  def endheaders(self):
    ...

引数が1つしかない。ようやく原因特定。
呼び出し先の引数を増やしても、呼び出し元の引数を減らしても一応動いたけど、引数減らすのはちょっと怖いので、webstemmerフォルダのC:\Python27\lib\httplib.pyに書き換えた。よく分からないしこれ以上触りたくない。

エラー2

『ステップ4. 学習したパターンを使って本文を抽出する』を実行し、やっと終わったと思いきや。

C:\Python27\Lib\site-packages\webstemmer>extract.py -C euc-jp asahi.pat asahi.201107111623.zip
PATTERN: 201107111602/www.asahi.com/digital/av/TKY201106280357.html
MAIN-6: 召ツ宵エ。&#57817;・将ツ。・。・娼。将苡ェイ召エ宵ョ将」召&#57422;ェツ。&#57798;ェ。。&#57798;ェゥ。&#57798;ェメ。&#57798;ェ。\将「\宵ェ\召エ召ツ娼ャ。&#57798;ェ。。&#57798;ェゥ。&#57798;ェメ。&#57798;ェ。召ツ娼ホ。&#57798;ェ。。&#57798;ゥテ。&#57798;ゥテ。&#57798;ォタ。&#57798;ォゥ \。&#57560;宵ェ\。゚\将ウ召&#57422;ェチ召&#57422;「・\召・召&#57422;ェョ召・召・召ツ宵繽「・召&#57422;ォア宵イ。ュ召ト将イ召・召&#57422;「ト???将モ召&#57422;ェョ召・召ツ宵ウ

文字化け!これは辛い。文字コード周りについて一から調べることになってしまった。
色々調べて分かったことは、

  • 元の文字列はUnicode文字列
  • EUC-JP,shift-jis,utf-8などひと通りエンコード試したけど全て文字化け
  • PyhtonはUnicode文字列を直接表示できない
    • 直接表示しようとすると"UnicodeEncodeError: 'cp932' codec can't encode character u'\xa1' in position 0:illegal multibyte sequence"
  • try-except文を使って1文字ずつ↑のエラー吐かせてみた
    • u'\xa1' u'\xca' u'\xa3' u'\xb6' u'\xb7' u'\xee' u'\xa3' u'\xb2' ...
    • Unicode対応 文字コード表EUC文字コードを2バイトずつ確認すると、"a1ca,a3b6,b7ee,a3b2"→"(,6,月,2"
    • 元記事の本文始めは「(6月29日発売のAERAムック『AERA×Apple アップルはお好きですか』から抜粋した記事です)」
      • 合ってるし!

ここまでやって今日は諦めた。文字コード自体はeuc-jpで合ってるっぽいので、文字列の扱い方が良くないんだろうか?なんとか明日中に解決したい。