20201028のC#に関する記事は8件です。

[C#] 文字列の中のyyyyMMdd(20201016等)とtimezone(+0900等)を取り出してDateTimeに変換する

もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

やりたいこと

ファイル名に日付が入ってるログファイルがあって、それがあるフォルダにまとめて入れられている。
そういうログファイルたちの中から、ファイルの更新日付ではなく「ファイル名に付いてる日付」が一番新しいものの日付を取り出したい、ということがあった。

※下図のようなファイルがある場合は、「testlog_21201017+0900.log」の日付が一番新しいので、DateTimeの「2120/10/17」を取り出したい。
image.png

今回の場合は、ファイル名にyyyyMMddだけではなくタイムゾーンを示す+0900というのが入った形式だった。

調べた結果、日付の文字列(20201016など)を一旦DateTimeに直して、その値でどれが新しいか判断することにしたが、特にタイムゾーンのところをDateTimeに直すときに少しクセがあったのでメモを残しておく。

やったこと

下記のようなことをした。

  1. Directory.GetFiles()で拡張子を指定して、フォルダ内のログファイルをすべて列挙
  2. その中から、Linqと正規表現を使って日付の部分を取り出す
  3. 取り出した日付をDateTime.ParseExact()でDateTimeに変換
  4. その中の一番新しいものを取り出し

具体的な流れは、下のサンプルコードを参照だが、
3番のDateTime.ParseExact()をするところに少しクセがあった。

DateTime.ParseExact()のクセ

DateTime.ParseExact()は、書式を指定して、文字列からDateTimeに変換するものだが、
上の図にあったようなファイル名に含まれる、20201016+0900をDateTimeに変換する際、そのままの文字列と書式指定のyyyyMMddzzzDateTime.ParseExact()に渡してもうまくいかなかった。

対策は、
20201016+090020201016 +09:00に直したうえで、DateTime.ParseExact()に渡すということ。

// こういうイメージ
DateTime.ParseExact("20201016+0900", "yyyyMMdd zzz", null)

これで、タイムゾーンも込みで、DateTimeに変換することができた。

DateTime.ParseExact()のクセはそれほどでもなかった(20/10/31追記)

@htsignさんからコメント頂いた。

// こうではなく
DateTime.ParseExact("20201016+0900", "yyyyMMdd zzz", null)
// こうだとInsertとかしなくてもうまくいく様子
DateTime.ParseExact("20201016+0900", "yyyyMMddzzz", null) // ←「zzz」の前のスペースが余計だった

上記だと、特に特殊なことをしなくてもOKということが判明。ありがとうございます。
この記事の肝だと思ってた部分が崩れる感じでなんだかお恥ずかしいですが、シンプルにできることがわかってすっきりです。

実験コード

using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace ConsoleApp19
{
    class Program
    {
        static void Main(string[] args)
        {
            // フォルダ内のログファイルを列挙
            string targetDirPath = @"C:\Users\masa\Desktop\test";
            string targetExt = "*.log";
            string[] logs = Directory.GetFiles(targetDirPath, targetExt, SearchOption.TopDirectoryOnly);

            // ファイル名についてる「時間+タイムゾーン」の文字列(「20191011+0900」みたいなの=「yyyyMMddzzz」を取り出すための正規表現)
            string reg = "[0-9]{8}[+|-][0-9]{4}";
            string format = "yyyyMMdd zzz";

            var latestLogDate = logs.Where(x => Regex.Match(x, reg).Value != "")            // 指定の正規表現に一致するものに絞って、
                                    .Select(x => Regex.Match(x, reg))                       // 一致した部分の文字列だけ取り出し、
                                    .Select(x => x.Value.Insert(11, ":").Insert(8, " "))    // +0900の+09と00の間に「:」を挿入し、さらに日付とタイムゾーンの間に半角スペースを入れ、
                                    .Select(x => DateTime.ParseExact(x, format, null))      // ParseExactでDateTimeにパースして、
                                    .Max();                                                 // 一番新しい日付のものを取り出す
            Console.WriteLine("一番新しいログの日付は " + latestLogDate.ToString("yyyy/MM/dd") + " のログです。");
            Console.ReadLine();
        }
    }
}

※ファイル名から日付を取り出す正規表現は、改善の余地ありかも。

実験コードその②(20/10/31追記、クセ考慮部分の見直し)

ParseExact()のクセがそうでもないことが分かったので、その辺を考慮していた部分を外してすっきりさせ、かつLinqについてもコメント頂いていたのでその部分もちょっと手直ししたのが下記。だいぶわかりやすくなった。

static void Main(string[] args)
{
    // フォルダ内のログファイルを列挙
    string targetDirPath = @"C:\Users\masa\Desktop\test";
    string targetExt = "*.log";
    string[] logs = Directory.GetFiles(targetDirPath, targetExt, SearchOption.TopDirectoryOnly);

    // ファイル名についてる「時間+タイムゾーン」の文字列(「20191011+0900」みたいなの=「yyyyMMddzzz」を取り出すための正規表現)
    string reg = "[0-9]{8}[+|-][0-9]{4}";
    string format = "yyyyMMddzzz";

    var latestLogDate = logs.Select(x => Regex.Match(x, reg))                       // 一致した部分の文字列だけ取り出し、
                            .Where(x => x.Success)                                  // 一致するものがあったものだけに絞り、
                            .Select(x => DateTime.ParseExact(x.Value, format, null))// ParseExactでDateTimeにパースして、
                            .Max();                                                 // 一番新しい日付のものを取り出す
    Console.WriteLine("一番新しいログの日付は " + latestLogDate.ToString("yyyy/MM/dd") + " のログです。");
    Console.ReadLine();
}

参考

■標準の日時の書式指定文字列
https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/standard-date-and-time-format-strings

■カスタムの日時の書式指定文字列
ParseExactのフォーマット指定(yyyyMMdd zzzとか)に何を指定すればよいかがここで分かる。
https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/custom-date-and-time-format-strings
→これによると、zzzは標準の書式指定文字列ではなく、DateTimeOffset用のカスタム書式指定文字列らしい。
image.png

■標準の日時の書式指定文字列の日本語解説ページ
https://dotnet.programmer-reference.com/csharp-date-format/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

閉じる(×)ボタン無効化

×ボタンを押しても閉じられなくする方法

image02.png

MainWindow.xaml
<Window
    x:Class="SampleApp1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:SampleApp1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="200"
    Closing="Window_Closing"
    ResizeMode="CanResizeWithGrip"
    mc:Ignorable="d">
    <Grid>
        <Button
            Margin="20"
            Click="Button_Click"
            Content="終了" />
    </Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;

namespace SampleApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// 終了フラグ
        /// </summary>
        public bool IsTerminate { get; set; } = false;

        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Closeが呼び出された直後
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (IsTerminate)
            {
                // 終了
                return;
            }

            MessageBox.Show("終了できません。");
            // 終了処理をキャンセル
            e.Cancel = true;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            IsTerminate = true;
            Close();
        }
    }
}

×ボタンを無効化する方法

image03.png

MainWindow.xaml
<Window
    x:Class="SampleApp2.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:SampleApp2"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="200"
    ResizeMode="CanResizeWithGrip"
    SourceInitialized="Window_SourceInitialized"
    mc:Ignorable="d">
    <Grid>
        <Button
            Margin="20"
            Click="Button_Click"
            Content="終了" />
    </Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Runtime.InteropServices;
using System.Windows;

namespace SampleApp2
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// メニューのハンドル取得
        /// </summary>
        /// <param name="hWnd"></param>
        /// <param name="bRevert"></param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

        /// <summary>
        /// メニュー項目の削除
        /// </summary>
        /// <param name="hMenu"></param>
        /// <param name="uPosition"></param>
        /// <param name="uFlags"></param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        private static extern bool RemoveMenu(IntPtr hMenu, uint uPosition, uint uFlags);

        /// <summary>
        /// ウィンドウを閉じる
        /// </summary>
        private const int SC_CLOSE = 0xf060;

        /// <summary>
        /// uPositionに設定するのは項目のID
        /// </summary>
        private const int MF_BYCOMMAND = 0x0000;

        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 初期化時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_SourceInitialized(object sender, EventArgs e)
        {
            IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper((Window)sender).Handle;
            IntPtr hMenu = GetSystemMenu(hwnd, false);
            RemoveMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最大化ボタン無効化

image01.png

MainWindow.xaml
<Window
    x:Class="SampleApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:SampleApp"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="200"
    ResizeMode="CanResizeWithGrip"
    SourceInitialized="Window_SourceInitialized"
    mc:Ignorable="d">
    <Grid />
</Window>
MainWindow.xaml.cs
using System;
using System.Runtime.InteropServices;
using System.Windows;

namespace SampleApp
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// ウィンドウに関するデータを取得
        /// </summary>
        /// <param name="hWnd"></param>
        /// <param name="nIndex"></param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        private static extern int GetWindowLong(IntPtr hWnd, int nIndex);

        /// <summary>
        /// ウィンドウの属性を変更
        /// </summary>
        /// <param name="hWnd"></param>
        /// <param name="nIndex"></param>
        /// <param name="dwNewLong"></param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

        /// <summary>
        /// ウィンドウスタイル
        /// </summary>
        private const int GWL_STYLE = -16;

        /// <summary>
        /// 最大化ボタン
        /// </summary>
        private const int WS_MAXIMIZEBOX = 0x0001_0000; // C#7より前の場合は 0x00010000

        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 初期化時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_SourceInitialized(object sender, EventArgs e)
        {
            IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper((Window)sender).Handle;
            int value = GetWindowLong(hwnd, GWL_STYLE);
            SetWindowLong(hwnd, GWL_STYLE, (int)(value & ~WS_MAXIMIZEBOX));
        }
    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityからのWebSocket接続時にハンドシェイクでエラーが出た際の対処法

websocket-sharpを使ってWebSocketに接続

websocket-sharpはUnityで使えるWebSocket接続用のライブラリ。
基本的な使い方は以下の通りで、WebSocketインスタンスを作ってURLを引数として渡し、Connectメソッドで接続するだけと非常にお手軽。

WebSocketController.cs
using System;
using UnityEngine;
using WebSocketSharp;

public class WebSocketController : MonoBahaviour
{
    private WebSocket webSocket;

    // wssから始まるWebSocketの接続先URL。
    private string baseURL = "wss://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

    // WebSocketインスタンスを作成し、接続先URLを渡す。
    webSocket = new WebSocket(baseURL);

    // WebSocketへ接続。非同期で行いたい場合はwebSocket.ConnectAsync()を使う。
    webSocket.Connect();
}

An error has occurred during a TLS handshake.

接続をしようとすると上記のエラーメッセージが出た。
URLがwssから始まるセキュア通信用のプロトコルだったため、webSocketインスタンスをセキュア通信ができるように弄ってあげる必要があるみたい。
そこで以下のようにコードを修正。

WebSocketController.cs
using System;
using UnityEngine;
using WebSocketSharp;

public class WebSocketController : MonoBahaviour
{
    private WebSocket webSocket;

    private string baseURL = "wss://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

    webSocket = new WebSocket(baseURL);

    // この一文を追加する。
    webSocket.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12;

    webSocket.Connect();
}

webSocket.SslConfigurationはセキュアな通信を確立するために使われるクラスで、その中のEnabledSslProtocolsというプロパティが認証用のプロトコル(SslProtocols 列挙型)を格納するプロパティとなっている。

そこへSslProtocolsの「Tls12」をセットしてあげることで、セキュアな通信ができるようになる。

あとは通常時と同じくConnectメソッドで接続してあげればエラーは解消された。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webサーバー 'IIS Express' に接続できませんでした。が出たときの対応

結論

  1. Visual Studioを閉じる
  2. ソリューションフォルダ内の.vsフォルダを削除する
  3. Visual Studioを起動してデバッグ実行する

理由とか

.vsの中にある config/applicationhost.config のポートの整合性が合わなくなっているのか…詳しいことはあまり調べていませんが。
一度削除して再度起動すればフォルダごと再作成されるので、デバッグはできるようになります。

参考

https://developercommunity.visualstudio.com/content/problem/720271/unable-to-start-iisexpress.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

is演算子のキャストと早期returnの組み合わせの際の注意点

この記事で述べていること

  • is演算子でのキャストは結果がtrueになる際のみ行われます。だが、結果に関係なくキャスト後の変数の宣言はなされます(falseの場合中身は未初期化になる)
  • また、ifステートメントの条件式内でis演算子を用いてキャストすると、キャスト後の変数のスコープはそのifを囲むブロック内となります。
  • この性質のせいで、早期returnで弾くついでにキャストし、キャスト後の変数をその後用いようとすると怒られます。
  • そういう時はasとnullチェックを使いましょう。

説明

is演算子を使ってifステートメントの条件式内でキャストを行った場合、キャスト後の変数のスコープはそのifを囲むブロック内となります。

また、is演算子でのキャストはインスタンスの型が一致した場合のみ行われます。一致しない場合変数は宣言されますが、中身は未初期化になります(nullが入るわけでもありません)

この性質のせいで、早期returnでインスタンスの型が一致しない場合を弾いた後、キャスト後の変数を後々の処理に用いようとするとちょっと問題が生じます。

インスタンスの型チェックと早期return
public void hoge(Object obj){
    //こうしてobjのインスタンスの型が任意の型でない場合を弾いた後
    if((obj is int x) == false) return;

    //後々の処理にxを使おうとすると
    //未初期化の可能性があると言われ怒られる
    Console.WriteLine(x);

    //同名変数の再宣言も不可で怒られる
    int x = 1;
}

コンパイラから見たらintじゃない場合最初のifで弾かれてるからifの外で使っても大丈夫なんてわからないので、怒られます。

かといって変数が死んでいるわけではないので、同名変数の再宣言も不可です。怒られます。

考えてみれば当たり前の話で馬鹿らしいですが、私はこれで数十分無駄にしました。

解決策

ではどうするか。as演算子とnullチェックを組み合わせましょう。

as演算子との組み合わせ
public void hoge(Object obj){
    int x = obj as int;
    if(x == null) return;

    //怒られずにnullチェック済みのxを使って処理が可能
    Console.WriteLine(x);
}

これで解決ですね。

最後に

マサカリお待ちしてます。

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cscの作法 その42

概要

cscの作法、調べてみた。
dllinjectやってみた。
pidを指定するタイプやってみた。

サンプルコード

using System;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;

class Test {
    [Flags]
    enum AllocationType {
        MEM_COMMIT = 0x1000,
        MEM_RESERVE = 0x2000,
    }
    [Flags]
    enum MemoryProtection {
        PAGE_EXECUTE_READWRITE = 0x40,
    }
    [Flags]
    enum FreeType {
        MEM_RELEASE = 0x8000,
    }
    [Flags]
    enum DesiredAccess {
        PROCESS_ALL_ACCESS = 0x1fffff,
        PROCESS_QUERY_INFORMATION = 0x400,
        PROCESS_CREATE_THREAD = 0x2,
        PROCESS_VM_OPERATION = 0x8,
        PROCESS_VM_WRITE = 0x20,
    }
    [DllImport("Kernel32.dll")]
    extern static IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
    [DllImport("Kernel32.dll")]
    extern static bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, FreeType dwFreeType);
    [DllImport("Kernel32.dll")]
    extern static IntPtr OpenProcess(DesiredAccess dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
    [DllImport("Kernel32.dll")]
    extern static bool CloseHandle(IntPtr handle);
    [DllImport("kernel32.dll")]
    extern static bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, String lpBuffer, uint nSize, IntPtr lpNumberOfBytesWritten);
    [DllImport("kernel32.dll")]
    extern static IntPtr LoadLibrary(String lpFileName);
    [DllImport("kernel32.dll")]
    extern static IntPtr GetProcAddress(IntPtr hModule, String lpProcName);
    [DllImport("kernel32.dll")]
    extern static IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
    static void Main(string[] args) {
        IntPtr tp,
            kh,
            dr,
            la,
            retval;
        int pid = Convert.ToInt32(args[0]);
        String dllpath = "c:\\ore\\c\\spy.dll";
        uint dllpathLength = (uint) Encoding.GetEncoding("UTF-8").GetByteCount(dllpath);
        tp = OpenProcess(DesiredAccess.PROCESS_ALL_ACCESS, false, (uint) pid);
        dr = VirtualAllocEx(tp, (IntPtr) null, dllpathLength + 1, AllocationType.MEM_COMMIT, MemoryProtection.PAGE_EXECUTE_READWRITE);
        WriteProcessMemory(tp, dr, dllpath, dllpathLength + 1, (IntPtr) null);
        kh = LoadLibrary("kernel32.dll");
        la = GetProcAddress(kh, "LoadLibraryA");
        retval = CreateRemoteThread(tp, (IntPtr) null, 0, la, dr, 0, (IntPtr) null);
        VirtualFreeEx(tp, dr, (uint) (dllpath.Length + 1), FreeType.MEM_RELEASE);
        CloseHandle(tp);
    }
}





以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PdfSharpでPDFを結合する

はじめに

PDFを結合するプログラムを日本語で調べると、出てくる多くはiTextSharpを使う方法です。
これでもいいのですが、無料版のライセンスがAPGLであり商用ソフトを作る場合には支障をきたします。
そこで今回は、MITライセンスのpdfSharpを用いてPDFを結合してみます。

※ライセンスの違いは以下の記事を参照
 「オープンソースライセンス、どれなら使っても良いの??」@fate_shelled
 https://qiita.com/fate_shelled/items/a928709d7610cee5aa66

準備


まずはC#のプロジェクトをvisual studioで作成します。
次にそのプロジェクトにPdfSharpをインストールします。

 1.プロジェクト > NuGet パッケージの管理を開く
 2.検索欄に"pdfSharp"を入力する
 3.右側"インストール"ボタンをクリックする

コード実装

いよいよコードを実装します。
まずはライブラリをコードにインポートします。

using PdfSharp.Pdf;
using PdfSharp.Pdf.IO;

つついて実際に処理を実装します。
1.結合後PDFのオブジェクトを作成

// PDFオブジェクト作成
PdfDocument document = new PdfDocument()

2.結合するPDFを開く

// file:結合するファイルのパス
// PdfDocumentOpenMode.Import:PDFを読み取りモードで開く
// 結合するPDFオブジェクトを作成
PdfDocument inputDocument = PdfReader.Open(file, PdfDocumentOpenMode.Import)

3.結合するPDFの全ページを結合後PDFに追加する

// 頁全件ループ
foreach (PdfPage page in inputDocument.Pages)
{
    // PDF頁を追加
    document.AddPage(page);
}
// 結合するPDFを閉じる
inputDocument.Close();

4.2,3を結合するPDFの数だけ繰り返す
5.PDFを保存し閉じる

// PDF保存
document.Save(selectedPath);
// PDFを閉じる
document.Close();

このようにして実際に作成したコードは以下の中の関数"pdfMerge"です。
https://github.com/HagiAyato/PDFmerger/blob/main/PDFmerger/IOpg.cs

以下補足
・結合するファイルのパスはListにまとめておき、foreachを回して上記処理2~4を繰り替えすのがおすすめです。
・PDFオブジェクト作成時はusing句を使うことで、確実に処理終了時の解放ができます。
・例外処理(try-catch等)もお忘れなく。

ライセンス表記


PdfSharpはMITライセンスのライブラリで、使用する際はライセンスの表記が必要です。
ソースコード中やreadmeなどにライセンス表記を入れましょう。
/*
 * PdfSharpライセンス表記
 * Creator of PDFsharp is empira Software GmbH
 * Kirchstrase 19 53840 Troisdorf Germany
 * http://www.empira.de
 * PDFsharp (R) is a registered trademark of empira Software GmbH
 * Released under the MIT license
 * http://www.pdfsharp.net/PDFsharp_License.ashx?AspxAutoDetectCookieSupport=1
 */
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む