Spaces:
Sleeping
Sleeping
| # 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) | |