Read strings from a binary stream#
Awkward Array implements support for ragged strings as ragged lists of code-units. As such, successive strings are closely packed in memory, leading to high-performance operations.
Let’s imagine that we want to read some logging output that is stored in a text file. For example, a subset of logs from the Android Application framework.
import gzip
import itertools
import pathlib
# Preview logs
log_path = pathlib.Path("..", "samples", "Android.head.log.gz")
with gzip.open(log_path, "rt") as f:
for line in itertools.islice(f, 8):
print(line, end="")
12-17 19:31:36.263 1795 1825 I PowerManager_screenOn: DisplayPowerStatesetColorFadeLevel: level=1.0
12-17 19:31:36.263 5224 5283 I SendBroadcastPermission: action:android.com.huawei.bone.NOTIFY_SPORT_DATA, mPermissionType:0
12-17 19:31:36.264 1795 1825 D DisplayPowerController: Animating brightness: target=21, rate=40
12-17 19:31:36.264 1795 1825 I PowerManager_screenOn: DisplayPowerController updatePowerState mPendingRequestLocked=policy=BRIGHT, useProximitySensor=true, useProximitySensorbyPhone=true, screenBrightness=33, screenAutoBrightnessAdjustment=0.0, brightnessSetByUser=true, useAutoBrightness=true, blockScreenOn=false, lowPowerMode=false, boostScreenBrightness=false, dozeScreenBrightness=-1, dozeScreenState=UNKNOWN, useTwilight=false, useSmartBacklight=true, brightnessWaitMode=false, brightnessWaitRet=true, screenAutoBrightness=-1, userId=0
12-17 19:31:36.264 1795 2750 I PowerManager_screenOn: DisplayPowerState Updating screen state: state=ON, backlight=823
12-17 19:31:36.264 1795 2750 I HwLightsService: back light level before map = 823
12-17 19:31:36.264 1795 1825 D DisplayPowerController: Animating brightness: target=21, rate=40
12-17 19:31:36.264 1795 1825 V KeyguardServiceDelegate: onScreenTurnedOn()
To begin with, we can read the decompressed log-files as an array of np.uint8
dtype using NumPy, and convert the resulting array to an Awkward Array
import awkward as ak
import numpy as np
with gzip.open(log_path, "rb") as f:
# `gzip.open` doesn't return a true file descriptor that NumPy can ingest
# So, instead we read into memory.
arr = np.frombuffer(f.read(), dtype=np.uint8)
raw_bytes = ak.from_numpy(arr)
raw_bytes.type.show()
1150841 * uint8
Awkward Array doesn’t support scalar values, so we can’t treat these characters as a single-string. Instead we need at least one dimension. Let’s unflatten our array of characters, to form a length-1 array of characters.
array_of_chars = ak.unflatten(raw_bytes, len(raw_bytes))
array_of_chars
[[49, 50, 45, 49, 55, 32, 49, 57, 58, ..., 99, 114, 105, 109, 40, 41, 13, 10]] ------------------------------------------------------------------------------ type: 1 * 1150841 * uint8
We can then ask Awkward Array to treat this array of lists of characters as an array of strings, using ak.enforce_type()
string = ak.enforce_type(array_of_chars, "string")
string.type.show()
1 * string
The underlying mechanism for implementing strings as lists of code-units can be seen if we inspect the low-level layout that builds the array
string.layout
<ListOffsetArray len='1'>
<parameter name='__array__'>'string'</parameter>
<offsets><Index dtype='int64' len='2'>[ 0 1150841]</Index></offsets>
<content><NumpyArray dtype='uint8' len='1150841'>
<parameter name='__array__'>'char'</parameter>
[49 50 45 ... 41 13 10]
</NumpyArray></content>
</ListOffsetArray>
The __array__
parameter is special. It is reserved by Awkward Array, and signals that the layout is a special pre-undertood built-in type. In this case, that type of the outer ak.contents.ListOffsetArray
is “string”. It can also be seen that the inner ak.contents.NumpyArray
also has an __array__
parameter, this time with a value of char
. In Awkward Array, an array of strings must look like this layout; a list with the __array__="string"
parameter wrapping a ak.contents.NumpyArray
with the __array__="char"
parameter.
A single (very long) string isn’t much use. Let’s split this string at the line boundaries
split_at_newlines = ak.str.split_pattern(string, "\n")
split_at_newlines
[[...]] ---------------------- type: 1 * var * string
Now we can remove the temporary length-1 outer dimension that was required to treat the data as a string
lines = split_at_newlines[0]
lines
['12-17 19:31:36.263 1795 1825 I PowerManager_screenOn: DisplayPowerStatesetColorFadeLevel: level=1.0\r', '12-17 19:31:36.263 5224 5283 I SendBroadcastPermission: action:android.com.huawei.bone.NOTIFY_SPORT_DATA, mPermissionType:0\r', '12-17 19:31:36.264 1795 1825 D DisplayPowerController: Animating brightness: target=21, rate=40\r', '12-17 19:31:36.264 1795 1825 I PowerManager_screenOn: DisplayPowerController updatePowerState mPendingRequestLocked=policy=BRIGHT, useProximitySensor=true, useProximitySensorbyPhone=true, screenBrightness=33, screenAutoBrightnessAdjustment=0.0, brightnessSetByUser=true, useAutoBrightness=true, blockScreenOn=false, lowPowerMode=false, boostScreenBrightness=false, dozeScreenBrightness=-1, dozeScreenState=UNKNOWN, useTwilight=false, useSmartBacklight=true, brightnessWaitMode=false, brightnessWaitRet=true, screenAutoBrightness=-1, userId=0\r', '12-17 19:31:36.264 1795 2750 I PowerManager_screenOn: DisplayPowerState Updating screen state: state=ON, backlight=823\r', '12-17 19:31:36.264 1795 2750 I HwLightsService: back light level before map = 823\r', '12-17 19:31:36.264 1795 1825 D DisplayPowerController: Animating brightness: target=21, rate=40\r', '12-17 19:31:36.264 1795 1825 V KeyguardServiceDelegate: onScreenTurnedOn()\r', '12-17 19:31:36.264 1795 1825 I WindowManger_keyguard: onScreenTurnedOn()\r', '12-17 19:31:36.264 1795 1825 D DisplayPowerController: Display ready!\r', ..., '12-17 19:31:56.801 2852 2866 D KeyguardService: KGSvcCall onScreenTurningOn.\r', '12-17 19:31:56.801 2852 2866 D KeyguardViewMediator: notifyScreenOn\r', '12-17 19:31:56.801 2852 2852 D KeyguardViewMediator: handleNotifyScreenTurningOn\r', '12-17 19:31:56.801 2852 2852 I PhoneStatusBar: onScreenTurningOn\r', '12-17 19:31:56.801 2852 2852 D KeyguardViewMediator: handleNotifyScreenTurnedOn End with : 110\r', '12-17 19:31:56.801 1795 15987 I WindowManger_keyguard: **** SHOWN CALLED ****\r', '12-17 19:31:56.801 1795 15987 D WindowManager: mKeyguardDelegate.ShowListener.onDrawn.\r', '12-17 19:31:56.801 1795 15987 I WindowManger_keyguard: hideScrim()\r', ''] --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- type: 10001 * string
In the low-level layout, we can see that these lines are still just variable-length lists
lines.layout
<ListOffsetArray len='10001'>
<parameter name='__array__'>'string'</parameter>
<offsets><Index dtype='int64' len='10002'>
[ 0 102 228 ... 1140773 1140841 1140841]
</Index></offsets>
<content><NumpyArray dtype='uint8' len='1140841'>
<parameter name='__array__'>'char'</parameter>
[49 50 45 ... 40 41 13]
</NumpyArray></content>
</ListOffsetArray>
Bytestrings vs strings#
In general, whilst strings can fundamentally be described as lists of bytes (code-units), many string operations do not operate at the byte-level. The ak.str
submodule provides a suite of vectorised operations that operate at the code-point (not code-unit) level, such as computing the string length. Consider the following simple string
large_code_point = ak.Array(["Å"])
In Awkward Array, strings are UTF-8 encoded, meaning that a single code-point may comprise up to four code-units (bytes). Although it looks like this is a single character, if we look at the layout it’s clear that the number of code-units is in-fact two
large_code_point.layout
<ListOffsetArray len='1'>
<parameter name='__array__'>'string'</parameter>
<offsets><Index dtype='int64' len='2'>
[0 2]
</Index></offsets>
<content><NumpyArray dtype='uint8' len='2'>
<parameter name='__array__'>'char'</parameter>
[195 133]
</NumpyArray></content>
</ListOffsetArray>
This is reflected in the ak.num()
function
ak.num(large_code_point)
[2] --------------- type: 1 * int64
The ak.str
module provides a function for computing the length of a string
ak.str.length(large_code_point)
[1] --------------- type: 1 * int64
Clearly this function is code-point aware.
If one wants to drop the UTF-8 string abstraction, and instead deal with strings as raw byte arrays, there is the bytes
type
large_code_point_bytes = ak.enforce_type(large_code_point, "bytes")
large_code_point_bytes
[b'\xc3\x85'] --------------- type: 1 * bytes
The layout of this array has different "bytestring"
and "byte"
parameters
large_code_point_bytes.layout
<ListOffsetArray len='1'>
<parameter name='__array__'>'bytestring'</parameter>
<offsets><Index dtype='int64' len='2'>
[0 2]
</Index></offsets>
<content><NumpyArray dtype='uint8' len='2'>
<parameter name='__array__'>'byte'</parameter>
[195 133]
</NumpyArray></content>
</ListOffsetArray>
Many of the functions in the ak.str
module treat bytestrings and strings differently; in the latter case, strings are often manipulated in terms of code-points instead of code-units. Consider ak.str.length()
for this array
ak.str.length(large_code_point_bytes)
[2] --------------- type: 1 * int64
This is clearly counting the bytes (code-units), not code-points.