СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Получение курсов валют на PHP в черновиках Из песочницы

PHP
В интернете описано множество решений по получению актуальных курсов валют. Большинство решений основано на бесплатном сервисе Центробанка (подробнее о сервисе на сайте Центробанка).

Казалось бы, хороший и удобный интерфейс от главного экономического регулятора РФ, однако у него есть несколько неприятных особенностей. Самый значительный его минус в том, что IP, с которого вы будете парсить их сервис курсов валют может, быть заблокирован в любой момент совершенно неожиданно для вас. В моем случае я обновлял курс через сервис ЦБ РФ около 2 месяцев, каждый день каждые два часа (для организации очень важно иметь актуальные данные по курсу валют). Причем на сайте не указано никаких ограничений по использованию данного сервиса, более того, они предлагают свой скрипт для получения актуального курса. Если вас заблокировали, с данного IP вы даже не сможете зайти на сайт регулятора. Для заблокированных пользователей они выводят страницу с бесконечной переадресацией. Обычно блокировка длится не более 24 часов, однако нет никаких гарантий, объяснений причин и т. п. В общем и целом предоставляемый ЦБ РФ сервис ненадежен.

Еще один небольшой минус сервиса в том что приходится парсить XML, а это расход ресурсов и потеря быстродействия. Конечно, можно так настроить, что можно будет парсить всего 4-5 строк, однако в идеале и их бы сократить до одной.
В результате поиска альтернатив пришел к выводу, что достойных сервисов предоставляющих курс валюты в удобном формате, соответствующих критериям (надежность, скорость работы, простота использования), просто нет. Кто-то скажет, что можно парсить сервис Европейского ЦБ, ссылку приводить не буду, скажу лишь, что сервис почти такой же как у ЦБ РФ. Однако курсы Европейского ЦБ значительно отличаются от курсов ЦБ РФ и не подходят для использования в России.

Единственным достойным конкурентом сервису нашего ЦБ, является сервис cbrates.rbc.ru. В программе 1С уже давно используют данный сервис и за время использования он доказал свою надежность. Использовать его проще простого: если нужно получить динамику курса, используем следующую ссылку — cbrates.rbc.ru/tsv/cb/Код_валюты.tsv. Например, для доллара это будет ссылка cbrates.rbc.ru/tsv/cb/840.tsv В результате получаем пары дата курс, разделенных табуляцией. Разобрать такие пары на массивы проще простого.

Для получения конкретного курса на конкретную дату используется ссылка cbrates.rbc.ru/tsv/Код_валюта/год/месяц/день.tsv, например, для долара это будет cbrates.rbc.ru/tsv/840/2014/11/07.tsv. По ссылке выдается всего одна строка с порядковым номером 1 и курсом на эту дату. Парсить такую строку намного проще, чем XML — сервис нашего ЦБ, а надежность и быстродействие будут выше. Для получения курса я написал простенький класс:

class rbc{
	const url = 'http://cbrates.rbc.ru/tsv/';
	const file = '.tsv';
	private $date = 0;
	public function __construct($date = null){
		if ($date == null){
			$date = time();
		}
		$this -> date = $date;
	}
	public function curs($currency_code){
		$url = self::url;
		$curs = 0;
		try{
			if (!is_numeric($currency_code)){
				throw new Exception('Передан неверный код валюты');
			}
			$url .= $currency_code . '/';
			if ($this -> date <= 0){
				throw new Exception('Передана неверная дата');
			}
			$url .= date('Y/m/d', $this -> date);
			$url .= self::file;

			$page = file_get_contents($url);
			$curs = $this -> parse($page);
		}
		catch (Exception $e) {
			echo 'Не удалось получить курс валюты. ',  $e -> getMessage();
		}
		return $curs;
	}
	private function parse($file){
		if (empty($file)){
			throw new Exception('Возможно указан неверный код валюты, также возможно на указанную дату еще не установлен курс валюты, либо сервер "cbrates.rbc.ru" недоступен.');
		}
		$curs = explode("\t", $file);
		if (!empty($curs[1])){
			return $curs[1];
		}
		else{
			throw new Exception('Сервер не выдал результатов по данной валюте на указнную дату');
		}
	}
}

Использовать можно следующим образом:

$today = new rbc(); //Курс сегодня
echo $today -> curs(840); //Курс долара, в скобках официальный код валюты

$tommorow= new rbc(strtotime("+1 day")); //Курс на завтра
echo $tommorow -> curs(840);

При создании класса передается дата в формате unix time, если нужно получить курс на дату отличную от текущей. Можно ввести проверку кодов валют, но я не стал усложнять класс.

Коды валют можно взять например с сайта ЦБ
www.cbr.ru/currency_base/daily.aspx?date_req=08.11.2014

комментарии (42)

+1
SonkoDmitry ,  
Оформите в виде пакета для composer и будет совсем замечательно.
+6
eandr_67 ,  
Извините, но зачем надо было обращаться каждые 2 часа к сервису, обнавляющему выдаваемую информацию раз в сутки?
+3
Groove ,  
Видимо затем же, зачем делают контрольный выстрел в голову — чтоб наверняка! :)
0
dom1n1k ,  
Видимо потому, что разные валюты обновляются неодновременно.
Я не помню точно, но весь процесс обновлений размазан там, кажется, часа на 4.

Но обращения круглые сутки, включая ночь, это конечно, сильно :)
+1
+2 –1
E_STRICT ,  
Еще один небольшой минус сервиса в том что приходится парсить XML, а это расход ресурсов и потеря быстродействия.
А вы делали бенчмарки? Сколько ваше приложение теряет ресурсов при обратботке этого XML?
0
merk ,  
Есть отличный набор классов для получения курсов CBR и ECB — github.com/redco/redcode-currency-rate
Бандл для symfony2 — github.com/redco/redcode-currency-rate-bundle
+10
Quiz ,  
Интересно, теперь все статьи на Хабре будут из разряда «как парсить XML»?
0
neolink ,  
в статье идет разбор tsv (tab-separated values)
+1
z0rg ,  
Что является еще более простым случаем парсинга.
0
egor_bulychev ,  
openexchangerates.org/ очень удобно. 1000 запросов в месяц бесплатно.
0
hedgehog ,  
private $url = 'http://cbrates.rbc.ru/tsv/';
Вы в эту переменную не пишите. Для таких вещей придумали константы.

private $file = '.tsv';
Аналогично.

if (!is_int($currency_code)){
	throw new Exception('Передан неверный код валюты');
}
В чём смысл этой проверки? Что она проверяет? -1 она например проглотит. Является ли -1 правильным кодом валюты? Зато рассово верное '840' отметёт (ваш код могут использовать в различных местах, и не всегда достоверно известно, что этот параметр придёт не строкой, либо это может привести к дополнительным телодвижения по преобразованию его число, что в общем-то не совсем нужно).
Можно ввести проверку кодов валют, но я не стал усложнять класс.
Вот и не надо было.

Ваш метод parse используется только 1 раз и не содержит серьёзной логики. Не вижу смысла вынесения в отдельный метод.
0
akubintsev ,  
+1
Добавлю ещё, что всё-таки не стоит кидать \Exception, лучше использовать специфичные для данного функционала исключения.
0
+2 –2
NorthDakota ,  
бл**. Как же меня убивают эти пробелы…
 $this -> url;
0
NorthDakota ,  
И вообще, всё намного проще.

<?php
$url = "https://rate-exchange.appspot.com/currency?from=USD&to=UAH&q=1";
$result = file_get_contents($url);
$result = json_decode($result);
var_dump($result);
0
kosar91 ,   * (был изменён)
Честно говоря больше интересуют курс рубля и доллара. Также интересует вопрос надежности. В приведенном примере используется проверенный ресурс, который выдает курс относительно рубля.
0
NorthDakota ,  
$url = «rate-exchange.appspot.com/currency?from=USD&to=RUB&q=1»;
0
kosar91 ,  
Посмотрел по ссылке выдается 47.044400000000003, при этом на том же ЦБ сейчас курс 45,1854 на сегодня и на завтра 47,8774, т.е. что то не сходится.
0
kosar91 ,   * (был изменён)
Да вы правы, логичнее использовать константы. Смысл в изменении url, есть вероятность что пользователь введет текст аля «dollar/usa» вместо кода валюты, а для работы класса требуется именно числовой код валюты.
0
Akuma ,   * (был изменён)
is_int проверяет на int, а не на «числовое». Вам нужно is_numeric().

Например:

is_int('111') => false
is_numeric('111') => true
0
kosar91 ,  
Спасибо, учту.
0
hedgehog ,  
В is_numeric есть куча форматов, которые сфейлят всю схему.
0
Akuma ,  
Но это лучше чем is_int
0
hedgehog ,  
Исходную задачу — проверку кода валюты это тоже не решает. Так зачем тогда вообще? Если делать проверку, то правильно со всеми нюансами. Или не делать вовсе.
0
Akuma ,  
И все же, это лучшем чем ничего. И, как и отметил автор, сработает при передаче 'dollar', например.
0
kosar91 ,   * (был изменён)
Можно было бы проверять введенный код валюты на существование, предварительно занеся все нужные коды в БД например или еще каким нибудь образом проверить его валидность. Можно даже заморочиться чтобы вместо кода валюты вводить название типа USD. Но здесь цель была просто показать один из способов решения задачи. Каждый может дописать для себя такую проверку которую считает нужным.
+1
Aliance ,  
0
hedgehog ,   * (был изменён)
Я не стал про него писать, т.к. там тоже есть нюансы (см. пример 2 в документации). Но из всех 3х функций эта ближе всего к цели.
0
Blumfontein ,  
Не увидел в статье ссылку на список кодов валют
0
kosar91 ,  
Спасибо, добавил
–1
movl ,  
  def self.crb_rates
    response = Net::HTTP.get_response(URI.parse(CRB_URL))
    case response
    when Net::HTTPOK
      xml = Hash.from_xml(response.body)
      @crb_rates = xml["ValCurs"]["Valute"].inject({}){ |r, i| r[i["CharCode"]] = BigDecimal(i["Value"].tr(",", ".")); r }
      @crb_rates["RUB"] = BigDecimal(1)
    else
      @crb_rates = {}
    end
    @crb_rates
  end


У меня на руби, когда понадобилось подобное, лаконичнее решение получилось. А вы могли бы кеширование прикрутить, хотя бы внутри одного объекта класса, чтобы не запрашивать каждый раз данные и парсить их, при обращении к функции. Особенно, если вы говорите, что это минус стороннего сервиса.
+1
smileonl ,  
@crb_rates = xml["ValCurs"]["Valute"].inject({}){ |r, i| r[i["CharCode"]] = BigDecimal(i["Value"].tr(",", ".")); r }

Боюсь узнать что в вашем понятии не лаконично )
0
movl ,  
Тогда я только постигал руби, и в том проекте есть гораздо более страшные конструкции.

@full_tariff = h.inject([]){|r,(k,v)| l=r.last; r.push((!l.nil?&&(v.nil?||v==0)) ? [k,l[1]/days[l[0]]*days[k]] : [k,v])}.inject({}){|r,v| r[v[0]]=v[1];r}


Но как сейчас мне кажется, эта строка все равно не достаточно обфусцирована.
0
shushu ,  
Вот сразу челябинцев заметно :)
+1
dron_k ,  
В моем случае я обновлял курс через сервис ЦБ РФ около 2 месяцев, каждый день каждые два часа.

Еще один небольшой минус сервиса в том что приходится парсить XML, а это расход ресурсов и потеря быстродействия.


А насколько критично быстродействие скрипта выполняющегося раз в 2 часа?
Ну окей вместо 3мс скрипт отработает за 10мс… и будет ждать еще 2 часа, с этим какието проблемы?
–1
kosar91 ,  
Никаких проблем, для меня это не критично, этот пункт указан как дополнение. Основной проблемой была именно ненадежность из-за возможной блокировки при использовании сервиса ЦБ РФ.
0
Aliance ,  
Неужели вы в реалтайме парсили XML, что аж лишние строчки там считали? Если же все-таки кроном, то какая разница?

По code style приведенного решения конечно очень все плохо.
— и как уже говорили о пробелах между контекстом и полем/методом ($this -> blablabla);
— и об кидании общих ошибок \Exception (имхо, лучше варианты с \InvalidArgumentException, \LogicException, \MySuperRateClassException);
— также по всему коду где-то ставятся пробелы после/перед скобками, где-то нет. где-то есть перевод строки перед else (не понятно зачем), где-то его нет;
explode('	', $file)

конечно вообще повеселил. если там tsv, то логичнее писать так:
explode("\t", $file)
0
neolink ,  
var_dump("\t" === '     '); // bool(true)

видимо это тоже оптимизация
–2
kosar91 ,  
Насчет производительности этот пункт приведен как дополнение, т.е. лично для меня не критичен. Насчет табуляции, не вижу разницы — дело вкуса.
+1
Sild ,  
Это не дело вкуса, это ещё читаемость кода. Пока я не увидел \t, я понятия не имел что это за 2см пустого места. Пробелы там, табуляция, или может просто невидимые спец символы — кто его разберет.
–1
Sild ,   * (был изменён)
del
0
R0ckwi11 ,  
>curs
>$curs
>$curs_today
$spisokTovarovFinal = $tovary->getTovary();
+1
zelenin ,  
ни psr-1, ни psr-2, не логичных названий переменных (file = '.tsv' — у вас так файл называется?). Кодстайл «привет из 90х». Пробелы какие-то, в том числе отделяющие ->.
Ужас.