Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
<FilesMatch "\.gz$">
AddEncoding identity .gz
SetEnv no-gzip 1
</FilesMatch>
この設定により、.gzファイルは圧縮されたままクライアントに送信されるようになります。各ディレクティブの詳細は以下の通りです:<FilesMatch "\.gz$">: このディレクティブは、.gzで終わるファイル名にマッチします。AddEncoding identity .gz:
このディレクティブは、.gzファイルに対して"identity"エンコーディングを指定します。これにより、サーバーはファイルを変更せずにそのまま送信します。SetEnv no-gzip 1: このディレクティブは、"no-gzip"環境変数を設定します。これにより、サーバーは.gzファイルを圧縮せずに送信します。
createCategorizedIndexFromCSV()
/**
* CSVファイルからデータを読み込み、キーワードで分類してインデックスを作成する非同期関数。
* 処理中はローディングメッセージを表示し、完了後にキーワードリスト、ダウンロードボタン、未来の日付の記事数を表示する。
* エラーが発生した場合はエラーメッセージを表示する。
*/
async function createCategorizedIndexFromCSV() {
// ローディングメッセージの表示
displayLoadingMessage();
// CSVファイルからデータを非同期で読み込む
const rows = await fetchDataFromCSV('../data.csv');
// 今日の日付を取得し、時間をリセット
const today = resetTimeToMidnight(new Date());
// 今日の日付以前のデータのみをフィルタリング
const filteredRows = filterRowsByDate(rows, today);
// キーワードマップと未分類記事の初期化
let { keywordMap, uncategorizedArticles } = initializeKeywordMapping();
// 形態素解析器をビルドし、キーワードを抽出
keywordMap = await buildTokenizerAndExtractKeywords(filteredRows, keywordMap, uncategorizedArticles);
// キーワードマップから記事数が5未満のキーワードを削除
keywordMap = filterKeywordsByArticleCount(keywordMap, 5);
// 未分類の記事を「その他」カテゴリに追加
keywordMap = addUncategorizedArticlesToKeywordMap(keywordMap, uncategorizedArticles);
// 分類されたデータを設定
categorizedData = keywordMap;
// インデックスコンテナをクリア
clearIndexContainer();
// キーワードリストを作成し、イベントリスナーを追加
const keywordsList = createKeywordsList(keywordMap);
// インデックスコンテナにキーワードリストを追加
appendKeywordsListToIndexContainer(keywordsList);
// ダウンロードボタンを作成し、イベントリスナーを追加
const downloadButton = createDownloadButton(categorizedData);
// インデックスコンテナにダウンロードボタンを追加
appendDownloadButtonToIndexContainer(downloadButton);
// 未来の日付の記事数を計算し、表示
displayFutureArticlesCount(rows, filteredRows);
// ローディングメッセージを削除
removeLoadingMessage();
}
/**
* ローディングメッセージを表示する。
*/
function displayLoadingMessage() {
const loadingMessage = document.createElement('div');
loadingMessage.id = 'loading-message';
loadingMessage.textContent = 'データを読み込んでいます...';
loadingMessage.style.cssText = 'text-align: center; margin-top: 20px; font-family: "Yu Mincho", serif;';
document.getElementById('content-display').appendChild(loadingMessage);
}
/**
* CSVファイルからデータを非同期で読み込む。
* @param {string} filePath - CSVファイルのパス
* @returns {Promise<Array<Array<string>>>} - CSVデータの行の配列
*/
async function fetchDataFromCSV(filePath) {
const response = await fetch(filePath, { cache: 'no-cache' });
const data = await response.text();
return data.split('\n').map(row => row.split(',')).filter(row => row.length > 1 && row[0]);
}
/**
* 日付の時間を00:00:00にリセットする。
* @param {Date} date - リセットする日付
* @returns {Date} - 時間がリセットされた日付
*/
function resetTimeToMidnight(date) {
date.setHours(0, 0, 0, 0);
return date;
}
/**
* 今日の日付以前のデータのみをフィルタリングする。
* @param {Array<Array<string>>>} rows - CSVデータの行の配列
* @param {Date} today - 今日の日付
* @returns {Array<Array<string>>>} - フィルタリングされた行の配列
*/
function filterRowsByDate(rows, today) {
return rows.filter(row => {
const rowDate = new Date(row[2]);
return rowDate <= today;
});
}
/**
* キーワードマップと未分類記事を初期化する。
* @returns {{ keywordMap: {}, uncategorizedArticles: [] }} - キーワードマップと未分類記事のオブジェクト
*/
function initializeKeywordMapping() {
return { keywordMap: {}, uncategorizedArticles: [] };
}
/**
* 形態素解析器をビルドし、キーワードを抽出する。
* @param {Array<Array<string>>>} filteredRows - フィルタリングされた行の配列
* @param {{}} keywordMap - キーワードマップ
* @param {[]} uncategorizedArticles - 未分類記事の配列
* @returns {Promise<{}>} - 更新されたキーワードマップ
*/
async function buildTokenizerAndExtractKeywords(filteredRows, keywordMap, uncategorizedArticles) {
return new Promise((resolve) => {
kuromoji.builder({ dicPath: "https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/" }).build(function (err, tokenizer) {
filteredRows.forEach(row => {
const description = row[3];
const tokens = tokenizer.tokenize(description);
const keywords = tokens.filter(token => ['名詞', '形容詞', '動詞'].includes(token.pos))
.map(token => token.surface_form)
.filter(word => word.length > 1);
let keywordFound = false;
if (keywords.length > 0) {
for (let i = 0; i <= keywords.length - 2; i++) {
const combinedKeyword = keywords.slice(i, i + 2).join(' ');
if (!keywordMap[combinedKeyword]) {
keywordMap[combinedKeyword] = [];
}
keywordMap[combinedKeyword].push(row);
keywordFound = true;
}
}
if (!keywordFound) {
uncategorizedArticles.push(row);
}
});
resolve(keywordMap);
});
});
}
/**
* キーワードマップから記事数が指定数未満のキーワードを削除する。
* @param {{}} keywordMap - キーワードマップ
* @param {number} minCount - 最小記事数
* @returns {{}} - 更新されたキーワードマップ
*/
function filterKeywordsByArticleCount(keywordMap, minCount) {
for (const keyword in keywordMap) {
if (keywordMap[keyword].length < minCount) {
delete keywordMap[keyword];
}
}
return keywordMap;
}
/**
* 未分類の記事をキーワードマップに追加する。
* @param {{}} keywordMap - キーワードマップ
* @param {[]} uncategorizedArticles - 未分類記事の配列
* @returns {{}} - 更新されたキーワードマップ
*/
function addUncategorizedArticlesToKeywordMap(keywordMap, uncategorizedArticles) {
if (uncategorizedArticles.length > 0) {
keywordMap['その他'] = uncategorizedArticles;
}
return keywordMap;
}
/**
* インデックスコンテナをクリアする。
*/
function clearIndexContainer() {
const indexContainer = document.getElementById('content-display');
indexContainer.innerHTML = '';
}
/**
* キーワードリストを作成し、イベントリスナーを追加する。
* @param {{}} keywordMap - キーワードマップ
* @returns {HTMLElement} - キーワードリストのul要素
*/
function createKeywordsList(keywordMap) {
const keywordsList = document.createElement('ul');
keywordsList.id = 'keywords-list';
keywordsList.style.listStyle = 'none';
keywordsList.style.padding = '0';
keywordsList.style.marginBottom = '20px';
keywordsList.style.display = 'flex';
keywordsList.style.flexWrap = 'wrap';
keywordsList.style.gap = '10px';
for (const keyword in keywordMap) {
const count = keywordMap[keyword].length;
const keywordItem = document.createElement('li');
keywordItem.textContent = `${keyword} (${count})`;
keywordItem.style.backgroundColor = '#f0f0f0';
keywordItem.style.padding = '5px 10px';
keywordItem.style.borderRadius = '5px';
keywordItem.style.cursor = 'pointer';
keywordItem.style.fontFamily = "'Yu Mincho", serif";
keywordItem.style.fontWeight = 'bold';
keywordItem.addEventListener('click', () => {
displayArticlesByKeyword(keyword, keywordMap);
});
keywordsList.appendChild(keywordItem);
}
return keywordsList;
}
/**
* インデックスコンテナにキーワードリストを追加する。
* @param {HTMLElement} keywordsList - キーワードリストのul要素
*/
function appendKeywordsListToIndexContainer(keywordsList) {
const indexContainer = document.getElementById('content-display');
indexContainer.appendChild(keywordsList);
}
/**
* ダウンロードボタンを作成し、イベントリスナーを追加する。
* @param {{}} categorizedData - 分類されたデータ
* @returns {HTMLElement} - ダウンロードボタンのbutton要素
*/
function createDownloadButton(categorizedData) {
const downloadButton = document.createElement('button');
downloadButton.textContent = 'データをダウンロード';
downloadButton.style.cssText = 'margin-top: 20px; padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; font-family: "Yu Mincho", serif;';
downloadButton.addEventListener('click', () => {
downloadCategorizedData(categorizedData);
});
return downloadButton;
}
/**
* インデックスコンテナにダウンロードボタンを追加する。
* @param {HTMLElement} downloadButton - ダウンロードボタンのbutton要素
*/
function appendDownloadButtonToIndexContainer(downloadButton) {
const indexContainer = document.getElementById('content-display');
indexContainer.appendChild(downloadButton);
}
/**
* 未来の日付の記事数を計算し、表示する。
* @param {Array<Array<string>>>} rows - CSVデータの行の配列
* @param {Array<Array<string>>>} filteredRows - フィルタリングされた行の配列
*/
function displayFutureArticlesCount(rows, filteredRows) {
const hiddenArticlesCount = rows.length - filteredRows.length;
if (hiddenArticlesCount > 0) {
const hiddenArticlesMessage = document.createElement('p');
hiddenArticlesMessage.textContent = `※現在、未来の日付の記事が ${hiddenArticlesCount} 件あります。`;
hiddenArticlesMessage.style.marginTop = '10px';
hiddenArticlesMessage.style.fontFamily = "'Yu Mincho", serif";
const indexContainer = document.getElementById('content-display');
indexContainer.appendChild(hiddenArticlesMessage);
}
}
/**
* ローディングメッセージを削除する。
*/
function removeLoadingMessage() {
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage && loadingMessage.parentNode) {
loadingMessage.parentNode.removeChild(loadingMessage);
}
}
displayArticlesByKeyword(keyword, keywordMap)div要素)を準備します。
// 記事を表示するためのdiv要素を作成
articlesDiv = document.createElement('div');
articlesDiv.id = 'articles-container';
articlesDiv.classList.add('articles-div');
articlesDiv.style.cssText = 'margin: 10px 0 20px; border: 1px solid #ccc; border-radius: 8px; padding: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);';
keywordMapオブジェクトから、指定されたキーワードに関連する記事データ(行の配列)を取得します。
// displayArticlesByKeyword関数の引数としてkeywordMapが渡される
function displayArticlesByKeyword(keyword, keywordMap) {
// ...
// 重複する記事を除外
const uniqueRows = [...new Set(keywordMap[keyword])];
// ...
}
Setオブジェクトを使用して重複を除外します。
// 重複する記事を除外
const uniqueRows = [...new Set(keywordMap[keyword])];
h4要素で表示され、スタイル(フォント、太さ、余白、色、サイズ)が設定されます。ul要素で表示され、各記事はli要素で表示されます。li要素には、タイトル、リンク、公開日が含まれ、スタイル(余白、枠線、パディング)が設定されます。a要素で表示され、新しいタブで開くように設定されます。span要素でタイトルが表示されます。
// キーワードの記事の見出しを作成
const keywordTitle = document.createElement('h4');
keywordTitle.textContent = `「${keyword}」の記事`;
keywordTitle.style.cssText = 'font-family: "Yu Mincho", serif; font-weight: bold; margin: 0 0 5px; color: #333; font-size: 1em;';
articlesDiv.appendChild(keywordTitle);
// 記事を表示するためのul要素を作成
const ul = document.createElement('ul');
ul.style.cssText = 'list-style: none; padding: 0; font-family: "Yu Gothic", "Meiryo", sans-serif;';
// 各記事のli要素を作成し、ul要素に追加
uniqueRows.forEach(row => {
const li = document.createElement('li');
li.style.cssText = 'margin-bottom: 8px; border-bottom: 1px solid #eee; padding-bottom: 8px;';
// リンク要素またはスパン要素を作成
let linkElement = row[1] ? `<a href="${row[1]}" target="_blank" style="text-decoration: none; color: #0077cc; font-weight: bold; font-size: 1em;">${row[3]}</a>` : `<span style="font-weight: bold; font-size: 1em;">${row[3]}</span>`;
// li要素のHTMLを設定
li.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between; font-size: 0.9em;">
${linkElement}
<div style="color: #777; font-size: 0.8em;">${row[2]}</div>
</div>
`;
// エスケープ処理を追加
li.innerHTML = li.innerHTML.replace(/</g, '<').replace(/>/g, '>');
ul.appendChild(li);
});
// 記事のdiv要素にul要素を追加
articlesDiv.appendChild(ul);
p要素で表示され、赤色で表示されます。
} catch (error) {
console.error('エラー:', error);
const indexContainer = document.getElementById('content-display');
if (indexContainer) {
const errorMessage = document.createElement('p');
errorMessage.textContent = error.message || '記事の読み込みに失敗しました。';
errorMessage.style.color = 'red';
indexContainer.appendChild(errorMessage);
}
}