pátek 6. ledna 2023

Tweety a tooty jako plaintext

Twitter umožňuje stažení dat, které o nás nashromáždil, stačí požádat na twitter.com/settings/account a počkat si 24 hodin. Po rozbalení stažené zálohy ji můžeme zkoumat internetovým prohlížečem v souboru "Your archive.html". To mi ale moc nevyhovovalo, protože archiv tweetů je rozdělen na sloupce Tweets, Replies, Retweets a hlavně proto, že je při scrolování dotahuje dynamicky javascriptem, takže v prohlížeči pořádně nefunguje vyhledávání pomocí Ctrl-F. Tim Hutton napsal script parser.py, který vylepšuje několik nedostatků, ale problém prohledávání všech twítů najednou neřeší.

Vyrobil jsem si tedy script, který převádí twíty z formátu objektů JSON v souboru "data/tweets.js" na prostý text, nahradí zkrácené internetové adresy uváděné v textu za jejich plné, funkční verze a chronologicky seřadí vlastní twíty včetně těch odpovědních. Nelze bohužel zobrazovat celé konverzace, protože cizí twíty v archivu nejsou. Textová podoba archivu může vypadat nějak takto:

--- 2015-06-29 (Mon) 17:51 [4419] https://twitter.com/vitsoft/status/615548191421939712
Manželka neječí. Je prý to její normální hlas a mne zmátlo, že první měsíce po svatbě jen šeptala. 
#genderboj

--- 2015-06-30 (Tue) 10:56 [4420] https://twitter.com/vitsoft/status/615806000742797313
RT @selner84: Tsipras: "Zdravíme Českou republiku." https://pbs.twimg.com/media/CIu4mRiUsAARtdf.jpg

--- 2015-06-30 (Tue) 16:07 [4421] https://twitter.com/vitsoft/status/615884292237971457
Ale chlapci, vždyť my už ten Peroutkův článek hledáme sedmý rok! A to už, nezlobte se, 
to už přestává být vtipné. #Cimrman

--- 2015-07-01 (Wed) 08:59 [4422] https://twitter.com/vitsoft/status/616139003679612928
               in reply to https://twitter.com/mppoky/status/615983067157647360
@mppoky Výhodou vhodně zvolených nicků je, že pokud jim chceme něco sdělit, 
netřeba to psát, stačí mention. @lehni_kua

Chcete-li můj script použít, je potřeba mít na počítači instalován PHP a následující kód uložit pomocí schránky do souboru nazvaného např. "tweet2text.php". Před spuštěním v příkazovém okně pomocí "\cesta\k\php.exe" tweet2text.php je třeba upravit parametry v jeho záhlaví, především adresář, ve kterém je rozbalen vstupní soubor, a název výstupního textového souboru. Syntaxe PHP vyžaduje, aby zpětná lomítka \ byla v cestě k souborům zdvojena.
Parametr $OutEncoding="UTF-16//IGNORE" je vhodný k prohlížení výstupního textu Notepadem nebo PSPadem, aby se správně zobrazovala asijská písma a emoji. Lze ovšem nastavit i osmibitové ANSI kodování "CP1250". Přidání //IGNORE přikazuje ignorovat (vynechávat) chybně zakódované znaky z textu twítu. Na Linuxu by mělo fungovat $OutEncoding="UTF-8".

<?php  /* Script for conversion of your tweets from input file "tweets.js" to a plain-text output file.
Download and unzip the archive of your data from https://twitter.com/settings/account
Edit the following parameters in this script and execute it with PHP interpreter.        */ 

$System="https://twitter.com/";                       // URL of the server. 
$Account="vitsoft";                                   // Screen_name of the account owner. 
$InpFile="D:\\DAT\\BACKUP\\twitter\\data\\tweets.js"; // JSON-formated archive of tweets in UTF-8 encoding.
$OutFile="D:\\DAT\\BACKUP\\twitter\\tweets.txt";      // Plain-text output file in $OutEncoding.
$OutEncoding="UTF-16//IGNORE";                        // Encoding wanted in $OutFile (UTF-16 for MS Notepad).
$DateFormat="Y-m-d (D) H:i";

if (!file_exists($InpFile)) die(PHP_EOL."Input file \"$InpFile\" was not found.");
if (!$OutHandle=fopen($OutFile,"w")) die(PHP_EOL."Cannot write to the output file \"$OutFile\".");
echo PHP_EOL."Parsing input file \"$InpFile\" ...";
$FileHeader="window.YTD.tweets.part0 = "; // This header needs to be removed from "tweets.js".  
$InpArray=json_decode(substr(file_get_contents($InpFile),strlen($FileHeader)),true);                 
if (json_last_error()!=JSON_ERROR_NONE) die(PHP_EOL."Last JSON-error in \"tweets.js\": ".json_last_error_msg());
if (!$OutHandle=fopen($OutFile,"w")) die(PHP_EOL."Cannot write to the output file \"$OutFile\".");
fwrite($OutHandle,iconv("UTF-8",$OutEncoding,
"--- Plain-text tweets converted from \"$InpFile\" ".date($DateFormat).PHP_EOL));
$OutArray=array(); // Convert relevant data from $InpArray to $OutArray.
foreach ($InpArray as $TtNr => $Tweet)
{$OutArray[$TtNr]['Date']=date($DateFormat,strtotime($Tweet['tweet']['created_at']));
 $OutArray[$TtNr]['TtId']=$Tweet['tweet']['id_str'];
 if (array_key_exists('in_reply_to_status_id_str',$Tweet['tweet'])) // If $Tweet is a reply to another tweet.  
  {$OutArray[$TtNr]['ReplyToTtId']=$Tweet['tweet']['in_reply_to_status_id_str'];
   if (array_key_exists('in_reply_to_screen_name',$Tweet['tweet']))  
      $OutArray[$TtNr]['ReplyToName']=$Tweet['tweet']['in_reply_to_screen_name'];
   else $OutArray[$TtNr]['ReplyToName']=$Account; // In case of reply to deleted tweet, renamed account etc.  
  } $TweetText=$Tweet['tweet']['full_text']; // Replace displayed URL in $TweetText with the real expanded URL.
 if (array_key_exists('urls',$Tweet['tweet']['entities']))  
    foreach ($Tweet['tweet']['entities']['urls'] as $UA) 
      $TweetText=str_replace($UA['url'],$UA['expanded_url'],$TweetText);
 if (array_key_exists('media',$Tweet['tweet']['entities'])) 
    foreach ($Tweet['tweet']['entities']['media'] as $MA) 
      $TweetText=str_replace($MA['url'],$MA['media_url_https'],$TweetText);
 $OutArray[$TtNr]['Txt']=$TweetText; 
} // Important information from $InpArray has been moved to $OutArray.
function ByDate($a,$b) // Callback function for chronological sort of $OutArray. 
{if ($a['Date'] == $b['Date']) return 0; return ($a['Date'] < $b['Date'])?-1:1;} 
echo PHP_EOL."Sorting tweets by date ..."; usort($OutArray,"ByDate");
echo PHP_EOL."Writing output file \"$OutFile\" ...";
foreach ($OutArray as $TtNr => $Tweet) // Convertion of $OutArray to a plain text.
{$PlainTweet=PHP_EOL."--- ".$Tweet['Date']." [".$TtNr."] $System$Account/status/".$Tweet['TtId'];
if (array_key_exists('ReplyToTtId',$Tweet)) 
  $PlainTweet.=PHP_EOL."               in reply to $System".$Tweet['ReplyToName']."/status/".$Tweet['ReplyToTtId'];
$PlainTweet.=PHP_EOL.$Tweet['Txt'].PHP_EOL; 
fwrite($OutHandle,iconv("UTF-8",$OutEncoding,$PlainTweet));
} fclose($OutHandle); echo PHP_EOL."Twitter archive was converted to the output file in $OutEncoding encoding.";
?>

Podobný skript bude fungovat i na mastodonový archiv tootů, o který lze požádat na adrese
https://your-mastodon-system/settings/export a po rozbalení jej opět v JSON podobě nalezneme v souboru "outbox.json". Místo your-mastodon-system je samozřejmě třeba dosadit doménu serveru s naším účtem, např. mastodonczech.cz, cztwitter.cz apod.

<?php /* Script for conversion of your toots from input file "outbox.json" to a plain-text output file.
Download and unzip the archive of your Mastodon data from your-mastodon-system/settings/export.
Edit the following parameters in this script and execute it with PHP interpreter.        */

$System="https://mastodonczech.cz/system/";             // URL of path to the attached media files. 
$InpFile="D:\\DAT\\BACKUP\\mastodonczech\\outbox.json"; // JSON-formated archive of toots in UTF-8 encoding.
$OutFile="D:\\DAT\\BACKUP\\mastodonczech\\mastodonarchive.txt";  // Plain-text output file in $OutEncoding.
$OutEncoding="UTF-16//IGNORE";                          // Encoding wanted in $OutFile (UTF-16 for MS Notepad).
$DateFormat="Y-m-d (D) H:i";

if (!file_exists($InpFile)) die(PHP_EOL."Input file \"$InpFile\" was not found.");
echo PHP_EOL."Parsing input file \"$InpFile\" ...";
$InpArray=json_decode(file_get_contents($InpFile),true);             
if (json_last_error()<>JSON_ERROR_NONE) die(PHP_EOL."Last JSON-error in \"$InpFile\": ".json_last_error_msg());
if (!$OutHandle=fopen($OutFile,"w")) die(PHP_EOL."Cannot write to the output file \"$OutFile\".");
fwrite($OutHandle,iconv("UTF-8",$OutEncoding,
"--- Plain-text Mastodon toots converted from \"$InpFile\" ".date($DateFormat).PHP_EOL));
$OutArray=array(); // Convert relevant data from $InpArray to $OutArray.
foreach ($InpArray['orderedItems'] as $TtNr => $Toot)
{$Id=$Published=$InReplyTo=$Boosted=$Content="";$Attachments=array();
 if (is_array($Toot['object']))
 {$Id=$Toot['object']['id'];
  $Content=$Toot['object']['content'];
  $Published=$Toot['object']['published'];
  $InReplyTo=$Toot['object']['inReplyTo'];
  if (@is_array($Toot['object']['attachment'])) $Attachments=$Toot['object']['attachment'];
} else { $Id=$Toot['id'];$Boosted=$Toot['object'];$Published=$Toot['published'];}
$OutArray[$TtNr]['Date']=date($DateFormat,strtotime($Published));
$OutArray[$TtNr]['Id']=$Id;
if ($InReplyTo) $OutArray[$TtNr]['InReplyTo']=$InReplyTo;
if ($Boosted) $OutArray[$TtNr]['Boosted']=$Boosted;
if (count($Attachments)) $OutArray[$TtNr]['Attachments']=$Attachments;
$OutArray[$TtNr]['Txt']=str_replace(array('<p>','</p>','<br />'),array('',PHP_EOL,PHP_EOL),strip_tags($Content,'<p><br>')); 
} // Important information from $InpArray has been moved to $OutArray.
function ByDate($a,$b)  // Callback function for chronological sort of $OutArray. 
{if ($a['Date'] == $b['Date']) return 0; return ($a['Date'] < $b['Date'])?-1:1;} 
echo PHP_EOL."Sorting toots by date ..."; usort($OutArray,"ByDate");
echo PHP_EOL."Writing output file \"$OutFile\" ...";
foreach ($OutArray as $TtNr => $Toot) // Conversion of $OutArray to a plain text.
{$PlainToot=PHP_EOL."--- ".$Toot['Date']." [".$TtNr."] ".$Toot['Id'];
if (array_key_exists('InReplyTo',$Toot)) 
  $PlainToot.=PHP_EOL."               in reply to ".$Toot['InReplyTo'];
if (array_key_exists('Boosted',  $Toot)) 
  $PlainToot.=PHP_EOL."               boosted  ".$Toot['Boosted'];
if (array_key_exists('Attachments',$Toot))  
  foreach ($Toot['Attachments'] as $Attachment) 
    $PlainToot.=PHP_EOL."              attached ".$Attachment['mediaType']." ".
                $System.$Attachment['url']." ".$Attachment['name'];
if (array_key_exists('Txt',$Toot)) 
  $PlainToot.=PHP_EOL.$Toot['Txt'];    
fwrite($OutHandle,iconv("UTF-8",$OutEncoding,$PlainToot));
} fclose($OutHandle); echo PHP_EOL."Mastodon archive was converted to the output file in $OutEncoding encoding.";
?>