sugitora's picture
Update app.R
d458c49 verified
# 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)