Facebook
From Perl Flamingo, 3 Years ago, written in Python.
Embed
Download Paste or View Raw
Hits: 195
  1. import locale
  2. import os
  3. import math
  4. import re
  5. import datetime
  6. import dateutil.parser
  7. from typing import Dict, Tuple, List
  8. import urllib.request
  9. import urllib.parse
  10.  
  11. """
  12. ニコニコ実況のコメントをスレッドごとに保存するpythonスクリプト
  13. 保存場所は./logs/
  14. ライセンス:NYSL http://www.kmonos.net/nysl/
  15.  
  16. todo
  17. ・コメントのxmlのバリデーションが不十分。コメントを削除された時は?コメント本文に</chat> がある場合は?
  18.  
  19. コメントxmlのレアパターン
  20. ・コメントに改行が入る場合がある。ので、正規表現はdotall必須。
  21. ・vposが-4707799 とかのすごい値になる事がある。dateプロパティと前後のコメントは普通。
  22. """
  23.  
  24.  
  25. class ChatXml:
  26.     def __init__(self, input: str) -> None:
  27.         self.originalXml = input
  28.         self.formattedXml = self._chatElementInsertStr(input)
  29.         self.xmlData = self._getChatXmlData(input)
  30.         self.number = self.xmlData[0]
  31.  
  32.     def numerVposDateFormatStr(self) -> str:
  33.         dateStr = f"{self.xmlData[2]:%Y/%m/%d %H:%M:%S}"
  34.         return f"no = {self.xmlData[0]:>10} , vpos = {int(self.xmlData[1])} , date = {dateStr}"
  35.  
  36.     def _getChatXmlData(self, input: str) -> Tuple[int, int, datetime.datetime]:
  37.         """
  38.        xmlの文字列から、no vpos dateのプロパティを取得する。dateはdate型にする
  39.        """
  40.         noMatch = re.findall(r"no=\"(\d+)\"", input)
  41.         vposMatch = re.findall(r"vpos=\"(-?\d+)\"", input)
  42.         dateMatch = re.findall(r"date=\"(\d+)\"", input)
  43.         if len(noMatch) != 1:
  44.             raise Exception("no属性が0個もしくは2個以上")
  45.         if len(vposMatch) != 1:
  46.             raise Exception("vpos属性が0個もしくは2個以上")
  47.         if len(dateMatch) != 1:
  48.             raise Exception("date属性が0個もしくは2個以上")
  49.         return int(noMatch[0]), int(vposMatch[0]), datetime.datetime.fromtimestamp(int(dateMatch[0]))
  50.  
  51.     def _chatElementInsertStr(self, input: str) -> str:
  52.         """
  53.        <chat thread="1270407602" no="14239" vpos="7637459" date="1270483976" name="hoge" user_id="719" premium="3">アニヲタ</chat>
  54.        を、人間に見やすいように↓にする
  55.        <chat date_str="2020/01/01(月)00:00:00" vpos_str="00:00.000" thread="1270407602" no="14239" vpos="7637459" date="1270483976" name="hoge" user_id="719" premium="3">アニヲタ</chat>
  56.        """
  57.         if input.startswith("<chat ") == False:
  58.             raise Exception(f"chatエレメントが検出できない。 \"{input}\"")
  59.         xmlData = self._getChatXmlData(input)
  60.         locale.setlocale(locale.LC_ALL, 'ja_JP.UTF-8')
  61.         fDate = f"{xmlData[2]:%Y/%m/%d(%a)%H:%M:%S}"
  62.         fVpos = f"{(int(xmlData[1])/10):>10.1f}"
  63.         vposInt = int(xmlData[1])/100
  64.         if vposInt < 3600:
  65.             # 0分0.0秒
  66.             min = math.floor(vposInt/60)
  67.             sec = vposInt % 60
  68.             fVpos = f"{min}:{sec:05.2f}"
  69.         else:
  70.             # 0時間00分0.0秒
  71.             hour = math.floor(vposInt/3600)
  72.             min = math.floor(vposInt/60) % 60
  73.             sec = vposInt % 60
  74.             fVpos = f"{hour}:{min:02}:{sec:05.2f}"
  75.         result = input.replace("<chat ", f"<chat date_str=\"{fDate}\" vpos_str=\"{fVpos}\" ")
  76.         return result
  77.  
  78.  
  79. class Jikkyo:
  80.     def __init__(self, cookie: str, jkId: str, startDateUnixTimeSec: int) -> None:
  81.         self.cookie = cookie
  82.         self.jkId = jkId
  83.         self.startDateUnixTimeSec = startDateUnixTimeSec
  84.         self.getFlv = {}  # type: Dict[str,str]
  85.         self.waybackKey = ""
  86.  
  87.     def start(self):
  88.         self._getFlv()
  89.         self._getWaybackKey()
  90.         self._getThread()
  91.  
  92.     def _getFlv(self):
  93.         # endTimeは不要?
  94.         url = f"http://jk.nicovideo.jp/api/v2/getflv?v={self.jkId}&start_time={self.startDateUnixTimeSec}"
  95.         headers = {
  96.             'Content-Type': 'application/json',
  97.             "Cookie": f"user_session={self.cookie}"
  98.         }
  99.         req = urllib.request.Request(url, None, headers)
  100.         result: Dict[str, str] = {}
  101.         with urllib.request.urlopen(req) as res:
  102.             apiResponse = str(res.read().decode("utf-8")).split("&")
  103.             for a in apiResponse:
  104.                 [b, c] = a.split("=", 1)
  105.                 result[b] = urllib.parse.unquote(c)
  106.         self.getFlv = result
  107.         if True:
  108.             print(f"thread_id  : {unixTimeToStr(float(result['thread_id']))}")
  109.             print(f"base_time  : {unixTimeToStr(float(result['base_time']))}")
  110.             print(f"open_time  : {unixTimeToStr(float(result['open_time']))}")
  111.             print(f"start_time : {unixTimeToStr(float(result['start_time']))}")
  112.             print(f"end_time   : {unixTimeToStr(float(result['end_time']))}")
  113.         if result['thread_id'] != result['base_time'] or result['base_time'] != result['open_time'] or result['open_time'] != result['start_time']:
  114.             raise Exception("一致するはずの値が不一致")
  115.  
  116.     def _getWaybackKey(self):
  117.         url = f"http://jk.nicovideo.jp/api/v2/getwaybackkey?thread={self.getFlv['thread_id']}"
  118.         headers = {
  119.             'Content-Type': 'application/json',
  120.             "Cookie": f"user_session={self.cookie}"
  121.         }
  122.         req = urllib.request.Request(url, None, headers)
  123.         with urllib.request.urlopen(req) as res:
  124.             apiResponse = str(res.read().decode("utf-8")).split("&")
  125.             for a in apiResponse:
  126.                 [b, c] = a.split("=", 1)
  127.                 if b == "waybackkey":
  128.                     self.waybackKey = urllib.parse.unquote(c)
  129.                     return
  130.         raise Exception("waybackKeyが取得出来ませんでした")
  131.  
  132.     def _getThread(self):
  133.         apiVersion = "20061206"
  134.         whenParameter = self.getFlv['end_time']
  135.         serverHost = f"{self.getFlv['ms']}:{self.getFlv['http_port']}"
  136.         userId = self.getFlv['user_id']
  137.         totalResult = []  # type : list[ChatXml]
  138.         while True:
  139.             print(f"thread request. when = {datetime.datetime.fromtimestamp(int(whenParameter)):%Y/%m/%d %H:%M:%S}")
  140.             url = f"http://{serverHost}/api/thread?thread={self.getFlv['thread_id']}&res_from=-1000&version={apiVersion}&when={whenParameter}&user_id={userId}&waybackkey={self.waybackKey}"
  141.             req = urllib.request.Request(url)
  142.             with urllib.request.urlopen(req) as res:
  143.                 apiResponse = str(res.read().decode("utf-8"))
  144.                 chatMatchresult = re.findall(r"<chat .+?</chat>", apiResponse, re.DOTALL)
  145.                 if len(chatMatchresult) == 0:
  146.                     break
  147.                 print("↓start")
  148.                 print("\n".join([ChatXml(i).numerVposDateFormatStr() for i in chatMatchresult[0:2]]))
  149.                 print("↓end")
  150.                 print("\n".join([ChatXml(i).numerVposDateFormatStr() for i in chatMatchresult[-2:]]))
  151.                 print("end")
  152.                 minDateUnixTimeSec = 9999999999
  153.                 maxValue = 0
  154.                 formattedChatXml = []  # type: List[str]
  155.                 for chat in chatMatchresult:
  156.                     chatXml = ChatXml(chat)
  157.                     totalResult.append(chatXml)
  158.                     maxValue = max(maxValue, chatXml.number)
  159.                     minDateUnixTimeSec = min(minDateUnixTimeSec, int(chatXml.xmlData[2].timestamp()))
  160.                     formattedChatXml.append(chat)
  161.                 if len(chatMatchresult) == 1:
  162.                     # 最後は1つしか返ってこない
  163.                     break
  164.                 whenParameter = minDateUnixTimeSec
  165.         if len(totalResult) == 0:
  166.             print("no chat xml reseived")
  167.         else:
  168.             self._saveResult(totalResult)
  169.  
  170.     def _saveResult(self, chatXmlList: List[ChatXml]):
  171.         chatXmlList = sorted(chatXmlList, key=lambda x: x.number)
  172.         os.makedirs(f"./logs/{self.jkId}", exist_ok=True)
  173.         threadNo = int(self.getFlv["thread_id"])
  174.         endDateObj = datetime.datetime.fromtimestamp(int(self.getFlv['end_time']))
  175.         startDateObj = datetime.datetime.fromtimestamp(int(self.getFlv['start_time']))
  176.         endDate = f"{endDateObj:%Y年%m月%d日(%a)%H時%M分%S秒}"
  177.         startDate = f"{startDateObj:%Y年%m月%d日(%a)%H時%M分%S秒}"
  178.         # ファイル名は人間が読みやすい方式。nicojk系ツールと同じフォーマットにする場合はここを編集
  179.         # 1270407602-__15257.res_2010年04月05日(月)04時00分02秒~2010年04月06日(火)04時04分56秒
  180.         saveFileName = f"{threadNo}-{len(chatXmlList):_>7}.res_{startDate}~{endDate}.txt"
  181.         #saveFileName = f"{threadNo}.txt"
  182.         saveFilePath = f"./logs/{self.jkId}/{saveFileName}"
  183.         with open(saveFilePath, mode='w', encoding="utf-8") as f:
  184.             for chatXml in chatXmlList:
  185.                 f.write(chatXml.formattedXml)
  186.                 f.write("\n")
  187.         print(f"save {len(chatXmlList)} comments. {saveFilePath}")
  188.  
  189.  
  190. def unixTimeToStr(unixTimeSec: float) -> str:
  191.     dt = datetime.datetime.fromtimestamp(unixTimeSec)
  192.     return f"{dt:%Y/%m/%d %H:%M:%S}"
  193.  
  194.  
  195. def getUnixTimeSec(str: str) -> int:
  196.     a = dateutil.parser.parse(str).timestamp()
  197.     return int(a)
  198.  
  199.  
  200. if __name__ == "__main__":
  201.     # cookie = "user_session_0000000000000_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  202.     cookie = "xxxxxx"
  203.     startDate = getUnixTimeSec("2011/03/11 04:04:00")
  204.     channel = "jk1"
  205.     jikkyo = Jikkyo(cookie, channel, startDate)
  206.     jikkyo.start()
  207.  

Replies to Untitled rss

Title Name Language When
Re: Untitled Small Capybara python 3 Years ago.