Comments 21
Когда то давно хотел на лету парсить конкретные csv файлы из zip-архива чтобы не тратить много памяти. Позже выяснилось, что нужно полностью скачивать весь архив, т.к. важные для распаковки метаданные находятся в конце этого файла (central directory). Но недавно на хабре видел статью, где автор утверждал, что его библиотека на js (https://github.com/greggman/unzipit) умеет это делать (статью похоже удалили). Я немного изучил его реализацию, похоже это действительно возможно, есть несколько путей:
1) Считывать архив дважды (возможно будет долго, но не придется хранить целиком)
2) Использовать метаданные файлов (local file header), которые тоже встречаются в архиве (но как понял, на них не стоит рассчитывать).
3) Если сервер поддерживает HTTP Range requests, то можно сразу обратиться к концу файла и сразу получить нужные метаданные для распаковки.
В любом случае, я думаю вам стоит попробовать эту библиотеку. Отпишитесь, если найдете решение получше.
Спасибо. Решение получше придётся написать самому и, похоже, теоретически сложного-то ничего нет. Возможно, некоторые проблемы могут возникнуть только в больших размерах получаемых данных (тут у меня возник другой вопрос, "а как оптимально скачивать cUrl-ом >20Гб в PHP"). JS (ес-но для NodeJS) меня не устраивает. Вспомнил:
Простой PHP код для создания ZIP файлов на лету (который я применяю для docx, xlsx...)
class ZIPMaker {
private $archiveFiles = 0;
private $archiveData = '';
private $archiveHeader = '';
private $relOffset = 0;
private function unix2DosTime( $ts = 0 ){
$timeRR = getdate( $ts ? $ts : time() );
if ( $timeRR['year'] < 1980) {
$timeRR['year'] = 1980;
$timeRR['mon'] = $timeRR['mday'] = 1;
$timeRR['hours'] = $timeRR['minutes'] = $timeRR['seconds'] = 0;
}
return
( ( $timeRR['year'] - 1980 ) << 25 ) |
( $timeRR['mon'] << 21 ) |
( $timeRR['mday'] << 16 ) |
( $timeRR['hours'] << 11 ) |
( $timeRR['minutes'] << 5 ) |
( $timeRR['seconds'] >> 1 );
}
function addFile( $name , $data , $ts = 0 ){
$name = str_replace('\\', '/', $name);
$HEXTime = pack('V', $this->unix2DosTime($ts));
$crc = crc32($data);
$dataGZ = gzcompress($data,9);
$dataGZ = substr(substr($dataGZ, 0, strlen($dataGZ) - 4), 2); // fix crc bug
$bin =
"\x50\x4b\x01\x02".
"\x00\x00". // version made by
"\x14\x00". // version needed to extract
"\x00\x00". // gen purpose bit flag
"\x08\x00". // compression method
$HEXTime. // last mod time & date
pack('V', $crc). // crc32
pack('V', strlen($dataGZ)). // compressed filesize
pack('V', strlen($data)). // uncompressed filesize
pack('v', strlen($name)). // length of filename
pack('v', 0). // extra field length
pack('v', 0). // file comment length
pack('v', 0). // disk number start
pack('v', 0). // internal file attributes
pack('V', 32). // external file attributes
pack('V', $this->relOffset). // relative offset of local header
$name;
$this->archiveHeader .= $bin;
$bin =
"\x50\x4b\x03\x04".
"\x14\x00". // ver needed to extract
"\x00\x00". // gen purpose bit flag
"\x08\x00". // compression method
$HEXTime. // last mod time and date
pack('V', $crc). // crc32
pack('V', strlen($dataGZ)). // compressed filesize
pack('V', strlen($data)). // uncompressed filesize
pack('v', strlen($name)). // length of filename
pack('v', 0). // extra field length
$name.
$dataGZ;
$this->archiveData .= $bin;
$this->relOffset += strlen($bin);
$this->archiveFiles++;
}
function bin(){
return
$this->archiveData.
$this->archiveHeader.
"\x50\x4b\x05\x06\x00\x00\x00\x00".
pack('v', $this->archiveFiles). // total #of entries "on this disk"
pack('v', $this->archiveFiles). // total #of entries overall
pack('V', strlen($this->archiveHeader)). // size of central dir
pack('V', $this->relOffset). // offset to start of central dir
"\x00\x00"; // .zip file comment length
}
function passthru( $fileName = 'attachment.zip' , $mimeType = 'application/zip' ){
header('Content-type: '.$mimeType);
header('Content-length: '.strlen($ctx = $this->bin()));
header('Content-Disposition: attachment; filename='.$fileName);
echo $ctx;
}
}
И благодаря:
return
$this->archiveData.
$this->archiveHeader.
"\x50\x4b\x05\x06\x00\x00\x00\x00".
pack('v', $this->archiveFiles). // total #of entries "on this disk"
pack('v', $this->archiveFiles). // total #of entries overall
pack('V', strlen($this->archiveHeader)). // size of central dir
pack('V', $this->relOffset). // offset to start of central dir
"\x00\x00"; // .zip file comment length
Всё относительно очевидно, если сервер отдаёт размер, то считываем и кэшируем заголовок с файлами, а дальше знаем что и откуда получить.
a) Потому что всё равно. Да и статья-то не про это, я даже постарался ни одной строчки кода в ней не разместить. Тот же код и алгоритм у меня сделает (и делает) PostgreSQL. Я бы сам придрался к php :)
b) Потому что LAMP.
Кстати, массовый поиск по UUID при кейсе "выбираем только актуальное" нужен крайне редко (в реальности, только если зачем-то пришлось проверять старые адреса, ес-но хранящие этот UUID, обычно улицы, на возможно изменившийся новый). При менее 1.5млн записей в 280Мб данных не напрягает даже на MySQL.
Не вижу смысла текстового поиска с учетом опечаток по таким данным, когда выбор по первым двум буквам улицы внутри населенного пункта сокращает результат до десятков, а чаще меньше, вариантов. И выбор самого населенного пункта по 2-3 буквам с учетом количества населения\числа домов - тоже десятки вариантов.
В 2017 делал парсинг XML ФИАС в Oralce, когда она весила 24 Гб.
Многопоточное приложение на Delphi.
Основной поток разбирал по кусочкам XML с буфером около 2 Мб. И скармливал дочерним потокам в текстовый массив, а они в свою очередь делали инсерты.
Лучшая производительность была при установке потоков = кол-ву виртуальных ядер на клиенте.
Первые 5 Гб сервер съедал быстро, по 6 Мб/сек на поток, а потом начинал тормозить. Так и не выяснили почему.
Всего на 24 ГБ уходило около 4 часов. На клиенте прога занимала менее 50 Мб оперативки.
Вот только же не было такого размера в 2017. Не доросла она до 24Гб, остановилась на 12Гб. А 19,7 Гб версия ГАР БД ФИАС появилась уже в 2020 году только (о чем до сих пор написано на сайте ИФНС) и за год выросла до 28,9 Гб.
Аналогичные данные, что здесь (без owner_mun) 31.08.2021 получались на php за 3 часа. Не спешно, в один поток.
Мы же про разархивированные данные говорим?
Коллеги, а как используете эту базу? Чисто для подсказок такие объемы выглядят дико... а других применений мне пока не приходит в голову...
Объемы-то для подсказок небольшие. Адреса от региона до улицы - 273Мб, и все дома с почтовыми индексами - 400Мб (если не нужны индексы и того меньше). Справляется даже слабенькая VDS.
Когда заключается договор, адреса (по прописке; или доставки, когда работаешь по куче регионов) следует ввести не абы как менеджеру или клиенту захочется. Что бы не было опечаток. Поэтому только выбор.
Дико только парсить такие объемы, чтобы получить этот результат :(
Дома ещё иногда встречаются отсутствующие или почтовые индексы неопределенные\неверные у новых домов. Улицы уже давно без проблем. Произвольный "хвост" никто не отменял, но используется всё реже и реже. Сейчас посмотрел по двум базам - в совокупности последние 10К реальных адресов (все 2021 год) - не используется, т.е. улица была полностью определена выбором. А дом хранится без привязки к ФИАС, только при выборе в подсказках, статистики о его существовании у меня нет.
По прописке встречается - если отсутствует актуальный адрес в ФИАС, то обычно проблемы: например, человек прописан в деревне, дома (или даже целой деревни) уже и на карте-то нет. С пожилыми людьми обычно возникает.
Автор спасибо за статью. Можно код на git hub выложить ?
А подскажите по домам,как теперь нормально собрать номер дома например: дом 1 корпус 4 строение 3 ? Не понятно с ADDNUM1,ADDNUM2,ADDTYPE1, ADDTYPE2
Как и раньше, только теперь обозначение числовое. Я пока ещё не использую ГАР в системах подсказки (и ещё не писал "упаковщик" данных домов, чтобы это снова стало менее 500Мб с постовыми индексами), но заметил, что ADDNUM2\ADDTYPE2 отсутствуют в доступных (т.е. актуальных и до которых можно добраться по иерархии) домах.
Запись реальная, правда недостижимая (мне ИФНС ещё не ответила на письмо выше, глюк у меня или, как обычно, будет исправлено)
HOUSENUM="37" ADDNUM1="10" ADDNUM2="10" HOUSETYPE="1" ADDTYPE1="1" ADDTYPE2="2"
влд. 37 влд. 10 д. 10
HOUSENUM="4" ADDNUM1="94" HOUSETYPE="10" ADDTYPE1="2"
корп. 4 д. 94 (В ГАР именно так. Приоритеты, что дом раньше делать самостоятельно.)
Кстати, сможете назвать хоть один такой реальный такой "сложный" адрес, как в вашем примере?
Это как раз то, из-за чего нецензурно выражаться хочется. С улицами всё хорошо, с домами, похоже, стало хуже. Полная запись этого дома в ГАР сейчас выглядит так:
HOUSE ID="40479359" OBJECTID="67218762" OBJECTGUID="ed525b52-8467-467f-aa0a-ca802d9432b6" CHANGEID="100222233" HOUSENUM="1-3" ADDNUM1="4" HOUSETYPE="2" ADDTYPE1="2" OPERTYPEID="10" PREVID="0" NEXTID="0" UPDATEDATE="2017-04-05" STARTDATE="1900-01-01" ENDDATE="2079-06-06" ISACTUAL="1" ISACTIVE="1"
Существенное только: HOUSENUM="1-3" ADDNUM1="4" HOUSETYPE="2" ADDTYPE1="2"
И адрес получается "д. 1-3 д. 4". (Либо надо ещё раз очень внимательно перечитать документацию...)
А ещё таких HOUSETYPE="2" ADDTYPE1="2" в актуальных - 368670 домов (1.41%) и, похоже, это всё - проблемы.
В ФИАС этот же дом HOUSENUM="1-3" , STRUCNUM (строение) = "4" - все логично.
p.s. Кстати, видите, что на карте Яндекса нет "корпуса".
p.p.s. Меня пока только одно радует - такие сложности, обычно, в нежилых домах.
Отправил вопрос в ИФНС, на предыдущий (по поводу недостижимых улиц) мне пока не ответили. Но это нормально, всего-то чуть более 10 дней прошло.
С этими домами всё хорошо, для ADDTYPE1 и ADDTYPE2 существует отдельный справочник: AS_ADDHOUSE_TYPES где 2 это строение:
<HOUSETYPES>
<HOUSETYPE ID="1" NAME="Корпус" SHORTNAME="к." />
<HOUSETYPE ID="2" NAME="Строение" SHORTNAME="стр." />
<HOUSETYPE ID="3" NAME="Сооружение" SHORTNAME="соор." />
<HOUSETYPE ID="4" NAME="Литера" SHORTNAME="литера" />
</HOUSETYPES>
ГАР БД ФИАС или очень полная БД ФИАС