[PHP]rawurldecode関数の使い方を徹底解説!URLデコード処理をマスター

PHP

PHPでURL処理を行う際、エンコードされたURLを元の文字列に戻す必要がよくあります。API通信、ファイル名の処理、パラメータの取得など、様々な場面でURLデコードが必要です。この記事では、PHPのrawurldecode関数について、基本的な使い方から実践的な活用方法まで詳しく解説していきます。

rawurldecode関数とは?

rawurldecodeは、RFC 3986に準拠したURLエンコードされた文字列をデコードする関数です。%XX形式でエンコードされた文字を元の文字に戻します。

基本的な構文

string rawurldecode(string $string)

パラメータ:

  • $string: デコードする文字列

戻り値:

  • デコードされた文字列

エンコード方式:

  • %XX – 16進数2桁でエンコードされた文字
  • 英数字とハイフン、ピリオド、アンダースコア、チルダ(-._~)はそのまま
  • スペースは%20でエンコード(+ではない)

urldecodeとの違い

比較表

項目rawurldecode()urldecode()
RFC準拠RFC 3986RFC 1738
スペース%20%20 または +
用途パス、ファイル名クエリパラメータ
推奨度より新しい標準古い標準

実際の違い

<?php
// スペースの扱い
$encoded1 = "hello%20world";
$encoded2 = "hello+world";

echo rawurldecode($encoded1);  // 出力: hello world
echo rawurldecode($encoded2);  // 出力: hello+world (プラスはそのまま)

echo urldecode($encoded1);     // 出力: hello world
echo urldecode($encoded2);     // 出力: hello world (プラスがスペースに)

// 対応する関数
$text = "hello world";
echo rawurlencode($text);      // 出力: hello%20world
echo urlencode($text);         // 出力: hello+world
?>

基本的な使い方

シンプルなデコード

<?php
// 基本的なデコード
$encoded = "Hello%20World";
$decoded = rawurldecode($encoded);
echo $decoded;  // 出力: Hello World

// 日本語のデコード
$encoded = "%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF";
$decoded = rawurldecode($encoded);
echo $decoded;  // 出力: こんにちは

// 特殊文字のデコード
$encoded = "name%3DJohn%26age%3D30";
$decoded = rawurldecode($encoded);
echo $decoded;  // 出力: name=John&age=30
?>

URLパスのデコード

<?php
// ファイル名のデコード
$encoded = "My%20Document%20%282024%29.pdf";
$filename = rawurldecode($encoded);
echo $filename;  // 出力: My Document (2024).pdf

// パス全体のデコード
$encoded = "/path/to/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.txt";
$path = rawurldecode($encoded);
echo $path;  // 出力: /path/to/ファイル.txt

// URLの各部分をデコード
$url = "https://example.com/search?q=PHP%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0";
$parts = parse_url($url);
$query = rawurldecode($parts['query']);
echo $query;  // 出力: q=PHPプログラミング
?>

実践的な使用例

1. URL解析・パース処理

<?php
/**
 * URL解析クラス
 */
class UrlParser {
    
    /**
     * URLをパースしてデコード
     */
    public static function parse($url) {
        $parts = parse_url($url);
        
        if (!$parts) {
            return null;
        }
        
        $result = [
            'scheme' => $parts['scheme'] ?? '',
            'host' => $parts['host'] ?? '',
            'port' => $parts['port'] ?? null,
            'path' => isset($parts['path']) ? rawurldecode($parts['path']) : '',
            'query' => [],
            'fragment' => isset($parts['fragment']) ? rawurldecode($parts['fragment']) : ''
        ];
        
        // クエリパラメータをパース
        if (isset($parts['query'])) {
            parse_str($parts['query'], $result['query']);
            
            // 各値をデコード
            foreach ($result['query'] as $key => $value) {
                $result['query'][$key] = rawurldecode($value);
            }
        }
        
        return $result;
    }
    
    /**
     * パス部分を配列に分解
     */
    public static function getPathSegments($url) {
        $parts = parse_url($url);
        
        if (!isset($parts['path'])) {
            return [];
        }
        
        $path = rawurldecode($parts['path']);
        $segments = array_filter(explode('/', $path));
        
        return array_values($segments);
    }
    
    /**
     * ファイル名を取得
     */
    public static function getFilename($url) {
        $parts = parse_url($url);
        
        if (!isset($parts['path'])) {
            return null;
        }
        
        $path = rawurldecode($parts['path']);
        return basename($path);
    }
    
    /**
     * クエリパラメータを取得
     */
    public static function getQueryParams($url) {
        $parts = parse_url($url);
        
        if (!isset($parts['query'])) {
            return [];
        }
        
        parse_str($parts['query'], $params);
        
        // デコード
        foreach ($params as $key => $value) {
            $params[$key] = is_array($value) 
                ? array_map('rawurldecode', $value)
                : rawurldecode($value);
        }
        
        return $params;
    }
}

// 使用例
echo "=== URL解析 ===\n";

$url = "https://example.com/path/to/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.pdf?name=John%20Doe&age=30#section1";

$parsed = UrlParser::parse($url);
echo "スキーム: {$parsed['scheme']}\n";
echo "ホスト: {$parsed['host']}\n";
echo "パス: {$parsed['path']}\n";
echo "クエリ: " . print_r($parsed['query'], true);
echo "フラグメント: {$parsed['fragment']}\n";

$segments = UrlParser::getPathSegments($url);
echo "\nパスセグメント: " . implode(' > ', $segments) . "\n";

$filename = UrlParser::getFilename($url);
echo "ファイル名: {$filename}\n";

$params = UrlParser::getQueryParams($url);
echo "パラメータ: " . print_r($params, true);
?>

2. ファイルアップロード処理

<?php
/**
 * ファイルアップロードハンドラー
 */
class UploadHandler {
    private $uploadDir;
    
    public function __construct($uploadDir) {
        $this->uploadDir = rtrim($uploadDir, '/');
    }
    
    /**
     * エンコードされたファイル名を処理
     */
    public function handleEncodedFilename($encodedName) {
        // デコード
        $filename = rawurldecode($encodedName);
        
        // サニタイズ(セキュリティ対策)
        $filename = $this->sanitizeFilename($filename);
        
        return $filename;
    }
    
    /**
     * ファイル名をサニタイズ
     */
    private function sanitizeFilename($filename) {
        // 危険な文字を削除
        $filename = preg_replace('/[^\w\s\-\.\_\(\)()]/u', '', $filename);
        
        // 連続するスペースを1つに
        $filename = preg_replace('/\s+/', ' ', $filename);
        
        // 先頭と末尾の空白を削除
        $filename = trim($filename);
        
        return $filename;
    }
    
    /**
     * ユニークなファイル名を生成
     */
    public function generateUniqueFilename($originalName) {
        $decoded = rawurldecode($originalName);
        $pathInfo = pathinfo($decoded);
        
        $basename = $pathInfo['filename'];
        $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
        
        $basename = $this->sanitizeFilename($basename);
        
        // タイムスタンプを追加
        $uniqueName = $basename . '_' . time() . $extension;
        
        return $uniqueName;
    }
    
    /**
     * ファイルパスを取得
     */
    public function getFilePath($filename) {
        $decoded = rawurldecode($filename);
        $safe = $this->sanitizeFilename($decoded);
        
        return $this->uploadDir . '/' . $safe;
    }
}

// 使用例
echo "\n=== ファイルアップロード ===\n";

$handler = new UploadHandler('/var/www/uploads');

$encodedName = "My%20Document%20%282024%29.pdf";
$decoded = $handler->handleEncodedFilename($encodedName);
echo "デコード後: {$decoded}\n";

$uniqueName = $handler->generateUniqueFilename($encodedName);
echo "ユニーク名: {$uniqueName}\n";

$filePath = $handler->getFilePath($encodedName);
echo "ファイルパス: {$filePath}\n";
?>

3. APIレスポンス処理

<?php
/**
 * APIレスポンスデコーダー
 */
class ApiResponseDecoder {
    
    /**
     * エンコードされたJSONレスポンスをデコード
     */
    public static function decodeJsonResponse($encodedJson) {
        $decoded = rawurldecode($encodedJson);
        return json_decode($decoded, true);
    }
    
    /**
     * レスポンスヘッダーをデコード
     */
    public static function decodeHeaders($headers) {
        $decoded = [];
        
        foreach ($headers as $key => $value) {
            $decodedKey = rawurldecode($key);
            $decodedValue = is_array($value)
                ? array_map('rawurldecode', $value)
                : rawurldecode($value);
            
            $decoded[$decodedKey] = $decodedValue;
        }
        
        return $decoded;
    }
    
    /**
     * Content-Dispositionヘッダーからファイル名を取得
     */
    public static function extractFilename($contentDisposition) {
        // Content-Disposition: attachment; filename*=UTF-8''%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.pdf
        
        if (preg_match("/filename\*=UTF-8''(.+)/i", $contentDisposition, $matches)) {
            return rawurldecode($matches[1]);
        }
        
        if (preg_match('/filename="(.+)"/i', $contentDisposition, $matches)) {
            return rawurldecode($matches[1]);
        }
        
        return null;
    }
    
    /**
     * XMLレスポンスのエンコードされた値をデコード
     */
    public static function decodeXmlValues($xml) {
        if (!$xml instanceof SimpleXMLElement) {
            $xml = simplexml_load_string($xml);
        }
        
        $array = json_decode(json_encode($xml), true);
        
        return self::decodeArrayValues($array);
    }
    
    /**
     * 配列の値を再帰的にデコード
     */
    private static function decodeArrayValues($array) {
        foreach ($array as $key => $value) {
            if (is_array($value)) {
                $array[$key] = self::decodeArrayValues($value);
            } elseif (is_string($value)) {
                $array[$key] = rawurldecode($value);
            }
        }
        
        return $array;
    }
}

// 使用例
echo "\n=== APIレスポンス処理 ===\n";

$encodedJson = '%7B%22name%22%3A%22John%20Doe%22%2C%22age%22%3A30%7D';
$data = ApiResponseDecoder::decodeJsonResponse($encodedJson);
echo "JSONデコード: " . print_r($data, true);

$contentDisposition = "attachment; filename*=UTF-8''%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88.pdf";
$filename = ApiResponseDecoder::extractFilename($contentDisposition);
echo "ファイル名抽出: {$filename}\n";

$headers = [
    'X-Custom-Header' => 'Value%20with%20spaces',
    'Content-Type' => 'application%2Fjson'
];
$decoded = ApiResponseDecoder::decodeHeaders($headers);
echo "ヘッダーデコード: " . print_r($decoded, true);
?>

4. ルーティング処理

<?php
/**
 * ルーターシステム
 */
class Router {
    private $routes = [];
    
    /**
     * ルートを追加
     */
    public function add($path, $handler) {
        $this->routes[$path] = $handler;
    }
    
    /**
     * リクエストパスをマッチング
     */
    public function match($requestPath) {
        // パスをデコード
        $decodedPath = rawurldecode($requestPath);
        
        // 完全一致を確認
        if (isset($this->routes[$decodedPath])) {
            return [
                'handler' => $this->routes[$decodedPath],
                'params' => []
            ];
        }
        
        // パラメータ付きルートをチェック
        foreach ($this->routes as $route => $handler) {
            $params = $this->matchRoute($route, $decodedPath);
            
            if ($params !== false) {
                return [
                    'handler' => $handler,
                    'params' => $params
                ];
            }
        }
        
        return null;
    }
    
    /**
     * ルートパターンをマッチング
     */
    private function matchRoute($pattern, $path) {
        // {param}形式のパラメータを抽出
        $pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[^/]+)', $pattern);
        $pattern = '#^' . $pattern . '$#';
        
        if (preg_match($pattern, $path, $matches)) {
            // 数値インデックスを削除
            return array_filter($matches, function($key) {
                return !is_numeric($key);
            }, ARRAY_FILTER_USE_KEY);
        }
        
        return false;
    }
    
    /**
     * リクエストを処理
     */
    public function dispatch($requestPath) {
        $match = $this->match($requestPath);
        
        if (!$match) {
            return ['error' => 'Route not found', 'code' => 404];
        }
        
        // パラメータをデコード
        $params = array_map('rawurldecode', $match['params']);
        
        return call_user_func($match['handler'], $params);
    }
}

// 使用例
echo "\n=== ルーティング ===\n";

$router = new Router();

$router->add('/users/{id}', function($params) {
    return "ユーザーID: {$params['id']}";
});

$router->add('/products/{category}/{id}', function($params) {
    return "カテゴリ: {$params['category']}, 商品ID: {$params['id']}";
});

$router->add('/search', function($params) {
    return "検索";
});

// エンコードされたパスをディスパッチ
$result1 = $router->dispatch('/users/123');
echo $result1 . "\n";

$result2 = $router->dispatch('/products/%E9%9B%BB%E5%AD%90%E6%A9%9F%E5%99%A8/456');
echo $result2 . "\n";

$result3 = $router->dispatch('/products/electronics%20%26%20gadgets/789');
echo $result3 . "\n";
?>

5. ログ解析

<?php
/**
 * アクセスログ解析クラス
 */
class LogAnalyzer {
    
    /**
     * アクセスログエントリをパース
     */
    public static function parseLogEntry($logLine) {
        // Apache/Nginx形式: IP - - [日時] "リクエスト" ステータス サイズ
        $pattern = '/^(\S+) \S+ \S+ \[([^\]]+)\] "([^"]+)" (\d+) (\d+)/';
        
        if (preg_match($pattern, $logLine, $matches)) {
            return [
                'ip' => $matches[1],
                'datetime' => $matches[2],
                'request' => self::parseRequest($matches[3]),
                'status' => (int)$matches[4],
                'size' => (int)$matches[5]
            ];
        }
        
        return null;
    }
    
    /**
     * HTTPリクエストをパース
     */
    private static function parseRequest($requestString) {
        $parts = explode(' ', $requestString);
        
        if (count($parts) < 2) {
            return null;
        }
        
        $method = $parts[0];
        $url = rawurldecode($parts[1]);
        $protocol = $parts[2] ?? '';
        
        return [
            'method' => $method,
            'url' => $url,
            'protocol' => $protocol,
            'path' => parse_url($url, PHP_URL_PATH),
            'query' => parse_url($url, PHP_URL_QUERY)
        ];
    }
    
    /**
     * アクセスされたページのランキング
     */
    public static function getPageRanking($logLines) {
        $pages = [];
        
        foreach ($logLines as $line) {
            $entry = self::parseLogEntry($line);
            
            if ($entry && $entry['request']) {
                $path = $entry['request']['path'];
                
                if (!isset($pages[$path])) {
                    $pages[$path] = 0;
                }
                
                $pages[$path]++;
            }
        }
        
        arsort($pages);
        
        return $pages;
    }
    
    /**
     * 検索クエリを抽出
     */
    public static function extractSearchQueries($logLines) {
        $queries = [];
        
        foreach ($logLines as $line) {
            $entry = self::parseLogEntry($line);
            
            if ($entry && $entry['request'] && $entry['request']['query']) {
                parse_str($entry['request']['query'], $params);
                
                if (isset($params['q']) || isset($params['search'])) {
                    $query = rawurldecode($params['q'] ?? $params['search']);
                    
                    if (!isset($queries[$query])) {
                        $queries[$query] = 0;
                    }
                    
                    $queries[$query]++;
                }
            }
        }
        
        arsort($queries);
        
        return $queries;
    }
}

// 使用例
echo "\n=== ログ解析 ===\n";

$logLines = [
    '192.168.1.1 - - [10/Jan/2024:10:30:45 +0000] "GET /products/%E9%9B%BB%E5%AD%90%E6%A9%9F%E5%99%A8 HTTP/1.1" 200 1234',
    '192.168.1.2 - - [10/Jan/2024:10:31:22 +0000] "GET /search?q=PHP%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0 HTTP/1.1" 200 5678',
    '192.168.1.3 - - [10/Jan/2024:10:32:10 +0000] "GET /products/%E9%9B%BB%E5%AD%90%E6%A9%9F%E5%99%A8 HTTP/1.1" 200 1234',
];

$entry = LogAnalyzer::parseLogEntry($logLines[0]);
echo "パースされたエントリ:\n";
print_r($entry);

$ranking = LogAnalyzer::getPageRanking($logLines);
echo "\nページランキング:\n";
foreach ($ranking as $page => $count) {
    echo "  {$page}: {$count}回\n";
}

$searches = LogAnalyzer::extractSearchQueries($logLines);
echo "\n検索クエリ:\n";
foreach ($searches as $query => $count) {
    echo "  「{$query}」: {$count}回\n";
}
?>

よくある間違いと注意点

間違い1: urldecodeとの混同

<?php
$encoded = "hello+world";

// rawurldecode()はプラスをスペースに変換しない
echo rawurldecode($encoded);  // 出力: hello+world

// urldecode()はプラスをスペースに変換
echo urldecode($encoded);     // 出力: hello world

// ✅ 用途に応じて使い分ける
// パス部分 → rawurldecode()
// クエリパラメータ → urldecode() (またはparse_strが自動処理)
?>

間違い2: 二重デコードの問題

<?php
$text = "hello%2520world"; // %20がエンコードされている

// 1回目のデコード
$decoded1 = rawurldecode($text);
echo $decoded1;  // 出力: hello%20world

// 2回目のデコード
$decoded2 = rawurldecode($decoded1);
echo $decoded2;  // 出力: hello world

// ✅ 必要な回数だけデコード
?>

間違い3: セキュリティ上の注意

<?php
// ❌ デコードした値を直接ファイルパスに使用(危険)
$filename = rawurldecode($_GET['file']);
$path = "/var/www/uploads/" . $filename;  // ../ などで任意のファイルにアクセスされる可能性

// ✅ サニタイズとバリデーション
$filename = rawurldecode($_GET['file']);
$filename = basename($filename);  // パス部分を削除
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);  // 安全な文字のみ
$path = "/var/www/uploads/" . $filename;
?>

まとめ

rawurldecode関数は、PHPでURLエンコードされた文字列をデコードするための関数です。以下のポイントを押さえておきましょう。

  • RFC 3986準拠のURLデコード
  • %XX形式のエンコードを元の文字に変換
  • **スペースは%20**として扱う(+ではない)
  • rawurlencode()と対になる関数
  • urldecode()との違いを理解して使い分ける
  • URL解析、ファイル処理、API通信に活用
  • セキュリティ: デコード後は必ずサニタイズ
  • パス部分に使用、クエリパラメータはurldecode()

rawurldecodeを適切に使うことで、URLエンコードされたデータを安全かつ正確にデコードし、Web アプリケーションでの様々な処理を実現できます!

タイトルとURLをコピーしました