# LINE履歴TXT変換アプリ # LINEからエクスポートした.txtファイルを直接読み込み、構造化されたCSVに変換します # # 必要なパッケージ: # install.packages(c("shiny", "bslib", "tidyverse", "DT", "plotly", "shinyWidgets", "shinycssloaders")) library(shiny) library(bslib) library(tidyverse) library(DT) library(plotly) library(shinyWidgets) library(shinycssloaders) # --- カスタムCSS --- custom_css <- " .navbar { background-color: #06C755 !important; } .navbar-brand, .main-title { color: #ffffff !important; font-weight: bold; } .navbar-nav .nav-link { color: #ffffff !important; opacity: 0.8; } .navbar-nav .nav-link.active { color: #ffffff !important; opacity: 1; border-bottom: 3px solid #ffffff !important; } .card { border-radius: 15px; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .btn-primary { background-color: #06C755; border: none; } .btn-primary:hover { background-color: #05a346; } " # --- LINE TXTパース関数 --- parse_line_txt <- function(file_path) { # バイナリで読み込み raw_bytes <- readBin(file_path, "raw", file.info(file_path)$size) content <- rawToChar(raw_bytes) # BOM除去 content <- sub("^\ufeff", "", content) # 改行を統一(CRLF → LF) content <- gsub("\r\n", "\n", content) content <- gsub("\r", "\n", content) lines <- strsplit(content, "\n", fixed = TRUE)[[1]] records <- list() current_thread <- "" current_save_date <- "" current_date <- "" i <- 1 while (i <= length(lines)) { line <- lines[i] # トーク履歴ヘッダー [LINE] XXXとのトーク履歴 if (grepl("^\\[LINE\\].*とのトーク履歴", line)) { current_thread <- trimws(line) i <- i + 1 next } # 保存日時行 if (grepl("^保存日時", line)) { current_save_date <- trimws(line) i <- i + 1 next } # 空行をスキップ if (trimws(line) == "") { i <- i + 1 next } # 日付行 (例: 2025/11/10(月)) if (grepl("^\\d{4}/\\d{2}/\\d{2}\\(", line)) { current_date <- trimws(line) i <- i + 1 next } # メッセージ行: HH:MM\t送信者\tメッセージ(タブ区切り) if (grepl("^\\d{2}:\\d{2}\t", line) && nchar(current_date) > 0) { # タブで分割 parts <- strsplit(line, "\t", fixed = TRUE)[[1]] if (length(parts) >= 3) { time_val <- parts[1] sender <- parts[2] message <- paste(parts[3:length(parts)], collapse = "\t") # 全角スペースを半角に sender <- gsub("\u3000", " ", sender) # 複数行メッセージの処理(引用符で始まる場合) if (startsWith(message, '"')) { message <- substring(message, 2) # 開始引用符を除去 # 終了引用符を探す while (i < length(lines)) { msg_trimmed <- trimws(message) if (nchar(msg_trimmed) > 0 && endsWith(msg_trimmed, '"')) { break } i <- i + 1 # 改行を保持して結合 message <- paste0(message, "\n", lines[i]) } # 終了引用符を除去 message <- trimws(message) if (nchar(message) > 0 && endsWith(message, '"')) { message <- substr(message, 1, nchar(message) - 1) } } # 空でない送信者のみ追加 if (nchar(trimws(sender)) > 0) { records[[length(records) + 1]] <- data.frame( トーク履歴 = current_thread, 保存日時 = current_save_date, 日 = current_date, 時間 = time_val, 送信者 = trimws(sender), 内容 = message, stringsAsFactors = FALSE ) } } } i <- i + 1 } if (length(records) == 0) { return(data.frame( トーク履歴 = character(), 保存日時 = character(), 日 = character(), 時間 = character(), 送信者 = character(), 内容 = character(), stringsAsFactors = FALSE )) } bind_rows(records) } # --- UI --- ui <- page_navbar( title = span("LINE トーク履歴解析", class = "main-title"), theme = bs_theme(version = 5, bootswatch = "flatly", primary = "#06C755"), header = tags$head(tags$style(custom_css)), sidebar = sidebar( title = "操作パネル", fileInput("file1", "LINE履歴をアップロード", accept = c(".txt", ".csv")), helpText("※ LINEからエクスポートした.txtファイルを直接アップロードできます"), hr(), uiOutput("filter_ui"), hr(), downloadButton("downloadData", "CSVでエクスポート", class = "btn-primary w-100") ), nav_panel("📊 ダッシュボード", layout_column_wrap( width = 1/3, value_box(title = "総メッセージ数", value = textOutput("total_msg"), showcase = icon("comment-dots")), value_box(title = "アクティブ人数", value = textOutput("total_members"), showcase = icon("users")), value_box(title = "最多発言者", value = textOutput("top_sender"), showcase = icon("award")) ), layout_column_wrap( width = 1/2, card(card_header("発言者別の割合"), withSpinner(plotlyOutput("piePlot"))), card(card_header("時系列のアクティビティ"), withSpinner(plotlyOutput("timePlot"))) ) ), nav_panel("💬 トーク閲覧", card(full_screen = TRUE, card_header("メッセージ履歴"), DTOutput("table")) ), nav_panel("ℹ️ 使い方", card(markdown(" ### LINE 履歴解析ツール #### 使い方 1. LINEアプリから「トーク履歴を送信」で.txtファイルをエクスポート 2. .txtファイルをそのままアップロード 3. **ダッシュボード**タブで統計を確認 4. **トーク閲覧**タブでメッセージを検索・閲覧 5. 「CSVでエクスポート」で構造化されたCSVを出力 #### 出力CSVフォーマット - トーク履歴: [LINE] XXXとのトーク履歴 - 保存日時: 保存日時:YYYY/MM/DD HH:MM - 日: YYYY/MM/DD(曜日) - 時間: HH:MM - 送信者: 送信者名 - 内容: メッセージ内容(改行保持) ")) ) ) # --- Server --- server <- function(input, output, session) { # ファイルパース parsed_df <- reactive({ req(input$file1) parse_line_txt(input$file1$datapath) }) # 表示用データ(改行を除去) display_df <- reactive({ df <- parsed_df() if (nrow(df) > 0) { df %>% mutate(内容 = gsub("[\r\n]+", " ", 内容)) } else { df } }) # 動的フィルター output$filter_ui <- renderUI({ df <- parsed_df() req(nrow(df) > 0) tagList( pickerInput("thread_sel", "スレッド選択:", choices = unique(df$トーク履歴), multiple = TRUE, selected = unique(df$トーク履歴), options = list(`actions-box` = TRUE)), pickerInput("sender_sel", "送信者選択:", choices = unique(df$送信者), multiple = TRUE, selected = unique(df$送信者), options = list(`actions-box` = TRUE)), searchInput("keyword", "本文検索:", placeholder = "キーワード入力...", btnSearch = icon("search"), btnReset = icon("times")) ) }) # フィルタリング後のデータ filtered_df <- reactive({ df <- parsed_df() if (!is.null(input$thread_sel) && length(input$thread_sel) > 0) { df <- df %>% filter(トーク履歴 %in% input$thread_sel) } if (!is.null(input$sender_sel) && length(input$sender_sel) > 0) { df <- df %>% filter(送信者 %in% input$sender_sel) } if (!is.null(input$keyword) && input$keyword != "") { df <- df %>% filter(grepl(input$keyword, 内容, ignore.case = TRUE)) } df }) # 表示用フィルタリングデータ filtered_display_df <- reactive({ df <- filtered_df() if (nrow(df) > 0) { df %>% mutate(内容 = gsub("[\r\n]+", " ", 内容)) } else { df } }) # メトリクス output$total_msg <- renderText({ nrow(filtered_df()) }) output$total_members <- renderText({ n_distinct(filtered_df()$送信者) }) output$top_sender <- renderText({ df <- filtered_df() if (nrow(df) == 0) return("なし") res <- df %>% count(送信者) %>% slice_max(n, n = 1) %>% pull(送信者) if(length(res) > 0) res[1] else "なし" }) # グラフ output$piePlot <- renderPlotly({ df <- filtered_df() %>% count(送信者) req(nrow(df) > 0) plot_ly(df, labels = ~送信者, values = ~n, type = 'pie', hole = 0.4) %>% layout(showlegend = TRUE) }) output$timePlot <- renderPlotly({ df <- filtered_df() %>% mutate(Date = sub("\\(.*\\)$", "", 日)) %>% count(Date) req(nrow(df) > 0) p <- ggplot(df, aes(x = as.Date(Date, format = "%Y/%m/%d"), y = n)) + geom_line(color = "#06C755", linewidth = 1) + geom_point(color = "#06C755") + theme_minimal() + labs(x = "", y = "投稿数") ggplotly(p) }) # テーブル(表示用は改行除去) output$table <- renderDT({ datatable(filtered_display_df(), options = list(pageLength = 15, autoWidth = TRUE), rownames = FALSE) }) # CSVエクスポート(元の改行を保持) output$downloadData <- downloadHandler( filename = function() { paste0("LineDataBaseOutput_", format(Sys.Date(), "%Y%m%d"), ".csv") }, content = function(file) { df <- filtered_df() # UTF-8 BOM付きで出力 con <- file(file, "wb") writeBin(charToRaw("\ufeff"), con) close(con) write.table(df, file, sep = ",", row.names = FALSE, append = TRUE, fileEncoding = "UTF-8", quote = TRUE, na = "") } ) } shinyApp(ui, server)