OSDN Git Service

ユーザー名を含む<p>を正常に見つけられないのを修正
[pynv/pylib-nicovideo.git] / nicovideo / access.py
1 # -*- coding: utf-8 -*-
2
3 #       Copyright © 2014 dyknon
4 #
5 #       This file is part of Pylib-nicovideo.
6 #
7 #       Pylib-nicovideo is free software: you can redistribute it and/or modify
8 #       it under the terms of the GNU General Public License as published by
9 #       the Free Software Foundation, either version 3 of the License, or
10 #       (at your option) any later version.
11 #
12 #       This program is distributed in the hope that it will be useful,
13 #       but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #       GNU General Public License for more details.
16 #
17 #       You should have received a copy of the GNU General Public License
18 #       along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 """
21 注意事項
22         これはニコニコ動画のAPIなどを利用して
23         動画情報やらなにやらをお気楽に取得できるライブラリみたいなものです。
24         そのため使い方によってはサーバーに過大な負荷がかかる可能性もあります。
25         サーバーに負荷がかかりすぎないよう、十分注意して使ってください。
26 """
27
28 from . import err
29 from . import tools
30 from . import logger
31 import http.client
32 import urllib.parse
33 import xml.etree.ElementTree as XmlTree
34 import re
35 import io
36
37 class Video:
38         """ニコニコに投稿された動画にアクセスするためのClass"""
39         def __init__(self, wid):
40                 self.watch_id = wid
41                 self.info_downloaded = {}
42                 self.info = {}
43                 self.title = None
44
45         def url(self):
46                 return "http://www.nicovideo.jp/watch/" + self.watch_id
47
48         def standard_info_url(self):
49                 return "http://ext.nicovideo.jp/api/getthumbinfo/" + self.watch_id
50
51         def get_standard_info(self):
52                 #ニコニコ動画動画に関する基本的な情報を得ることのできる
53                 #有名なAPIであるhttp://ext.nicovideo.jp/api/getthumbinfo/を
54                 #利用して得ることのできる情報を取得します。
55                 #要修正:いくつか取得していない情報があります
56
57                 if "thumbinfo" in self.info_downloaded:
58                         return
59
60                 thumbinfo = {}
61
62                 thumbinfo["tags"] = []
63
64                 conn = http.client.HTTPConnection("ext.nicovideo.jp")
65                 conn.request("GET", "/api/getthumbinfo/" + self.watch_id)
66                 res = conn.getresponse()
67                 if res.status == 200:
68                         resdata = res.read()
69                         xmlroot = XmlTree.fromstring(resdata)
70                         if xmlroot.tag != "nicovideo_thumb_response":
71                                 raise err.InterpretErr("ext.nicovideo.jp/api/getthumbinfo/")
72                         if xmlroot.attrib["status"] != "ok":
73                                 errcode = "unknown"
74                                 for child in xmlroot:
75                                         if child.tag == "error":
76                                                 for cerror in child:
77                                                         if cerror.tag == "code":
78                                                                 errcode = cerror.text
79                                 raise err.NotFound(self.watch_id, errcode)
80                         if len(xmlroot) != 1 or \
81                                         xmlroot[0].tag != "thumb":
82                                 raise err.InterpretErr("ext.nicovideo.jp/api/getthumbinfo/")
83                         for info in xmlroot[0]:
84                                 if info.tag == "video_id":
85                                         self.info["videoid"] = info.text
86                                 elif info.tag == "title":
87                                         self.info["title"] = info.text
88                                         self.title = info.text
89                                 elif info.tag == "description":
90                                         self.info["description"] = info.text
91                                 elif info.tag == "thumbnail_url":
92                                         self.info["thumbnail_url"] = info.text
93                                 elif info.tag == "first_retrieve":
94                                         self.info["upload_time"] = tools.parse_time(info.text)
95                                 elif info.tag == "length":              #5:19
96                                         if not "length" in self.info:
97                                                 spt = info.text.split(":")
98                                                 self.info["length"] = int(spt[0])*60 + int(spt[1])
99                                 elif info.tag == "movie_type":  #flv
100                                         self.info["movie_type"] = info.text
101                                 elif info.tag == "size_high":   #21138631
102                                         if not "videosize" in self.info:
103                                                 self.info["videosize"] = {}
104                                         self.info["videosize"]["high"] = int(info.text)
105                                 elif info.tag == "size_low":    #17436492
106                                         if not "videosize" in self.info:
107                                                 self.info["videosize"] = {}
108                                         self.info["videosize"]["low"] = int(info.text)
109                                 elif info.tag == "view_counter":
110                                         self.info["view_counter"] = int(info.text)
111                                 elif info.tag == "comment_num":
112                                         self.info["comment_num"] = int(info.text)
113                                 elif info.tag == "mylist_counter":
114                                         self.info["mylist_counter"] = int(info.text)
115                                 elif info.tag == "last_res_body":       #comments_new
116                                         self.info["last_res"] = info.text
117                                 elif info.tag == "watch_url":
118                                         pass
119                                 elif info.tag == "thumb_type":  #video
120                                         self.info["thumb_type"] = info.text
121                                 elif info.tag == "embeddable":
122                                         self.info["downloadable"] = int(info.text) != 0
123                                 elif info.tag == "no_live_play":
124                                         self.info["live_play"] = int(info.text) == 0
125                                 elif info.tag == "tags":
126                                         for tag_entry in info:
127                                                 if tag_entry.tag != "tag":
128                                                         next
129                                                 if "lock" in tag_entry.attrib:
130                                                         if int(tag_entry.attrib["lock"]):
131                                                                 thumbinfo["tags"]. \
132                                                                         append([tag_entry.text, True])
133                                                         else:
134                                                                 thumbinfo["tags"]. \
135                                                                         append([tag_entry.text, False])
136                                                 else:
137                                                         thumbinfo["tags"].append([tag_entry.text, False])
138                                         self.info["tags"] = thumbinfo["tags"]
139                                 elif info.tag == "user_id":
140                                         self.info["user_id"] = info.text
141                                 thumbinfo[info.tag] = info.text
142                 else:
143                         conn.close()
144                         raise err.HttpErr(res.status, res.reason)
145                 conn.close()
146                 self.info_downloaded["thumbinfo"] = thumbinfo
147
148         def get_player_info(self):
149                 #外部プレイヤーの呼び出しを行うJavascriptを解析(笑)して
150                 #動画情報を取得します。
151                 #ここで取得できる情報には、コメントや動画URLを特定する手がかりが
152                 #多く存在していますが、同時に何を意味するのかわからない情報も
153                 #多く存在するので改良の余地があります。
154
155                 if "playerinfo" in self.info_downloaded:
156                         return
157
158                 player_info = {}
159
160                 rawdata = tools.http_get_text_data("ext.nicovideo.jp", "/thumb_watch/" + self.watch_id)
161                 pattern_playerURL_finder = re.compile(r"^\s*Nicovideo\.playerUrl\s*=\s*'(.+)'\s*;\s*$")
162                 pattern_start_varvideo_finder = re.compile(r"^\s*var\s+video\s*=\s*new\s+Nicovideo\.Video\s*\(\s*\{\s*$")
163                 pattern_end_varvideo_finder = re.compile(r"^(.*)\}\s*\)\s*;\s*$")
164                 pattern_end_varplayer_finder = re.compile(r"^\s*}")
165                 pattern_item_interrupter = re.compile(r"^\s*(.+)\s*:\s*(.+)\s*$")
166                 pattern_item_interrupter2 = re.compile(r"^\s*'(.+)'\s*:\s*'(.+)'\s*$")
167                 stage = 0
168                 for line in rawdata.split("\n"):
169                         if stage == 0:
170                                 mo = pattern_playerURL_finder.search(line)
171                                 if mo:
172                                         player_info["player_url"] = mo.group(1)
173                                         self.info["player_url"] = mo.group(1)
174                                         stage = 1
175                         elif stage == 1:
176                                 if pattern_start_varvideo_finder.search(line):
177                                         stage = 2
178                         elif stage == 2:
179                                 mo = pattern_end_varvideo_finder.search(line)
180                                 if mo:
181                                         line = mo.group(1)
182                                         stage = 3
183                                 for item in line.split(","):
184                                         mo = pattern_item_interrupter.search(item)
185                                         if mo:
186                                                 name = mo.group(1)
187                                                 value = mo.group(2)
188                                                 if name == "v":
189                                                         self.info["v_value"] = tools.un_javascript_escape(value[1:-1])
190                                                 elif name == "id":
191                                                         self.info["videoid"] = tools.un_javascript_escape(value[1:-1])
192                                                 elif name == "title":
193                                                         self.info["title"] = tools.un_javascript_escape(value[1:-1])
194                                                         self.title = self.info["title"]
195                                                 elif name == "description":
196                                                         self.info["description"] = tools.un_javascript_escape(value[1:-1])
197                                                 elif name == "thumbnail":
198                                                         self.info["thumbnail_url"] = tools.un_javascript_escape(value[1:-1])
199                                                 elif name == "postedAt":
200                                                         pass                    #面倒だから省略
201                                                 elif name == "length":
202                                                         self.info["length"] = int(value)
203                                                 elif name == "viewCount":
204                                                         self.info["view_counter"] = int(value)
205                                                 elif name == "mylistCount":
206                                                         self.info["mylist_counter"] = int(value)
207                                                 elif name == "commentCount":
208                                                         self.info["comment_num"] = int(value)
209                                                 elif name == "movieType":
210                                                         self.info["movie_type"] = tools.un_javascript_escape(value[1:-1])
211                                                 elif name == "isDeleted":
212                                                         self.info["is_deleted"] = (value == "true")
213                                                 elif name == "isMymemory":
214                                                         if value == "true":
215                                                                 self.info["thumb_type"] = "mymemory"
216                                                         else:
217                                                                 if not "thumb_type" in self.info:
218                                                                         self.info["thumb_type"] = "video"
219                                                 player_info[name] = value
220                         elif stage == 3:
221                                 stage = 4
222                         elif stage == 4:
223                                 #変数Playerは場合によって要素数が変化し、
224                                 #まだ未知の要素がある可能性もあるため、
225                                 #すべての要素を保存しておきます。
226                                 #また、あまり有用でないと思われる情報は
227                                 #特別に変数を割り当てていないので、これらには
228                                 #object.player_info[<要素名>] でアクセスしてください。
229                                 if pattern_end_varplayer_finder.search(line):
230                                         break
231                                 for item in line.split(","):
232                                         mo = pattern_item_interrupter2.search(item)
233                                         if mo:
234                                                 name = tools.un_javascript_escape(mo.group(1))
235                                                 value = tools.un_javascript_escape(mo.group(2))
236                                                 if name == "thumbPlayKey":
237                                                         self.info["thumb_playkey"] = value
238                                                 elif name == "playerTimestamp":
239                                                         self.info["thumb_player_timestamp"] = value
240                                                 elif name == "language":
241                                                         self.info["language"] = value
242                                                 #以下の要素は存在しない場合も確認されていますが、
243                                                 #有用なため、個別に変数を割り当てます。
244                                                 #なお、真偽値についてはFalseの場合に
245                                                 #要素が存在しないと考えられます。
246                                                 elif name == "has_owner_thread":
247                                                         if int(value) == 0:
248                                                                 self.info["comment_has_owner_thread"] = False
249                                                         else:
250                                                                 self.info["comment_has_owner_thread"] = True
251                                                 elif name == "isWide":
252                                                         if int(value) == 0:
253                                                                 self.info["is_wide"] = False
254                                                         else:
255                                                                 self.info["is_wide"] = True
256                                                 player_info[name] = value
257                 else:
258                         raise err.InterpretErr("ext.nicovideo.jp/thumb_watch/")
259                 self.info_downloaded["playerinfo"] = player_info
260
261         def get_play_info(self):
262                 #get_player_info()によって取得できた情報を元に、
263                 #動画ファイルへのアクセス権を獲得し、
264                 #動画ファイルのURLや、それにアクセスするための情報などを取得します。
265
266                 # === 注意 === #
267                 #この操作は本来
268                 #ニコニコ動画から提供されるプレイヤーが行うことなので、
269                 #連続アクセスなどでサーバーに負荷がかからないよう
270                 #特に注意してください。
271                 
272                 if "playinfo" in self.info_downloaded:
273                         return
274
275                 play_info = {}
276
277                 self.get_player_info()
278                 if      not "player_url" in self.info or \
279                         not "thumb_playkey" in self.info or \
280                         not "thumb_player_timestamp" in self.info:
281                         raise err.ApiUpdated("再生情報が十分に得られませんでした。")
282
283                 spurl = urllib.parse.urlsplit(self.info["player_url"])
284                 if spurl[0] != "http" or spurl[1] == "":
285                         raise err.ApiUpdated("外部プレイヤーURLがhttp://から始まっていない")
286                 conn = http.client.HTTPConnection(spurl[1])
287                 if spurl[3] == "":
288                         conn.request("GET", spurl[2])
289                 else:
290                         conn.request("GET", spurl[2] + "?" + spurl[3])
291                 res = conn.getresponse()
292                 if res.status != 200:
293                         conn.close()
294                         raise err.HttpErr(res.status, res.reason)
295                 conn.close()
296
297                 conn = http.client.HTTPConnection("ext.nicovideo.jp")
298                 conn.request("GET", "/swf/player/nicoplayer.swf?thumbWatch=1&ts=" + self.info["thumb_player_timestamp"])
299                 res = conn.getresponse()
300                 if res.status != 200:
301                         conn.close()
302                         raise err.HttpErr(res.status, res.reason)
303                 conn.close()
304
305                 postmes = "k=" + urllib.parse.quote_plus(self.info["thumb_playkey"]) + "&as3=1&v=" + urllib.parse.quote_plus(self.info["v_value"])
306                 logger.debug("play_info_req:\n" + postmes)
307                 conn = http.client.HTTPConnection("ext.nicovideo.jp")
308                 conn.request("POST", "/thumb_watch", body=postmes, headers={"Content-Length": len(postmes), "Content-Type": "application/x-www-form-urlencoded"})
309                 res = conn.getresponse()
310                 if res.status == 200:
311
312                         cookie_raw = res.getheader("Set-Cookie")
313                         if cookie_raw == None:
314                                 raise ApiUpdated("responce of /thumb_watch doesn't include needed information")
315                         cookie = cookie_raw[:cookie_raw.find(";")]
316                         self.info["playinfo_cookie"] = cookie
317
318                         #このレスポンスは解析があまり進んでいないため、
319                         #意味の理解できていない要素も含めてすべて保存します。
320                         raw_body = res.read().decode("ascii", "replace")
321                         logger.debug("play_info:\n" + raw_body)
322                         for item in raw_body.split("&"):
323                                 ind = item.find("=")
324                                 if ind == -1:
325                                         play_info[urllib.parse.unquote_plus(item)] = True
326                                 else:
327                                         play_info[urllib.parse.unquote_plus(item[:ind])] = urllib.parse.unquote_plus(item[ind+1:])
328                         if "thread_id" in play_info:
329                                 self.info["play_thread_id"] = play_info["thread_id"]
330                         if "nicos_id" in play_info:
331                                 self.info["play_nicos_id"] = play_info["nicos_id"]
332                         if "ms" in play_info:
333                                 self.info["play_comments_url"] = play_info["ms"]
334                         if "url" in play_info:
335                                 self.info["play_video_url"] = play_info["url"]
336                 else:
337                         conn.close()
338                         raise err.HttpErr(res.status, res.reason)
339                 conn.close()
340                 self.info_downloaded["playinfo"] = play_info
341
342         def generate_connection_to_coments(self):
343                 #get_play_info()などで取得した情報を元に、
344                 #ニコニコ動画のプレイヤーがサーバーから取得しているものに近い
345                 #コメントの情報を取得するための
346                 #HTTPConnectionオブジェクトを生成し、
347                 #requestまで行ったものを返します。
348                 #つまり、getresponse()でレスポンスを得ることができる状態です。
349
350                 # === 注意 === #
351                 #この操作は本来
352                 #ニコニコ動画から提供されるプレイヤーが行うことなので、
353                 #連続アクセスなどでサーバーに負荷がかからないよう
354                 #特に注意してください。
355
356                 self.get_play_info()
357                 self.get_standard_info()
358
359                 if      not "play_thread_id" in self.info or \
360                         not "play_comments_url" in self.info or \
361                         not "length" in self.info:
362                         raise ApiUpdated("コメント取得に必要な情報が取得できませんでした")
363
364                 #コメントAPIに送信する情報には謎が多いので
365                 #解明され次第修正されるべきです
366                 if self.info["length"] < 60:
367                         comment_from = -100
368                 elif self.info["length"] < 300:
369                         comment_from = -250
370                 elif self.info["length"] < 600:
371                         comment_from = -500
372                 else:
373                         comment_from = -1000
374                 comment_from = "{}".format(comment_from)
375
376                 threads = ""
377
378                 #通常のコメント取得分
379                 #threads += '<thread thread="' + self.play_thread_id + '" version="20061206" res_from="' + comment_from + '" scores="1"/>'
380                 threads += '<thread thread="' + self.info["play_thread_id"] + '" version="20090904" res_from="' + comment_from + '" scores="1"/>'
381                 #時間帯ごとのコメント追加分(暫定処理,多分要修正)
382                 #要解析:このスレッドについてはリクエストの詳細がわかりません。
383                 #lvnは↑で取得した個数(これに含まれないものが取得できる)
384                 #minsは動画の長さ(分)を小数点以下切り捨てしたものですが、
385                 #これが正しい指定方法かは不明です。
386                 #レスポンスから推測すると、何かしらの間違いがあると思われます。
387                 lvn = "{}".format(-int(comment_from))
388                 mins = "{}".format(int(self.info["length"] / 60))
389                 threads += '<thread_leaves thread="' + self.info["play_thread_id"] + '" scores="1">0-' + mins + ':100,' + lvn + '</thread_leaves>'
390
391                 #ニコスクリプトによって投稿されたコメント?
392                 #詳細不明。
393                 if "play_nicos_id" in self.info:
394                         threads += '<thread thread="' + self.info["play_nicos_id"] + '" version="20090904" res_from="' + comment_from + '" scores="1"/>'
395                         threads += '<thread_leaves thread="' + self.info["play_nicos_id"] + '" scores="1">0-' + mins + ':100,' + lvn + '</thread_leaves>'
396                 
397                 #投稿者コメント
398                 if "comment_has_owner_thread" in self.info and \
399                         self.info["comment_has_owner_thread"]:
400                         threads += '<thread thread="' + self.info["play_thread_id"] + '" version="20090904" res_from="-1000" fork="1" click_revision="-1" scores="1"/>'
401
402                 postmes = "<packet>" + threads + "</packet>";
403                 spurl = urllib.parse.urlsplit(self.info["play_comments_url"])
404                 if spurl[0] != "http" or spurl[1] == "":
405                         raise ApiUpdated("Comment API URL doesn't start whith http://")
406                 conn = http.client.HTTPConnection(spurl[1])
407                 posthead = {
408                         "Content-Length": len(postmes),
409                         "Content-Type": "text/xml",
410                         "Cookie": self.info["playinfo_cookie"]
411                 }
412                 if spurl[3] == "":
413                         conn.request("POST", spurl[2], body=postmes, headers=posthead)
414                 else:
415                         conn.request("POST", spurl[2] + "?" + spurl[3], body=postmes, headers=posthead)
416                 return conn
417
418         def download_comments_to_file(self, fileobj):
419                 #get_play_info()などで取得した情報を元に、
420                 #ニコニコ動画のプレイヤーがサーバーから取得しているものに近い
421                 #コメントの情報を取得し、fileobjに書き出します。
422                 #fileobjがio.TextIOBaseのサブクラスなら
423                 #文字列のデコードを行います。(非推奨)
424                 #そうでない場合はバイナリを書き込みます。
425
426                 # === 注意 === #
427                 #この操作は本来
428                 #ニコニコ動画から提供されるプレイヤーが行うことなので、
429                 #連続アクセスなどでサーバーに負荷がかからないよう
430                 #特に注意してください。
431
432                 conn = self.generate_connection_to_coments()
433                 res = conn.getresponse()
434                 if res.status == 200:
435                         if isinstance(fileobj, io.TextIOBase):
436                                 #これくらいならいっぺんに書き込んでもいいかな?
437                                 #というか、utf-8でエンコードされたものを
438                                 #分割してデコードするなんてレベル高すぎて無理です
439                                 fileobj.write(res.read().decode("utf-8", "replace"))
440                         else:
441                                 #小分けにしたほうが負荷が少ないんじゃないかと思うけど
442                                 #CPUに対する負荷はかえって増える気がする。(主に言語的に)
443                                 #けど分けてみる
444                                 while True:
445                                         buf = res.read(2048)
446                                         if len(buf) == 0:
447                                                 break
448                                         fileobj.write(buf)
449                 else:
450                         conn.close()
451                         raise err.HttpErr(res.status, res.reason)
452                 conn.close()
453
454         def generate_connection_to_video(self):
455                 #get_play_info()などで取得した情報を元に、
456                 #動画をダウンロードするための
457                 #HTTPConnectionオブジェクトを生成し、
458                 #requestまで行ったものを返します。
459                 #つまり、getresponse()でレスポンスを得ることができる状態です。
460
461                 # === 注意 === #
462                 #この操作は本来
463                 #ニコニコ動画から提供されるプレイヤーが行うことなので、
464                 #連続アクセスなどでサーバーに負荷がかからないよう
465                 #特に注意してください。
466
467                 self.get_play_info()
468                 if not "play_video_url" in self.info:
469                         raise ApiUpdated("動画DLに必要な情報を取得できませんでした。")
470
471                 spurl = urllib.parse.urlsplit(self.info["play_video_url"])
472                 if spurl[0] != "http" or spurl[1] == "":
473                         raise ApiUpdated("Video URL doesn't start whith http://")
474                 conn = http.client.HTTPConnection(spurl[1])
475                 gethead = {
476                         "Cookie": self.info["playinfo_cookie"]
477                 }
478                 if spurl[3] == "":
479                         conn.request("GET", spurl[2], headers=gethead)
480                 else:
481                         conn.request("GET", spurl[2] + "?" + spurl[3], headers=gethead)
482                 return conn
483
484         def download_movie_to_file(self, fileobj):
485                 #get_play_info()などで取得した情報を元に、
486                 #動画をDLし、fileobjに書き出します。
487
488                 # === 注意 === #
489                 #この操作は本来
490                 #ニコニコ動画から提供されるプレイヤーが行うことなので、
491                 #連続アクセスなどでサーバーに負荷がかからないよう
492                 #特に注意してください。
493
494                 conn = self.generate_connection_to_video()
495                 res = conn.getresponse()
496                 if res.status == 200:
497                         while True:
498                                 buf = res.read(2048)
499                                 if len(buf) == 0:
500                                         break
501                                 fileobj.write(buf)
502                 else:
503                         conn.close()
504                         raise err.HttpErr(res.status, res.reason)
505                 conn.close()
506
507 class User:
508         def __init__(self, uid):
509                 self.id = uid
510                 self.info_downloaded = {}
511                 self.info = {}
512                 self.name = None
513
514         def get_standard_info(self):
515                 #ユーザー情報埋め込み用のhttp://ext.nicovideo.jp/thumb_user/から
516                 #ユーザー名とアイコンを取得します。
517                 if "thumb" in self.info_downloaded:
518                         return
519
520                 gotten = {}
521
522                 data = tools.http_get_text_data(
523                         "ext.nicovideo.jp", "/thumb_user/" + self.id)
524
525                 mo = re.search("<p (?:[^>]* )?class=\"TXT12\"[^>]*>(.+?)</p>", data)
526                 if not mo:
527                         logger.debug("TXT12発見できず")
528                         raise err.ApiUpdated("ユーザー情報取得失敗")
529                 if mo.group(1).find("<strong>") < 0:
530                         raise err.NotFound(self.id, "ニコニコ曰く:" + mo.group(1))
531                 else:
532                         smo = re.search("<strong>([^<]*?)</strong>", mo.group(1))
533                         if not smo:
534                                 logger.debug("TXT12内にstrongブロック無し")
535                                 raise err.ApiUpdated("ユーザー情報取得失敗")
536                         self.name = smo.group(1)
537                         self.info["name"] = self.name
538                         gotten["name"] = self.name
539
540                 mo = re.search("<div (?:[^>]* )?class=\"user_img\"[^>]*>(.+?)</div>",
541                         data)
542                 if not mo:
543                         logger.debug("div id:user_img見つからず")
544                         raise err.ApiUpdated("ユーザー情報取得失敗")
545                 imo = re.search("<img (?:[^>]* )?src=\"([^\"]+)\"", mo.group(1))
546                 if not imo:
547                         logger.debug("div id:user_img内にimg見つからず")
548                         raise err.ApiUpdated("ユーザー情報取得失敗")
549                 self.info["img"] = imo.group(1)
550                 gotten["img"] = self.info["img"]
551                 if self.info["img"] == "http://res.nimg.jp/img/user/thumb/blank.jpg":
552                         self.info["blank_img"] = True
553                 else:
554                         self.info["blank_img"] = False
555                 gotten["blank_img"] = self.info["blank_img"]
556
557                 self.info_downloaded["thumb"] = gotten
558                 return