Ра­бота с тек­сто­выми фай­лами — одна из самых рас­простра­нен­ных задач в прог­рамми­рова­нии. В этой статье мы раз­берем­ся с пар­сингом тек­ста на Go, а для при­мера возь­мем кеш ARP. Будем исполь­зовать эти дан­ные, что­бы уста­новить соот­ветс­твие меж­ду адре­сами MAC и IP в локаль­ной сети.

В прос­тей­шем слу­чае файл содер­жит одно зна­чение (токен) на стро­ку — так обыч­но пос­тро­ены вор­длис­ты для бру­та или фаз­зинга. В дру­гих слу­чаях файл име­ет задан­ную регуляр­ную струк­туру, нап­ример все­воз­можные вари­ации CSV, хорошо под­ходящие для таб­личных дан­ных, телемет­рии и подоб­ных наборов.

Ху­же все­го, ког­да вход­ной файл изна­чаль­но пред­назна­чен для чте­ния челове­ком и содер­жит допол­нитель­ное фор­матиро­вание — отсту­пы, вырав­нивание про­бела­ми, под­заголов­ки, псев­догра­фику. Такие фай­лы мы не можем прос­то читать «как есть» — при­ходит­ся находить и исполь­зовать толь­ко отдель­ные фраг­менты.

В этой статье мы рас­смот­рим основные при­емы получе­ния инте­ресу­ющей нас информа­ции из тек­сто­вого фай­ла, то есть его пар­синг.

Прак­тичес­кий при­мер возь­мем нес­коль­ко отвле­чен­ный, но зато име­ющий­ся на любом компь­юте­ре — кеш ARP. Будем исполь­зовать дан­ные из это­го кеша, что­бы уста­новить соот­ветс­твие меж­ду IP- и MAC-адре­сами устрой­ств в локаль­ной сети.

Пос­коль­ку в раз­ных ОС дос­туп к содер­жимому кеша получа­ют нем­ного по‑раз­ному, одновре­мен­но раз­берем и работу с плат­формен­но зависи­мым кодом: в Linux мож­но про­читать содер­жимое /proc/net/arp, а в Windows — выпол­нить arp -a. Наконец, пре­обра­зуем написан­ный нами код пар­сера в пакет, при­год­ный для исполь­зования в дру­гих про­ектах.

info

Кеш ARP напол­няет­ся опе­раци­онной сис­темой авто­мати­чес­ки (обыч­но при попыт­ке уста­нов­ки соеди­нения с узлом локаль­ной сети). Дос­туп к этим све­дени­ям не тре­бует повыше­ния пол­номочий, в отли­чие от откры­тия сокета и отправ­ки ARP discovery пакета вруч­ную (а что­бы сде­лать такое на Windows-хос­те, обыч­но тре­бует­ся еще и уста­новить пред­варитель­но Npcap/WinPcap).

Нач­нем, как всег­да, с соз­дания нового пакета:

mkdir readarp && cd $_
go mod init readarp
touch main.go
touch arp.go

Пос­коль­ку часть кода будет зависеть от целевой плат­формы, сра­зу соз­дадим два фай­ла. В main.go будет наша точ­ка вхо­да, а в arp.go — код для работы с кешем ARP.

В будущем мы собира­емся получать MAC-адрес, соот­ветс­тву­ющий задан­ному IP. В таком сце­нарии нецеле­сооб­разно читать все содер­жимое кеша при каж­дом зап­росе. Гораз­до эффектив­нее сде­лать это один раз и сра­зу сох­ранить инте­ресу­ющие нас све­дения для пос­леду­ющих обра­щений.

Для хра­нения обра­ботан­ных дан­ных мы мог­ли бы соз­дать струк­туру с дву­мя полями — IP и MAC — и сох­ранить набор ее экзем­пля­ров в слай­се, в котором потом будем искать нуж­ное. Но луч­ше при­менить дру­гой под­ход и хра­нить дан­ные в виде кол­лекции пар ключ — зна­чение, где IP-адре­са будут уни­каль­ными клю­чами, для чего мы исполь­зуем струк­туру дан­ных map.

Хо­тя пока мы ничего такого не дела­ем, решение о выборе струк­туры дан­ных при­нима­ем уже сей­час, что­бы потом не перепи­сывать мно­го кода.

Со­дер­жимое фай­ла arp.go:

package main
func retrieveArpTable() map[string]string {
result := make(map[string]string)
// TODO not implemented
return result
}

Здесь мы объ­явля­ем фун­кцию, которая обра­баты­вает дан­ные из кеша и воз­вра­щает сло­варь с нуж­ными нам зна­чени­ями.

И содер­жимое фай­ла main.go:

package main
import "fmt"
func main() {
arpResuls := retrieveArpTable()
fmt.Print(arpResuls)
}

Здесь мы прос­то выводим на экран дан­ные из сло­варя, сфор­мирован­ного фун­кци­ей retrieveArpTable(). Пока это не сов­сем то, что мы хотим сде­лать в ито­ге: это прос­то фун­кция‑заг­лушка, дающая нам воз­можность уви­деть про­межу­точ­ные резуль­таты выпол­нения. Чуть поз­же мы от нее вооб­ще изба­вим­ся.

Об­рати вни­мание: хоть фай­лы и раз­ные, они отно­сят­ся к одно­му пакету — в дан­ном слу­чае main, — и мы в фай­ле main.go без каких‑либо допол­нитель­ных дей­ствий вызыва­ем фун­кцию, опре­делен­ную в arp.go.

Отличие структуры от структуры

Воз­можно, к это­му момен­ту у тебя в голове воз­никла некото­рая путани­ца. Рань­ше мы говори­ли о струк­туре как о типе дан­ных, который объ­явля­ем с клю­чевым сло­вом struct и далее работа­ем с его полями. Но так­же мы исполь­зуем такое выраже­ние, как «струк­тура дан­ных», говоря о map или slice.

Дей­стви­тель­но, и map, и slice — внут­ри струк­тура с соот­ветс­тву­ющи­ми полями и метода­ми. Но дело здесь в дру­гом. В computer science «струк­тура дан­ных» — это фун­дамен­таль­ное понятие, опи­сыва­ющее кон­крет­ный спо­соб раз­мещения и орга­низа­ции кол­лекции дан­ных в памяти.

Струк­туры дан­ных — это такие вещи, как спис­ки, сте­ки, оче­реди, деревья, хеш‑таб­лицы и про­чее. А реали­зова­ны они могут быть с помощью струк­тур (которые struct), или клас­сов, или еще как‑нибудь. В свою оче­редь, тип (type MySet struct), явля­ющий­ся реали­заци­ей той или иной струк­туры дан­ных, мы в оби­ходе обыч­но тоже называ­ем прос­то струк­турой дан­ных, то есть здесь име­ет мес­то некото­рое раз­мывание гра­ниц меж­ду поняти­ями.

 

Словари, или карты (map)

Фун­кция retrieveArpTable() воз­вра­щает сло­варь (клю­чевое сло­во map) с клю­чами типа string (тип клю­ча ука­зыва­ется в квад­ратных скоб­ках) и зна­чени­ями типа string (тип зна­чения ука­зыва­ется пос­ле квад­ратных ско­бок).

info

Map — это кол­лекция пар ключ — зна­чение, где клю­чи уни­каль­ны и при­меня­ются для извле­чения зна­чений, подоб­но тому как в мас­сивах или сре­зах исполь­зует­ся индекс. В раз­ной литера­туре по раз­ным язы­кам прог­рамми­рова­ния встре­чают­ся раз­ные наз­вания этой струк­туры: «ассо­циатив­ный мас­сив», «сло­варь», «хеш‑таб­лица», дос­ловный перевод «кар­та» и даже каль­ка с англий­ско­го «мапа». В источни­ках по Go обыч­но исполь­зуют пос­ледние два вари­анта, тем не менее я пред­почту «сло­варь». Сло­варь хорош тем, что обес­печива­ет дос­туп к зна­чению по клю­чу за кон­стантное вре­мя O(1), незави­симо от раз­мера кол­лекции — в отли­чие от поис­ка в мас­сиве или сре­зе.

Как и сре­зы, сло­вари соз­дают­ся коман­дой make. Вто­рым аргу­мен­том мож­но ука­зать емкость кол­лекции, если она извес­тна заранее. При пре­выше­нии емкости она будет авто­мати­чес­ки уве­личе­на — здесь все так же, как и в слу­чае со сре­зами.

Дос­туп к эле­мен­ту кол­лекции про­исхо­дит по клю­чу, который переда­ется в квад­ратных скоб­ках, так же как индекс мас­сива или сре­за: value := result[key]. Если ключ не най­ден, вер­нется зна­чение по умол­чанию для это­го типа (zero value).

Воз­можно, у тебя здесь сра­зу воз­ник воп­рос: а что, если zero value — легитим­ное зна­чение для эле­мен­та нашей кол­лекции? Как отли­чить две ситу­ации: в кол­лекции есть иско­мый ключ, и с ним свя­зано зна­чение 0 (или пус­тая стро­ка), или в кол­лекции нет это­го клю­ча, и мы получи­ли 0 (или пус­тую стро­ку) как зна­чение по умол­чанию для типа int (или string)?

На самом деле здесь воз­вра­щают­ся два зна­чения: value, ok := result[key], и как раз вто­рое ука­зыва­ет, был ли най­ден ключ. Если нам нуж­но толь­ко про­верить сущес­тво­вание клю­ча, пер­вое зна­чение мож­но вооб­ще отбро­сить: _, ok := result[key].

За­пись зна­чения про­исхо­дит похожим обра­зом: result[key] = value. При этом зна­чение будет добав­лено в кол­лекцию, если такого клю­ча в ней еще нет, или замене­но, если такой ключ уже сущес­тву­ет.

Для уда­ления клю­ча исполь­зует­ся фун­кция delete(). В отли­чие от append(), с которой мы поз­накоми­лись ранее, delete() изме­няет сущес­тву­ющую кол­лекцию, а не воз­вра­щает новую. Если уда­ляемый ключ и так отсутс­тву­ет в кол­лекции, delete() завер­шает­ся без оши­бок.

Реализация множеств

В некото­рых язы­ках прог­рамми­рова­ния ты мог видеть еще такую кол­лекцию дан­ных, как set (мно­жес­тво). Мно­жес­тво похоже на map в том пла­не, что это тоже набор уни­каль­ных клю­чей и обес­печива­ет кон­стантную слож­ность дос­тупа O(1). Отли­чие сос­тоит в том, что мно­жес­тво содер­жит толь­ко клю­чи, без зна­чений.

Ес­ли тебе понадо­бит­ся такая струк­тура дан­ных в Go, то иди­ома­тич­ной реали­заци­ей явля­ется map с клю­чами тре­буемо­го типа и зна­чени­ями типа «пус­тая струк­тура» — struct{}. Почему имен­но пус­тая струк­тура? Все прос­то: в Go ее раз­мер сос­тавля­ет ров­но ноль байт.

// Создаем set строк
mySet := make(map[string]struct{})
// Добавляем элемент
mySet["newElement"] = struct{}{}
// Удаляем элемент
delete(mySet, "spareElement")
// Проверяем наличие элемента
_, ok := mySet["checkElement"]
if ok {
// Найден!
}
 

Пакет strings

Те­перь перей­дем к пар­сингу тек­сто­вых дан­ных. В этом нам поможет пакет strings из стан­дар­тной биб­лиоте­ки Go (смот­ри, сколь­ко все­го мы уже сде­лали, и до сих пор обхо­дим­ся средс­тва­ми стан­дар­тной биб­лиоте­ки!). Как ты уже догадал­ся по наз­ванию, этот пакет объ­еди­няет средс­тва работы со стро­ками.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии